Problema de comprensión de la contravarianza de covarianza con genéricos en C #

115

No puedo entender por qué el siguiente código C # no se compila.

Como puede ver, tengo un método genérico estático Algo con un IEnumerable<T>parámetro (y Testá restringido a ser una IAinterfaz), y este parámetro no se puede convertir implícitamente a IEnumerable<IA>.

Cual es la explicacion (No busco una solución alternativa, solo para entender por qué no funciona).

public interface IA { }
public interface IB : IA { }
public class CIA : IA { }
public class CIAD : CIA { }
public class CIB : IB { }
public class CIBD : CIB { }

public static class Test
{
    public static IList<T> Something<T>(IEnumerable<T> foo) where T : IA
    {
        var bar = foo.ToList();

        // All those calls are legal
        Something2(new List<IA>());
        Something2(new List<IB>());
        Something2(new List<CIA>());
        Something2(new List<CIAD>());
        Something2(new List<CIB>());
        Something2(new List<CIBD>());
        Something2(bar.Cast<IA>());

        // This call is illegal
        Something2(bar);

        return bar;
    }

    private static void Something2(IEnumerable<IA> foo)
    {
    }
}

Error me pongo en Something2(bar)línea:

Argumento 1: no se puede convertir de 'System.Collections.Generic.List' a 'System.Collections.Generic.IEnumerable'

BenLaz
fuente
12
No se ha limitado Ta los tipos de referencia. Si usa la condición, where T: class, IAentonces debería funcionar. La respuesta vinculada tiene más detalles.
Dirk
2
@Dirk No creo que esto deba marcarse como un duplicado. Si bien es cierto que el problema del concepto aquí es un problema de covarianza / contravarianza frente a los tipos de valor, el caso específico aquí es "qué significa este mensaje de error", así como que el autor no se da cuenta de que simplemente incluir "clase" soluciona su problema. Creo que los futuros usuarios buscarán este mensaje de error, encontrarán esta publicación y se irán felices. (Como hago a menudo)
Reginald Blue
También puede reproducir la situación simplemente diciendo Something2(foo);directamente. No es necesario dar vueltas .ToList()para obtener un List<T>( Tes su parámetro de tipo declarado por el método genérico) para comprender esto (a List<T>es un IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, iba a publicar lo mismo. Las respuestas similares no hacen una pregunta duplicada.
UuDdLrLrSs

Respuestas:

218

El mensaje de error es insuficientemente informativo y es culpa mía. Lo siento por eso.

El problema que está experimentando es una consecuencia del hecho de que la covarianza solo funciona en tipos de referencia.

Probablemente estés diciendo "pero IAes un tipo de referencia" en este momento. Sí lo es. Pero no dijiste que eso T es igual a IA . Dijiste que Tes un tipo que implementa IA y un tipo de valor puede implementar una interfaz . Por lo tanto, no sabemos si la covarianza funcionará y la rechazamos.

Si desea que la covarianza funcione, debe decirle al compilador que el parámetro de tipo es un tipo de referencia con la classrestricción y la IArestricción de interfaz.

El mensaje de error realmente debería decir que la conversión no es posible porque la covarianza requiere una garantía de tipo de referencia, ya que ese es el problema fundamental.

Eric Lippert
fuente
3
¿Por qué dijiste que es tu culpa?
user4951
77
@ user4951: Porque implementé toda la lógica de verificación de conversión, incluidos los mensajes de error.
Eric Lippert
@BurnsBA Esto es solo una "falla" en el sentido causal: técnicamente, la implementación y el mensaje de error son perfectamente correctos. (Es solo que la declaración de error de inconvertibilidad podría explicar las razones reales. Pero producir buenos errores con genéricos es difícil, en comparación con los mensajes de error de la plantilla C ++ de hace unos años, esto es lúcido y conciso)
Peter - Reinstate Monica
3
@ PeterA.Schneider: Se lo agradezco. Pero uno de mis objetivos principales para diseñar la lógica de notificación de errores en Roslyn fue, en particular, capturar no solo qué regla se violó, sino además, identificar la "causa raíz" cuando fuera posible. Por ejemplo, ¿para qué debería ser el mensaje de error customers.Select(c=>c.FristName)? La especificación de C # es muy clara en cuanto a que se trata de un error de resolución de sobrecarga : el conjunto de métodos aplicables denominado Select que puede tomar esa lambda está vacío. Pero la causa principal es que FirstNametiene un error tipográfico.
Eric Lippert
3
@ PeterA.Schneider: Trabajé mucho para asegurarme de que los escenarios que involucraban la inferencia de tipo genérico y lambdas usaran la heurística apropiada para deducir qué mensaje de error era más probable que ayudara al desarrollador. Pero hice un trabajo mucho menos bueno con los mensajes de error de conversión, particularmente en lo que respecta a la variación. Siempre lo he lamentado.
Eric Lippert
26

Solo quería complementar la excelente respuesta privilegiada de Eric con un ejemplo de código para aquellos que pueden no estar tan familiarizados con las restricciones genéricas.

SomethingLa firma del cambio es así: la classrestricción tiene que ser lo primero .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
fuente
2
Tengo curiosidad ... ¿cuál es exactamente la razón detrás del significado del pedido?
Tom Wright
5
@TomWright: la especificación, por supuesto, no incluye la respuesta a muchos "¿Por qué?" preguntas, pero en este caso deja en claro que hay tres tipos distintos de restricciones, y cuando se usan las tres tienen que ser específicamenteprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@TomWright: Damien tiene razón; No hay ninguna razón en particular de la que tenga conocimiento que no sea la conveniencia del autor del analizador. Si tuviera mis druthers, la sintaxis para las restricciones de tipo sería considerablemente más detallada. classes malo porque significa "tipo de referencia", no "clase". Hubiera sido más feliz con algo detallado comowhere T is not struct
Eric Lippert