Implementing a struct in C#
The C# struct type can be a confusing topic for developers. The documentation focuses mostly on the mechanics of structs; the scenarios in which they are useful are more unclear.
Here we will discuss the struct type, when it is warranted, and how to implement one in C#.
What is a struct?
The common knowledge about structs is they are value types, while their counterpart, classes, are reference types. A struct variable directly contains its data, while a class variable only contains a reference to its data. This allows different semantics: structs do not get allocated on the heap, they are always passed by value on the stack. This increases efficiency because a struct variable can simply be copied from stack frame to stack frame.
The uncommon knowledge about structs is that they represent concepts with no intrinsic identity. Instead, the identity of a value type is derived from the data it contains.
For example, think about writing something down. You don’t care which pen you use, just that it has ink. You could easily swap out another pen and it wouldn’t make a difference. In this case, the pen is a value type.
If you had a choice of pens, their differing characteristics would influence your decision: “I can use a blue pen or black pen, but not a red pen.” Here, the identity of a pen is derived from its color. If you had 2 red pens, they would have the same identity, which would not be true for a reference type.
This is exactly how struct types behave. Let’s say you want to represent the first day of this year. You could do this with the DateTime struct:
var firstOfThisYear = new DateTime(2009, 1, 1);
Now, let’s say we created a second variable with the same value:
var firstOfThisYear2 = new DateTime(2009, 1, 1);
We could use both interchangably. Since DateTime is a value type, it doesn’t matter which instance we use, just that it represents the first day of the year. In fact, we can see this reflected in the equality operator:
firstOfThisYear == firstOfThisYear2 // true
Where have I seen a struct before?
As mentioned previously, DateTime is a struct. In fact, all C# primitive types, such as int, double, and bool, are as well.
Other core framework types, such as KeyValuePair<>, Color, Point, Guid, and most collection enumerators, are struct types.
When should I consider using a struct?
A struct best serves types similar to those we have already seen: numbers, dates, and other small pieces of information. Anything that matters what but not which is a candidate.
A convenient use of struct is to limit the values of other general-purpose types. For example, the concept of probability, which ranges from 0-1, can be represented by a double, but double can represent a lot more than probability values. To fix this mismatch, and prevent countless bounds checks, we can introduce the Probability struct (seen in the example below).
Note:
All struct types are given a parameterless constructor which you cannot override. The constructor is called for every new instance and initializes fields to their default values. This stems from the fact that a value type cannot be null; instead, the default value of a struct is an instance whose fields have their default values.
Because of these semantics, struct types generally have a well-defined zero value. If the type to be implemented does not have a sensible zero value, considering writing a class instead.
An example: Probability
As mentioned before, using a double to represent probability has the drawback that there are many legal double values which are not legal probability values. This puts the onus of bounds-checking on all code which accepts probabilities. Encapsulating this concern would not only clean up code but also more fully express intent by making explicit the concept of “Probability”.
Probability meets our criteria for creating a struct: it is a small bit of information, it matters what but not which, and it has a natural zero value. We are essentially making a new kind of number.
First, let’s define the bare skeleton:
public struct Probability
{
private double _value;
public Probability(double value) : this()
{
if(value < 0 || value > 1)
{
throw new ArgumentOutOfRangeException(“value”);
}
_value = value;
}
public double ToDouble()
{
return _value;
}
}
This scopes the double value to just the legal values of probability. We also included a method to get the original double value (similar to the ToString() method).
Now we define identity semantics. First, we implement a type-safe equality method:
public struct Probability : IEquatable<Probability>
{
// …
public bool Equals(Probability other)
{
return _value.Equals(other._value);
}
}
Next, we override System.Object methods to implement untyped equality:
public override bool Equals(object obj)
{
return obj is Probability && Equals((Probability) obj);
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
Since it makes sense for this specific type, we’ll also implement IComparable<>:
public struct Probability : IEquatable<Probability>, IComparable<Probability>
{
//…
public int CompareTo(Probability other)
{
return _value.CompareTo(other._value);
}
}
The final step in defining the semantics of our struct is operator overloads:
public static bool operator ==(Probability x, Probability y)
{
return x.Equals(y);
}
public static bool operator !=(Probability x, Probability y)
{
return !(x == y);
}
public static bool operator >(Probability x, Probability y)
{
return x.CompareTo(y) > 0;
}
public static bool operator <(Probability x, Probability y)
{
return x.CompareTo(y) < 0;
}
public static bool operator >=(Probability x, Probability y)
{
return x.CompareTo(y) >= 0;
}
public static bool operator <=(Probability x, Probability y)
{
return x.CompareTo(y) <= 0;
}
We now have a fully-functional struct with value type semantics. An instance will be equal to another only if their values are equal. This includes both through the equality operator and the Equals method.
Another common pattern within struct types is to define instances of known values. This includes Int32.MaxValue, TimeSpan.Zero, and Double.PositiveInfinity. There are some common probabilities which we can define:
public static readonly Probability Zero = new Probability(0);
public static readonly Probability Half = new Probability(0.5);
public static readonly Probability One = new Probability(1);
Adding Value
Probability restricts the value of a double, removing the need to repeat bounds-checks throughout our code. It also gives us some common values. However, it’s not very useful otherwise: we still have to call ToDouble() in order to do anything interesting.
A probability value is often used to weight outcomes of random events when determining expected value. While this is implemented as multiplication, that is the mechanism; weighting a value is the intent.
With this in mind, rather than have client code ask for the value (via ToDouble), we’ll have it tell Probability to multiply it:
public double WeightOutcome(double outcome)
{
return _value * outcome;
}
This emphasizes the intent of the method while encapsulating implementation details.
Conclusion
There we have it! Probability is a struct written in the same vein as the C# primitives. It has the same identity semantics as other value types such as DateTime and Int32.
Those semantics are representative of the fact that, if you have a probability, you don’t care which you have, only what its value is. Any type with this trait may benefit from implementation as a struct.




This is such a great resource that you are providing and you give it away for free. I enjoy seeing websites that understand the value of providing a prime resource for free. I truly loved reading your post. Thanks!