C# Struct Copy Behavior: Value Type Semantics Explained
c# struct copy behavior: Understand how C# structs are copied by value, why it matters for performance and correctness, and how to control copy behavior with ref, read...
In C#, structs are value types, which means that the runtime copies the entire struct when you assign it to another variable, pass it to a method, or return it from a method. Understanding c# struct copy behavior is essential for writing performant and correct code, especially when working with large data structures or collections. This article explains exactly when copying occurs, the implications for memory and performance, and how to control copy behavior with ref, readonly, and in parameters.
What Happens When You Assign a Struct to Another Variable
Consider a simple struct definition:
public struct Point { public int X; public int Y; }
When you assign one Point variable to another, the runtime copies the values of all fields from the source to the destination. The two variables now refer to independent copies of the same data. Modifying one does not affect the other.
Point a = new Point { X = 1, Y = 2 }; Point b = a; b.X = 10; Console.WriteLine(a.X); // Output: 1 Console.WriteLine(b.X); // Output: 10
This behavior is fundamentally different from reference types like classes, where assignment copies only the reference, and both variables point to the same object. For structs, the copy is shallow: if the struct contains reference-type fields, those references are copied, but the underlying objects are not duplicated.
Passing Structs to Methods: By Value vs. By Ref
When you pass a struct to a method without a modifier, the method receives a copy. Any changes made to the parameter inside the method do not affect the original variable.
public void ModifyPoint(Point p) { p.X = 100; } Point original = new Point { X = 1, Y = 2 }; ModifyPoint(original); Console.WriteLine(original.X); // Still 1
To allow the method to modify the original struct, pass it by reference using the ref keyword:
public void ModifyPoint(ref Point p) { p.X = 100; } ModifyPoint(ref original); Console.WriteLine(original.X); // Now 100
The out keyword works similarly for output-only parameters, and in passes a read-only reference, which we'll discuss later. The choice between passing by value and by reference directly affects both correctness and performance.
Struct Copy Semantics with Arrays and Collections
When you store structs in an array or a List<T>, the collection holds the actual struct values. Accessing an element by index returns a copy, not a reference. For example:
Point[] points = new Point[2]; points[0] = new Point { X = 1, Y = 2 }; Point first = points[0]; first.X = 100; Console.WriteLine(points[0].X); // Still 1
To modify an element in place, you must use the indexer directly or use ref returns, which we'll cover later. This copy-on-access behavior is a common source of confusion for developers coming from reference-type backgrounds.
Performance Implications of Struct Copying
Copying a struct involves copying all its fields. For small structs like Point, this is cheap. However, for larger structs with many fields or containing arrays, the copy cost can become significant, especially in hot paths or when copying happens repeatedly.
Consider a struct that holds a large buffer:
public struct LargeBuffer { public byte[] Data; // Reference field, but the struct itself is large if it has many value fields public int Size; // ... other fields }
Even if the struct contains reference fields, the copy operation copies the reference itself, not the referenced object. The actual cost depends on the number of value-type fields and the size of those fields. When performance matters, you can avoid unnecessary copies by using ref parameters, ref returns, or by designing structs to be small and immutable.
Readonly Structs and In Parameters
The readonly modifier on a struct declares that its fields are read-only, which prevents accidental modification of a copy. More importantly, when you pass a readonly struct as an in parameter, the runtime can pass a reference instead of a copy, avoiding the copy overhead while still preventing modification.
public readonly struct ImmutablePoint { public int X { get; } public int Y { get; } public ImmutablePoint(int x, int y) { X = x; Y = y; } } public double DistanceFromOrigin(in ImmutablePoint p) { return Math.Sqrt(p.X * p.X + p.Y * p.Y); }
The in parameter is a read-only reference. It avoids copying the struct into the method, which is beneficial for large structs. However, the compiler may still make a defensive copy if the struct is not marked readonly and the method could potentially modify it. Marking the struct as readonly guarantees that no defensive copy is needed.
Using Ref Returns to Avoid Copies
When you need to return a struct from a method or access an element in a collection without copying, you can use ref returns. This is particularly useful when working with large structs stored in arrays.
private Point[] _points; public ref Point GetPoint(int index) { return ref _points[index]; } // Usage ref Point p = ref GetPoint(0); p.X = 42; // Modifies the array element directly
This technique avoids copying the entire struct on return and allows the caller to modify the original data. It should be used carefully, as it exposes internal storage and can lead to aliasing issues if not managed properly.
Common Pitfalls and Misconceptions
One common mistake is assuming that assigning a struct to a new variable creates a reference. This leads to code that expects modifications to propagate but finds that they don't. Another pitfall is modifying a struct returned from a property or indexer, which often results in a compiler error because the return value is a copy and modifications would be lost.
For example:
points[0].X = 100; // This works because the indexer returns a ref? Actually, for arrays, it does.
But for List<T>, the indexer returns a copy, so the following fails:
List<Point> pointList = new List<Point>(); pointList.Add(new Point()); pointList[0].X = 100; // Error: Cannot modify the return value because it is not a variable
To modify a struct in a List<T>, you must assign a new struct to the index:
Point p = pointList[0]; p.X = 100; pointList[0] = p;
Understanding these nuances is critical for avoiding subtle bugs.
Choosing Between Struct and Class Based on Copy Behavior
The copy semantics of structs influence whether you should use a struct or a class. Structs are appropriate when the value is small, logically immutable, and represents a single value, such as a coordinate, a color, or a range. Classes are better when identity matters, when the data is large, or when you need reference semantics.
| Criterion | Struct | Class |
|---|---|---|
| Copy behavior | Copied by value | Reference copied |
| Memory allocation | Typically on stack or inline | Heap allocated |
| Performance for large data | Copy cost can be high | Reference copy is cheap |
| Identity | No identity; two copies are equal | Each instance has identity |
| Typical use | Small, immutable values | Larger, mutable objects |
If you need to avoid copying large structs, you can use ref and in modifiers, but this adds complexity. In many cases, a class is simpler and more maintainable for large data structures.
When Copy Behavior Causes Unexpected Aliasing
Even though structs are copied by value, they can contain reference-type fields. When you copy a struct, the reference fields are copied, meaning the two structs point to the same underlying object. This can lead to unexpected sharing of mutable state.
public struct Container { public List<int> Items; } Container c1 = new Container { Items = new List<int> { 1, 2, 3 } }; Container c2 = c1; c2.Items.Add(4); Console.WriteLine(c1.Items.Count); // 4, because both share the same List
This is a subtle aspect of struct copy behavior. If you intend for structs to be fully independent, you must implement deep copying or design the struct to only contain value types.
Final Technical Consideration: Defensive Copies and the in Modifier
When you pass a struct by in, the compiler may create a defensive copy if the struct is not marked readonly. This happens because the method could potentially call a method on the struct that modifies its state, and the compiler must ensure the original is not changed. This defensive copy negates the performance benefit of in. To avoid it, always mark structs that you plan to pass by in as readonly.
public struct MutablePoint { public int X; public int Y; } public void Check(in MutablePoint p) { // The compiler may copy p here to prevent modification // if Check calls a method that could modify p. }
By understanding when defensive copies occur, you can make informed decisions about struct design and parameter passing, ensuring that your code is both correct and efficient.