If you’ve ever wanted to use operators with generic types or thought that interfaces could be improved by supporting the ability to define static methods as part of their contract, then this blog post is for you. With .NET 6 we will be shipping a preview of the new generic math and static abstracts in interfaces features. These features are shipping in preview form so that we can get feedback from the community and build a more compelling feature overall. As such, they are not supported for use in a production environment in .NET 6. It is highly recommended that you try the feature out and provide feedback if there are scenarios or functionality you feel is missing or could otherwise be improved.
Requires preview features attribute
Central to everything else is the new RequiresPreviewFeatures attribute and corresponding analyzer. This attribute allows us to annotate new preview types and new preview members on existing types. With this capability, we can ship an unsupported preview feature inside a supported major release. The analyzer looks for types and members being consumed that have the RequiresPreviewFeatures
attribute and will give a diagnostic if the consumer is not marked with RequiresPreviewFeatures
itself. To provide flexibility in the scope of a preview feature, the attribute can be applied at the member, type, or assembly level.
Because preview features are not supported for use in production and the APIs will likely have breaking changes before becoming supported, you must opt-in to using them. The analyzer will produce build errors for any call sites that haven’t been opted-into preview feature usage. The analyzer is not available in .NET 6 Preview 7, but will be included in .NET 6 RC1.
Static Abstracts in Interfaces
C# is planning on introducing a new feature referred to as Static Abstracts in Interfaces. As the name indicates, this means you can now declare static abstract methods as part of an interface and implement them in the derived type. A simple but powerful example of this is in IParseable
which is the counterpart to the existing IFormattable
. Where IFormattable
allows you to define a contract for generating a formatted string for a given type, IParseable
allows you to define a contract for parsing a string to create a given type:
public interface IParseable<TSelf>
where TSelf : IParseable<TSelf>
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}
public readonly struct Guid : IParseable<Guid>
{
public static Guid Parse(string s, IFormatProvider? provider)
{
/* Implementation */
}
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Guid result)
{
/* Implementation */
}
}
A quick overview of the feature is:
- You can now declare interface members that are simultaneously
static
andabstract
- These members do not currently support Default Interface Methods (DIMs) and so
static
andvirtual
is not a valid combination - This functionality is only available to interfaces, it is not available to other types such as
abstract class
- These members are not accessible via the interface, that is
IParseable<Guid>.Parse(someString, null)
will result in a compilation error
To elaborate on the last point, normally abstract
or virtual
members are invoked via some kind of virtual dispatch. For static methods we don’t have any object or instance in which to carry around the relevant state for true virtual dispatch and so the runtime wouldn’t be able to determine that IParseable<Guid>.Parse(...)
should resolve to Guid.Parse
. In order for this to work, we need to specify the actual type somewhere and that is achievable through generics:
public static T InvariantParse<T>(string s)
where T : IParseable<T>
{
return T.Parse(s, CultureInfo.InvariantCulture);
}
By using generics in the fashion above, the runtime is able to determine which Parse
method should be resolved by looking it up on the concrete T
that is used. If a user specified InvariantParse<int>(someString)
it would resolve to the parse method on System.Int32
, if they specified InvariantParse<Guid>(someString)
it would resolve to that on System.Guid
, and so on. This general pattern is sometimes referred to as the Curiously Recurring Template Pattern (CRTP) and is key to allowing the feature to work.
More details on the runtime changes made to support the feature can be found here.
Generic Math
One long requested feature in .NET is the ability to use operators on generic types. Using static abstracts in interfaces and the new interfaces being exposed in .NET, you can now write this code:
public static TResult Sum<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult result = TResult.Zero;
foreach (var value in values)
{
result += TResult.Create(value);
}
return result;
}
public static TResult Average<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult sum = Sum<T, TResult>(values);
return TResult.Create(sum) / TResult.Create(values.Count());
}
public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : IFloatingPoint<TResult>
{
TResult standardDeviation = TResult.Zero;
if (values.Any())
{
TResult average = Average<T, TResult>(values);
TResult sum = Sum<TResult, TResult>(values.Select((value) => {
var deviation = TResult.Create(value) - average;
return deviation * deviation;
}));
standardDeviation = TResult.Sqrt(sum / TResult.Create(values.Count() - 1));
}
return standardDeviation;
}
This is made possible by exposing several new static abstract interfaces which correspond to the various operators available to the language and by providing a few other interfaces representing common functionality such as parsing or handling number, integer, and floating-point types. The interfaces were designed for extensibility and reusability and so typically represent single operators or properties. They explicitly do not pair operations such as multiplication and division since that is not correct for all types. For example, Matrix4x4 * Matrix4x4
is valid, Matrix4x4 / Matrix4x4
is not. Likewise, they typically allow the input and result types to differ in order to support scenarios such as double = TimeSpan / TimeSpan
or Vector4 = Vector4 * float
.
If you’re interested to learn more about the interfaces we’re exposing, take a look at the design document which goes into more detail about what is exposed.
Operator Interface Name | Summary |
---|---|
IParseable | Parse(string, IFormatProvider) |
ISpanParseable | Parse(ReadOnlySpan<char>, IFormatProvider) |
IAdditionOperators | x + y |
IBitwiseOperators | x & y , x | y , x ^ y , and ~x |
IComparisonOperators | x < y , x > y , x <= y , and x >= y |
IDecrementOperators | --x and x-- |
IDivisionOperators | x / y |
IEqualityOperators | x == y and x != y |
IIncrementOperators | ++x and x++ |
IModulusOperators | x % y |
IMultiplyOperators | x * y |
IShiftOperators | x << y and x >> y |
ISubtractionOperators | x - y |
IUnaryNegationOperators | -x |
IUnaryPlusOperators | +x |
IAdditiveIdentity | (x + T.AdditiveIdentity) == x |
IMinMaxValue | T.MinValue and T.MaxValue |
IMultiplicativeIdentity | (x * T.MultiplicativeIdentity) == x |
IBinaryFloatingPoint | Members common to binary floating-point types |
IBinaryInteger | Members common to binary integer types |
IBinaryNumber | Members common to binary number types |
IFloatingPoint | Members common to floating-point types |
INumber | Members common to number types |
ISignedNumber | Members common to signed number types |
IUnsignedNumber | Members common to unsigned number types |
The binary floating-point types are System.Double
(double
), System.Half
, and System.Single
(float
). The binary-integer types are System.Byte
(byte
), System.Int16
(short
), System.Int32
(int
), System.Int64
(long
), System.IntPtr
(nint
), System.SByte
(sbyte
), System.UInt16
(ushort
), System.UInt32
(uint
), System.UInt64
(ulong
), and System.UIntPtr
(nuint
). Several of the above interfaces are also implemented by various other types including System.Char
, System.DateOnly
, System.DateTime
, System.DateTimeOffset
, System.Decimal
, System.Guid
, System.TimeOnly
, and System.TimeSpan
.
Since this feature is in preview, there are various aspects that are still in flight and that may change before the next preview or when the feature officially ships. For example, we will likely be changing the name of INumber<TSelf>.Create
to INumber<TSelf>.CreateChecked
and INumber<TSelf>.CreateSaturating
to INumber<TSelf>.CreateClamped
based on feedback already received. We may also expose new or additional concepts such as IConvertible<TSelf>
or interfaces to support vector types and operations.
If any of the above or any other features are important to you or you feel may impact the usability of the feature in your own code, please do provide feedback (.NET Runtime or Libraries, C# Language, and C# Compiler are generally good choices). In particular:
- Checked operators are not currently possible and so
checked(x + y)
will not detect overflow: csharplang#4665 - There is no easy way to go from a signed type to an unsigned type, or vice versa, and so selecting logical (unsigned) vs arithmetic (signed) shift is not possible: csharplang#4682
- Shifting requires the right-hand side to be
System.Int32
and so additional conversions may be required: csharplang#4666 - All APIs are currently explicitly implemented, many of these will likely become implicitly available on the types when the feature ships
Trying out the features
In order to try out the features there are a few steps required:
- Create a new C# console application targeting .NET 6 on the command line or in your favorite IDE
- Edit the project file to opt into using preview features by setting the
EnablePreviewFeatures
property to true, and to reference the System.Runtime.Experimental NuGet package.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
</ItemGroup>
</Project>
- Create a generic type or method and constrain it to one of the new static abstract interfaces
// See https://aka.ms/new-console-template for more information
using System.Globalization;
static T Add<T>(T left, T right)
where T : INumber<T>
{
return left + right;
}
static T ParseInvariant<T>(string s)
where T : IParseable<T>
{
return T.Parse(s, CultureInfo.InvariantCulture);
}
Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());
Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());
Console.WriteLine($"Result: {Add(left, right)}");
- Run the program and observe the output
First number: 5
Second number: 3.14
Result: 8.14
Closing
While we only briefly covered the new types and gave a simple example of their usage, the potential applications are much broader. We are looking forward to your feedback and seeing what awesome ways you can use this to improve your existing code or in the creation of new code. You can log feedback on any of the existing issues linked above or open new issues, as appropriate, on the relevant GitHub repository (.NET Runtime or Libraries, C# Language, and C# Compiler are generally good choices).
This is a long overdue, but amazing step forward!
Looks like I can drop a solid 6,000+ lines of crazy generic dispatch systems for implementing add/sub/div/mul/mod/etc generic functions for the generic math we do in our project (and the almost as large static analyzer we’ve made to ensure they’re optimised away / the compiler plays nice)
I was playing with the feature tonight, and I noticed that BigInteger doesn’t seem to have the interfaces to participate in generic math functions, such as the Add() example given in the article. Are there plans to update BigInteger such that it implements INumber, IAdditionOperators, etc.?
I've been waiting for this for so long 🙂
Will it be possible to write something like
<code>
or will we have to write
<code>
Yes. You only need
T
andTResult
when you want to support the input and output types differing, such as if you want to take inintegers
and outputfloating-point
.You're saying that:
For static methods we don’t have any object or instance in which to carry around the relevant state for true virtual dispatch and so the runtime wouldn’t be able to determine that should resolve to .
But isn't all the information you need right there? should clearly resolve to and nothing else.
Not currently. This would require the language to be able to specially annotate and recognize a specific generic parameter as the concept.
I proposed a couple different syntaxes for this here: https://github.com/dotnet/csharplang/issues/4436#issuecomment-896981841
<code>
I'm excited with this feature! But I can't not agree with others that at least some interfaces should not be inherited by INumber. In particular, with parsing/formatting functionality this is a great example of Interface Segregation Principle. I'm pretty sure that it will be hard to find an example of generic code (meaning some common algorithm over numbers) which will need to use math operators and parsing/formatting at the same time. And in rare cases...
The section "A quick overview of the feature is" states "You can now declare interface members that are simultaneously static and abstract". I created a small test project like shown in this article. Attention on Console Application, net 6 preview 7, System.Runtime.Experimental, EnablePreviewFeatures = true and LangVersion = preview.
The code:
<code>
After compilation I got the following error:
<code>
What is the correct way to declare interface members static and abstract simultaneously?
Provided you have .NET 6 Preview 7 installed, are referencing the System.Runtime.Experimental NuGet package, and have
EnablePreviewFeatures=true
/LangVersion=preview
set everything should work.It may be possible that you additionally need to ensure Visual Studio is on the latest preview or that
Use previews of the .NET SDK
needs to be checked in your Visual Studio options (Tools->Options->Environemtn->Preview Features`).This is really cool!
How does this relate to the case where you just have a type object and a string lets say?
Since this is a compiler feature (mostly?) i guess you’d still do MakeGenericType on some other generic method with your type object and pass the value in there?
The method exposed on the generic interface is
abstract
and not directly callable. Any method exposed which uses the generic interface as a constraint is just a standard generic method and should be invokable via reflection.One thing I don't really understand about MSFT's approach here is leaving pushing this out to the community as a 'preview feature'. In my mind that basically nixes any use of it in any business development. So what real world non-production exposure is MSFT expecting the community to provide this preview feature? Don't get me wrong I LOVE THIS. I do think that adding inheritance muddies the water. A generic T always and forever represents...
We regularly push out previews for upcoming .NET features or releases. Doing so gives the community the opportunity to provide early feedback and influence the design if there are things that meaningfully impact their usage of it. The same goes for doing the design work in the open on GitHub and live streaming the API Reviews every week.
Some features just take longer to bake than others and this is one of them. This has been...
I guess my point is that this seems like an instance were a good feature is being held back in preview in the vein of building the perfect feature. Generics weren't created in a single release, they evolved over time, keeping static abstract under wraps till at least .NET 7 just seems like needless waiting.
My point was that extending static abstract to abstract classes/inheritance seems counterintuitive in that static methods have always been directly referenced...
This looks really familiar. Almost like I designed it a year ago, wrote an article about it, and never got credit.
I think it’s strange to want “credit” for concepts that have existed for over a decade. Scala has had numeric traits exactly like this in their standard library since 2009.
If you think you are the first to have had this “idea” (about a year ago even!) then you are fooling yourself. Things get reinvented independently all the time and this stuff has been on the radar of a lot of people for a long time.
This was an effort primarily among .NET team members and I'm not aware of your prior project. Can you point to that article and design? It would be great to take a look at your approach.
Notably this was a culmination of past attempts by Microsoft (going back to 2010 and prior), recent investigation by the Xamarin team (January 2020: https://github.com/xamarin/partydonk), and various design work by the various teams starting in June 2020 when "partydonk" was...
I think Patrick is referring to this article: https://dev.to/entomy/real-traits-in-c-4fpk, from September 2020, which is very clever. I saw it on Twitter too: https://twitter.com/pkell7/status/1425593504777089025?s=20
I suspect his concern is about the style used to propose the new INumber interface and how it resembles the idea in the traits article and the source code of his GitHub projects. So yes, he has a good point.
Hello Jorge,
We had the INumber design in early Feburary 2020, before this post you referenced from September 2020 (7 months later), but I just remembered an interesting historical nugget that I wanted to share.
When we started looking at this, and liked Swift's associated type, in particular "Self", which made expressing this problem a piece of cake. This took place in October 2019, just when I had freshly started on AI work (which was...
@Richard Lander, I've had generic operators in my Sasa library since 2012.
The current version is even more efficient, doesn't need runtime compilation, and handles all operators with multiple possible overloads, ie. var baz = Operators<Foo, Bar, Baz>.Add(someFoo, someBar);
This version has been available publicly since 2013.
More prior art:
Also see ImmutableArray<T>: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs
To add a little more color on the 2010 research.
Carol Eidt and some other folks had designed a solution that was almost the same solution that we ended up redesigning in January 2020, we were not aware at the time of the specific implementation details, but we did learn of her previous work, and were very happy that we were in alignment around the implementation specifics - which was a good validating point for the...
I assume this is the article you are referring to, Patrick? https://dev.to/entomy/generics-systems-83n
Thanks for writing it. It does a good job of covering the problem space and also provides good context from other programming languages. For folks that want a broader view on this topic to help better understand the C# solution, I recommend looking at Patrick's article. I took away new insight from reading it.
For fairness, the article does not present a design, more a...
Nope, different article.
And as for you, ever since we emailed two years ago I’ve been noticing remarkably similar code to what I’ve written show up in the product you manage, so you and I have nothing to speak about. MSFT did this with other projects numerous times, and it’s absolute BS.
Please share the article and same for the other instances. We take these sorts of claims very seriously. I'm assuming that the article must be public for someone to have copied it. If it is a public, sharing the link should be easy.
You could reasonably describe our design process as being too insular. That's our bias. I do not believe -- on the other end of the spectrum -- that anyone on my team is...
Oh man, this is so cool. Echo @Sandro Magi’s comment – have been hoping for this for ages (Know Mads has too…). Can’t wait to dig in!