Compartir a través de


Miembros abstractos estáticos en interfaces

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/4436

Resumen

En una interfaz se permite especificar miembros estáticos abstractos que las clases y estructuras implementadoras deben proporcionar una implementación explícita o implícita de. Se puede acceder a los miembros fuera de los parámetros de tipo restringidos por la interfaz.

Motivación

Actualmente no hay ninguna manera de abstraer miembros estáticos y escribir código generalizado que se aplica a través de tipos que definen esos miembros estáticos. Esto es especialmente problemático para los tipos de miembros que solo existen en un formato estático, especialmente los operadores.

Esta característica permite algoritmos genéricos sobre tipos numéricos, representados por restricciones de interfaz que especifican la presencia de operadores dados. Por lo tanto, los algoritmos se pueden expresar en términos de estos operadores:

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

Sintaxis

Miembros de interfaz

La característica permitiría que los miembros de la interfaz estática se declararan virtuales.

Reglas anteriores a C# 11

Antes de C# 11, los miembros de instancia de las interfaces son implícitamente abstractos (o virtuales si tienen una implementación predeterminada), pero opcionalmente pueden tener un modificador abstract (o virtual). Los miembros de instancia no virtual deben marcarse explícitamente como sealed.

Los miembros de interfaz estática son implícitamente no virtuales y no permiten los modificadores abstract, virtual o sealed.

Propuesta

Miembros estáticos abstractos

También se permite que los miembros de la interfaz estática que no sean campos tengan el modificador abstract. No se permite que los miembros estáticos abstractos tengan un cuerpo (o, en el caso de las propiedades, los accesores no pueden tener un cuerpo).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
Miembros estáticos virtuales

También se permite que los miembros de la interfaz estática que no sean campos tengan el modificador virtual. Los miembros estáticos virtuales deben tener un cuerpo.

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
Miembros estáticos explícitamente no virtuales

Para mantener la simetría con los miembros de instancia no virtuales, los miembros estáticos (excepto los campos) deberían permitir un modificador opcional sealed, aunque por defecto no sean virtuales.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementación de miembros de interfaz

Reglas de hoy

Las clases y estructuras pueden implementar miembros de instancia abstractos de interfaces de forma implícita o explícita. Un miembro de interfaz implementado implícitamente es una declaración de miembro normal (virtual o no virtual) de la clase o struct que "casualmente" también implementa el miembro de interfaz. El miembro puede incluso heredarse de una clase base y, por tanto, no estar presente en la declaración de la clase.

Un miembro de interfaz implementado explícitamente usa un nombre calificado para identificar el miembro de interfaz en cuestión. La implementación no es accesible directamente como miembro de la clase o estructura, sino solo a través de la interfaz .

Propuesta

No se necesita ninguna sintaxis nueva en clases y estructuras para facilitar la implementación implícita de miembros de interfaz abstracta estática. Las declaraciones de miembros estáticos existentes sirven para ese propósito.

Las implementaciones explícitas de los miembros de interfaz abstracta estática usan un nombre calificado junto con el modificador static.

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

Semántica

Restricciones de operador

En la actualidad, todas las declaraciones de operador unario y binario tienen algún requisito que implique al menos uno de sus operandos para que sean de tipo T o T?, donde T es el tipo de instancia del tipo envolvente.

Estos requisitos deben relajarse para que se permita que un operando restringido sea de un parámetro de tipo que se considere como "el tipo de instancia del tipo de inclusión".

Para que un parámetro de tipo T se considere como "el tipo de instancia del tipo envolvente", debe cumplir los siguientes requisitos:

  • T es un parámetro de tipo directo en la interfaz en la que se produce la declaración del operador y
  • T está directamente restringido por lo que la especificación llama "tipo de instancia", es decir, la interfaz circundante con sus propios parámetros de tipo usados como argumentos de tipo.

Operadores y conversiones de igualdad

Se permitirán declaraciones abstractas o virtuales de operadores de == y !=, así como declaraciones abstractas o virtuales de operadores de conversión implícitos y explícitos en interfaces. También se permitirán interfaces derivadas para implementarlas.

Para los operadores == y !=, al menos un tipo de parámetro debe ser un parámetro de tipo que cuente como "el tipo de instancia del tipo envolvente", tal como se define en la sección anterior.

Implementación de miembros abstractos estáticos

Las reglas para determinar cuándo una declaración de miembro estático en una clase o estructura se considera que implementa un miembro estático de una interfaz abstracta, y los requisitos que se aplican cuando lo hace, son los mismos que los de los miembros de instancia.

TBD: puede haber reglas adicionales o diferentes necesarias aquí que aún no hemos pensado.

Interfaces como argumentos de tipo

Hemos analizado el problema generado por https://github.com/dotnet/csharplang/issues/5955 y decidimos agregar una restricción en torno al uso de una interfaz como argumento de tipo (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Esta es la restricción propuesta por https://github.com/dotnet/csharplang/issues/5955 y aprobada por el LDM.

Una interfaz que contiene o hereda un miembro abstracto o virtual estático que no tiene la implementación más específica en la interfaz no se puede usar como argumento de tipo. Si todos los miembros abstractos o virtuales estáticos tienen una implementación más específica, la interfaz se puede usar como argumento de tipo.

Acceso a los miembros de la interfaz abstracta estática

Se puede acceder a un miembro estático abstracto de la interfaz M en un parámetro de tipo T mediante la expresión T.M cuando T está restringido por una interfaz I, y M es un miembro estático abstracto accesible de I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

En tiempo de ejecución, la implementación del miembro real utilizada es la que existe en el tipo real proporcionado como argumento de tipo.

C c = M<C>(); // The static members of C get called

Como las expresiones de consulta se especifican como una reescritura sintáctica, C# te permite realmente usar un tipo como origen de la consulta, siempre que tenga miembros estáticos para los operadores de consulta que uses. En otras palabras, si encaja en la sintaxis, ¡lo permitimos! Creemos que este comportamiento no era intencional o importante en el LINQ original y no queremos hacer el trabajo para admitirlo en los parámetros de tipo. Si hay escenarios por ahí, oiremos hablar de ellos y podremos optar por adoptarlos más adelante.

Seguridad de la variación §18.2.3.2

Las reglas de seguridad de varianza deben aplicarse a las firmas de miembros abstractos estáticos. La adición propuesta en https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety debe ajustarse a partir de

Estas restricciones no se aplican a las apariciones de tipos dentro de declaraciones de miembros estáticos.

to

Estas restricciones no se aplican a las apariciones de tipos dentro de declaraciones de miembros estáticos no virtuales y no abstractos.

§10.5.4 Conversiones implícitas definidas por el usuario

Las siguientes viñetas

  • Determine los tipos S, S₀ y T₀.
    • Si E tiene un tipo, entonces que S sea ese tipo.
    • Si S o T son tipos de valor que aceptan valores NULL, deje que Sᵢ y Tᵢ sean sus tipos subyacentes; de lo contrario, deje que Sᵢ y Tᵢ sean S y T, respectivamente.
    • Si Sᵢ o Tᵢ son parámetros de tipo, deje que S₀ y T₀ sean sus clases base efectivas; de lo contrario, deje que S₀ y T₀ sean Sₓ y Tᵢ, respectivamente.
  • Busque el conjunto de tipos, D, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de S0 (si S0 es una clase o estructura), las clases base de S0 (si S0 es una clase) y T0 (si T0 es una clase o estructura).
  • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U. Este conjunto consta de los operadores de conversión implícitos, definidos por el usuario y elevados, declarados por las clases o estructuras de D que convierten de un tipo que incluye S a un tipo incluido por T. Si U está vacío, la conversión no está definida y se produce un error en tiempo de compilación.

se ajustan de la manera siguiente:

  • Determine los tipos S, S₀ y T₀.
    • Si E tiene un tipo, deje que S sea ese tipo.
    • Si S o T son tipos de valor que aceptan valores NULL, deje que Sᵢ y Tᵢ sean sus tipos subyacentes; de lo contrario, deje que Sᵢ y Tᵢ sean S y T, respectivamente.
    • Si Sᵢ o Tᵢ son parámetros de tipo, deje que S₀ y T₀ sean sus clases base efectivas; de lo contrario, deje que S₀ y T₀ sean Sₓ y Tᵢ, respectivamente.
  • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U.
    • Busque el conjunto de tipos, D1, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de S0 (si S0 es una clase o estructura), las clases base de S0 (si S0 es una clase) y T0 (si T0 es una clase o estructura).
    • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U1. Este conjunto consta de los operadores de conversión implícita definidos por el usuario y levantados, declarados por las clases o structs de D1, que convierten de un tipo que abarca S a un tipo abarcado por T.
    • Si U1 no está vacío, entonces U es U1. De lo contrario,
      • Busque el conjunto de tipos, D2, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de Sᵢconjunto eficaz de interfaz y sus interfaces base (si Sᵢ es un parámetro de tipo) y Tᵢconjunto eficaz de interfaz (si Tᵢ es un parámetro de tipo).
      • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U2. Este conjunto consta de los operadores de conversión implícita definidos por el usuario y levantados, declarados por las interfaces de D2, que convierten de un tipo que abarca S a un tipo abarcado por T.
      • Si U2 no está vacío, entonces U es U2
  • Si U está vacío, la conversión no está definida y se produce un error en tiempo de compilación.

§10.3.9 Conversiones explícitas definidas por el usuario

Las siguientes viñetas

  • Determine los tipos S, S₀ y T₀.
    • Si E tiene un tipo, que S sea ese tipo.
    • Si S o T son tipos de valor que aceptan valores NULL, deje que Sᵢ y Tᵢ sean sus tipos subyacentes; de lo contrario, deje que Sᵢ y Tᵢ sean S y T, respectivamente.
    • Si Sᵢ o Tᵢ son parámetros de tipo, deje que S₀ y T₀ sean sus clases base efectivas; de lo contrario, deje que S₀ y T₀ sean Sᵢ y Tᵢ, respectivamente.
  • Busque el conjunto de tipos, D, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de S0 (si S0 es una clase o estructura), las clases base de S0 (si S0 es una clase), T0 (si T0 es una clase o estructura) y las clases base de T0 (si T0 es una clase).
  • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U. Este conjunto consiste en los operadores de conversión implícitos o explícitos definidos por el usuario y levantados declarados por las clases o structs en D que convierten de un tipo englobado o englobado por S a un tipo englobado o englobado por T. Si U está vacío, la conversión no está definida y se produce un error en tiempo de compilación.

se ajustan de la manera siguiente:

  • Determine los tipos S, S₀ y T₀.
    • Si E tiene un tipo, que S sea ese tipo.
    • Si S o T son tipos de valor que aceptan valores NULL, deje que Sᵢ y Tᵢ sean sus tipos subyacentes; de lo contrario, deje que Sᵢ y Tᵢ sean S y T, respectivamente.
    • Si Sᵢ o Tᵢ son parámetros de tipo, deje que S₀ y T₀ sean sus clases base efectivas; de lo contrario, deje que S₀ y T₀ sean Sᵢ y Tᵢ, respectivamente.
  • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U.
    • Busque el conjunto de tipos, D1, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de S0 (si S0 es una clase o estructura), las clases base de S0 (si S0 es una clase), T0 (si T0 es una clase o estructura) y las clases base de T0 (si T0 es una clase).
    • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U1. Este conjunto consiste en los operadores de conversión implícitos o explícitos definidos por el usuario y levantados declarados por las clases o structs en D1 que convierten de un tipo englobado o englobado por S a un tipo englobado o englobado por T.
    • Si U1 no está vacío, entonces U es U1. De lo contrario,
      • Busque el conjunto de tipos, D2, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta de Sᵢconjunto de interfaz eficaz y sus interfaces base (si Sᵢ es un parámetro de tipo) y Tᵢconjunto de interfaz eficaz y sus interfaces base (si Tᵢ es un parámetro de tipo).
      • Encuentre el conjunto de operadores de conversión aplicables definidos por el usuario y levantados, U2. Este conjunto consiste en los operadores de conversión implícitos o explícitos definidos por el usuario y levantados, declarados por las interfaces de D2 que convierten de un tipo englobado o englobado por S a un tipo englobado o englobado por T.
      • Si U2 no está vacío, entonces U es U2
  • Si U está vacío, la conversión no está definida y se produce un error en tiempo de compilación.

Implementaciones predeterminadas

Una característica adicional para esta propuesta es permitir que los miembros virtuales estáticos de las interfaces tengan implementaciones predeterminadas, igual que lo hacen los miembros virtuales o abstractos de instancia.

Una complicación aquí es que las implementaciones predeterminadas tendrían que llamar a otros miembros virtuales estáticos "virtualmente". Permitir llamar a miembros virtuales estáticos directamente en la interfaz requeriría el flujo de un parámetro de tipo oculto que representa el tipo "self" en el que realmente se invocó el método estático actual. Esto parece complicado, costoso y potencialmente confuso.

Hemos analizado una versión más sencilla que mantiene las limitaciones de la propuesta actual de que los miembros virtuales estáticos pueden solo invocarse en parámetros de tipo. Dado que las interfaces con miembros virtuales estáticos a menudo tendrán un parámetro de tipo explícito que representa un tipo 'self', esto no sería una gran pérdida: se podría llamar a otros miembros virtuales estáticos en ese tipo 'self'. Esta versión es mucho más sencilla y parece bastante fácil.

En https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics decidimos admitir implementaciones predeterminadas de miembros estáticos siguiendo o expandiendo las reglas establecidas en https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md en consecuencia.

Coincidencia de patrones

Dado el código siguiente, un usuario podría esperar razonablemente que imprima "True" (como lo haría si el patrón de constante se escribió en línea):

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

Sin embargo, dado que el tipo de entrada del patrón no es double, el patrón constante 1 comprobará primero el T entrante contra int. Esto no es adecuado, por lo que se bloquea hasta que una versión futura de C# agrega un mejor control para la coincidencia numérica con los tipos derivados de INumberBase<T>. Para ello, diremos que, reconoceremos explícitamente INumberBase<T> como el tipo del que se derivarán todos los "números" y bloquearemos el patrón si intentamos hacer coincidir un patrón de constante numérico con un tipo de número que no podemos representar el patrón en (es decir, un parámetro de tipo restringido a INumberBase<T>o un tipo de número definido por el usuario que hereda de INumberBase<T>).

Formalmente, agregamos una excepción a la definición de compatibles con patrones constantes:

Un patrón constante comprueba el valor de una expresión con un valor constante. La constante puede ser cualquier expresión constante, como un literal, el nombre de una variable declarada const o una constante de enumeración. Cuando el valor de entrada no es un tipo abierto, la expresión constante se convierte implícitamente en el tipo de la expresión coincidente; si el tipo del valor de entrada no es compatibles con el patrón con el tipo de la expresión constante, la operación de coincidencia de patrones es un error. Si la expresión constante con la que se coincide es un valor numérico, el valor de entrada es un tipo que hereda de System.Numerics.INumberBase<T>y no hay ninguna conversión constante de la expresión constante al tipo del valor de entrada, la operación de coincidencia de patrones es un error.

También agregamos una excepción similar para los patrones relacionales:

Cuando la entrada es un tipo para el que se define un operador relacional binario integrado adecuado que se aplica con la entrada como su operando izquierdo y la constante dada como su operando derecho, la evaluación de ese operador se toma como el significado del patrón relacional. De lo contrario, convertimos la entrada al tipo de la expresión mediante una conversión explícita que acepta valores NULL o de unboxing. Es un error en tiempo de compilación si no existe dicha conversión. Se trata de un error en tiempo de compilación si el tipo de entrada es un parámetro de tipo restringido a o un tipo que hereda de System.Numerics.INumberBase<T> y el tipo de entrada no tiene definido ningún operador relacional binario integrado adecuado. El patrón se considera que no coincide si se produce un error en la conversión. Si la conversión se realiza correctamente, el resultado de la operación de coincidencia de patrones es el resultado de evaluar la expresión e OP v donde e es la entrada convertida, OP es el operador relacional y v es la expresión constante.

Inconvenientes

  • "static abstract" es un nuevo concepto y se agregará significativamente a la carga conceptual de C#.
  • No es una característica barata para crear. Debemos asegurarnos de que valga la pena.

Alternativas

Restricciones estructurales

Un enfoque alternativo sería tener "restricciones estructurales" directamente y requerir explícitamente la presencia de operadores específicos en un parámetro de tipo. Los inconvenientes de esto son: - Esto tendría que escribirse cada vez. Tener una restricción con nombre parece mejor. - Se trata de un nuevo tipo de restricción, mientras que la característica propuesta utiliza el concepto existente de restricciones de interfaz. - Solo funcionaría para operadores, no (de forma fácil) para otros tipos de miembros estáticos.

Preguntas sin resolver

Interfaces abstractas estáticas y clases estáticas

Consulte https://github.com/dotnet/csharplang/issues/5783 y https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes para obtener más información.

Reuniones de diseño