Understanding C# Struct Value Type Behavior
c# struct value type behavior: Learn how C# structs copy on assignment and method calls, where mutation goes wrong, and when value type behavior matters for performance.
When you assign a struct to another variable or pass it to a method, the entire value is copied. This is the defining characteristic of C# struct value type behavior, and it has consequences that often surprise developers coming from reference type habits. A struct is not a reference to an object on the heap; it is the data itself, stored inline wherever the variable lives.
Consider the following minimal example:
public struct Point { public int X; public int Y; } Point a = new Point { X = 1, Y = 2 }; Point b = a; b.X = 10; Console.WriteLine(a.X); // 1 Console.WriteLine(b.X); // 10
Because b receives a copy of a, changing b.X does not affect a. This is the core of struct value type behavior: each variable holds its own independent copy.
How Structs Are Copied in Assignment and Method Calls
Copying happens in two common scenarios: assignment and argument passing. When you assign one struct variable to another, the runtime copies every field. The same occurs when you pass a struct to a method by value. Inside the method, any modification to the parameter changes only the local copy, not the caller's variable.
public static void Move(Point p) { p.X += 5; } Point original = new Point { X = 1, Y = 2 }; Move(original); Console.WriteLine(original.X); // still 1
If you need the method to modify the original struct, you must pass it by reference using the ref modifier:
public static void Move(ref Point p) { p.X += 5; } Point original = new Point { X = 1, Y = 2 }; Move(ref original); Console.WriteLine(original.X); // now 6
This behavior is intentional. It gives you predictable, isolated data handling without the overhead of heap allocation and garbage collection.
Mutation and the Pitfall of Modifying Structs in Collections
A common mistake is assuming that modifying a struct stored in a collection works the same as modifying a class instance. With a class, you can retrieve an element, change a property, and the change persists because the collection holds a reference. With a struct, the collection returns a copy, so direct mutation is impossible.
List<Point> points = new List<Point> { new Point { X = 1, Y = 2 } }; points[0].X = 10; // This does not compile for a struct
The compiler prevents this because points[0] is a value, not a variable. To modify a struct in a list, you must replace the entire element:
Point p = points[0]; p.X = 10; points[0] = p;
This copy-modify-write pattern is verbose but necessary. The same limitation applies to arrays and other indexable collections. Understanding this behavior prevents subtle bugs where you think you changed a struct but the original remains untouched.
Structs as Parameters and Return Values
When a method returns a struct, the caller receives a copy. This is efficient for small structs because the data is returned in a CPU register or on the stack, avoiding heap allocation. However, large structs can cause performance problems because copying many fields takes time.
A struct with many fields or containing reference type members is not necessarily cheap to copy. The runtime copies all fields, including references, but the referenced objects themselves are not duplicated. The copy cost is proportional to the struct's total size.
For example, a struct containing a string field copies the reference to the same string, not the string's contents. This distinction matters when you design data structures for high-frequency operations.
Performance Implications of Value Type Copying
C# struct value type behavior directly affects performance. Because structs are copied by value, they avoid heap allocation and reduce pressure on the garbage collector. This is why small, immutable data types like DateTime, TimeSpan, and KeyValuePair are implemented as structs.
But copying has a cost. Passing a large struct by value to a method copies all its fields on every call. If the method is called in a tight loop, the overhead can exceed the benefit of avoiding heap allocation. In such cases, passing by ref or in can eliminate the copy:
public static double Distance(in Point a, in Point b) { int dx = a.X - b.X; int dy = a.Y - b.Y; return Math.Sqrt(dx * dx + dy * dy); }
The in parameter passes a read-only reference, avoiding a copy while still preventing modification. This is a good compromise for large structs that should not be mutated.
Another performance consideration is the use of structs in collections. An array of structs stores the data contiguously, which improves cache locality. A List<T> also stores structs inline, but operations like Add may cause reallocation and copying of existing elements. The tradeoff between memory efficiency and copy overhead should guide your choice.
When to Choose Struct Over Class
The decision between struct and class depends on the semantics you need. Use a struct when all of the following are true:
- The type represents a single, small value (typically 16 bytes or less).
- The type is logically immutable, or you are comfortable with copy semantics.
- You do not need inheritance or polymorphism.
- You want to avoid heap allocation for performance-critical paths.
Conversely, use a class when the type has identity, is large, or needs to be shared and mutated across multiple references. A class gives you reference semantics: assigning one variable to another makes them point to the same object.
The following table summarizes the key differences:
| Aspect | Struct | Class |
|---|---|---|
| Allocation | Stack or inline | Heap |
| Copy behavior | Copies entire value | Copies reference |
| Default assignment | Independent copy | Shared reference |
| Inheritance | No inheritance | Supports inheritance |
| Nullability | Non-nullable by default | Nullable reference |
| Use case | Small, immutable values | Larger, mutable objects |
Choosing a struct when you need reference semantics leads to confusing bugs. Choosing a class when you need value semantics forces you to manually clone objects. The correct choice depends on the behavior you want, not just on size.
Common Misconceptions About Structs and Reference Types
One misconception is that structs are always faster than classes. While they avoid heap allocation, copying large structs can be slower than dereferencing a pointer. Another is that structs cannot have methods or properties; they can, and they often do. The difference is in how they are stored and passed.
A more subtle issue is that structs with mutable fields can lead to unexpected behavior when used as dictionary keys. If you modify a struct after it has been inserted into a Dictionary, the hash code changes and the key becomes unfindable. This is why structs used as keys should be immutable.
public struct MutableKey { public int Id; public string Name; } var dict = new Dictionary<MutableKey, string>(); var key = new MutableKey { Id = 1, Name = "a" }; dict.Add(key, "value"); key.Name = "b"; // Hash code changes, key is lost
This behavior is a direct consequence of value type copying: the dictionary stores a copy of the key, and mutating the original does not update that copy. Always design structs used as keys to be immutable.
Another common misunderstanding is that readonly structs are always immutable. The readonly modifier prevents modification of fields after construction, but it does not prevent copying. A readonly struct is still copied on assignment, and its fields cannot be changed. This is a good practice for value types that should never change.
Understanding C# struct value type behavior is not just about syntax; it is about predicting how data moves through your program. Copying, mutation, and allocation all follow from the value type model. Once you internalize that a struct is a value, not a reference, most of the surprises disappear.