Union types are common in functional languages, but have yet to make it to C# and similar. These types solve a simple and very common problem, data that is one of a finite set of possibilities. Here’s we’ll look at how to accomplish the same in C#.
Short version
Record types provide value equality semantics and short definitions. Pattern matching improvements in recent versions make for concise handling.
Example
|
|
Unions in F#
First, a short rundown of union types. They’re also called sum types and “or” types. They represent a limited set of alternatives. This could be enum-like
|
|
but cases can also come with different data
|
|
The “cases” are all acceptable alternatives from some view. The union type allows us to treat them as one type, and break out cases only when it is important.
Typically the union types is handled with pattern matching.
|
|
Object-Oriented Equivalent
The motivation of unions may sound familiar. Unions types are really about polymorphism.
Polymorphism in OO is usually achieved through inheritance. For example
|
|
This works, but it has several downsides.
- We can’t limit the number of cases, causing potentially unhandled data
- UPDATE: We actually can limit the number of cases by making the base class’s constructor internal or private. Pattern matching in C# currently will not recognize that limitation, but we can prevent unhandled data and unexpected extension.
- The intent as a set of alternatives isn’t as clear
- We can’t prevent methods and behavior from being mixed with the data
- Classes have reference equality, but unions usually want value semantics
Some of those cons are really just misplaced features. Interchangeable behaviors is a great design goal, but those are usually driven by interfaces these days. Allowing unforeseen cases can also be a feature, though how it should be handled depends on the situation.
Overall, the inheritance approach holds little benefit over unions, now that we know coupling data and behavior is a recipe for rigid designs. I find that union types eliminate the vast majority of non-interface inheritance.
Improvements in C#
C# added pattern matching as of version 7. This makes the inheritance-based union from the last section tenable
|
|
C# 8 improves pattern matching
|
|
C# 9 improves the union definition with positional record definitions, which also include value equality semantics by default
|
|
As of C# 9, that makes the full example code shorter than the original type definition.
|
|
Summary
There is no true equivalent to union types in C#. However, approximating them with inheritance has greatly improved in recent versions. As of C#9, a combination of positional records and pattern matching make for a concise and pleasant approximation.