A struct is a value type you define yourself. It looks like a class, with fields, properties, methods and constructors, but a variable of a struct type contains the data directly instead of a reference to an object. That one difference changes how assignment, method calls, equality and null behave.
Value semantics: assignment copies
The built-in numeric types, bool, char, DateTime, TimeSpan and Guid are all structs. Your own work the same way:
Output:
struct: s1.X = 1, s2.X = 50
class: c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150
With the struct, s2 is an independent copy, and Move works on its own copy too. To let a method change the caller's struct, pass it by reference with ref (see ref and out). To avoid copying a large struct without allowing changes, C# 7.2 added in parameters.
Other consequences of being a value type:
- A struct variable can never be
null.PointS p = null;does not compile. UsePointS?(a nullable value type) when "no value" is meaningful. - An uninitialized struct is all zeros: numeric fields
0,boolfieldsfalse, reference fieldsnull. - A struct cannot inherit from another struct or class, and nothing can inherit from a struct. It can implement interfaces.
- Local struct variables usually live on the stack or inside the containing object, so creating one does not allocate on the heap.
The List of structs trap: CS1612
Because reading a struct produces a copy, this innocent-looking line does not compile:
var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable
The List<T> indexer is a method that returns a copy of the element. Setting X on that temporary copy would change nothing, so the compiler stops you. The same error appears when a property returns a struct: order.Location.X = 10. Copy, modify, write back:
Output:
35
7
Arrays are the exception: array[0] is the element itself, not a copy. The recurring pain here is the reason for the standard advice to make structs immutable: if a struct cannot be modified, there is nothing to lose by modifying a copy.
Constructors and default values
In C# 7 through 9, the rules for struct constructors are strict:
- You cannot declare a parameterless constructor.
new PointS()always exists and zeroes every field. - A constructor you do declare must assign every field (and auto-property) before it returns.
- Field initializers (
public int X = 1;) are not allowed on instance fields.
Output:
19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)
Money also shows why the default value matters: a zeroed Money has a null currency, and your code must handle it, because arrays, default(T) and uninitialized fields all produce one.
Newer versions relaxed these rules:
// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
public int Retries = 3;
public Settings() { }
}
// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.
A C# 10 parameterless constructor runs for new Settings() but not for default(Settings) or array elements, which are still all zeros. That split surprises people, so use it carefully.
readonly struct (C# 7.2)
Marking the struct itself readonly makes the compiler enforce immutability: every field must be readonly and every auto-property get-only.
public readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta); // returns a new value
}
Besides documenting intent, it helps performance: when a non-readonly struct is stored in a readonly field or passed as an in parameter, the compiler copies it before every method call (it cannot know the method will not modify it). A readonly struct needs no such defensive copies. On C# 7.0 you can still make every field readonly, as Money does with get-only properties: that gives you the immutability, but not the saved copies, because the compiler only trusts a struct declared readonly.
Equality
Equals on a struct compares field by field by default, which is the value behavior you want. But the default implementation (ValueType.Equals) may use reflection and is slow, and the == operator is not defined at all: a == b on your own struct is error CS0019. Implement both when the struct will be compared:
Output:
True
True
True
Implementing IEquatable<T> matters for collections: HashSet<T>, Dictionary<TKey, TValue> and List<T>.Contains call Equals(GridCell) directly instead of boxing each value to call Equals(object). Record structs (below) generate all of this for you.
Boxing
Converting a struct to object or to an interface type boxes it: the runtime copies the value into a new heap object. The box and the original are then independent:
Output:
2
0
Boxing costs an allocation each time, which is why old non-generic collections like ArrayList were slow with value types and why generics replaced them. It also means a mutable struct accessed through an interface is changed in its box, not in the original, which is one more reason to keep structs immutable.
Struct vs class
| struct | class | |
|---|---|---|
| Kind | value type | reference type |
| Assignment and parameters | copy the data | copy the reference |
Can be null | no (T? can) | yes |
| Default value | all fields zeroed | null |
| Inheritance | none; can implement interfaces | single base class, interfaces |
== | not defined unless you overload it | reference equality unless overloaded |
Equals default | compares fields | compares references |
| Allocation | inline (stack or containing object) | heap, garbage collected |
| Good for | small immutable values | entities, large or shared state |
When to use a struct
Choose a struct when all of these hold: the type is one logical value (a coordinate, a money amount, a color, a range of dates), it is small (Microsoft's guideline is about 16 bytes, roughly four ints), it is immutable, and you do not need inheritance. Structs pay off when you create very many of them, for example millions of points in an array, because they avoid a heap allocation and a garbage collector entry per item.
Choose a class for anything with identity (a customer, an order), anything large, anything mutated from several places, and whenever you are unsure. A large mutable struct gives you the costs of copying plus the confusion of modifying copies.
record struct (C# 10)
C# 10 added record struct, which generates value equality, ==, ToString and with support for a struct in one line:
public readonly record struct Coordinate(double Lat, double Lng);
var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same); // True
Console.WriteLine(home); // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };
A plain record struct has mutable positional properties; readonly record struct makes them init-only, which is usually what you want. The records page covers the class version and shows the members the compiler writes.
Common mistakes
- Modifying a struct through a copy.
list[0].X = 1(CS1612), aforeachvariable (CS1654), a property getter's result. Write the modified copy back, or make the struct immutable. - Large structs. Every assignment and call copies all of it. Past a few fields, a class is usually faster.
- Forgetting the zeroed default. Arrays and
default(T)create struct values without running your constructor, so fields you validate there can still be zero ornull. - Comparing with
==without defining it. CS0019. ImplementIEquatable<T>and the operators, or use a record struct. - Mutable structs behind interfaces. Boxing means the change happens to a copy you are not looking at.
Frequently Asked Questions
What is a struct in C#?
A struct is a user-defined value type: struct Point { public int X; public int Y; }. A variable of a struct type holds the data itself rather than a reference to an object, so assignment and passing to a method copy the whole value. int, double, DateTime and Guid are all structs.
What is the difference between a struct and a class in C#?
A class is a reference type: variables share one object, and null is allowed. A struct is a value type: each variable has its own copy, it cannot be null (unless you use Point?), it cannot inherit or be inherited from, and its default value is all fields zeroed. Structs suit small, immutable values; classes suit entities with identity and behavior.
When should I use a struct instead of a class in C#?
When the type represents a single small value (a coordinate, a money amount, a date range), is ideally immutable, is compared by its contents, and is created in large numbers where avoiding heap allocations matters. Microsoft's guidance adds a size of roughly 16 bytes or less. If the type has identity, many fields, or is mutated through references, use a class.
Why do I get "Cannot modify the return value because it is not a variable"?
That is error CS1612, typically from list[0].X = 5 on a List<Point> of structs. The indexer returns a copy of the struct, so changing the copy would be lost, and the compiler refuses. Copy the element into a variable, change it, and assign it back: var p = list[0]; p.X = 5; list[0] = p;. Arrays do not have this problem because array[0] refers to the element itself.
What is a readonly struct in C#?
readonly struct (C# 7.2) declares that no member of the struct modifies its state: all fields must be readonly and auto-properties get-only. The compiler enforces it, and it lets the compiler avoid defensive copies when the struct is passed with in or stored in a readonly field. Most structs should be readonly.