`Guid.Empty` is not a `Guid`

Just a quick rant today.

Guid struct encapsulates properties of universally unique identifier: ID once generated locally in this way has a very low probability of being generated again, at a different place or in a different time. This is accomplished using theoretically unsound, but in practice very effective tricks like taking into account MAC addresses of the generating computer.

The .NET implementation sucks at this encapsulation job inevitably, as struct encapsulation in .NET environment is broken by enforced parameterless constructor. This is a performance trade-off: when writing new int[10000], you don’t want to run ten thousand constructors, you just allocate memory.

You can’t guarantee uniqueness though in a world when you have to be able instantiate with nothing.

(This is by the way something that reference types don’t suffer not because they are on the heap, but because they are nullable, something that is now recognized as a clear design mistake. Notice how recent reference type nullability efforts dance around what new string[10000] should mean.)

So you are necessarily able to say new Guid(); and have identifier that is not only not unique, au contraire, that’s forced to be exactly the same as all other guids generated like this on the planet. The only other alternative in .NET is to make some GuidRef class, but this would be obviously very wasteful. Guids are otherwise very good fit for value types as they are short and immutable.

What is really surprising: there is a widely used static property Guid.Empty that offers the same empty value as the one “generated” by new Guid();. This is entirely unprovoked offer for doing the wrong thing, subverting type system that is trying to say something (“this is a unique id!”) by introducing meaningless noise (“this is 00000000-0000-0000-0000-000000000000”).

Guid predates Nullable<T>, so the whole thing might just be a relict of times when it was practical to do things like saying “customer ID not provided” by passing “customerId: -1”. I remember the past though and I’m quite sure people didn’t like it even then: the advice was to create overloaded method without the possibly empty argument, or encapsulate such “possible customer” by hand in an object. Now, at whatever place you expect that Guid can be missing, you should use Guid? to inform the consumer about the situation and then just pass null when necessary.

One can argument that, given broken encapsulation of struct, it’s important to check Guids for emptiness all the time – and that Guid.Empty makes that easier and more explicit. You know what would make it even easier? Two extension methods like this:

public static bool IsEmpty(this Guid g)
    => g == new Guid();

public static bool IsEmpty(this Guid? g)
    => !g.HasValue || g.Value == new Guid();

All in all, one should be mindful about role of types when using them. Even in fairly basic type systems without sum types (like C# has) there are ways to distinguish between value presence and non-presence at the type level using parametric polymorphism (Nullable<T>) and such distinction is crucial for any kind of type-safe work with the values provided.

Using Guid.Empty is a bad practice. Let’s hope it won’t spread.

Update: There is a good point from user takerukoushirou at Reddit: UUID RFC defines Nil value that corresponds to Guid.Empty. This redirects lots of my critique towards the standard (“why is any UUID actually Guid?”), but I don’t want to criticize standards. They are not valuable because they conform to my particular view of the world, they are valuable because they establish any not-totally-insane common ground for us all.

The important point of this article remains: in a strongly typed program, non-Nil GUID should have its type to use in domain and database logic as we almost never want to have implicit nullability. Almost everyone can use Guid as this non-Nil type as long as they won’t touch Nil/Guid.Empty outside of boundary of the application. At the boundary, we must respect that any other side can send us Nil/Guid.Empty and still conform to the RFC.