Structs in .NET

Overview: This document provides a comprehensive guide to understanding and utilizing structs in .NET, covering their fundamental properties, differences from classes, and best practices for effective use.
Table of Contents

Introduction to Structs

In the .NET Framework, both struct and class keywords are used to define types. However, they represent fundamentally different concepts: value types and reference types, respectively. Structs are value types, meaning instances of structs contain their data directly. When you assign a struct to a new variable, the data is copied, and each variable has its own independent copy of the data.

Understanding structs is crucial for efficient memory management and for building performant .NET applications. They are particularly useful for small, immutable data structures that represent single values.

Structs vs. Classes

The primary distinction between structs and classes lies in their memory management and behavior:

Tip: Think of structs like primitive types (e.g., int, bool) where operations directly manipulate the value itself. Classes are more like pointers to objects.

Defining Your Own Structs

You define a struct using the struct keyword, similar to how you define a class with the class keyword.

Defining a simple Point struct


public struct Point
{
    public int X;
    public int Y;

    // Structs can have methods
    public void Move(int xOffset, int yOffset)
    {
        X += xOffset;
        Y += yOffset;
    }

    // Structs can have properties (though less common for simple data)
    public int Magnitude => (int)Math.Sqrt(X * X + Y * Y);

    // Structs cannot have parameterless constructors (pre-C# 10)
    // Public parameterless constructors are now allowed in C# 10 and later.
    // public Point() { X = 0; Y = 0; } // Allowed in C# 10+

    // You can define constructors with parameters
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}
            

Value Types and Reference Types

All structs in .NET are value types. This has significant implications:

Note: Starting with C# 8.0, you can use nullable reference types, but structs themselves are inherently non-nullable unless explicitly made so with Nullable<T>.

Struct Initialization and Default Values

Every struct has a default value. For most structs, this default value is an instance where all its fields are set to their default values (0 for numeric types, false for bool, null for reference type fields within the struct, etc.).

Before C# 10: Structs could not have explicit parameterless constructors. The system-provided parameterless constructor would always initialize fields to their default values. You could only use parameterless constructors if they were explicitly defined and allowed (e.g., within a class). You had to explicitly assign values to fields or use a constructor with parameters.

C# 10 and later: You can now define an explicit parameterless constructor for a struct. If you do, it will be used for initialization. If you don't, the compiler will provide one that sets all fields to their default values.

Struct Initialization Examples


// Before C# 10:
// You MUST initialize fields or use a constructor with parameters.
Point p1; // p1 is a valid Point struct, X=0, Y=0
p1.X = 10;
p1.Y = 20; // Now p1.X=10, p1.Y=20

Point p2 = new Point(5, 15); // Uses the parameterized constructor

// C# 10 and later:
// You can define a parameterless constructor:
public struct MyStructWithParamlessCtor
{
    public int Value { get; set; }

    public MyStructWithParamlessCtor() // Explicit parameterless constructor
    {
        Value = 100;
    }
}

MyStructWithParamlessCtor m1 = new MyStructWithParamlessCtor(); // m1.Value will be 100

// If no parameterless constructor is defined (C# 10+),
// the compiler provides one that initializes fields to default:
Point p3 = new Point(); // If Point has no explicit parameterless ctor, X=0, Y=0
            

Performance Considerations

Structs can offer performance benefits in certain scenarios:

However, be mindful of the copying overhead:

When to Use Structs

Structs are generally recommended for types that:

Examples include:

Common Patterns and Examples

Structs are excellent for encapsulating simple data and providing related functionality.

A more complex Currency struct


using System;

public readonly struct Currency // 'readonly struct' enforces immutability
{
    public decimal Amount { get; }
    public string Symbol { get; }

    public Currency(decimal amount, string symbol)
    {
        if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount), "Amount cannot be negative.");
        if (string.IsNullOrWhiteSpace(symbol)) throw new ArgumentNullException(nameof(symbol));

        Amount = amount;
        Symbol = symbol;
    }

    public override string ToString()
    {
        return $"{Symbol}{Amount:N2}"; // Format with 2 decimal places
    }

    // Implementing common operators for convenience
    public static Currency operator +(Currency c1, Currency c2)
    {
        if (c1.Symbol != c2.Symbol)
        {
            throw new InvalidOperationException("Cannot add currencies with different symbols.");
        }
        return new Currency(c1.Amount + c2.Amount, c1.Symbol);
    }

    public static Currency operator -(Currency c1, Currency c2)
    {
        if (c1.Symbol != c2.Symbol)
        {
            throw new InvalidOperationException("Cannot subtract currencies with different symbols.");
        }
        decimal newAmount = c1.Amount - c2.Amount;
        if (newAmount < 0) newAmount = 0; // Prevent negative amounts if desired
        return new Currency(newAmount, c1.Symbol);
    }
}

// Usage:
// Currency price = new Currency(19.99m, "$");
// Currency tax = new Currency(1.50m, "$");
// Currency total = price + tax;
// Console.WriteLine(total); // Output: $21.49
            

By using readonly struct, you ensure that the struct's fields cannot be modified after initialization, promoting immutability which is a good practice for value types.