Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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₀
yT₀
.- Si
E
tiene un tipo, entonces queS
sea ese tipo. - Si
S
oT
son tipos de valor que aceptan valores NULL, deje queSᵢ
yTᵢ
sean sus tipos subyacentes; de lo contrario, deje queSᵢ
yTᵢ
seanS
yT
, respectivamente. - Si
Sᵢ
oTᵢ
son parámetros de tipo, deje queS₀
yT₀
sean sus clases base efectivas; de lo contrario, deje queS₀
yT₀
seanSₓ
yTᵢ
, respectivamente.
- Si
- Busque el conjunto de tipos,
D
, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta deS0
(siS0
es una clase o estructura), las clases base deS0
(siS0
es una clase) yT0
(siT0
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 deD
que convierten de un tipo que incluyeS
a un tipo incluido porT
. SiU
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₀
yT₀
.- Si
E
tiene un tipo, deje queS
sea ese tipo. - Si
S
oT
son tipos de valor que aceptan valores NULL, deje queSᵢ
yTᵢ
sean sus tipos subyacentes; de lo contrario, deje queSᵢ
yTᵢ
seanS
yT
, respectivamente. - Si
Sᵢ
oTᵢ
son parámetros de tipo, deje queS₀
yT₀
sean sus clases base efectivas; de lo contrario, deje queS₀
yT₀
seanSₓ
yTᵢ
, respectivamente.
- Si
- 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 deS0
(siS0
es una clase o estructura), las clases base deS0
(siS0
es una clase) yT0
(siT0
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 deD1
, que convierten de un tipo que abarcaS
a un tipo abarcado porT
. - Si
U1
no está vacío, entoncesU
esU1
. 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 deSᵢ
conjunto eficaz de interfaz y sus interfaces base (siSᵢ
es un parámetro de tipo) yTᵢ
conjunto eficaz de interfaz (siTᵢ
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 deD2
, que convierten de un tipo que abarcaS
a un tipo abarcado porT
. - Si
U2
no está vacío, entoncesU
esU2
- Busque el conjunto de tipos,
- Busque el conjunto de tipos,
- 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₀
yT₀
.- Si
E
tiene un tipo, queS
sea ese tipo. - Si
S
oT
son tipos de valor que aceptan valores NULL, deje queSᵢ
yTᵢ
sean sus tipos subyacentes; de lo contrario, deje queSᵢ
yTᵢ
seanS
yT
, respectivamente. - Si
Sᵢ
oTᵢ
son parámetros de tipo, deje queS₀
yT₀
sean sus clases base efectivas; de lo contrario, deje queS₀
yT₀
seanSᵢ
yTᵢ
, respectivamente.
- Si
- Busque el conjunto de tipos,
D
, desde el que se considerarán los operadores de conversión definidos por el usuario. Este conjunto consta deS0
(siS0
es una clase o estructura), las clases base deS0
(siS0
es una clase),T0
(siT0
es una clase o estructura) y las clases base deT0
(siT0
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 enD
que convierten de un tipo englobado o englobado porS
a un tipo englobado o englobado porT
. SiU
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₀
yT₀
.- Si
E
tiene un tipo, queS
sea ese tipo. - Si
S
oT
son tipos de valor que aceptan valores NULL, deje queSᵢ
yTᵢ
sean sus tipos subyacentes; de lo contrario, deje queSᵢ
yTᵢ
seanS
yT
, respectivamente. - Si
Sᵢ
oTᵢ
son parámetros de tipo, deje queS₀
yT₀
sean sus clases base efectivas; de lo contrario, deje queS₀
yT₀
seanSᵢ
yTᵢ
, respectivamente.
- Si
- 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 deS0
(siS0
es una clase o estructura), las clases base deS0
(siS0
es una clase),T0
(siT0
es una clase o estructura) y las clases base deT0
(siT0
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 enD1
que convierten de un tipo englobado o englobado porS
a un tipo englobado o englobado porT
. - Si
U1
no está vacío, entoncesU
esU1
. 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 deSᵢ
conjunto de interfaz eficaz y sus interfaces base (siSᵢ
es un parámetro de tipo) yTᵢ
conjunto de interfaz eficaz y sus interfaces base (siTᵢ
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 deD2
que convierten de un tipo englobado o englobado porS
a un tipo englobado o englobado porT
. - Si
U2
no está vacío, entoncesU
esU2
- Busque el conjunto de tipos,
- Busque el conjunto de tipos,
- 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 deSystem.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
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-05.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-06.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md
C# feature specifications