Pour obtenir une clé
d'essai remplissez le formulaire ci-dessous
Demandez des tariffs
Nouvelle licence
Renouvellement de licence
--Sélectionnez la devise--
USD
EUR
RUB
* En cliquant sur ce bouton, vous acceptez notre politique de confidentialité

Free PVS-Studio license for Microsoft MVP specialists
To get the licence for your open-source project, please fill out this form
** En cliquant sur ce bouton, vous acceptez notre politique de confidentialité.

I am interested to try it on the platforms:
** En cliquant sur ce bouton, vous acceptez notre politique de confidentialité.

Votre message a été envoyé.

Nous vous répondrons à


Si vous n'avez toujours pas reçu de réponse, vérifiez votre dossier
Spam/Junk et cliquez sur le bouton "Not Spam".
De cette façon, vous ne manquerez la réponse de notre équipe.

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

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

27 Oct 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
Sorting in C#: OrderBy.OrderBy or OrderBy.ThenBy? What's more effective and why?

Date: 20 Sep 2022

Author: Sergey Vasiliev

Suppose we need to sort the collection by multiple keys. In C#, we can do this with the help of OrderBy().OrderBy() or OrderBy().ThenBy(). But what is the difference between these calls? To answer th…
ML.NET: can Microsoft's machine learning be trusted?

Date: 08 Sep 2022

Author: Andrey Moskalev

In 2018, Microsoft created ML.NET, a machine learning framework for .NET developers. Since then, the machine learning library has undergone significant changes and acquired new features to identify p…
The risks of using vulnerable dependencies in your project, and how SCA helps manage them

Date: 06 Sep 2022

Author: Nikita Lipilin

Most applications today use third-party libraries. If such a library contains a vulnerability, an app that uses this library may also be vulnerable. But how can you identify such problematic dependen…
Build to order? Checking MSBuild for the second time

Date: 01 Sep 2022

Author: Nikita Panevin

MSBuild is a popular open-source build platform created by Microsoft. Developers all over the world use MSBuild. In 2016, we checked it for the first time and found several suspicious places. Can we …
The Orchard Core threequel. Rechecking the project with PVS-Studio

Date: 25 Aoû 2022

Author: Aleksey Avdeev

In this article, we check the Orchard Core project with the help of the PVS-Studio static analyzer. We are going to find out if the platform code is as good as the sites created on its basis. May the…

Comments (0)

Next comments
Unicorn with delicious cookie
Nous utilisons des cookies pour améliorer votre expérience de navigation. En savoir plus
Accepter