Valores devueltos de covariante
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 .
Resumen
Soporta tipos de retorno covariantes. En concreto, permite que la invalidación de un método declare un tipo de valor devuelto más derivado que el método que invalida y, de forma similar, permita la invalidación de una propiedad de solo lectura para declarar un tipo más derivado. Las declaraciones de reemplazo que aparezcan en tipos más derivados deberán proporcionar un tipo de retorno al menos tan específico como el que aparece en las sobreescrituras de sus tipos base. Los invocadores del método o propiedad recibirían estáticamente el tipo de retorno más refinado de una invocación.
Motivación
Es un patrón común en el código que deben inventarse nombres de método diferentes para sortear la restricción de lenguaje de que las anulaciones deben devolver el mismo tipo que el método anulado.
Esto sería útil en el patrón de fábrica. Por ejemplo, en la base de código Roslyn tendríamos
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Diseño detallado
Esta es una especificación para tipos de retorno covariantes en C#. Nuestra intención es permitir que la sobrescritura de un método devuelva un tipo de retorno más derivado que el método que sobrescribe y, de forma similar, permitir que la sobrescritura de una propiedad de solo lectura devuelva un tipo de retorno más derivado. Los invocadores del método o de la propiedad recibirían estáticamente el tipo de retorno más refinado de una invocación, y las modificaciones que aparezcan en tipos más derivados deberían proporcionar un tipo de retorno al menos tan específico como el que aparece en las modificaciones de sus tipos base.
Sobreescritura de métodos de clase
La restricción existente sobre los métodos de sobrescritura de clase (§15.6.5)
- El método override y el método base invalidado tienen el mismo tipo de valor devuelto.
se modifica a
- El método invocado debe tener un tipo de retorno que sea convertible mediante una conversión de identidad o (si el método tiene un retorno de valor, no un retorno de referencia, véase §13.1.0.5) una conversión de referencia implícita al tipo de retorno del método base invocado.
Y los siguientes requisitos adicionales se anexan a esa lista:
- El método invocado debe tener un tipo de retorno que sea convertible mediante una conversión de identidad o (si el método tiene un retorno de valor: no un retorno de referencia, véase §13.1.0.5) una conversión de referencia implícita al tipo de retorno de cada método invocado del método base invocado que esté declarado en un tipo base (directo o indirecto) del método invocado.
- El tipo de retorno del método sobrescritura debe ser al menos tan accesible como el método sobrescritura (Dominios de accesibilidad - §7.5.3).
Esta restricción permite que un método invocado en una clase tenga un tipo private
de retorno private
. Sin embargo, exige que un método de sustitución public
de un tipo public
tenga un tipo de retorno public
.
Sobreescritura de propiedades de clase e indexadores
La restricción existente sobre las propiedades de sobreescritura de clase (§15.7.6)
Una declaración de propiedad sobrescrita debe especificar exactamente los mismos modificadores de accesibilidad y el mismo nombre que la propiedad heredada, y debe haber una conversión de identidad
entre el tipo de la propiedad sobrescrita y el de la propiedad heredada. Si la propiedad heredada solo tiene un único descriptor de acceso (es decir, si la propiedad heredada es de solo lectura o de solo escritura), la propiedad sustituida solo incluirá ese descriptor de acceso. Si la propiedad heredada incluye ambos descriptores de acceso (es decir, si la propiedad heredada es de lectura-escritura), la propiedad de sustitución puede incluir un único descriptor de acceso o ambos.
se modifica a
La declaración de una propiedad de sustitución debe especificar exactamente los mismos modificadores de accesibilidad y el mismo nombre que la propiedad heredada, y debe haber una conversión de identidad o (si la propiedad heredada es de solo lectura y tiene un retorno de valor -no un retorno de referencia§13.1.0.5) una conversión de referencia implícita del tipo de la propiedad de sustitución al tipo de la propiedad heredada. Si la propiedad heredada solo tiene un único descriptor de acceso (es decir, si la propiedad heredada es de solo lectura o de solo escritura), la propiedad sustituida solo incluirá ese descriptor de acceso. Si la propiedad heredada incluye ambos descriptores de acceso (es decir, si la propiedad heredada es de lectura-escritura), la propiedad de sustitución puede incluir un único descriptor de acceso o ambos. El tipo de la propiedad invocada debe ser al menos tan accesible como el de la propiedad invocada (Dominios de accesibilidad - §7.5.3).
El resto de la especificación siguiente propone una extensión adicional a los retornos covariantes de los métodos de interfaz que se deben considerar más adelante.
Sobreescritura de métodos, propiedades e indexadores de la interfaz
Con la adición de la característica DIM en C# 8.0 a los tipos de miembros permitidos en una interfaz, también agregamos compatibilidad con los miembros override
junto con los retornos covariantes. Siguen las reglas de los miembros override
especificadas para las clases, con las siguientes diferencias:
El texto siguiente en las clases:
El método invalidado por una declaración de invalidación se conoce como el método base invalidado . Para un método de invalidación
M
declarado en una claseC
, el método base invalidado se determina mediante el examen de cada clase base deC
, empezando por la clase base directa deC
y continuando con cada clase base directa sucesiva, hasta que en un tipo de clase base determinado se encuentra al menos un método accesible que tiene la misma firma queM
después de la sustitución de argumentos de tipo.
se da la especificación correspondiente para las interfaces:
El método invalidado por una declaración de invalidación se conoce como el método base invalidado . Para un método de invalidación
M
declarado en una interfazI
, el método base invalidado se determina examinando cada interfaz base directa o indirecta deI
, recopilando el conjunto de interfaces que declaran un método accesible que tiene la misma firma queM
después de la sustitución de argumentos de tipo. Si este conjunto de interfaces tiene un tipo más derivado, al que existe una identidad o conversión de referencia implícita desde cada tipo en este conjunto, y ese tipo contiene una única declaración de método, entonces ese es el método base sobrescrita.
Del mismo modo, permitimos propiedades override
e indexadores en interfaces como se especifica para las clases en §15.7.6 Descriptores de acceso virtuales, sellados, sobrescritos y abstractos.
Búsqueda de nombres
La búsqueda de nombres en presencia de declaraciones de clase override
modifica actualmente el resultado de la búsqueda de nombres imponiendo al miembro encontrado detalles de la declaración más derivada override
en la jerarquía de clases a partir del tipo del calificador del identificador (o this
cuando no hay calificador). Por ejemplo, en §12.6.2.2 Parámetros correspondientes tenemos
Para los métodos virtuales y los indexadores definidos en clases, la lista de parámetros se extrae de la primera declaración o anulación del miembro de la función que se encuentra al comenzar con el tipo estático del receptor y buscar a través de sus clases base.
a esto agregamos
Para métodos virtuales e indexadores definidos en interfaces, la lista de parámetros se escoge de la declaración o sobrescritura del miembro de la función que se encuentra en el tipo más derivado entre los tipos que contienen la declaración o sobrescritura del miembro de la función. Si no existe un único tipo de este tipo, se produce un error de compilación.
Para el tipo de resultado de un acceso a una propiedad o a un indexador, el texto existente
- Si
I
identifica una propiedad de instancia, el resultado es un acceso de propiedad con una expresión de instancia asociada deE
y un tipo asociado que es el tipo de la propiedad. SiT
es un tipo de clase, el tipo asociado se elige de la primera declaración o invalidación de la propiedad encontrada al comenzar conT
y buscar en sus clases base.
se completa con
Si
T
es un tipo de interfaz, el tipo asociado se toma de la declaración o de la modificación de la propiedad que se encuentra en la interfaz más derivada deT
o en su interfaz base directa o indirecta. Se produce un error en tiempo de compilación si no existe un tipo único de este tipo.
Debería hacerse un cambio similar en §12.8.12.3 Acceso al indizador
En §12.8.10 Expresiones de invocación aumentamos el texto existente
- En caso contrario, el resultado es un valor, con un tipo asociado del tipo de retorno del método o delegado. Si la invocación es de un método de instancia y el receptor es de un tipo de clase
T
, el tipo asociado se elige de la primera declaración o invalidación del método encontrado al comenzar conT
y buscar entre sus clases base.
con
Si la invocación es de un método de instancia, y el receptor es de un tipo de interfaz
T
, el tipo asociado se escoge de la declaración o sobrescritura del método que se encuentra en la interfaz más derivada de entreT
y sus interfaces base directas e indirectas. Si no existe un único tipo de este tipo, se produce un error de compilación.
Implementaciones de interfaz implícitas
Esta sección de la especificación
Con fines de asignación de interfaz, un miembro de clase
A
coincide con un miembro de interfazB
cuando:
A
yB
son métodos, y las listas de nombres, tipos y parámetros formales deA
yB
son idénticas.A
yB
son propiedades, el nombre y el tipo deA
yB
son idénticos, yA
tiene los mismos descriptores de acceso queB
(A
puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz).A
yB
son eventos, y el nombre y el tipo deA
yB
son idénticos.A
yB
son indizadores, las listas de parámetros formales y de tipo deA
yB
son idénticas yA
tiene los mismos descriptores de acceso queB
(A
puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz).
se modifica de la manera siguiente:
Con fines de asignación de interfaz, un miembro de clase
A
coincide con un miembro de interfazB
cuando:
A
yB
son métodos, y el nombre y las listas formales de parámetros deA
yB
son idénticos, y el tipo de retorno deA
es convertible al tipo de retorno deB
mediante una identidad de conversión de referencia implícita al tipo de retorno deB
.A
yB
son propiedades, el nombre deA
yB
son idénticos,A
tiene los mismos descriptores de acceso queB
(A
puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz) y el tipo deA
se puede convertir al tipo de valor devuelto deB
a través de una conversión de identidad o, siA
es una propiedad readonly, una conversión de referencia implícita.A
yB
son eventos, y el nombre y el tipo deA
yB
son idénticos.A
yB
son indexadores, las listas de parámetros formales deA
yB
son idénticas,A
tiene los mismos descriptores de acceso queB
(A
puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz) y el tipo deA
se puede convertir al tipo de valor devuelto deB
a través de una conversión de identidad o, siA
es un indizador de solo lectura, una conversión de referencia implícita.
Técnicamente, se trata de un cambio de última hora, ya que el siguiente programa imprime "C1.M" en la actualidad, pero imprimiría "C2.M" con la revisión propuesta.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
Debido a este cambio importante, podríamos considerar no admitir tipos de retorno covariante en implementaciones implícitas.
Restricciones en la implementación de interfaz
Necesitaremos una regla de que una implementación de interfaz explícita debe declarar un tipo de valor devuelto que no sea menos derivado que el tipo de valor devuelto declarado en cualquier sobreescritura de sus interfaces base.
Implicaciones de compatibilidad de API
Por determinar
Problemas abiertos
La especificación no dice cómo obtiene el invocador el tipo de retorno más refinado. Es de suponer que se haría de forma similar a la forma en que los invocadores obtienen las especificaciones de los parámetros de la sobrescritura más derivada.
Si tenemos las siguientes interfaces:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Tenga en cuenta que en I3
, los métodos I1.M()
y I2.M()
se han "combinado". Cuando se implementa I3
, es necesario implementar ambos a la vez.
Por lo general, se requiere una implementación explícita para hacer referencia al método original. La cuestión es, en una clase
class C : I1, I2, I3
{
C IN.M();
}
¿Qué significa eso aquí? ¿cuál debería ser N?
Sugiero que permitamos implementar I1.M
o I2.M
(pero no ambos) y tratarlo como una implementación de ambos.
Inconvenientes
- Cada cambio de idioma debe justificarse por sí mismo.
- [ ] Debemos asegurarnos de que el rendimiento sea razonable, incluso en el caso de jerarquías de herencia profunda.
- [ ] Debemos asegurarnos de que los artefactos de la estrategia de traducción no afectan a la semántica del lenguaje, incluso al consumir nuevos IL de compiladores antiguos.
Alternativas
Podríamos relajar las reglas del idioma ligeramente para permitirlo en el texto original.
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Preguntas sin resolver
- [ ] ¿Cómo funcionarán las API que se han compilado para usar esta característica en versiones anteriores del lenguaje?
Reuniones de diseño
- Alguna discusión en https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Discusión offline hacia la decisión de soportar overriding de métodos de clase solo en C# 9.0.
C# feature specifications