As I explained in the post C# 11 static abstract members, C# 11 let’s write static abstract
members in an interface. This feature was mostly introduced to implement the new .NET 7.0 generic math library, also explained in this article.
Introduction to the Interface IParsable<TSelf>
However static abstract
members in an interface open the door to a whole new range of syntax possibilities. One of such opportunity is generic parsing through the interface System.IParsable<TSelf>
introduced with .NET 7.0:
1 2 3 4 5 6 7 8 |
namespace System { // Summary: Defines a mechanism for parsing a string to a value. // TSelf: The type that implements this interface. public interface IParsable<TSelf> where TSelf : IParsable<TSelf>? { static abstract TSelf Parse(string s, IFormatProvider? provider); static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result); } } |
Thanks to this interface and its static abstract
methods, it is now possible to have a generic extension method that parses a string to anything. Here is an example of such Parse<T>()
method with a class Person
implementing IParsable<Person>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System.Diagnostics.CodeAnalysis; // Required for [NotNullWhen(true)] [MaybeNullWhen(false)] // int can be parsed because in .NET 7.0 System.Int32 implements IParsable<int> int i = "691".Parse<int>(); // Our Person class can be instantiated through string parsing too, by the same Parse<T>() method Person person = "Bill,Gates,US".Parse<Person>(); Console.ReadKey(); static class ExtensionMethods { // Generic parsing internal static T Parse<T>(this string s) where T : IParsable<T> { return T.Parse(s, null); } } sealed class Person : IParsable<Person> { public string FirstName { get; } public string FullName { get; } public string Country { get; } // Private constructor used from the Parse() method below private Person(string firstName, string fullName, string country) { FirstName = firstName; FullName = fullName; Country = country; } // IParsable<Person> implementation public static Person Parse(string s, IFormatProvider? provider) { string[] strings = s.Split(new[] { ',', ';' }); if(strings.Length != 3) { throw new OverflowException("Expect: FirstName,LastName,Country"); } return new Person(strings[0], strings[1], strings[2]); } public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Person result) { result = null; if (s == null) { return false; } try { result = Parse(s, provider); return true; } catch { return false; } } } |
Notes on IParsable<T>
As the code sample above suggests, in .NET 7.0 IParsable<T>
is implemented by all numeric types Int32
, Byte
, Double
… but also by other types usually subject to parsing like DateTime
, DateOnly
or Guid
.
One limitation of these new parsing APIs is that it assumes that the entire string is the value. You can easily provide your own parsing interfaces that take the start index as input and return the number of characters consumed. This way one can chain parsing operations on a string while avoiding allocating new strings through calls to SubString()
:
1 2 3 |
interface IMyParsable<TSelf> where TSelf : IParsable<TSelf>? { static abstract TSelf Parse(string s, int index, out int nbCharRead); } |
Alternatively, .NET 7.0 provides a similar interface ISpanParsable<T>
to parse from any ReadOnlySpan<Char>
that typically represents a sub-string.
Generic Parsing before .NET 7.0
Just think about it: there was no easy way to implement generic parsing before .NET 7.0 and C# 11. One could have used Reflection as shown in the code sample below:
1 2 3 4 5 6 7 8 |
static class ExtensionMethods { internal static T Parse<T>(this string s) { var type = typeof(T); var method = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, new[] { typeof(string), typeof(IFormatProvider) }); var result = (T)method!.Invoke(null, new object?[] { s })!; return result; } } |
But this implementation is slower because of reflection cost. And one certainly doesn’t want a performance penalty each time a parsing occurs.
Even worse, the compile-time safety is not here to prevent calling Parse<T>()
on a type T
that has no Parse(string):T
method.
Generic Factory Pattern
Generic parsing is an example of the Generic Factory Pattern made possible with C#11 static abstract
. We can now have in our code base sweet interfaces like IParsebleFromJson
or IParsebleFromXml
that promote abstraction and reuse through generic algorithms.
But parsing is not the only use-case of generic factories. The code sample below proposes an extension method BuildVector<TNumeric>()
that builds a vector from an array of numerics:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System.Numerics; Vector<int> v1 = new[] { 1, 2, 3 }.BuildVector(); Vector<double> v2 = new[] { 1d, 2d, 3d, 4d }.BuildVector(); Console.ReadKey(); static class VectorFactory { internal static Vector<TNumeric> BuildVector<TNumeric>(this TNumeric[] values) where TNumeric : INumber<TNumeric> { return new Vector<TNumeric>(values); } } sealed class Vector<TNumeric> where TNumeric : INumber<TNumeric> { internal Vector(TNumeric[] values) { this.Values = values.ToArray(); // Clone the input array to make sure our vector is immutable } internal TNumeric[] Values { get; } } |
The Curiously Recurring Template Pattern (CRTP)
Actually the syntax interface IParsable<TSelf> where TSelf : IParsable<TSelf>
is known in C++ as the Curiously Recurring Template Pattern (CRTP). This syntax allows an interface to declare methods with parameters or return type typed with the concrete type that implements the interface (like Parse(....)
that returns a TSelf
).
However, there is no syntax to enforce that TSelf
refers to the type that actually implements the interface. In the screenshot below we can see that class Person : IParsable<Employee>
compiles and runs properly. This is why the new Roslyn Analyzer CA2260 emits a warning in this situation.
Conclusion
The IParsable<TSelf>
interface, introduced in .NET 7.0, may seem like a minor addition, but it’s the tip of the iceberg revealing a new expanse of convenient syntaxes rooted in static abstraction.
Awesome – great explanations and insights. Could you offer a thought on how to best wrap unit testing around these?
As with all generic impl, these new syntaxes make testing easier since not necessarily all generic types parameter consumed at runtime have to be tested. However if you care for 100% testing coverage (and you should IMHO) all parsing implementation should be thoroughly tested.