¿Por qué el compilador de C # se queja de que "los tipos pueden unificarse" cuando derivan de diferentes clases base?

77

Mi código de no compilación actual es similar a esto:

public abstract class A { }

public class B { }

public class C : A { }

public interface IFoo<T>
{
    void Handle(T item);
}

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

El compilador de C # se niega a compilar esto, citando la siguiente regla / error:

'MyProject.MyFoo <TA>' no puede implementar tanto 'MyProject.IFoo <TA>' y 'MyProject.IFoo <MyProject.B>' porque pueden unificarse para algunas sustituciones de parámetros de tipo

Entiendo lo que significa este error; si TApudiera ser cualquier cosa, técnicamente también podría ser un Bque introduciría ambigüedad sobre las dos Handleimplementaciones diferentes .

Pero TA no puede ser nada. Según la jerarquía de tipos, TA no puede ser B, al menos, no creo que pueda. TAdebe derivar de A, que no deriva de B, y obviamente no hay herencia de clases múltiples en C # / .NET.

Si quito el parámetro genérico y sustituir TAcon C, o incluso A, que se compila.

Entonces, ¿por qué obtengo este error? ¿Es un error o falta de inteligencia general del compilador, o hay algo más que me falta?

¿Hay alguna solución alternativa o simplemente tendré que volver a implementar la MyFooclase genérica como una clase no genérica separada para cada TAtipo derivado posible ?

Aaronaught
fuente
2
Creo que TItem debería leer TA, ¿no?
Jeff
Es poco probable que haya un error en el compilador. Para ser justos, el mensaje de error usa las palabras "puede unificar", mi suposición es que es porque usas ambas interfaces.
Sabueso de seguridad
Editar: No importa, leí que B es un parámetro de tipo. <strike> ¿Qué te impide pasar lo mismo al parámetro de tipo Bcuando pasas TA? </strike>
Josh
1
@JoshEinstein: Bno es un parámetro de tipo, es un tipo real. El único parámetro de tipo es TA.
Aaronaught
1
@Ramhound No veo por qué es "poco probable" que sea un error del compilador. No veo otra explicación, de verdad. Parece un error fácil de cometer y una situación que no surge con mucha frecuencia.
kmkemp

Respuestas:

50

Esto es una consecuencia de la sección 13.4.2 de la especificación C # 4, que establece:

Si cualquier tipo construido posible creado a partir de C, después de que los argumentos de tipo se sustituyan en L, provoque que dos interfaces en L sean idénticas, entonces la declaración de C no es válida. Las declaraciones de restricción no se consideran al determinar todos los tipos construidos posibles.

Tenga en cuenta esa segunda oración allí.

Por tanto, no es un error del compilador; el compilador es correcto. Se podría argumentar que es una falla en la especificación del lenguaje.

En términos generales, las restricciones se ignoran en casi todas las situaciones en las que se debe deducir un hecho sobre un tipo genérico. Las restricciones se utilizan principalmente para determinar la clase base efectiva de un parámetro de tipo genérico, y poco más.

Desafortunadamente, eso a veces conduce a situaciones en las que el lenguaje es innecesariamente estricto, como ha descubierto.


En general, es un mal olor de código implementar "la misma" interfaz dos veces, de alguna manera distinguida sólo por argumentos de tipo genérico. Es extraño, por ejemplo, tener class C : IEnumerable<Turtle>, IEnumerable<Giraffe>: ¿qué es C que es tanto una secuencia de tortugas como una secuencia de jirafas al mismo tiempo ? ¿Puede describir lo que realmente está tratando de hacer aquí? Puede haber un patrón mejor para resolver el problema real.


Si, de hecho, su interfaz es exactamente como la describe:

interface IFoo<T>
{
    void Handle(T t);
}

Entonces, la herencia múltiple de la interfaz presenta otro problema. Podría razonablemente decidir hacer que esta interfaz sea contravariante:

interface IFoo<in T>
{
    void Handle(T t);
}

Ahora suponga que tiene

interface IABC {}
interface IDEF {}
interface IABCDEF : IABC, IDEF {}

Y

class Danger : IFoo<IABC>, IFoo<IDEF>
{
    void IFoo<IABC>.Handle(IABC x) {}
    void IFoo<IDEF>.Handle(IDEF x) {}
}

Y ahora las cosas se ponen realmente locas ...

IFoo<IABCDEF> crazy = new Danger();
crazy.Handle(null);

¿Qué implementación de Handle se llama ???

Consulte este artículo y los comentarios para obtener más ideas sobre este tema:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

Eric Lippert
fuente
1
@asawyer: Sí. Está definida por la implementación qué conversión toma CLR en este caso. Y de hecho, puede construir situaciones más complejas en las que se definen en la especificación CLI que se debe tomar la implementación y el CLR tiene un "mal". No estoy seguro de si el equipo CLR y los propietarios de la especificación CLI han resuelto esa disputa todavía; Mi opinión personal es que las reglas CLI en realidad dan menos resultados buenos que las reglas CLR en los casos más probables. (No es que ninguno de estos casos sea común; todos son casos de esquina bastante extraños.)
Eric Lippert
3
Definitivamente, no tiene sentido cuando las interfaces son los parámetros de tipo; Había asumido incorrectamente que el uso de clases concretas solucionaría el problema. En cuanto al escenario, la clase es una Saga, que debe implementar varios manejadores de mensajes (uno por cada mensaje que forma parte de la saga). Hay alrededor de 10 sagas casi idénticas cuya única diferencia es el tipo concreto exacto de uno de los mensajes, pero no puedo tener un controlador de mensajes abstracto, así que pensé en intentar usar una clase base genérica y solo usar un montón de clases de stub; la única alternativa es copiar y pegar mucho.
Aaronaught
22
@Eric: La situación de implementar la misma interfaz genérica con más de una vez es más probable si considera una interfaz como IComparable<T>o en IEquatable<T>lugar de IEnumerable<T>. Es bastante plausible tener un objeto que pueda compararse con más de un tipo de tipo ... de hecho, me he encontrado con problemas de unificación de tipos varias veces para este caso exacto.
LBushkin
4
@Eric, LBushkin tiene razón. El hecho de que algunos usos no tengan sentido no implica que todos los usos no tengan sentido. Este problema también me mordió, porque estaba implementando una interfaz de análisis abstracta IFoo <T0, T1, ..> que implementa un IParseable <T0>, IParseable <T1>, ... con un conjunto de métodos de extensión definidos en IParseable , pero ahora eso parece imposible. C # / CLR tiene muchos casos de esquina frustrantes como este.
naasking el
1
@EricLippert ¿Estoy en lo cierto al suponer que se han agregado características para permitir la ambigüedad en "ciertas" condiciones? Parece que ahora (.NET 4.5) tiene permitido expresar el ejemplo anterior, mientras que una versión genérica todavía no está permitida, class Danger : IFoo<IABC>, IFoo<DEF> {...}es decir, está permitida, mientras que class Danger<T1,T2> : IFoo<T1>, IFoo<T2>todavía no está permitida. Parece que la desambiguación se realiza mediante un arbitraje determinista, donde se utiliza la primera implementación más específica definida (donde se aplican múltiples).
micdah
8

Aparentemente fue por diseño como se discutió en Microsoft Connect:

Y la solución alternativa es definir otra interfaz como:

public interface IIFoo<T> : IFoo<T>
{
}

Luego implemente esto en su lugar como:

public class MyFoo<TA> : IIFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Ahora se compila bien, por mono .

Nawaz
fuente
Votó a favor del enlace Connect; desafortunadamente, la solución alternativa no se compila con el compilador de Microsoft.
Aaronaught
@Aaronaught: Intenta hacer IIFooun abstract classthen.
Nawaz
Eso se compila, aunque obviamente me impide derivar de una clase base diferente. Creo que en realidad podría hacer que esto funcione, aunque exigirá otro nivel intermedio (hackish / inútil) en la jerarquía de clases.
Aaronaught
Esto en realidad viola las especificaciones si no me equivoco. La sección que Eric citó especifica que no debería ser posible, ¿no es así?
configurador
2
@Aaronaught: es legal que un tipo implemente la misma interfaz dos veces a través de su cadena de herencia (para ocultar y mitigar la clase base frágil). Pero no es legal que el mismo tipo implemente la misma interfaz dos veces en el mismo lugar, porque no hay una "mejor".
configurador
4

Puedes esconderlo por debajo del radar si pones una interfaz en una clase base.

public interface IFoo<T> {
}

public class Foo<T> : IFoo<T>
{
}

public class Foo<T1, T2> : Foo<T1>, IFoo<T2>
{
}

Sospecho que esto funciona porque si los tipos "unifican" está claro que la implementación de la clase derivada gana.

Colin
fuente
2

Vea mi respuesta a básicamente la misma pregunta aquí: https://stackoverflow.com/a/12361409/471129

Hasta cierto punto, ¡esto se puede hacer! Utilizo un método de diferenciación, en lugar de calificadores que limitan los tipos.

No se unifica, de hecho, podría ser mejor que si lo hiciera porque puede separar las interfaces separadas.

Vea mi publicación aquí, con un ejemplo completamente funcional en otro contexto. https://stackoverflow.com/a/12361409/471129

Básicamente, lo que haces es agregar otro parámetro de tipo a IIndexer , para que se convierta en IIndexer <TKey, TValue, TDifferentiator>.

Luego, cuando lo usa dos veces, pasa "Primero" al primer uso y "Segundo" al segundo uso.

Entonces, la prueba de clase se convierte en: clase Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

Por lo tanto, puede hacer new Test<int,int>()

donde First y Second son triviales:

interface First { }

interface Second { }
Erik Eidt
fuente
1

Sé que ha pasado un tiempo desde que se publicó el hilo, pero para aquellos que vienen a este hilo a través del motor de búsqueda en busca de ayuda. Tenga en cuenta que 'Base' significa clase base para TA y B a continuación.

public class MyFoo<TA> : IFoo<Base> where TA : Base where B : Base
{
    public void Handle(Base obj) 
    { 
       if(obj is TA) { // TA specific codes or calls }
       else if(obj is B) { // B specific codes or calls }
    }

}
Yohan Chung
fuente
0

Adivinando ahora ...

¿No podrían declararse A, B y C en ensamblajes externos, donde la jerarquía de tipos puede cambiar después de la compilación de MyFoo <T>, provocando estragos en el mundo?

La solución sencilla es implementar Handle (A) en lugar de Handle (TA) (y usar IFoo <A> en lugar de IFoo <TA>). No puede hacer mucho más con Handle (TA) que los métodos de acceso desde A (debido a la restricción A: TA) de todos modos.

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}
hermana
fuente
No a esto >> Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo<T>, bringing havoc into the world?.
Nawaz
Cambiar la jerarquía de tipos invalidaría casi cualquier programa; No tiene mucho sentido para mí que el compilador base sus decisiones en otra cosa que no sea la jerarquía de tipos en este momento (aunque, supongo que no tiene menos sentido para mí que el error mismo en este momento. ..) En cuanto a la solución, bueno, eso se compilará pero ya no es genérico, por lo que realmente no ayuda mucho.
Aaronaught
0

Hmm, ¿qué pasa con esto?

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    void IFoo<TA>.Handle(TA a) { }
    void IFoo<B>.Handle(B b) { }
}
luqui
fuente
2
No, el error está en la propia clase; no depende de la forma en que se implemente la interfaz.
Qwertie