To get a trial key
fill out the form below
Team License (a basic version)
Enterprise License (an extended version)
* By clicking this button you agree to our Privacy Policy statement

Request our prices
New License
License Renewal
--Select currency--
USD
EUR
GBP
RUB
* By clicking this button you agree to our Privacy Policy statement

Free PVS-Studio license for Microsoft MVP specialists
* By clicking this button you agree to our Privacy Policy statement

To get the licence for your open-source project, please fill out this form
* By clicking this button you agree to our Privacy Policy statement

I am interested to try it on the platforms:
* By clicking this button you agree to our Privacy Policy statement

Message submitted.

Your message has been sent. We will email you at


If you haven't received our response, please do the following:
check your Spam/Junk folder and click the "Not Spam" button for our message.
This way, you won't miss messages from our team in the future.

>
>
>
Generic Math: C# super feature availabl…

Generic Math: C# super feature available in .NET 6 Preview 7

Oct 27 2021
Author:

On August 10th, 2021, Microsoft announced the .NET 6 Preview 7 release.

We published and translated this article with the copyright holder's permission. The author is DistortNeo. The article was originally published on Habr.

[The link to the .NET 6 Preview 7 announcement.]

Besides another "spoonful" of syntactic sugar, enhanced libraries functionality, improved UTF-8 support, and so on, Microsoft demonstrates super feature — static abstract interface methods. These allow you to implement arithmetic operators in generics:

T Add<T>(T lhs, T rhs)
    where T : INumber<T>
{
    return lhs + rhs;
}
0878_Generic_Math_in_CSharp/image1.png

Introduction

So far, in C# you couldn't distract from static methods and write generalized code. This is extremely challenging for methods that exist only as static methods, such as, operators.

For example, in LINQ to objects, .Max, .Sum, .Average functions and so on are implemented separately for each of the simple types. For user-defined types, it is proposed to pass a delegate. This is inconvenient and inefficient — you can make a mistake with multiple code duplication. And the delegate call is not free (however, zero-cost delegates implementation in the JIT compiler is already discussed).

The feature allows writing generalized code as compared to, for example, numeric types, that are restricted by interfaces with the necessary operators. Thus, algorithms may have the following form:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}
// Classes and structs (including built-ins) can implement interface
struct Int32 : ..., IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}
// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}
// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Implementation

Syntax

Static members that are the interface contract's part are declared with static and abstract keywords.

Although the word static is a proper word to describe such methods, one of the recent updates allowed to declare helper static methods in interfaces. That's why, to distinguish helper methods from static contract members, it was decided to use the abstract modifier.

In general, not only operators can be contract members. Any static methods, properties, events also can be contract members. Static interface members are naturally implemented in the class.

You can call static interface methods only via generic type and only if the specific constraint is defined for the type:

public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;            // Correct
    T result2 = IAddable<T>.Zero; // Incorrect
}

Moreover, static methods never were and never will be virtual:

interface IStatic
{
    static abstract int StaticValue { get; }
    int Value { get; }
}
class Impl1 : IStatic
{
    public static int StaticValue => 1;
    public int Value => 1;
}
class Impl2 : Impl1, IStatic
{
    public static int StaticValue => 2;
    public int Value => 2;
}
static void Print<T>(T obj)
    where T : IStatic
{  
    Console.WriteLine("{0}, {1}", T.StaticValue, obj.Value);
}
static void Test()
{
    Impl1 obj1 = new Impl1();
    Impl2 obj2 = new Impl2();
    Impl1 obj3 = obj2;
    Print(obj1);    // 1, 1
    Print(obj2);    // 2, 2
    Print(obj3);    // 1, 2
}

The static interface method call is defined at the compilation stage (actually, during JIT compilation, not during the C# code build). Thus, we can exclaim: yay, now C# has static polymorphism!

Under the hood

Take a look at the generated IL code for the simplest function adding up two numbers:

.method private hidebysig static !!0/*T*/
  Sum<(class [System.Runtime]System.INumber`1<!!0/*T*/>) T>(
    !!0/*T*/ lhs,
    !!0/*T*/ rhs
  ) cil managed
{
  .maxstack 8
  // [4903 17 - 4903 34]
  IL_0000: ldarg.0      // lhs
  IL_0001: ldarg.1      // rhs
  IL_0002: constrained. !!0/*T*/
  IL_0008: call !2/*T*/ class ....::op_Addition(!0/*T*/, !1/*T*/)
  IL_000d: ret
} // end of method GenericMathTest::Sum

Nothing special: just a non-virtual call of the static interface method for the T type (callvirt – for virtual calls). Of course: you can't make a virtual call without an object.

At first, I thought that this was sugar produced by some magical objects created in a single instance for each type-interface pair. Actually, no. This is a decent implementation of a new feature at the JIT compiler level: for simple types, the compiler generates the instruction of the corresponding operation; for other types, it calls the corresponding method. Therefore, the code with new features will not work on older runtime versions.

Also, we can guess that each combination of generalized types, for whom static interface methods are called, will have the method compiled by the JIT compiler. That is, the performance of generalized methods that call static interface methods should not differ from the performance of individual implementations.

Status

Despite an opportunity to try this feature right now, it is scheduled for the .NET 7 release. After the .NET 6 release, it remains in the preview state. Now, this feature is under development. The details of its implementation may change, so you can't use it right away.

How to try it

To try the new feature, you need to add the EnablePreviewFeatures=true property to the project file and install the NuGet package – System.Runtime.Experimental:

<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>

Of course, you have to install .NET 6 Preview 7 SDK and define net6.0 as the target platform.

My experience

Tried it and loved it. This is something that I've been waiting for a long time. Previously, I had to use duct tapes to solve the problem. For example:

interface IOperationProvider<T>
{
    T Sum(T lhs, T rhs)
}
void SomeProcessing<T, TOperation>(...)
    where TOperation : struct, IOperationProvider<T>
{
    T var1 = ...;
    T var2 = ...;
    T sum = default(TOperation).Sum(var1, var2);  // This is zero cost!
}

Instead of such duct tape, you can use the IOperation implementation with the T type and the var1.Sum(var2) call. In this case, virtual calls cause a loss of performance. Moreover, you can't get into all classes and add the interface.

Another benefit is the performance! I ran some benchmarks: the runtime of the usual code and the code with Generic Math turned out to be the same. That is, earlier, I was right about the JIT compilation.

But I was slightly disappointed to know that this feature does not work with enums. You still have to compare them via EqualityComparer<T>.Default.Equals.

Also, I didn't like that I had to use abstract as a duct tape. C# seems to get complicated. Now it's difficult to add new features without affecting previous features. In fact, C# becomes more and more like C++.

Popular related articles
The Evil within the Comparison Functions

Date: May 19 2017

Author: Andrey Karpov

Perhaps, readers remember my article titled "Last line effect". It describes a pattern I've once noticed: in most cases programmers make an error in the last line of similar text blocks. Now I want t…
Characteristics of PVS-Studio Analyzer by the Example of EFL Core Libraries, 10-15% of False Positives

Date: Jul 31 2017

Author: Andrey Karpov

After I wrote quite a big article about the analysis of the Tizen OS code, I received a large number of questions concerning the percentage of false positives and the density of errors (how many erro…
Technologies used in the PVS-Studio code analyzer for finding bugs and potential vulnerabilities

Date: Nov 21 2018

Author: Andrey Karpov

A brief description of technologies used in the PVS-Studio tool, which let us effectively detect a large number of error patterns and potential vulnerabilities. The article describes the implementati…
PVS-Studio for Java

Date: Jan 17 2019

Author: Andrey Karpov

In the seventh version of the PVS-Studio static analyzer, we added support of the Java language. It's time for a brief story of how we've started making support of the Java language, how far we've co…
Static analysis as part of the development process in Unreal Engine

Date: Jun 27 2017

Author: Andrey Karpov

Unreal Engine continues to develop as new code is added and previously written code is changed. What is the inevitable consequence of ongoing development in a project? The emergence of new bugs in th…
The Last Line Effect

Date: May 31 2014

Author: Andrey Karpov

I have studied many errors caused by the use of the Copy-Paste method, and can assure you that programmers most often tend to make mistakes in the last fragment of a homogeneous code block. I have ne…
Free PVS-Studio for those who develops open source projects

Date: Dec 22 2018

Author: Andrey Karpov

On the New 2019 year's eve, a PVS-Studio team decided to make a nice gift for all contributors of open-source projects hosted on GitHub, GitLab or Bitbucket. They are given free usage of PVS-Studio s…
PVS-Studio ROI

Date: Jan 30 2019

Author: Andrey Karpov

Occasionally, we're asked a question, what monetary value the company will receive from using PVS-Studio. We decided to draw up a response in the form of an article and provide tables, which will sho…
The Ultimate Question of Programming, Refactoring, and Everything

Date: Apr 14 2016

Author: Andrey Karpov

Yes, you've guessed correctly - the answer is "42". In this article you will find 42 recommendations about coding in C++ that can help a programmer avoid a lot of errors, save time and effort. The au…
Appreciate Static Code Analysis!

Date: Oct 16 2017

Author: Andrey Karpov

I am really astonished by the capabilities of static code analysis even though I am one of the developers of PVS-Studio analyzer myself. The tool surprised me the other day as it turned out to be sma…

Comments (0)

Next comments
This website uses cookies and other technology to provide you a more personalized experience. By continuing the view of our web-pages you accept the terms of using these files. If you don't want your personal data to be processed, please, leave this site.
Learn More →
Accept