Preview Features in .NET 6 – Generic Math
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).
54 comments
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 a single type and that statics are tied directly to the type, unless all statics are going to apply inheritance no statics should.
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 a long requested feature in .NET, going back to when generics were first released and its not something that’s “easy” to get right. We certainly could have rushed out something “final” in .NET 6 or even kept it hidden/secret until .NET 7 or some future version when it is “ready”, but that would have blocked out valuable feedback and would not have given interested parties the opportunity to try this out first hand.
We’re already getting valuable feedback and input on the feature and I expect that we will continue getting valuable feedback as users think of how they could use it in their own code and begin trying it out and writing their own preview logic. Their will likely be tweaks on both sides throughout the process and changes required, but the overall concept ideally doesn’t change too much.
Could you elaborate on this as I’m not quite sure I’m capturing what you mean?
The functionality needs to be represented in the type system and exposed in a manner that allows other languages to consume the feature. Interfaces are a “natural” way to represent a shared contract that isn’t tied to a particular type. Likewise, generics are a way that allows a concrete type to be provided by the consumer of a type or API, allowing better code generation and reduced abstraction costs.
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 by using the type. With the current proposal that still holds true (even though that type has been genericized), if the feature were to be expanded to abstract classes that would not be the case.
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.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:
After compilation I got the following error:
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`).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 when this is needed it will be pretty ok to include several interface constraints in such a method.
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
IParseable<Guid>.Parse(...)
should resolve toGuid.Parse
.But isn’t all the information you need right there?
IParseable<Guid>
should clearly resolve toGuid
and nothing else.Not currently. This would require the language to be able to specially annotate and recognize a specific generic parameter as the
this
concept.I proposed a couple different syntaxes for this here: https://github.com/dotnet/csharplang/issues/4436#issuecomment-896981841
I’ve been waiting for this for so long 🙂
Will it be possible to write something like
or will we have to write
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
.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.?
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)