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:

var prompt = $"Course #{course.Code.Code}";

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:

public override string ToString() => Code;

With this complete, an interpolated string doesn’t need to drill down that last level:

var prompt = $"Course #{course.Code}";

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:

public int CompareTo(CourseCode other)
    => string.Compare(Code, other?.Code,
        StringComparison.InvariantCultureIgnoreCase);

Given this, we can write a static Compare method:

public static int Compare(CourseCode left, CourseCode right)
    => ReferenceEquals(left, right)
        ? 0
        : left?.CompareTo(right) ?? 1;

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 (and left is not) - CompareTo() will do our comparison.
  • When left is null (and right 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:

public static bool operator <(CourseCode left, CourseCode right)
    => Compare(left, right) < 0;

public static bool operator <=(CourseCode left, CourseCode right)
    => Compare(left, right) <= 0;

public static bool operator >(CourseCode left, CourseCode right)
    => Compare(left, right) > 0;

public static bool operator >=(CourseCode left, CourseCode right)
    => Compare(left, right) >= 0;

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}”.

[DebuggerDisplay("[CourseCode {Code}]")]
public sealed class 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
Next Post
Throw Rich Exceptions  11 Jun 2016
Prior Post
Semantic Types in C#6  27 May 2016
Related Posts
When (not) to use Var  16 Jul 2016
Semantic Types in C#6  27 May 2016
Property Enhancements for C#  20 Dec 2015
Language Extensions for C#  19 May 2014
When should methods be Static?  09 Oct 2012
Of Method Naming and more  29 Sep 2012
Someone needs an intervention  16 Dec 2011
CallerInfo in C# 5  08 Dec 2011
Regions in C#  16 May 2011
Lambda expressions and Block syntax  15 Apr 2011
More csharp posts »
Related Pages
June 2016 archive