C# Structs
Introduction
In C#, a struct (short for structure) is a value type that can encapsulate data and related functionality. While structs may appear similar to classes at first glance, they have fundamental differences in behavior that make them suitable for specific scenarios. This guide will walk you through everything you need to know about structs in C#, from basic definitions to advanced usage patterns.
What is a Struct?
A struct is a value type that represents a simple data structure. Unlike classes (which are reference types), structs are stored on the stack rather than the heap, making them more efficient for small, short-lived data structures or when you need to avoid the overhead of garbage collection.
Let's define a simple struct:
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString() => $"({X}, {Y})";
}
Structs vs Classes: Key Differences
Before diving deeper into structs, it's essential to understand how they differ from classes:
-
Value Type vs. Reference Type:
- Structs are value types, stored on the stack
- Classes are reference types, stored on the heap
-
Memory Allocation:
- Structs have direct memory allocation
- Classes have reference-based allocation
-
Default Initialization:
- Structs cannot have parameterless constructors but are automatically initialized
- Classes can have parameterless constructors and are initialized to
null
-
Inheritance:
- Structs cannot inherit from other structs or classes (except they implicitly inherit from
System.ValueType
) - Classes support inheritance
- Structs cannot inherit from other structs or classes (except they implicitly inherit from
-
Performance:
- Structs are generally more efficient for small data structures
- Classes are better for larger data structures and when reference semantics are needed
Creating and Using Structs
Basic Declaration and Usage
Here's how to declare and use a simple struct:
using System;
public struct Temperature
{
public double Celsius;
public double Fahrenheit
{
get { return Celsius * 9 / 5 + 32; }
set { Celsius = (value - 32) * 5 / 9; }
}
}
class Program
{
static void Main()
{
// Creating a struct instance
Temperature temp = new Temperature();
temp.Celsius = 25;
Console.WriteLine($"Temperature: {temp.Celsius}°C / {temp.Fahrenheit}°F");
// Modifying through the Fahrenheit property
temp.Fahrenheit = 100;
Console.WriteLine($"Updated temperature: {temp.Celsius:F2}°C / {temp.Fahrenheit}°F");
}
}
Output:
Temperature: 25°C / 77°F
Updated temperature: 37.78°C / 100°F
Struct Constructors
Structs can have constructors, but they must initialize all fields:
public struct Rectangle
{
public double Width;
public double Height;
// Constructor
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}
// Usage
Rectangle rect = new Rectangle(5.0, 3.0);
Console.WriteLine($"Area: {rect.Area}, Perimeter: {rect.Perimeter}");
Output:
Area: 15, Perimeter: 16
Struct Methods and Properties
Structs can contain methods, properties, and other members similar to classes:
public struct ComplexNumber
{
public double Real;
public double Imaginary;
public ComplexNumber(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
// Method to add two complex numbers
public ComplexNumber Add(ComplexNumber other)
{
return new ComplexNumber(
Real + other.Real,
Imaginary + other.Imaginary
);
}
// Property to get magnitude
public double Magnitude => Math.Sqrt(Real * Real + Imaginary * Imaginary);
public override string ToString() => $"{Real} + {Imaginary}i";
}
// Usage
ComplexNumber c1 = new ComplexNumber(3, 4);
ComplexNumber c2 = new ComplexNumber(1, 2);
ComplexNumber result = c1.Add(c2);
Console.WriteLine($"c1: {c1}, Magnitude: {c1.Magnitude}");
Console.WriteLine($"c2: {c2}, Magnitude: {c2.Magnitude}");
Console.WriteLine($"c1 + c2 = {result}");
Output:
c1: 3 + 4i, Magnitude: 5
c2: 1 + 2i, Magnitude: 2.23606797749979
c1 + c2 = 4 + 6i
Value Semantics and Behavior
Copy Behavior
One of the most important aspects of structs is their copy behavior. When you assign a struct to another variable or pass it to a method, a complete copy of the struct is created:
public struct Point3D
{
public int X;
public int Y;
public int Z;
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public override string ToString() => $"({X}, {Y}, {Z})";
}
// Usage demonstrating copy behavior
Point3D original = new Point3D(1, 2, 3);
Point3D copy = original; // Creates a complete copy
// Modifying the copy doesn't affect the original
copy.X = 10;
Console.WriteLine($"Original: {original}"); // (1, 2, 3)
Console.WriteLine($"Copy: {copy}"); // (10, 2, 3)
Output:
Original: (1, 2, 3)
Copy: (10, 2, 3)
Method Parameters
When passing structs to methods, be aware of the copy behavior:
public static void ModifyPoint(Point3D point)
{
// This modifies a copy, not the original
point.X = 100;
point.Y = 200;
point.Z = 300;
Console.WriteLine($"Inside method: {point}");
}
// Usage
Point3D pt = new Point3D(5, 5, 5);
Console.WriteLine($"Before method call: {pt}");
ModifyPoint(pt);
Console.WriteLine($"After method call: {pt}"); // Unchanged!
Output:
Before method call: (5, 5, 5)
Inside method: (100, 200, 300)
After method call: (5, 5, 5)
To modify the original struct, use the ref
keyword:
public static void ModifyPointByRef(ref Point3D point)
{
// This modifies the original
point.X = 100;
point.Y = 200;
point.Z = 300;
}
// Usage
Point3D pt = new Point3D(5, 5, 5);
Console.WriteLine($"Before method call: {pt}");
ModifyPointByRef(ref pt);
Console.WriteLine($"After method call: {pt}"); // Changed!
Output:
Before method call: (5, 5, 5)
After method call: (100, 200, 300)
When to Use Structs
Structs are best used in the following scenarios:
-
Small Data Structures: When you have a small data structure (typically less than 16 bytes).
-
Short-Lived Objects: For objects that are created and discarded frequently.
-
Value Semantics: When you need true value semantics (copying instead of referencing).
-
Immutable Design: When designing immutable data types.
-
Performance-Critical Code: For performance-critical scenarios where heap allocations should be minimized.
Practical Examples
Example 1: Game Development - Vector2D
public struct Vector2D
{
public float X;
public float Y;
public Vector2D(float x, float y)
{
X = x;
Y = y;
}
// Vector addition
public static Vector2D operator +(Vector2D a, Vector2D b) =>
new Vector2D(a.X + b.X, a.Y + b.Y);
// Vector multiplication by scalar
public static Vector2D operator *(Vector2D v, float scalar) =>
new Vector2D(v.X * scalar, v.Y * scalar);
// Vector length
public float Magnitude => (float)Math.Sqrt(X * X + Y * Y);
// Normalize the vector
public Vector2D Normalize()
{
float mag = Magnitude;
if (mag > 0)
{
return new Vector2D(X / mag, Y / mag);
}
return this;
}
public override string ToString() => $"({X}, {Y})";
}
// Usage in a game context
Vector2D playerPosition = new Vector2D(10, 5);
Vector2D movement = new Vector2D(1.5f, -0.5f);
// Update player position
playerPosition += movement;
Console.WriteLine($"New player position: {playerPosition}");
// Apply velocity vector
Vector2D velocity = new Vector2D(0.5f, 0.7f);
playerPosition += velocity * 2; // Move for 2 time units
Console.WriteLine($"Position after velocity: {playerPosition}");
// Get movement direction
Vector2D direction = movement.Normalize();
Console.WriteLine($"Movement direction: {direction}");
Output:
New player position: (11.5, 4.5)
Position after velocity: (12.5, 5.9)
Movement direction: (0.9486833, -0.31622776)
Example 2: Financial Calculations - Money
public struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Addition operator
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(a.Amount + b.Amount, a.Currency);
}
// Subtraction operator
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Cannot subtract different currencies");
return new Money(a.Amount - b.Amount, a.Currency);
}
public Money WithTax(decimal taxRate)
{
return new Money(Amount * (1 + taxRate), Currency);
}
public override string ToString() => $"{Amount.ToString("C")} {Currency}";
}
// Usage in financial calculations
Money price = new Money(19.99m, "USD");
Money shipping = new Money(5.00m, "USD");
Money totalBeforeTax = price + shipping;
Money finalTotal = totalBeforeTax.WithTax(0.08m); // 8% tax
Console.WriteLine($"Product price: {price}");
Console.WriteLine($"Shipping cost: {shipping}");
Console.WriteLine($"Total before tax: {totalBeforeTax}");
Console.WriteLine($"Final total with tax: {finalTotal}");
Output:
Product price: $19.99 USD
Shipping cost: $5.00 USD
Total before tax: $24.99 USD
Final total with tax: $26.99 USD
Advanced Struct Topics
Readonly Structs
C# 7.2 introduced readonly structs, which guarantee that the struct's instance data will not be modified:
public readonly struct ImmutablePoint
{
public readonly int X { get; }
public readonly int Y { get; }
public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}
// All methods must be marked as readonly
public readonly override string ToString() => $"({X}, {Y})";
// Can still compute values
public readonly double DistanceFromOrigin => Math.Sqrt(X * X + Y * Y);
}
Ref Structs
C# 7.2 also introduced ref structs, which can only live on the stack, never on the heap:
public ref struct StackOnlyBuffer
{
private Span<byte> _buffer;
public StackOnlyBuffer(int size)
{
_buffer = new Span<byte>(new byte[size]);
}
public byte this[int index]
{
get => _buffer[index];
set => _buffer[index] = value;
}
public int Length => _buffer.Length;
}
// Usage (must be in a method)
static void ProcessData()
{
StackOnlyBuffer buffer = new StackOnlyBuffer(1024);
// Work with buffer...
buffer[0] = 42;
Console.WriteLine($"First byte: {buffer[0]}");
}
Default Values
Unlike classes, structs cannot have parameterless constructors before C# 10. Instead, a struct is initialized with the default value, which sets all fields to their default values:
// Default struct initialization
Point defaultPoint = default; // All fields set to 0
Console.WriteLine($"Default point: {defaultPoint}"); // (0, 0)
// With C# 10+, you can have parameterless constructors
public struct PointWithParamlessCtor
{
public int X;
public int Y;
// C# 10 feature: parameterless constructor
public PointWithParamlessCtor()
{
X = 100;
Y = 100;
}
}
Best Practices for Structs
-
Keep Them Small: Structs should ideally be small (less than 16 bytes).
-
Consider Immutability: When possible, make structs immutable using
readonly
or design them with immutable fields. -
Be Careful with Equality: Override
Equals()
andGetHashCode()
when implementing custom equality. -
Avoid Boxing: Be aware of operations that cause boxing, which negates the performance benefits.
-
Use for Value Semantics: Choose structs when you need true value semantics rather than reference semantics.
-
Avoid Deep Copying of Large Data: If your struct contains large arrays or collections, consider alternatives.
Summary
Structs in C# provide a powerful way to create lightweight, efficient value types that are suitable for small data structures with value semantics. Their stack-based allocation and copying behavior make them ideal for performance-critical scenarios where minimizing heap allocations is important.
Key points to remember:
- Structs are value types, stored on the stack
- They're copied when assigned or passed as parameters
- They're ideal for small, immutable data structures
- Use the
ref
keyword when you need to modify a struct through a method - Modern C# features like readonly structs extend their utility
By understanding when and how to use structs effectively, you can write more efficient and cleaner C# code that leverages the full power of the language's type system.
Exercises
-
Create a
Fraction
struct that represents a mathematical fraction with numerator and denominator. Implement addition, subtraction, multiplication, and division operations. -
Design an
RGB
struct to represent colors. Include methods to blend colors and convert to hex format. -
Implement a
DateRange
struct with start and end dates. Add methods to check if a date is within the range and if two ranges overlap. -
Create an immutable
GeoCoordinate
readonly struct with latitude and longitude. Implement a method to calculate distance between two coordinates. -
Design a benchmark to compare the performance of a small class versus a struct for a million operations to demonstrate the performance difference.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)