I previously wrote on the difficulties of normalized result types in C#. Let me be clear, result types are still not great in C#, but C# 9 at least makes them practical.
When I’m thinking practical, I mean
- It’s easy enough to create a result type that I don’t hesitate
- Don’t have to implement a bunch of boiler plate for every type definition
- A tame amount of generic type arguments. Definitely not multiple explicit arguments for every operation
- Result interoperability
- Should be able to operate on groups of results to produce new results
- Some basic operations should be polymorphic between all result types
- like
IsSuccess
orIsFailure
- like
- Should be able to map between result types generically
The key feature that makes it possible is covariant returns. Derived classes can override parent methods with implementations that return a more specific type of the original return value. For example,
|
|
This allows some shenanigans with the generic system that result in practically usable result types. Observe
|
|
Note that we end up with
- a generic result type
- the ability to make strongly typed results in one line
- interoperability between all our result types
Generic result types enable arbitrary success/failure combos with strong typing. This is especially useful when aggregating result types.
There is also minimal generic type argument to deal with. They need only be specified once, when the result type is defined. We also benefit from chained actions on the result type, since even inherited operations can return the derived type.
The downside is that result actions are tied to particular types. There is no way to nicely share a static module of functions between result types.
This means our code looks more like new SavePersonResult().Fail("oops")
rather than Result<SavePersonResult>.Fail("oops")
or Result.Fail("oops")
.
All in all, I still consider this a big win for improved design expression in C#!