Problema di comprensione della covarianza controvarianza con i generici in C #


115

Non riesco a capire perché il seguente codice C # non viene compilato.

Come puoi vedere, ho un metodo generico statico Qualcosa con un IEnumerable<T>parametro (ed Tè vincolato a essere IAun'interfaccia) e questo parametro non può essere convertito implicitamente in IEnumerable<IA>.

Qual è la spiegazione? (Non cerco una soluzione alternativa, solo per capire perché non funziona).

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)
    {
    }
}

Errore mi metto in Something2(bar)linea:

Argomento 1: impossibile convertire da "System.Collections.Generic.List" a "System.Collections.Generic.IEnumerable"



12
Non ti sei limitato Tai tipi di riferimento. Se usi la condizione where T: class, IA, dovrebbe funzionare. La risposta collegata ha più dettagli.
Dirk

2
@Dirk Non penso che questo dovrebbe essere contrassegnato come duplicato. Mentre è vero che il problema del concetto qui è un problema di covarianza / controvarianza di fronte ai tipi di valore, il caso specifico qui è "cosa significa questo messaggio di errore" così come l'autore che non si rende conto semplicemente di includere "classe" risolve il suo problema. Credo che i futuri utenti cercheranno questo messaggio di errore, troveranno questo post e se ne andranno felici. (Come faccio spesso)
Reginald Blue

Puoi anche riprodurre la situazione semplicemente dicendo Something2(foo);direttamente. Andare in giro .ToList()per ottenere un List<T>( Tè il tuo parametro di tipo dichiarato dal metodo generico) non è necessario per capirlo (a List<T>è an IEnumerable<T>).
Jeppe Stig Nielsen

@ReginaldBlue 100%, avrebbe postato la stessa cosa. Risposte simili non fanno una domanda duplicata.
UuDdLrLrSs

Risposte:


218

Il messaggio di errore non è sufficientemente informativo ed è colpa mia. Mi dispiace per quello.

Il problema che stai riscontrando è una conseguenza del fatto che la covarianza funziona solo sui tipi di riferimento.

Probabilmente stai dicendo "ma IAè un tipo di riferimento" in questo momento. Sì. Ma non hai detto che T è uguale a IA . Hai detto che Tè un tipo che implementa IA e un tipo di valore può implementare un'interfaccia . Pertanto non sappiamo se la covarianza funzionerà e non lo consentiamo.

Se vuoi che la covarianza funzioni, devi dire al compilatore che il parametro di tipo è un tipo di riferimento con il classvincolo e il IAvincolo dell'interfaccia.

Il messaggio di errore dovrebbe davvero dire che la conversione non è possibile perché la covarianza richiede una garanzia di tipo di riferimento, poiché questo è il problema fondamentale.


3
Perché hai detto che è colpa tua?
user4951

77
@ user4951: Perché ho implementato tutta la logica di controllo della conversione, inclusi i messaggi di errore.
Eric Lippert

@BurnsBA Questo è solo un "difetto" in senso causale - l'implementazione tecnica e il messaggio di errore sono perfettamente corretti. (È solo che la dichiarazione di errore di inconvertabilità potrebbe elaborare le ragioni effettive. Ma produrre buoni errori con i generici è difficile - rispetto ai messaggi di errore del modello C ++ di alcuni anni fa questo è chiaro e conciso.)
Peter - Reinstate Monica

3
@ PeterA.Schneider: lo apprezzo. Ma uno dei miei obiettivi principali per la progettazione della logica di segnalazione degli errori in Roslyn era in particolare quello di catturare non solo quale regola era stata violata, ma, inoltre, identificare la "causa principale" ove possibile. Ad esempio, a cosa dovrebbe servire il messaggio di errore customers.Select(c=>c.FristName)? La specifica C # è molto chiara sul fatto che si tratta di un errore di risoluzione dell'overload : il set di metodi applicabili denominato Select che può accettare quel lambda è vuoto. Ma la causa principale è che FirstNameha un errore di battitura.
Eric Lippert

3
@ PeterA.Schneider: ho lavorato molto per garantire che gli scenari che coinvolgono inferenza di tipo generico e lambda utilizzassero l'euristica appropriata per dedurre quale messaggio di errore avrebbe potuto aiutare meglio lo sviluppatore. Ma ho fatto un lavoro molto meno buono sui messaggi di errore di conversione, in particolare per quanto riguarda la varianza. Me ne sono sempre pentito.
Eric Lippert

26

Volevo solo completare l'eccellente risposta di Eric con un esempio di codice per coloro che potrebbero non avere familiarità con i vincoli generici.

Cambia Somethingla firma in questo modo: il classvincolo deve venire prima .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA

2
Sono curioso ... qual è esattamente il motivo dietro il significato dell'ordinamento?
Tom Wright

5
@ TomWright - le specifiche, ovviamente, non includono la risposta a molti "Perché?" domande, ma in questo caso chiarisce che ci sono tre tipi distinti di vincoli, e quando tutti e tre vengono usati devono essere specificamenteprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever

2
@ TomWright: Damien ha ragione; non ci sono ragioni particolari di cui sono a conoscenza se non la comodità dell'autore del parser. Se avessi i miei druthers, la sintassi per i vincoli di tipo sarebbe notevolmente più prolissa. classè negativo perché significa "tipo di riferimento", non "classe". Sarei stato più felice con qualcosa di verboso comewhere T is not struct
Eric Lippert
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.