As I explained in the post C# 11 static abstract members, C# 11 let’s write static abstract
members in interface. This feature was mostly introduced to implement the new .NET 7.0 genetic math library, also explained in this article.
However static abstract
members in interface opens the door to a whole new range of syntax possibility. One of this possibility is generic parsing through the new .NET 7.0 System.IParsable<TSelf>
interface:
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 provides your own parsing interfaces that take the start index as input and returns the number of char consumed. This way one can chain parsing operations on a string:
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 the 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 were no easy way to implement generic parsing before .NET 7.0 and C# 11. One could have used Reflection as shown in the code ample 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 worst, 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 just 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 re-use 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 any 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 rule CA2260 emits a warning in this situation.
Conclusion
This new IParsable<TSelf>
interface looks like a small new feature but it’s a tree that hides the forest of a new range of convenient syntaxes based on 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.