Structs
A value type you define: the variable holds the data, assignment copies it, and nothing can inherit from it.
public 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
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)
// TrueWhen 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.
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
// 1Struct 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.
// 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
// nullWrapping 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.
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