métodos de interfaz predeterminados
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 del campeón: https://github.com/dotnet/csharplang/issues/52
Resumen
Se ha añadido compatibilidad para métodos de extensión virtual: métodos con implementaciones concretas en interfaces. Se requiere una clase o struct que implemente dicha interfaz para tener una sola implementación más específica para el método de interfaz, ya sea implementada por la clase o struct, o heredada de sus clases o interfaces base. Los métodos de extensión virtual permiten al autor de una API añadir métodos a una interfaz en futuras versiones sin romper la compatibilidad binaria o de código fuente con las implementaciones existentes de dicha interfaz.
Son similares a los "Métodos por defecto" de Java.
(Basada en la técnica de implementación probable) esta característica requiere el soporte correspondiente en la CLI/CLR. Los programas que aprovechan esta característica no pueden ejecutarse en versiones anteriores de la plataforma.
Motivación
Las principales motivaciones de esta característica son
- Los métodos de interfaz predeterminados permiten al autor de una API añadir métodos a una interfaz en futuras versiones sin romper la compatibilidad binaria o de código fuente con las implementaciones existentes de dicha interfaz.
- Esta característica permite a C# interoperar con API dirigidas a Android (Java) e iOS (Swift), que admiten características similares.
- Resulta que, agregar implementaciones predeterminadas de interfaz proporciona los elementos de la funcionalidad del lenguaje "rasgos" (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Los rasgos han demostrado ser una técnica de programación eficaz (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).
Diseño detallado
La sintaxis de una interfaz se amplía para permitir
- declaraciones de miembros que declaren constantes, operadores, compiladores estáticos y tipos anidados;
- un cuerpo para un método o indexador, propiedad o descriptor de acceso de evento (es decir, una implementación "por defecto").
- declaraciones de miembros que declaran campos estáticos, métodos, propiedades, indexadores y eventos;
- declaraciones de miembros que utilizan la sintaxis de implementación explícita de la interfaz; y
- Modificadores de acceso explícitos (el acceso predeterminado es
public
).
Los miembros con cuerpo permiten que la interfaz proporcione una implementación "por defecto" para el método en clases y estructuras que no proporcionan su propia implementación.
Es posible que las interfaces no contengan estado de instancia. Aunque los campos estáticos ahora están permitidos, los campos de instancia no se permiten en las interfaces. Las propiedades automáticas de instancia no se admiten en las interfaces, ya que declararían de forma implícita un campo oculto.
Los métodos estáticos y privados permiten una refactorización y organización útiles del código utilizado para implementar la API pública de la interfaz.
Una sobrescritura de método en una interfaz debe usar la sintaxis de implementación explícita de interfaz.
Es un error declarar un tipo de clase, un tipo de estructura o un tipo de enumeración dentro del alcance de un parámetro de tipo que fue declarado con una anotación de variancia . Por ejemplo, la declaración de C
siguiente es un error.
interface IOuter<out T>
{
class C { } // error: class declaration within the scope of variant type parameter 'T'
}
Métodos concretos en interfaces
La forma más sencilla de esta característica es la posibilidad de declarar un método concreto en una interfaz, que es un método con un cuerpo.
interface IA
{
void M() { WriteLine("IA.M"); }
}
Una clase que implemente esta interfaz no necesita implementar su método concreto.
class C : IA { } // OK
IA i = new C();
i.M(); // prints "IA.M"
La anulación final de IA.M
en la clase C
es el método concreto M
declarado en IA
. Tenga en cuenta que una clase no hereda miembros de sus interfaces; esto no cambia con esta función:
new C().M(); // error: class 'C' does not contain a member 'M'
Dentro de un miembro de instancia de una interfaz, this
tiene el tipo de interfaz envolvente.
Modificadores en interfaces
La sintaxis de una interfaz es más flexible para permitir modificadores en sus miembros. Se permiten lo siguiente: private
, protected
, internal
, public
, virtual
, abstract
, sealed
, static
, extern
y partial
.
Un miembro de una interfaz cuya declaración incluya un cuerpo es un miembro virtual
a menos que se utilice el modificador sealed
o private
. El modificador virtual
se puede usar en un miembro de función que, de lo contrario, sería implícitamente virtual
. Del mismo modo, aunque abstract
es el valor predeterminado para los miembros de la interfaz que carecen de cuerpos, ese modificador puede especificarse explícitamente. Un miembro no virtual puede declararse utilizando la palabra clave sealed
.
Es un error que un miembro de función private
o sealed
de una interfaz no tenga cuerpo. Un miembro de función private
no puede tener el modificador sealed
.
Los modificadores de acceso se pueden utilizar en los miembros de interfaz de todos los tipos permitidos. El nivel de acceso public
es el predeterminado, pero puede indicarse explícitamente.
problema abierto: Necesitamos especificar el significado preciso de los modificadores de acceso, como
protected
yinternal
, y qué declaraciones los sobrescriben o no (en una interfaz derivada) o los implementan (en una clase que implementa la interfaz).
Las interfaces pueden declarar miembros static
, incluidos tipos anidados, métodos, indexadores, propiedades, eventos y constructores estáticos. El nivel de acceso por defecto para todos los miembros de una interfaz es public
.
Las interfaces no pueden declarar compiladores, destructores o campos de instancia.
Cuestión cerrada: ¿Deberían permitirse las declaraciones de operadores en una interfaz? Probablemente no los operadores de conversión, pero, ¿qué pasa con los demás? Decision: Se permiten los operadores , excepto para los operadores de conversión, igualdad y desigualdad.
Asunto cerrado: ¿Debería permitirse
new
en las declaraciones de miembros de interfaz que ocultan a los miembros de las interfaces base? Decisión: Sí.
Problema cerrado: No permitimos actualmente
partial
en una interfaz, ni en sus miembros. Esto requeriría una propuesta aparte. Decisión: Sí. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface
Implementación explícita en interfaces
Las implementaciones explícitas permiten al programador proporcionar una implementación muy específica de un miembro virtual en una interfaz en la que, de otro modo, el compilador o el tiempo de ejecución no encontrarían ninguna. Se permite que una declaración de implementación implemente explícitamente un método de interfaz base determinado calificando la declaración con el nombre de la interfaz (no se permite ningún modificador de acceso en este caso). Las implementaciones implícitas no están permitidas.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); } // Explicit implementation
}
interface IC : IA
{
void M() { WriteLine("IC.M"); } // Creates a new M, unrelated to `IA.M`. Warning
}
Las implementaciones explícitas en interfaces no pueden declararse sealed
.
Los miembros de función virtual
públicos de una interfaz solo se pueden implementar explícitamente en una interfaz derivada (al calificar el nombre de la declaración con el tipo de interfaz que originalmente declaró el método y omitir un modificador de acceso). El miembro debe ser accesible donde se implementa.
Reabstracción
Un método virtual (concreto) declarado en una interfaz puede reabstraerse en una interfaz derivada.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.
El modificador abstract
es necesario en la declaración de IB.M
, para indicar que IA.M
se está reabstrayendo.
Esto es útil en interfaces derivadas en las que la implementación por defecto de un método es inapropiada y las clases implementadoras deben proporcionar una implementación más adecuada.
Regla de la implementación más específica
Es necesario que cada interfaz y clase tenga una implementación más específica para cada miembro virtual entre las implementaciones que aparecen en el tipo o en sus interfaces directas e indirectas. La implementación más específica es una implementación única que es más específica que cualquier otra implementación. Si no existe ninguna implementación, el propio miembro se considera la implementación más específica.
Una implementación M1
se considera más específica que otra implementación si M2
si M1
se declara en el tipo T1
, M2
se declara en el tipo T2
, y
-
T1
contieneT2
entre sus interfaces directas o indirectas, o -
T2
es un tipo de interfazT1
pero no es un tipo de interfaz.
Por ejemplo:
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // compiles, but error when a class implements 'ID'
abstract class C : IB, IC { } // error: no most specific implementation for 'IA.M'
abstract class D : IA, IB, IC // ok
{
public abstract void M();
}
public class E : ID { } // Error. No most specific implementation for 'IA.M'
La regla de la implementación más específica garantiza que un conflicto (es decir, una ambigüedad derivada de la herencia en diamante) sea resuelto explícitamente por el programador en el punto en el que surge el conflicto.
Dado que admitimos las reabstracciones explícitas en las interfaces, podríamos hacerlo también en las clases
abstract class E : IA, IB, IC // ok
{
abstract void IA.M();
}
Cuestión cerrada: ¿deberíamos admitir implementaciones abstractas explícitas de interfaces en las clases? Decisión: NO
Además, es un error si en una declaración de clase la implementación más específica de algún método de interfaz es una implementación abstracta que se declaró en una interfaz. Se trata de una regla existente reformulada utilizando la nueva terminología.
interface IF
{
void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'
Es posible que una propiedad virtual declarada en una interfaz tenga una implementación más específica para su descriptor de acceso get
en una interfaz y una implementación más específica para su descriptor de acceso set
en una interfaz diferente. Esto se considera una violación de la regla de implementación más específica, y genera un error de compilador.
métodos static
y private
Dado que ahora las interfaces pueden contener código ejecutable, es útil abstraer el código común en métodos privados y estáticos. Ahora se permiten en interfaces.
Cuestión cerrada: ¿Deberíamos admitir métodos privados? ¿Deberíamos admitir métodos estáticos? Decisión: SÍ
Problema abierto : ¿Deberíamos dejar que los métodos de interfaz sean
protected
ointernal
u otro acceso? Si es así, ¿cuáles son las semánticas? ¿Sonvirtual
por defecto? En caso afirmativo, ¿hay alguna forma de hacerlos no virtuales?
Cuestión cerrada: Si admitimos métodos estáticos, ¿deberíamos admitir operadores (estáticos)? Decisión: SÍ
Invocaciones a la interfaz base
La sintaxis de esta sección no se ha implementado. Sigue siendo una propuesta activa.
El código de un tipo que deriva de una interfaz con un método por defecto puede invocar explícitamente la implementación "base" de esa interfaz.
interface I0
{
void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
// an explicit override that invoke's a base interface's default method
void I0.M() { I2.base.M(); }
}
Se permite que un método de instancia (no estático) invoque la implementación de un método de instancia accesible en una interfaz base directa de forma no virtual nombrándola mediante la sintaxis base(Type).M
. Esto resulta útil cuando una sobrecarga, que es necesaria proporcionar debido a la herencia en diamante, se resuelve delegando en una implementación base específica.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
override void IA.M() { WriteLine("IC.M"); }
}
class D : IA, IB, IC
{
void IA.M() { base(IB).M(); }
}
Cuando se accede a un miembro virtual
o abstract
mediante la sintaxis base(Type).M
, es necesario que Type
contenga una única invalidación más específica para M
.
Cláusulas básicas vinculantes
Las interfaces contienen ahora tipos. Estos tipos pueden utilizarse en la cláusula base como interfaces base. Al enlazar una cláusula base, puede que necesitemos conocer el conjunto de interfaces base para enlazar esos tipos (por ejemplo, para buscar en ellos y resolver accesos protegidos). El significado de la cláusula base de una interfaz queda así definido circularmente. Para romper el ciclo, añadimos una nueva regla lingüística correspondiente a una regla similar ya existente para las clases.
Mientras se determina el significado de la interfaz_base de una interfaz, se asume temporalmente que las interfaces base están vacías. Intuitivamente, esto garantiza que el significado de una cláusula base no pueda depender recursivamente de sí misma.
Antes teníamos las siguientes reglas:
"Cuando una clase B deriva de una clase A, es un error de compilación que A dependa de B. Una clase depende directamente de su clase base directa (si existe), y la clase depende directamente de la clasedentro de la cual está inmediatamente anidada (si existe)." Dada esta definición, el conjunto completo de clases sobre las que depende una clase es el cierre reflexivo y transitivo de la relación depende directamente de".
Es un error de compilación que una interfaz herede directa o indirectamente de sí misma. Las interfaces base de una interfaz son las interfaces base explícitas y sus interfaces base. En otras palabras, el conjunto de interfaces base es el cierre transitivo completo de las interfaces base explícitas, de sus propias interfaces base explícitas, y así sucesivamente.
Las ajustamos como sigue:
Cuando una clase B se deriva de una clase A, que A dependa de B es un error en tiempo de compilación. Una clase depende directamente de su clase base directa (si existe) y depende directamente de el tipo dentro del cual se anida inmediatamente (si existe).
Cuando una interfaz IB extiende una interfaz IA, es un error en tiempo de compilación que IA dependa de IB. Una interfaz depende directamente de sus interfaces base directas (si las hay) y depende directamente del tipo en el que está anidado inmediatamente (si existe).
Dadas estas definiciones, el conjunto completo de tipos sobre los que depende un tipo es el cierre reflexivo y transitivo de la relación depende directamente de.
Efecto sobre los programas existentes
Las reglas presentadas aquí no pretenden tener ningún efecto sobre el significado de los programas existentes.
Ejemplo 1:
interface IA
{
void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
public static void M() { } // method unrelated to 'IA.M' because static
}
Ejemplo 2:
interface IA
{
void M();
}
class Base: IA
{
void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
Las mismas reglas dan resultados similares a la situación análoga que afecta a los métodos de interfaz por defecto:
interface IA
{
void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
Cuestión cerrada: confirmar que se trata de una consecuencia prevista de la especificación. Decisión: SÍ
Resolución de métodos en tiempo de ejecución
problema cerrado: La especificación debe describir el algoritmo para la resolución de métodos en tiempo de ejecución en presencia de métodos predeterminados de la interfaz. Debemos asegurarnos de que la semántica es coherente con la semántica del lenguaje, por ejemplo, qué métodos declarados anulan o implementan un método
internal
y cuáles no.
API de soporte de CLR
Para que los compiladores detecten cuándo están compilando para un tiempo de ejecución que admite esta característica, las bibliotecas para dichos tiempos de ejecución se modifican para anunciar ese hecho a través de la API descrita en https://github.com/dotnet/corefx/issues/17116. Añadimos
namespace System.Runtime.CompilerServices
{
public static class RuntimeFeature
{
// Presence of the field indicates runtime support
public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
}
}
Problema abierto: ¿Es ese el mejor nombre para la característica de CLR? La función CLR hace mucho más que eso (por ejemplo, relaja las restricciones de protección, admite anulaciones en interfaces, etc.). ¿Quizás debería llamarse algo parecido a "métodos concretos en interfaces" o "características"?
Otras áreas que deben especificarse
- [ ] Sería útil catalogar los tipos de efectos de compatibilidad binaria y de código fuente causados por la adición de métodos de interfaz predeterminados y modificaciones de interfaces existentes.
Inconvenientes
Esta propuesta requiere una actualización coordinada de la especificación CLR (para soportar métodos concretos en interfaces y resolución de métodos). Por lo tanto, es bastante caro y puede que valga la pena hacerlo en combinación con otras funcionalidades que también anticipamos requerirían cambios CLR.
Alternativas
Ninguno.
Preguntas sin resolver
- Las preguntas abiertas se señalan a lo largo de la propuesta, anteriormente.
- Véase también https://github.com/dotnet/csharplang/issues/406 para una lista de preguntas abiertas.
- La especificación detallada debe describir el mecanismo de resolución utilizado en tiempo de ejecución para seleccionar el método preciso que debe invocarse.
- La interacción de los metadatos producidos por los nuevos compiladores y consumidos por los compiladores más antiguos debe resolverse en detalle. Por ejemplo, tenemos que asegurarnos de que la representación de metadatos que utilizamos no provoque que la adición de una implementación por defecto en una interfaz rompa una clase existente que implemente esa interfaz al ser compilada por un compilador antiguo. Esto puede afectar a la representación de metadatos que podemos utilizar.
- El diseño debe considerar la interoperabilidad con otros lenguajes y compiladores existentes para otros lenguajes.
Preguntas resueltas
Sobrescritura abstracta
La especificación de borrador anterior contenía la capacidad de "re-abstractar" un método heredado.
interface IA
{
void M();
}
interface IB : IA
{
override void M() { }
}
interface IC : IB
{
override void M(); // make it abstract again
}
Mis notas de 20/03/2017 mostraron que decidimos no permitir esto. Sin embargo, hay al menos dos casos de uso para ello:
- Las API de Java, con las que algunos usuarios de esta característica esperan interoperar, dependen de esta facilidad.
- La programación con traits se beneficia de ello. La reabstracción es uno de los elementos de la función del lenguaje "traits" (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Lo siguiente está permitido con las clases:
public abstract class Base
{
public abstract void M();
}
public abstract class A : Base
{
public override void M() { }
}
public abstract class B : A
{
public override abstract void M(); // reabstract Base.M
}
Desafortunadamente, este código no se puede refactorizar como un conjunto de interfaces (traits) a menos que esté permitido. Por el principio Jared de codicia, debería permitirse.
Cuestión cerrada: ¿Debería permitirse la reabstracción? [SÍ] Mis notas eran erróneas. Las notas LDM indican que se permite la reabstracción en una interfaz. No en una clase.
Modificador virtual vs Modificador sellado
De Aleksey Tsingauz:
Decidimos permitir modificadores explícitamente indicados en miembros de interfaz, a menos que haya una razón para no permitir algunos de ellos. Esto plantea una cuestión interesante en torno al modificador virtual. ¿Debería ser obligatorio en los miembros con implementación por defecto?
Podríamos decir que:
- si no hay ninguna implementación y no se especifica ni virtual ni sellado, suponemos que el miembro es abstracto.
- si hay una implementación y no se especifican ni abstracto ni sellado, se asume que el miembro es virtual.
- El modificador sellado es necesario para que un método no sea virtual ni abstracto.
Alternativamente, podríamos decir que el modificador virtual es necesario para un miembro virtual. Es decir, si hay un miembro con una implementación no marcada explícitamente con el modificador virtual, no es ni virtual ni abstracto. Este enfoque podría proporcionar una mejor experiencia cuando un método se mueve de una clase a una interfaz:
- un método abstracto sigue siendo abstracto.
- un método virtual permanece virtual.
- un método sin modificador no es ni virtual ni abstracto.
- el modificador sealed no se puede aplicar a un método que no sea una invalidación.
¿Qué opina?
Problema cerrado: ¿Debe un método concreto (con implementación) ser implícitamente
virtual
? [SÍ]
Decisiones: Tomadas en el LDM 2017-04-05:
- no virtual debe expresarse explícitamente a través de
sealed
oprivate
. -
sealed
es la palabra clave para crear miembros de instancia de interfaz con cuerpos que no son virtuales - Queremos permitir todos los modificadores en las interfaces
- La accesibilidad por defecto para los miembros de la interfaz es pública, incluyendo los tipos anidados
- los miembros de función privada de las interfaces están sellados implícitamente y no se permite
sealed
en ellos. - Se permiten clases privadas (en interfaces) y pueden ser selladas, lo que significa sellado en el sentido de sellado de clases.
- Si no hay una buena propuesta, siguen sin permitirse elementos parciales en interfaces ni en sus miembros.
Compatibilidad binaria 1
Cuando una biblioteca proporciona una implementación por defecto
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}
Entendemos que la implementación de I1.M
en C
es I1.M
. ¿Qué ocurre si se modifica el ensamblado que contiene I2
de la siguiente manera y se recompila
interface I2 : I1
{
override void M() { Impl2 }
}
pero no se vuelve a compilar C
. ¿Qué ocurre cuando se ejecuta el programa? Una invocación de (C as I1).M()
- Ejecuta
I1.M
- Ejecuta
I2.M
- Lanza algún tipo de error en tiempo de ejecución
Decisión: Tomada el 11-04-2017: Se ejecuta I2.M
, que es la sobreescritura más específica y clara en tiempo de ejecución.
Descriptores de acceso de eventos (cerrado)
Cuestión cerrada: ¿Se puede anular un evento "a trozos"?
Considere este caso:
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
// error: "remove" accessor missing
}
}
Esta implementación parcial del evento no está permitida porque, al igual que en una clase, la sintaxis para la declaración de un evento no permite que solo haya un descriptor de acceso; se deben proporcionar ambos o ninguno. Puede lograr lo mismo al permitir que el método de acceso abstracto 'remove' sea implícitamente abstracto debido a la falta de un cuerpo en la sintaxis.
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
remove; // implicitly abstract
}
}
Tenga en cuenta que se trata de una nueva sintaxis (propuesta). En la gramática actual, los descriptores de acceso de eventos tienen un cuerpo obligatorio.
Problema cerrado: ¿Un descriptor de acceso de eventos puede ser (implícitamente) abstracto por la omisión de un cuerpo, de forma similar a la forma en que los métodos de las interfaces y los descriptores de acceso de propiedad son (implícitamente) abstractos por la omisión de un cuerpo?
Decisión: (18-04-2017) No, las declaraciones de eventos requieren ambos descriptores de acceso concretos (o ninguno).
Reabstracción en una Clase (cerrado)
Cuestión cerrada: Deberíamos confirmar que esto está permitido (de lo contrario, añadir una implementación por defecto sería un cambio de ruptura):
interface I1
{
void M() { }
}
abstract class C : I1
{
public abstract void M(); // implement I1.M with an abstract method in C
}
Decisión: (18-04-2017) Sí, agregar un cuerpo a una declaración de miembro de interfaz no debe interrumpir el lenguaje C.
Invalidación sellada (cerrado)
La pregunta anterior asume implícitamente que el modificador sealed
puede aplicarse a un en una interfaz override
. Esto contradice el borrador de la especificación. ¿Queremos permitir el sellado de una invalidación? Se deben tener en cuenta los efectos de compatibilidad de origen y binario del sellado.
Problema cerrado: ¿Deberíamos permitir el sellado de una invalidación?
Decisión: (18-04-2017) No permitamos sealed
en sobrescrituras en interfaces. El único uso de sealed
en los miembros de la interfaz es convertirlos en no virtuales en su declaración inicial.
Herencia de diamantes y clases (cerrado)
El borrador de la propuesta prefiere las anulaciones de clase sobre las de interfaz en escenarios de herencia en diamante.
Es necesario que cada interfaz y clase tenga una invalidación más específica para cada método de interfaz entre las sobrescrituras que aparecen en el tipo o en sus interfaces directas e indirectas. La anulación más específica es una anulación singular que es más específica que cualquier otra anulación. Si no hay ninguna invalidación, el propio método se considera la invalidación más específica.
Una invalidación
M1
se considera más específica que otra invalidación siM2
siM1
se declara en el tipoT1
,M2
se declara en el tipoT2
, y
T1
contieneT2
entre sus interfaces directas o indirectas, oT2
es un tipo de interfazT1
pero no es un tipo de interfaz.
El escenario es el siguiente
interface IA
{
void M();
}
interface IB : IA
{
override void M() { WriteLine("IB"); }
}
class Base : IA
{
void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
static void Main()
{
IA a = new Derived();
a.M(); // what does it do?
}
}
Deberíamos confirmar este comportamiento (o decidir lo contrario)
Problema cerrado: Confirmar la especificación preliminar mencionada anteriormente para la invalidación más específica, tal como se aplica a clases e interfaces mixtas (una clase tiene prioridad sobre una interfaz). Vea https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes.
Métodos de interfaz frente a structs (cerrado)
Existen algunas interacciones desafortunadas entre los métodos de interfaz por defecto y los structs.
interface IA
{
public void M() { }
}
struct S : IA
{
}
Tenga en cuenta que los miembros de la interfaz no se heredan:
var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'
Por lo tanto, el cliente debe encapsular la estructura para invocar los métodos de la interfaz.
IA s = default(S); // an S, boxed
s.M(); // ok
Al hacer boxing de esta manera anula las ventajas principales de un tipo de struct
. Además, los métodos de mutación no tendrán ningún efecto aparente, ya que funcionan en una copia aislada de la estructura:
interface IB
{
public void Increment() { P += 1; }
public int P { get; set; }
}
struct T : IB
{
public int P { get; set; } // auto-property
}
T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0
Cuestión cerrada: ¿Qué podemos hacer al respecto?
- Prohibir que un
struct
herede una implementación por defecto. Todos los métodos de interfaz se tratarían como abstractos en astruct
. Entonces podemos tomarnos tiempo más tarde para decidir cómo hacer que funcione mejor.- Diseñe una estrategia para generar código que evite el boxing. Dentro de un método como
IB.Increment
, el tipo dethis
sería quizás similar a un parámetro de tipo restringido aIB
. Junto con eso, para evitar el encapsulamiento en el llamador, los métodos no abstractos se heredarían de las interfaces. Esto podría aumentar sustancialmente el trabajo del compilador y de implementación del CLR.- No se preocupe por ello.
- ¿Otras ideas?
Decisión: No se preocupe por ello. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations.
Invocaciones de interfaz base (cerrado)
Esta decisión no se implementó en C# 8. La sintaxis base(Interface).M()
no está implementada.
El borrador de la especificación sugiere una sintaxis para las invocaciones de interfaz base inspirada en Java: Interface.base.M()
. Necesitamos seleccionar una sintaxis, al menos para el prototipo inicial. Mi favorita es base<Interface>.M()
.
Cuestión cerrada: ¿Cuál es la sintaxis para la invocación de un miembro base?
Decisión: La sintaxis es base(Interface).M()
. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. La interfaz así llamada debe ser una interfaz base, pero no es necesario que sea una interfaz base directa.
Problema Abierto: ¿Se permiten invocaciones de interfaz base en miembros de clase?
Decisión: Sí. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation
Invalidación de miembros de interfaz no públicos (cerrado)
En una interfaz, los miembros no públicos de las interfaces base se invalidan mediante el modificador override
. Si es una invalidación "explícita" que asigna un nombre a la interfaz que contiene el miembro, se omite el modificador de acceso.
Problema cerrado: Si es una invalidación "implícita" que no menciona la interfaz, ¿debe coincidir el modificador de acceso?
Decisión: Solo se pueden invalidar implícitamente los miembros públicos y el acceso debe coincidir. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
Problema abierto: ¿El modificador de acceso es necesario, opcional o se omite en una invalidación explícita, como
override void IB.M() {}
?
Problema abierto: ¿
override
es necesario, opcional o se omite en una invalidación explícita, comovoid IB.M() {}
?
¿Cómo se implementa un miembro no público de una interfaz en una clase? ¿Quizás deba hacerse explícitamente?
interface IA
{
internal void MI();
protected void MP();
}
class C : IA
{
// are these implementations? Decision: NO
internal void MI() {}
protected void MP() {}
}
Problema cerrado: ¿Cómo implementa un miembro de interfaz no público en una clase?
Decisión: Solo puede implementar miembros de interfaz no públicos de manera explícita. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
Decision: No se permite ninguna palabra clave override
en los miembros de la interfaz. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member
Compatibilidad binaria 2 (cerrado)
Considere el siguiente código en el que cada tipo está en un ensamblado separado
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}
Entendemos que la implementación de I1.M
en C
es I2.M
. ¿Qué ocurre si se modifica el ensamblado que contiene I3
de la siguiente manera y se recompila
interface I3 : I1
{
override void M() { Impl3 }
}
pero no se vuelve a compilar C
. ¿Qué ocurre cuando se ejecuta el programa? Una invocación de (C as I1).M()
- Ejecuta
I1.M
- Ejecuta
I2.M
- Ejecuta
I3.M
- 2 o 3, de forma determinista
- Lanza algún tipo de excepción en tiempo de ejecución
Decisión: Lanzar una excepción (5). Vea https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-interface-methods.
¿Se permite partial
en la interfaz? (cerrado)
Dado que las interfaces pueden utilizarse de forma análoga a como se utilizan las clases abstractas, puede ser útil declararlas partial
. Esto sería especialmente útil frente a los generadores.
Propuesta: Eliminar la restricción de idioma que impide que las interfaces y los miembros de las interfaces se declaren
partial
.
Decisión: Sí. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface.
¿Main
en una interfaz? (cerrado)
Problema abierto: ¿Es el método
static Main
de una interfaz un candidato a ser el punto de entrada del programa?
Decisión: Sí. Vea https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#main-in-an-interface.
Confirmar la intención de admitir métodos públicos no virtuales (cerrado)
¿Podemos confirmar (o revocar) nuestra decisión de permitir métodos públicos no virtuales en una interfaz?
interface IA
{
public sealed void M() { }
}
Problema semicerrado: (18-04-2017) Creemos que va a ser útil, pero volveremos a ello. Esto es un obstáculo para los modelos mentales.
Decisión: Sí. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#confirm-that-we-support-public-non-virtual-methods.
¿Introduce un override
en una interfaz un nuevo miembro? (cerrado)
Hay varias formas de comprobar si una declaración de anulación introduce un nuevo miembro o no.
interface IA
{
void M(int x) { }
}
interface IB : IA
{
override void M(int y) { } // 'override' not permitted
}
interface IC : IB
{
static void M2()
{
M(y: 3); // permitted? Decision: No.
}
override void IB.M(int z) { } // permitted? What does it override? Decision: No.
}
Problema abierto: ¿Introduce un nuevo miembro una declaración de invalidación en una interfaz? (cerrado)
En una clase, un método de invalidación es "visible" en cierto modo. Por ejemplo, los nombres de sus parámetros tienen prioridad sobre los nombres de los parámetros del método anulado. Es posible duplicar ese comportamiento en interfaces, ya que siempre hay una invalidación más específica. Pero, ¿queremos duplicar ese comportamiento?
Además, ¿es posible "anular" un método anulado? [Moot]
Decision: No se permite ninguna palabra clave override
en los miembros de la interfaz. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member.
Propiedades con un descriptor de acceso privado cerrado
Decimos que los miembros privados no son virtuales, y la combinación de virtual y privado no está permitida. ¿Qué ocurre con una propiedad con un accesor privado?
interface IA
{
public virtual int P
{
get => 3;
private set { }
}
}
¿Está permitido? ¿El descriptor de acceso set
es virtual
o no? ¿Se puede invalidar donde es accesible? ¿Implementa lo siguiente de forma implícita solo el accesor get
?
class C : IA
{
public int P
{
get => 4;
set { }
}
}
¿Es un error el siguiente, presumiblemente, porque IA.P.set no es virtual y también porque no es accesible?
class C : IA
{
int IA.P
{
get => 4;
set { } // Decision: Not valid
}
}
Decision: El primer ejemplo parece válido, mientras que el último no. Esto se resuelve de forma análoga a como ya funciona en C#. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#properties-with-a-private-accessor
Invocaciones a la interfaz base, ronda 2 (cerrado)
Esto no estaba implementado en C# 8.
Nuestra "resolución" anterior sobre cómo manejar las invocaciones base no proporciona realmente una capacidad de expresión suficiente. Resulta que en C# y el CLR, a diferencia de Java, es necesario especificar tanto la interfaz que contiene la declaración del método como la ubicación de la implementación que se desea invocar.
Propongo la siguiente sintaxis para llamadas base en interfaces. No estoy enamorado de ella, pero ilustra lo que cualquier sintaxis debe ser capaz de expresar:
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M(); // calls I3's implementation of I1.M
base<I4>(I1).M(); // calls I4's implementation of I1.M
}
void I2.M()
{
base<I3>(I2).M(); // calls I3's implementation of I2.M
base<I4>(I2).M(); // calls I4's implementation of I2.M
}
}
Si no hay ambigüedad, se puede escribir de forma más sencilla
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>.M(); // calls I3's implementation of I1.M
base<I4>.M(); // calls I4's implementation of I1.M
}
}
Or
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
void I1.M()
{
base(I1).M(); // calls I3's implementation of I1.M
}
void I2.M()
{
base(I2).M(); // calls I3's implementation of I2.M
}
}
Or
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
void I1.M()
{
base.M(); // calls I3's implementation of I1.M
}
}
Decisión: decidido en base(N.I1<T>).M(s)
, concediendo que si tenemos un enlace de invocación puede haber un problema aquí más adelante. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-14.md#default-interface-implementations
¿Advertencia para struct que no implementa método por defecto? (cerrado)
@vancem afirma que deberíamos considerar seriamente producir una advertencia si una declaración de tipo valor no anula algún método de interfaz, incluso si heredara una implementación de ese método de una interfaz. Dado que provoca boxing y socava las llamadas restringidas.
Decision: esto parece algo más adecuado para un analizador. También parece que esta advertencia podría ser ruidosa, ya que se desencadenaría incluso si nunca se llama al método de interfaz predeterminado y nunca ocurrirá ninguna conversión a objeto. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-not-implementing-default-method
Compiladores estáticos de interfaz (cerrado)
¿Cuándo se ejecutan los compiladores estáticos de interfaz? El borrador actual de la CLI propone que se produzca cuando se accede al primer método o campo estático. Si no hay ninguno de ellos, ¿podría ser que nunca se ejecute?
[09-10-2018 El equipo de CLR propone "Reflejar lo que hacemos para los tipos de valor (comprobación de cctor sobre el acceso a cada método de instancia)"]
Decision: los constructores estáticos también se ejecutan al iniciar los métodos de instancia, si el constructor estático no era beforefieldinit
, en cuyo caso los constructores estáticos se ejecutan antes del acceso al primer campo estático. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-static-constructors-run
Reuniones de diseño
08-03-2017 Notas de la reunión LDM21-03-2017 Notas de la reunión LDM23-03-2017 Notas de la reunión "Comportamiento CLR para Métodos de interfaz predeterminados"05-04-2017 Notas de la reunión LDM11-04-2017 Notas de la reunión LDM18-04-2017 Notas de la reunión LDM19-04-2017 Notas de la reunión LDM17-05-2017 Notas de la reunión LDM31-05-2017 Notas de la reunión LDM14-06-2017 Notas de la reunión LDM17-10-2018 Notas de la reunión LDM14-11-2018 Notas de la reunión LDM
C# feature specifications