Structs

A value type you define: the variable holds the data, assignment copies it, and nothing can inherit from it.

Syntaxpublic struct Name { public int X { get; } public int Y { get; } }

A struct is a type you write yourself that behaves like a number. A variable of that type holds the data itself. Types that work this way are called value types. A class is a reference type: the variable holds the address of an object stored elsewhere, and two variables can hold the same address.

Copy on assignment

Assigning a struct copies every field. Passing one to a method copies it. Returning one copies it again. So a change made through one variable never appears in another. That single rule explains most struct behaviour.

Reach for a struct for a small bundle of data with little behaviour: a point, a money amount, an identifier. .NET itself uses structs for int, bool, char and DateTime. Because copies are everywhere, prefer structs that never change after construction.

What a struct cannot do

A struct cannot inherit from another type, and nothing can inherit from a struct. It can implement interfaces. It cannot declare a finalizer. Every field must hold a value by the time a constructor finishes.

What trips people up

Structs already compare by value: the built-in Equals checks field against field. That version uses reflection and is slow, so a struct you use heavily should implement IEquatable<T> and override GetHashCode.

Shorthand and longhand: public int X { get; } is an auto-property, the same thing as a private backing field plus a property returning it. And Point p = new(3, 4); means exactly Point p = new Point(3, 4);. Since C# 10 a struct may declare its own parameterless constructor.

Example

Example · csharp
Point a = new Point(3, 4);
Point b = a;              // b gets a copy of a's data, not a link to it.
b.Shift(10, 0);           // So this cannot change a.

Console.WriteLine(a);
Console.WriteLine(b);

// Structs compare field by field, with no code written by you.
Console.WriteLine(a.Equals(new Point(3, 4)));

struct Point
{
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; private set; }
    public int Y { get; private set; }

    // Mutable structs are discouraged. This one exists to show the copy.
    public void Shift(int dx, int dy)
    {
        X += dx;
        Y += dy;
    }

    public override string ToString() => $"({X}, {Y})";
}

// Output:
// (3, 4)
// (13, 4)
// True

When to use it

  • Model a 2D point or a size that is passed through a tight loop without allocating an object for each one.
  • Store a money amount as integer cents in a type where two equal amounts compare equal.
  • Wrap an int identifier in its own type so an OrderId cannot be passed where a CustomerId is expected.
  • Use a small multi-field value as a Dictionary key, where equality must mean the same field values.

More examples

A struct with fast, explicit equality

The built-in struct equality works but goes through reflection on every call. Implementing IEquatable<T> gives the compiler a direct method to call, and overriding GetHashCode makes the type safe to use as a Dictionary key or in a HashSet. Write this whenever a struct is compared or hashed in a loop. Note that == and != are a matched pair: declaring one without the other is a compiler error.

Example · csharp
Money a = new Money(1250);
Money b = new Money(1250);

Console.WriteLine(a);
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));

// Two equal Money values collapse to one entry.
HashSet<Money> seen = [a, b];
Console.WriteLine(seen.Count);

readonly struct Money : IEquatable<Money>
{
    private readonly int _cents;

    public Money(int cents)
    {
        _cents = cents;
    }

    public bool Equals(Money other) => _cents == other._cents;

    public override bool Equals(object? obj) => obj is Money other && Equals(other);

    public override int GetHashCode() => _cents;

    // Operators must live in a type; they cannot be written at the top level.
    public static bool operator ==(Money left, Money right) => left.Equals(right);

    public static bool operator !=(Money left, Money right) => !left.Equals(right);

    public override string ToString() => $"{_cents / 100}.{_cents % 100:D2}";
}

// Output:
// 12.50
// True
// True
// 1

Struct copy versus class reference, side by side

The first four lines are the whole difference between a value type and a reference type in one screen. The last part is the trap: default(Measured) and the elements of new Measured[10] are produced by zeroing memory, so your parameterless constructor never runs for them. Read this example when a struct field is unexpectedly null or zero.

Example · csharp
// A struct is copied on assignment.
Counter counter = new Counter();
Counter copy = counter;
copy.Value = 5;
Console.WriteLine(counter.Value);

// A class is not. Both variables name the same object.
Box box = new Box();
Box alias = box;
alias.Value = 5;
Console.WriteLine(box.Value);

// A parameterless constructor runs for new, but default ignores it.
Console.WriteLine(new Measured().Label);
Console.WriteLine(default(Measured).Label ?? "null");

struct Counter
{
    public int Value;
}

class Box
{
    public int Value;
}

struct Measured
{
    // Allowed since C# 10. It must be public.
    public Measured()
    {
        Label = "set by constructor";
    }

    public string? Label { get; init; }
}

// Output:
// 0
// 5
// set by constructor
// null

Wrapping an int so it cannot be mixed up

Both identifiers are really just an int, so a method taking int accepts either one and a swapped pair of arguments compiles happily. Giving each its own struct makes that mistake a compiler error, and because a struct holds its data inline there is no object allocated per identifier. Reach for this on any codebase with several kinds of numeric id flowing through the same methods.

Example · csharp
OrderId order = new OrderId(17);
CustomerId customer = new CustomerId(17);

Console.WriteLine(order);
Console.WriteLine(customer);

// Ship(customer) would not compile: the types are different.
Ship(order);

static void Ship(OrderId id)
{
    Console.WriteLine($"shipping {id}");
}

readonly struct OrderId
{
    public OrderId(int value)
    {
        Value = value;
    }

    public int Value { get; }

    public override string ToString() => $"order/{Value}";
}

readonly struct CustomerId
{
    public CustomerId(int value)
    {
        Value = value;
    }

    public int Value { get; }

    public override string ToString() => $"customer/{Value}";
}

// Output:
// order/17
// customer/17
// shipping order/17

Discussion

  • Be the first to comment on this lesson.