2012-06-26 1 views
11

Ho un semplice metodo factory che fornisce un'istanza di implementazione concreta basata su un parametro di tipo generico fornito. Se le classi concrete ereditano da una classe base astratta comune con un parametro di tipo non posso lanciarle. Il compilatore mi dice Error 2 Cannot convert type 'Car' to 'VehicleBase<T>'. Funziona bene se sostituisco la classe astratta per un'interfaccia con lo stesso parametro di tipo, o se rimuovo il parametro di tipo generico dalla classe astratta.Impossibile trasmettere il tipo derivato alla classe astratta di base con il parametro type

interface IWheel 
{ 
} 

class CarWheel : IWheel 
{ 
} 

abstract class VehicleBase<T> 
{ 
} 

class Car : VehicleBase<CarWheel> 
{ 
} 

class VehicleFactory 
{ 
    public static VehicleBase<T> GetNew<T>() 
    { 
     if (typeof(T) == typeof(CarWheel)) 
     { 
      return (VehicleBase<T>)new Car(); 
     } 
     else 
     { 
      throw new NotSupportedException(); 
     } 
    } 
} 

questo non riesce a compilare il (VehicleBase<T>)new Car(). Si tratta di un difetto del compilatore o potrebbe essere una decisione deliberata del progetto di trattare le classi astratte e le interfacce con i parametri di tipo in modo diverso?

Come soluzione temporanea, posso sempre rendere la classe astratta implementare un'interfaccia e utilizzarla come valore di ritorno per il mio metodo factory, ma mi piacerebbe comunque sapere perché questo comportamento sta accadendo.

risposta

5

Questo non è un difetto del compilatore né una decisione deliberata. I parametri di tipo sulle classi generiche sono neither covariant nor contravariant, ovvero non esiste alcuna relazione di ereditarietà tra le specializzazioni della stessa classe generica. Dalla documentazione:

In .NET Framework versione 4, i parametri di tipo variante sono limitati all'interfaccia generica e ai tipi di delegati generici.

Il che significa che il seguente codice verrà compilato, perché utilizza un'interfaccia invece di una classe astratta:

interface IWheel 
{ 
} 

class CarWheel : IWheel 
{ 
} 

interface IVehicleBase<T> 
{ 
} 

class Car : IVehicleBase<CarWheel> 
{ 
} 

class VehicleFactory 
{ 
    public static IVehicleBase<T> GetNew<T>() 
    { 
     if (typeof(T) == typeof(CarWheel)) 
     { 
      return (IVehicleBase<T>)new Car(); 
     } 
     else 
     { 
      throw new NotSupportedException(); 
     } 
    } 
} 

Check "Covariance and Contravariance in Generics" per ulteriori informazioni ed esempi.

C'è anche un Covariance and Contravariance FAQ al C# FAQ blog con più informazioni, e di un 11-part series! in materia da Eric Lippert

+0

Questa è la soluzione che avevo usato. Grazie per avermi ricordato della varianza in interfacce/delegati - ha senso e risponde alla mia domanda. – dahvyd

+0

Grazie per avermelo ricordato. È fin troppo facile dimenticarsene quando devi solo avere un codice fuori dalla porta –

9

Questo non è dimostrabile , perché il codice generico bisogno di lavorare (con lo stesso IL) per ogni possibile T, e non c'è niente da dire che Car : VehicleBase<float>, per esempio. Il compilatore non sovrascrive il fatto che il controllo if scriva che T è CarWheel - il controllo statico tratta ogni istruzione separatamente, non tenta di capire il causa-effetto delle condizioni.

per forzarlo, cast object nel mezzo:

return (VehicleBase<T>)(object)new Car(); 

Tuttavia! Il tuo approccio non è realmente "generico" in quanto tale.

+0

sembra ragionevole, ma perché la differenza se uso un'interfaccia? Questo è quello che non capisco. – dahvyd

+0

Non è solo che questo non è dimostrabile. Solo le interfacce generiche possono avere tipi di varianti. Questo codice non funzionerebbe anche se hai specificato i vincoli di tipo corretto –

3

Questo sembra funzionare:

return new Car() as VehicleBase<T>; 

mio ipotesi perché è in questo modo:
Come le istanze di tipo generico di VehicleBase<T> non sono correlati, ma non può essere provato che li fusione potrebbe funzionare:

enter image description here

Se T è di tipo Blah, il cast non avrebbe funzionato. Non è possibile tornare all'oggetto e quindi prendere l'altro ramo (non vi è alcuna eredità multipla in C# dopo tutto).

Tornando indietro a object, si sta aprendo nuovamente la possibilità che il cast possa funzionare, perché potrebbe esserci ancora un percorso fino a VehicleBase<CarWheel>. Un'interfaccia, ovviamente, può apparire ovunque in questo albero al di sotto di object, quindi anche questo dovrebbe funzionare.