Many thanks to the observant readers who pointed out that I missed a few things in last week’s post on C# semantic types. To round out our semantic type, we need to add a few additional features.
ToString()
Every .NET type has a ToString()
method inherited from object. Unfortunately,
the standard implementation is somewhat unhelpful, simply returning the fully qualified
type name of the instance. (Though it is understandable why that’s the default
implementation - at least it gives you more than just an empty string.)
Without a custom implementation of ToString()
, we can’t use an instance directly in
a string.Format()
call, nor in the closely related C# 6 interpolated string without
explicitly drilling into our Code
property:
A simple implementation of ToString()
makes it easier for our users to concentrate
on their goals, not on our CourseCode
class.
Since our CourseCode
contains only a single member, the implementation is straightforward:
With this complete, an interpolated string doesn’t need to drill down that last level:
If your semantic type is more complicated than our example, do consider implementing the
IFormattable
interface and its more complex ToString(String, IFormatProvider)
so
that your type can be more richly formatted.
Comparisons
Consider the CompareTo
method already implemented:
Given this, we can write a static Compare
method:
This is one of our more complex methods, so it’s worth looking how it works. There are five cases to consider:
- When both values are null -
ReferenceEquals()
will return true, so we return 0. - When both values are the same instance -
ReferenceEquals()
will return true, so we return 0. - When neither value is null -
CompareTo()
will do our comparison. - When
right
is null (andleft
is not) -CompareTo()
will do our comparison. - When
left
is null (andright
is not) - the Elvis operator will return null and we return 1.
With that out of the way, providing implementations of the comparison operators is straightforward:
Debugging
By default the debugger will use ToString()
to display an instance. The attribute [DebuggerDisplay]
gives you more control how your type is shown inside the debugger.
It can be helpful, when debugging, to be able to easily see a description of an instance or two - imagine looking at a list in the debugger and seeing every item as “{CourseCode}”.
A Base Class?
One correspondent asked “Why not implement all of these details in a base class?” and save all the work?
Much as this might seem to be a good idea, it has a fatal flaw: having a common base type subverts the original motivation for introducing semantic types: we want to ensure that different values are type incompatible so that the compiler will catch errors as early as possible.
Comments
blog comments powered by Disqus