Understanding C# Struct Value Type Behavior
See how C# structs copy on assignment and method calls, where mutation of structs in collections can fail, 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, a collection may return a copy, so direct mutation is not always possible.
List<Point> points = new List<Point> { new Point { X = 1, Y = 2 } }; points[0].X = 10; // CS1612: cannot modify the return value because it is not a variable
The compiler prevents this for List<T> because the indexer returns a temporary copy. To modify a struct in a List<T>, you must replace the entire element:
Point p = points[0]; p.X = 10; points[0] = p;
Arrays are different: pointsArray[0].X = 10 is allowed because an array element is a storage location. Many other collections, such as Dictionary, also return copies through their indexers, so use the copy-modify-write pattern or a replacement method. Understanding this behavior prevents subtle bugs where you think you changed a struct but the original remains untouched.
Structs as Parameters and Return Values
By default, when a method returns a struct, the caller receives a copy. A method declared with a ref or ref readonly return is the exception. Small structs can be returned without a 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 value types, they are normally stored inline or on the stack and do not require a separate heap allocation unless boxed, which can 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 a mutable struct is risky as a dictionary key. When TKey is a struct, Dictionary<TKey,TValue> stores the key by value, so the struct you pass to Add is copied. If the hash code depends on mutable state, a lookup key can change after you have inserted it, and it may no longer match the stored copy.
public struct MutableKey { public int Id; public string Name; public override int GetHashCode() => unchecked(Id * 397) ^ (Name?.GetHashCode() ?? 0); } var dict = new Dictionary<MutableKey, string>(); var key = new MutableKey { Id = 1, Name = "a" }; dict.Add(key, "value"); key.Name = "b"; Console.WriteLine(dict.ContainsKey(key)); // false
The dictionary still holds the copy from when Add was called, but the key variable used for lookup now has a different hash, so ContainsKey misses the entry. This is why structs used as keys should be immutable.
Another common misunderstanding is that marking a struct readonly changes how copies behave. It does not: a readonly struct is still copied on assignment. What readonly does is enforce that instance fields cannot be modified after construction. It does not make referenced objects immutable, but it 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.