C# Structs — Value Types, Readonly Struct, Ref Struct, and Record Struct
In this tutorial, you will learn about C# Structs. We cover key concepts, practical examples, and best practices to help you master this topic.
C# structs are value types that provide stack-based allocation and value semantics, with readonly structs for immutability, ref structs for stack-only constraints, and record structs combining value semantics with record features.
What You'll Learn
You will master structs in C#: the difference between structs and classes, when to use value types for performance, readonly structs for immutability, ref structs for stack-only allocation, record structs for immutable value data, and best practices for struct design in .NET.
Why It Matters
Structs are essential for performance-critical code. They avoid heap allocation and Garbage Collection pressure. The .NET runtime and core libraries use structs extensively: int, double, DateTime, TimeSpan, Guid, decimal, and Span<T> are all structs. Choosing structs over classes in the right scenarios can dramatically reduce memory allocation and improve cache locality.
Real-World Use
Game engines use structs for Vector3, Quaternion, and Matrix4x4 to avoid heap allocation. High-frequency trading systems use structs for order book entries. Span
Learning Path
graph LR
A["15: Records"] --> B["16: Structs"]
B --> C["17: Strings"]
C --> D["18: Arrays & Collections"]
D --> E["19: Generics"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Struct
public struct Point
{
public double X;
public double Y;
public Point(double x, double y)
{
X = x;
Y = y;
}
public double DistanceTo(Point other)
{
double dx = X - other.X;
double dy = Y - other.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
var p1 = new Point(3, 4);
var p2 = new Point(0, 0);
Console.WriteLine($"Distance: {p1.DistanceTo(p2):F2}"); // 5.00
Struct vs Class Memory Behavior
public struct PointStruct
{
public int X;
public int Y;
}
public class PointClass
{
public int X;
public int Y;
}
// Struct: copy semantics
var ps1 = new PointStruct { X = 10, Y = 20 };
var ps2 = ps1; // Creates a copy
ps2.X = 99;
Console.WriteLine($"ps1: ({ps1.X}, {ps1.Y})"); // (10, 20)
// Class: reference semantics
var pc1 = new PointClass { X = 10, Y = 20 };
var pc2 = pc1; // Same object
pc2.X = 99;
Console.WriteLine($"pc1: ({pc1.X}, {pc1.Y})"); // (99, 20)
Readonly Struct
Prevents modification of fields after construction:
public readonly struct Vector3
{
public readonly float X;
public readonly float Y;
public readonly float Z;
public Vector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
// Methods must be readonly too
public readonly float Magnitude() =>
MathF.Sqrt(X * X + Y * Y + Z * Z);
public readonly Vector3 Normalized()
{
var mag = Magnitude();
return mag > 0 ? new(X / mag, Y / mag, Z / mag) : this;
}
}
var v = new Vector3(3, 4, 0);
Console.WriteLine($"Magnitude: {v.Magnitude()}"); // 5
Ref Struct
Ref structs are stack-only: they cannot be boxed, assigned to object, or used as a field of a class:
public ref struct SpanWrapper
{
private readonly Span<byte> _data;
public SpanWrapper(Span<byte> data)
{
_data = data;
}
public byte this[int index]
{
get => _data[index];
set => _data[index] = value;
}
public int Length => _data.Length;
}
// Usage
Span<byte> buffer = stackalloc byte[256];
var wrapper = new SpanWrapper(buffer);
wrapper[0] = 42;
Console.WriteLine(wrapper[0]); // 42
Record Struct (C# 10)
Combines value-type semantics with record features:
public readonly record struct Color(byte R, byte G, byte B, byte A = 255);
// Usage
var red = new Color(255, 0, 0);
var halfOpacity = red with { A = 128 };
Console.WriteLine(red); // Color { R = 255, G = 0, B = 0, A = 255 }
Console.WriteLine(halfOpacity); // Color { R = 255, G = 0, B = 0, A = 128 }
// Value equality
var red2 = new Color(255, 0, 0);
Console.WriteLine(red == red2); // True
When to Use Struct
Microsoft recommends using struct when:
- The type represents a single value (like Point, Color, Complex)
- The instance size is 16 bytes or less (fewer than 8 bytes ideal)
- The type is immutable
- The type will not be boxed frequently
- Short-lived instances in arrays are common
// Good candidate: small, immutable, value-like
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Currency mismatch");
return new Money(a.Amount + b.Amount, a.Currency);
}
}
Struct Constructors and Initialization
public struct Config
{
public int Timeout;
public string? ServerUrl;
public bool IsEnabled;
// Parameterized constructor
public Config(int timeout, string? serverUrl, bool isEnabled)
{
Timeout = timeout;
ServerUrl = serverUrl;
IsEnabled = isEnabled;
}
}
// Structs have an implicit parameterless constructor that zero-initializes
var defaultConfig = default(Config);
Console.WriteLine(defaultConfig.Timeout); // 0
Console.WriteLine(defaultConfig.ServerUrl); // (null)
// Usage
var config = new Config(30, "https://api.example.com", true);
Performance: Struct Array vs Class Array
public struct SmallStruct { public int X; public int Y; }
public class SmallClass { public int X; public int Y; }
// Struct array: contiguous memory, single allocation
var structs = new SmallStruct[1000];
// Class array: 1000 separate heap objects + array allocation
var classes = new SmallClass[1000];
for (int i = 0; i < 1000; i++) classes[i] = new SmallClass();
Struct arrays are significantly faster for iteration due to cache locality and reduced indirection.
Struct Limitations
public struct LimitedStruct
{
// Cannot have:
// - Parameterless constructor with field initializers (C# 10 allows default values)
// - Virtual or abstract members
// - Finalizer (destructor)
// - Inheritance (structs cannot inherit from other structs)
// - Default constructor without field initialization (before C# 10)
public int Id { get; set; }
}
Common Mistakes
Mistake 1: Making Structs Too Large
Structs over 16-24 bytes should generally be classes. Large structs cause excessive copying when passed to methods or stored in collections.
Mistake 2: Mutable Structs (Anti-Pattern)
Mutable structs cause subtle bugs. When stored in collections or passed as parameters, modifications apply to copies. Always prefer readonly struct with readonly fields.
Mistake 3: Boxing Structs Frequently
Passing structs to methods expecting object or IEnumerable causes boxing (heap allocation). Use generics to avoid this.
Mistake 4: Using Ref Struct As a Generic Parameter
Ref structs cannot be used as type arguments: List<SpanWrapper> does not compile. This is by design to prevent them from escaping the stack.
Mistake 5: Forgetting That Default Construction Zero-Initializes
new MyStruct() or default(MyStruct) zero-initializes all fields. Reference type fields become null. Accessing them without checking causes null reference exceptions.
Mistake 6: Assuming Struct Methods Cannot Modify this
In non-readonly structs, methods can modify fields. In readonly structs, methods must be marked readonly or they cannot modify state.
Practice Questions
- What are the main differences between a struct and a class?
- When would you use a
readonly structover a regular struct? - What is the purpose of
ref struct? What are its limitations? - Why should structs generally be small (under 16 bytes)?
- Write a
ComplexNumberstruct with value semantics and arithmetic operators.
Challenge
Create a readonly record struct Matrix2x2 with 4 float fields and methods for determinant, inverse, and multiplication. Compare its memory usage and performance with a class equivalent using BenchmarkDotNet (hypothetical).
FAQ
Mini Project
Create a 2D math library using structs:
public readonly struct Vector2
{
public readonly float X;
public readonly float Y;
public Vector2(float x, float y) => (X, Y) = (x, y);
public float Magnitude => MathF.Sqrt(X * X + Y * Y);
public Vector2 Normalized => Magnitude > 0 ? this / Magnitude : this;
public static Vector2 operator +(Vector2 a, Vector2 b) => new(a.X + b.X, a.Y + b.Y);
public static Vector2 operator -(Vector2 a, Vector2 b) => new(a.X - b.X, a.Y - b.Y);
public static Vector2 operator *(Vector2 v, float s) => new(v.X * s, v.Y * s);
public static Vector2 operator /(Vector2 v, float s) => new(v.X / s, v.Y / s);
public static float Dot(Vector2 a, Vector2 b) => a.X * b.X + a.Y * b.Y;
public override string ToString() => $"({X:F2}, {Y:F2})";
}
public readonly struct LineSegment
{
public readonly Vector2 Start;
public readonly Vector2 End;
public LineSegment(Vector2 start, Vector2 end) => (Start, End) = (start, end);
public float Length => (End - Start).Magnitude;
public Vector2 Midpoint => new((Start.X + End.X) / 2, (Start.Y + End.Y) / 2);
}
var v1 = new Vector2(3, 4);
var v2 = new Vector2(7, 1);
Console.WriteLine($"v1: {v1}, magnitude: {v1.Magnitude:F2}");
Console.WriteLine($"v1 normalized: {v1.Normalized}");
Console.WriteLine($"v1 + v2: {v1 + v2}");
Console.WriteLine($"v1 * 2: {v1 * 2}");
Console.WriteLine($"Dot product: {Vector2.Dot(v1, v2):F2}");
var segment = new LineSegment(v1, v2);
Console.WriteLine($"\nLine segment length: {segment.Length:F2}");
Console.WriteLine($"Midpoint: {segment.Midpoint}");
Expected output:
v1: (3.00, 4.00), magnitude: 5.00
v1 normalized: (0.60, 0.80)
v1 + v2: (10.00, 5.00)
v1 * 2: (6.00, 8.00)
Dot product: 25.00
Line segment length: 5.00
Midpoint: (5.00, 2.50)
What's Next
You have mastered structs in C# including value-type semantics, readonly structs, ref structs, and record structs. The next lesson covers strings: immutability, StringBuilder, interpolation, and verbatim strings.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro