Runar Ovesen Hjerpbakk

Software Philosopher

On structs: an underutilized part of C#

The 2014 book Writing High-Performance .NET Code contains tips on coding effective C#, above and beyond the Zen of Performance. It made me consider the usage of value types, struct instead of class.

By using struct for small structures, we achieve less memory usage, better memory locality and fewer garbage collections. Arrays of structs have good memory locality, utilising the CPU caches and improving performance due to more cache hits.

Design guidelines

  • As all other value types in C#, your structs should be immutable. Set your fields and properties readonly and create custom constructors. As structs can always be constructed using the default constructor, the default values for all fields and properties must make sense in your context.

  • Implement IEquatable<T>. The default Equals implementation uses reflection and boxing and should be avoided. Also override bool Equals(object obj) and int GetHashCode().

  • Keep them small, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries suggests under 16 bytes. Since value types are copied unless passed by reference (ref), performance will degrade if the struct becomes too big. C# classes have a pointer size of 4 bytes, 8 bytes for x64 applications, and an overhead of 8 bytes. Structs are just as big as the data they contain. Below are a table of the sizes of the built in value types:

Type Size
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(decimal) 16
sizeof(bool) 1

Recommendation

Use a struct when:

  • The data fits together logically as a single entity
  • It will have a small size
  • It will be immutable
  • It will not be used often in a way that will cause boxing

For all else, use class.

Example

public struct WidgetInformation : IEquatable<WidgetInformation>
{
  private readonly Guid id;
  private readonly string name;

  public WidgetInformation(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id { get { return id; } }
  public string Name { get { return name; } }

  public override bool Equals(object obj)
  {
    return obj != null && obj.GetType() == GetType() && base.Equals((WidgetInformation)obj);
  }

  public bool Equals(WidgetInformation other)
  {
    return id == null ? other.id == null : id.Equals(other.id);
  }
  
  public override int GetHashCode()
  {
    return id == null ? base.GetHashCode() : id.GetHashCode();
  }
}