Compartir a través de


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 y 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 especificaciones.

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

Resumen

Soporta tipos de retorno covariantes. Específicamente, permitir la anulación de un método para declarar un tipo de retorno más derivado que el método que anula, y de manera similar permitir la anulació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 anulado y el método base anulado tienen el mismo tipo de retorno.

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 a esa lista se añaden los siguientes requisitos adicionales:

  • 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 del proyecto de especificación que figura a continuación propone otra ampliación a los retornos covariantes de los métodos de interfaz que se 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 siguiente texto en las clases:

El método anulado por una declaración de anulación se conoce como método base anulado. Para un método anulado M declarado en una clase C, el método base overridden se determina examinando cada clase base de C, empezando por la clase base directa de C y continuando con cada clase base directa sucesiva, hasta que en un tipo de clase base dado se localice al menos un método accesible que tenga la misma firma que M tras la sustitución de los argumentos de tipo

se da la especificación correspondiente para interfaces:

El método anulado por una declaración de anulación se conoce como método base anulado. Para un método anulado M declarado en una interfaz I, el método base anulado se determina examinando cada interfaz base directa o indirecta de I, recogiendo el conjunto de interfaces que declaran un método accesible que tiene la misma firma que M tras 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 a una propiedad con una expresión de instancia asociada de E y un tipo asociado que es el tipo de la propiedad. Si T es un tipo de clase, el tipo asociado se elige de la primera declaración o invalidación de la propiedad encontrada al comenzar con T 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 de T o en su interfaz base directa o indirecta. Si no existe un único tipo de este tipo, se produce un error de compilación.

Debería hacerse un cambio similar en §12.8.12.3 Acceso al indizador

En la Sección 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 con T 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 entre T y sus interfaces base directas e indirectas. Si no existe un único tipo de este tipo, se produce un error de compilación.

Implementaciones implícitas de interfaces

Esta sección de la especificación

A efectos de la asignación de interfaces, un miembro de una clase A coincide con un miembro de una interfaz cuando B:

  • Ay B son métodos, y el nombre, tipo y listas de parámetros formales de A y B son idénticos.
  • A y B son propiedades, el nombre y el tipo de A y B son idénticos, y A tiene los mismos descriptores de acceso que B (A se permite que tenga descriptores de acceso adicionales si no es una implementación explícita de un miembro de la interfaz).
  • A y B son eventos, y el nombre y tipo de A y B son idénticos.
  • A y B son indexadores, el tipo y las listas formales de parámetros de A y B son idénticos, y A tiene los mismos descriptores de acceso que B (A se le permite tener descriptores de acceso adicionales si no es una implementación explícita de un miembro de la interfaz).

se modifica como sigue:

A efectos de la asignación de interfaces, un miembro de una clase A coincide con un miembro de una interfaz cuando B:

  • A y B son métodos, y el nombre y las listas formales de parámetros de A y B son idénticos, y el tipo de retorno de A es convertible al tipo de retorno de B mediante una identidad de conversión de referencia implícita al tipo de retorno de B.
  • A y B son propiedades, el nombre de A y B son idénticos, A tiene los mismos descriptores de acceso que B (A se le permite tener descriptores de acceso adicionales si no es una implementación explícita de un miembro de la interfaz), y el tipo de A es convertible al tipo de retorno de B mediante una conversión de identidad o, si A es una propiedad de solo lectura, una conversión de referencia implícita.
  • A y B son eventos, y el nombre y tipo de A y B son idénticos.
  • A y B son indizadores, las listas formales de parámetros de A y B son idénticas, A tiene los mismos descriptores de acceso que B (A se permite que tenga descriptores de acceso adicionales si no es una implementación explícita de un miembro de la interfaz), y el tipo de A es convertible al tipo de retorno de B mediante una conversión de identidad o, si A 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 interfaces

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 la 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(); }

Observe que en I3, los métodos I1.M() y I2.M() se han "fusionado". Cuando se implementa I3, es necesario implementar ambos a la vez.

Generalmente, necesitamos una implementación explícita para referirnos 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 que lo tratemos como una implementación de ambos.

Inconvenientes

  • Cada cambio de idioma debe justificarse por sí mismo.
  • [ ] Deberíamos asegurarnos de que el rendimiento es razonable, incluso en el caso de jerarquías de herencia profundas.
  • [Deberíamos asegurarnos de que los artefactos de la estrategia de traducción no afectan a la semántica del lenguaje, incluso cuando se consumen 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 APIs que han sido compiladas para utilizar esta característica en versiones antiguas del lenguaje?

Reuniones de diseño