using System;
using System.Collections.Generic;
struct PointS
{
public int X, Y;
public override string ToString() => $"({X},{Y})";
}
class PointC
{
public int X, Y;
public override string ToString() => $"({X},{Y})";
}
class Program
{
static void MoveFirstWrong(PointS[] arr)
{
// BUG: copies the struct, mutates the copy, never writes it back
var p = arr[0]; // value copy
p.X += 10;
p.Y += 10;
// arr[0] not updated -> no effect on array storage
}
static void MoveFirstRight(PointS[] arr)
{
// FIX: mutate a local copy and assign it back
var p = arr[0]; // value copy
p.X += 10;
p.Y += 10;
arr[0] = p; // write back
}
static void MoveFirstRef(PointC[] arr)
{
// Works directly because arr[0] is a reference to the same object
arr[0].X += 10;
arr[0].Y += 10;
}
// Optional: foreach pitfall with structs in a List<T>
static void IncrementAllWrong(List<PointS> list)
{
foreach (var item in list)
{
var tmp = item; // copy
tmp.X++; tmp.Y++; // mutate the copy only
// not assigned back -> list unchanged
}
}
static void IncrementAllRight(List<PointS> list)
{
for (int i = 0; i < list.Count; i++)
{
var tmp = list[i];
tmp.X++; tmp.Y++;
list[i] = tmp; // write back
}
}
static void Main()
{
// --- Struct array demo ---
var s = new[] { new PointS { X = 1, Y = 1 } };
Console.WriteLine($"Struct before: {s[0]}"); // (1,1)
MoveFirstWrong(s);
Console.WriteLine($"After Wrong : {s[0]}"); // still (1,1) <-- bug observed
MoveFirstRight(s);
Console.WriteLine($"After Right : {s[0]}"); // (11,11) <-- fixed
// --- Class array demo ---
var c = new[] { new PointC { X = 1, Y = 1 } };
Console.WriteLine($"\nClass before : {c[0]}"); // (1,1)
MoveFirstRef(c);
Console.WriteLine($"After Ref : {c[0]}"); // (11,11) <-- same object mutated
// --- Optional List<T> pitfall ---
var list = new List<PointS> { new PointS { X = 5, Y = 5 } };
Console.WriteLine($"\nList before : {list[0]}"); // (5,5)
IncrementAllWrong(list);
Console.WriteLine($"After Wrong : {list[0]}"); // (5,5) <-- unchanged
IncrementAllRight(list);
Console.WriteLine($"After Right : {list[0]}"); // (6,6) <-- changed
}
}
Output (SDK 10.0.302, identical in Debug and Release):
Struct before: (1,1) After Wrong : (1,1) After Right : (11,11) Class before : (1,1) After Ref : (11,11) List before : (5,5) After Wrong : (5,5) After Right : (6,6)
The line that matters is the second one. After Wrong : (1,1) is the whole exercise: MoveFirstWrong ran, it did modify a PointS, and the array is untouched. No exception, no compiler warning, no clue at the call site. arr[0] on a value-type array returns a copy, so p.X += 10 updated a local that then went out of scope. The class version three lines down does the same thing and works, because arr[0] there returns the reference, and .X += 10 reaches the one object both the array and the local point at.
The List<PointS> pair repeats the trap in the shape you are most likely to write it by accident: a foreach over a collection of structs hands you a copy per iteration, and the copy is where your change lands. Note that C# will not let you write the shorter version of that bug. item.X++ inside the foreach is a compile error (CS1654), and list[0].X++ is another (CS1612), because the indexer returns a value. The only version that compiles is the one that goes quiet, which is why this is worth practising once.