Best Practices for Structs in C#

Structs in C# are value types and thus are meant for holding data structures that are usually immutable. If you're going to write a struct, consider the following practices.

Read-Only

A good struct is almost always immutable. Using the readonly keyword in the struct declaration enforces immutability throughout the definition.

public readonly struct Foo
{
}

Static Instances

To aid in code readability, you can create static instances of specific struct configurations. For example, .NET does this with Guid.Empty.

public readonly struct Color
{
    public static Color Red { get; } = new Color(255, 0, 0);
    public static Color Green { get; } = new Color(0, 255, 0);
    public static Color Blue { get; } = new Color(0, 0, 255);
}

This makes code a little more readable, as instead of typing this:

public bool IsRed(Color color) => return color == new Color(255, 0, 0);

You could use this instead:

public bool IsRed(Color color) => return color == Color.Red;

Equality Comparison

It's generally a good idea to implement IEquatable<T> and to override the Equals and GetHashCode methods.

public readonly struct FileSize : IEquatable<FileSize>
{
    public long Bytes { get; }
    
    public FileSize(long bytes)
    {
        Bytes = bytes;
    }
    
    public bool Equals(FileSize other)
    {
        Bytes == other.Bytes;
    }
    
    public override bool Equals(object obj)
    {
        if (obj is FileSize fileSize)
            return Equals(fileSize);
        return base.Equals(obj);
    }
    
    public override int GetHashCode()
    {
        return Bytes.GetHashCode();
    }
}

If you're trying to determine equality based on multiple properties within your struct, try using the ValueTuple type that was introduced in C# 7.0 in conjunction with tuple literals to simplify your life.

public readonly struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    
    public bool Equals(Point other) =>
        (X, Y) == (other.X, other.Y);
    
    public override bool Equals(object other) =>
        (other is Point p) && Equals(p);
    
    public override int GetHashCode() =>
        (X, Y).GetHashCode();
}

Operators

Implementing equality operators allow users of your struct to write code like foo == bar or foo != bar.

public readonly struct FileSize
{
    public static bool operator ==(FileSize left, FileSize right)
    {
        return left.Equals(right);
    }
    
    public static bool operator !=(FileSize left, FileSize right)
    {
        return !(left == right);
    }
}

Don't stop there. Also implement other operators like >, >=, +, etc, if they make sense for your struct.

Parsing Methods

Parsing methods can often be found in structs. Declaring them on the struct itself allows for easy discoverability for consumers of your code.

public readonly struct Color
{
    public static bool TryParse(string hexCode, out Color color)
    {
        ...
    }
}