Compartir vía


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 clase C, el método base invalidado se determina mediante el examen de 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 determinado se encuentra al menos un método accesible que tiene la misma firma que M 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 interfaz I, el método base invalidado se determina examinando cada interfaz base directa o indirecta de I, recopilando el conjunto de interfaces que declaran un método accesible que tiene la misma firma que M 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 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. 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 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 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 interfaz B cuando:

  • A y B son métodos, y las listas de nombres, tipos y parámetros formales de A y B son idénticas.
  • 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 puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz).
  • A y B son eventos, y el nombre y el tipo de A y B son idénticos.
  • A y B son indizadores, las listas de parámetros formales y de tipo de A y B son idénticas y A tiene los mismos descriptores de acceso que B (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 interfaz B cuando:

  • 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 puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz) y el tipo de A se puede convertir al tipo de valor devuelto de B a través de una conversión de identidad o, si A es una propiedad readonly, una conversión de referencia implícita.
  • A y B son eventos, y el nombre y el tipo de A y B son idénticos.
  • A y B son indexadores, las listas de parámetros formales de A y B son idénticas, A tiene los mismos descriptores de acceso que B (A puede tener descriptores de acceso adicionales si no es una implementación explícita de miembro de interfaz) y el tipo de A se puede convertir al tipo de valor devuelto de B a través de 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 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