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 Guid
s 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.