Matemáticas genéricas:superfunción de C# disponible en .NET 6 Preview 7

 C Programming >> Programación C >  >> Tags >> .NET
Matemáticas genéricas:superfunción de C# disponible en .NET 6 Preview 7

El 10 de agosto de 2021, Microsoft anunció el lanzamiento de .NET 6 Preview 7.

Publicamos y traducimos este artículo con el permiso del titular de los derechos de autor. El autor es DistortNeo. El artículo fue publicado originalmente en Habr.

[El enlace al anuncio de .NET 6 Preview 7.]

Además de otra "cucharada" de azúcar sintáctica, funcionalidad de bibliotecas mejorada, soporte mejorado de UTF-8, etc., Microsoft demuestra una característica excelente:métodos de interfaz abstractos estáticos. Estos le permiten implementar operadores aritméticos en genéricos:

T Add<T>(T lhs, T rhs)
    where T : INumber<T>
{
    return lhs + rhs;
}

Introducción

Hasta ahora, en C# no podía distraerse de los métodos estáticos y escribir código generalizado. Esto es extremadamente desafiante para los métodos que existen solo como métodos estáticos, como los operadores.

Por ejemplo, en LINQ a objetos, .Max , .Suma , .Promedio Las funciones, etc., se implementan por separado para cada uno de los tipos simples. Para los tipos definidos por el usuario, se propone pasar un delegado. Esto es inconveniente e ineficiente:puede cometer un error con la duplicación de varios códigos. Y la llamada de delegado no es gratuita (sin embargo, ya se ha discutido la implementación de delegados de costo cero en el compilador JIT).

La función permite escribir código generalizado en comparación con, por ejemplo, tipos numéricos, que están restringidos por interfaces con los operadores necesarios. Así, los algoritmos pueden tener la siguiente forma:

// 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 });

Implementación

Sintaxis

Los miembros estáticos que forman parte del contrato de interfaz se declaran con static y resumen palabras clave.

Aunque la palabra estático es una palabra adecuada para describir tales métodos, una de las actualizaciones recientes permitió declarar métodos estáticos auxiliares en las interfaces. Por eso, para distinguir los métodos auxiliares de los miembros del contrato estáticos, se decidió utilizar el resumen modificador.

En general, no sólo los operadores pueden ser miembros del contrato. Cualquier método estático, propiedades, eventos también pueden ser miembros del contrato. Los miembros de la interfaz estática se implementan naturalmente en la clase.

Puede llamar a métodos de interfaz estática solo a través de un tipo genérico y solo si la restricción específica está definida para el tipo:

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

Además, los métodos estáticos nunca fueron y nunca serán virtuales:

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
}

La llamada al método de interfaz estática se define en la etapa de compilación (en realidad, durante la compilación JIT, no durante la compilación del código C#). Por lo tanto, podemos exclamar:¡bien, ahora C# tiene polimorfismo estático!

Debajo del capó

Eche un vistazo al código IL generado para la función más simple que suma dos números:

.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

Nada especial:solo una llamada no virtual del método de interfaz estática para el tipo T (callvirt – para llamadas virtuales). Por supuesto:no puedes hacer una llamada virtual sin un objeto.

Al principio, pensé que se trataba de azúcar producida por algunos objetos mágicos creados en una sola instancia para cada par tipo-interfaz. En realidad no. Esta es una implementación decente de una nueva característica a nivel del compilador JIT:para tipos simples, el compilador genera la instrucción de la operación correspondiente; para otros tipos, llama al método correspondiente. Por lo tanto, el código con nuevas funciones no funcionará en versiones de tiempo de ejecución anteriores.

Además, podemos suponer que cada combinación de tipos generalizados, para los que se llaman métodos de interfaz estática, tendrá el método compilado por el compilador JIT. Es decir, el rendimiento de los métodos generalizados que llaman a métodos de interfaz estática no debe diferir del rendimiento de las implementaciones individuales.

Estado

A pesar de la oportunidad de probar esta función ahora mismo, está programada para el lanzamiento de .NET 7. Después del lanzamiento de .NET 6, permanece en el estado de vista previa. Ahora, esta característica está en desarrollo. Los detalles de su implementación pueden cambiar, por lo que no puede usarlo de inmediato.

Cómo probarlo

Para probar la nueva función, debe agregar EnablePreviewFeatures=true propiedad en el archivo del proyecto e instale el paquete NuGet: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>

Por supuesto, debe instalar .NET 6 Preview 7 SDK y definir net6.0 como plataforma de destino.

Mi experiencia

Lo probé y me encantó. Esto es algo que he estado esperando durante mucho tiempo. Anteriormente, tenía que usar cintas adhesivas para conductos para resolver el problema. Por ejemplo:

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

En lugar de esa cinta adhesiva, puede usar la IOperación implementación con el tipo T y el var1.Sum(var2) llamar. En este caso, las llamadas virtuales provocan una pérdida de rendimiento. Además, no puede ingresar a todas las clases y agregar la interfaz.

¡Otro beneficio es el rendimiento! Ejecuté algunos puntos de referencia:el tiempo de ejecución del código habitual y el código con Generic Math resultó ser el mismo. Es decir, antes tenía razón sobre la compilación JIT.

Pero me decepcionó un poco saber que esta función no funciona con las enumeraciones. Todavía tienes que compararlos a través de EqualityComparer.Default.Equals .

Además, no me gustó que tuviera que usar abstract como cinta adhesiva. C# parece complicarse. Ahora es difícil agregar nuevas funciones sin afectar las funciones anteriores. De hecho, C# se parece cada vez más a C++.