Perché l'ereditarietà non funziona come penso dovrebbe funzionare?
-
09-06-2019 - |
Domanda
Sto riscontrando alcuni problemi di ereditarietà poiché ho un gruppo di classi astratte intercorrelate che devono essere sovrascritte insieme per creare un'implementazione client.Idealmente vorrei fare qualcosa del genere:
abstract class Animal
{
public Leg GetLeg() {...}
}
abstract class Leg { }
class Dog : Animal
{
public override DogLeg Leg() {...}
}
class DogLeg : Leg { }
Ciò consentirebbe a chiunque utilizzi la classe Dog di ottenere automaticamente DogLegs e chiunque utilizzi la classe Animal di ottenere Legs.Il problema è che la funzione sottoposta a override deve avere lo stesso tipo della classe base, quindi non verrà compilata.Non vedo perché non dovrebbe, dato che DogLeg è implicitamente lanciabile su Leg.So che ci sono molti modi per aggirare questo problema, ma sono più curioso di sapere perché questo non è possibile/implementato in C#.
MODIFICARE:L'ho leggermente modificato, poiché in realtà sto utilizzando proprietà anziché funzioni nel mio codice.
MODIFICARE:L'ho cambiato di nuovo in funzioni, perché la risposta si applica solo a quella situazione (covarianza sul parametro valore della funzione impostata di una proprietà non dovrebbe lavoro).Ci scusiamo per le fluttuazioni!Mi rendo conto che molte risposte sembrano irrilevanti.
Soluzione
La risposta breve è che GetLeg è invariante nel tipo restituito.La risposta lunga può essere trovata qui: Covarianza e controvarianza
Vorrei aggiungere che mentre l'ereditarietà è solitamente il primo strumento di astrazione che la maggior parte degli sviluppatori tira fuori dalla propria cassetta degli attrezzi, è quasi sempre possibile utilizzare invece la composizione.La composizione richiede un po' più di lavoro per lo sviluppatore dell'API, ma rende l'API più utile per i suoi consumatori.
Altri suggerimenti
Chiaramente, avrai bisogno di un cast se stai operando su un Dogleg rotto.
Dog dovrebbe restituire una Leg e non una DogLeg come tipo di restituzione.La classe effettiva potrebbe essere DogLeg, ma il punto è disaccoppiare in modo che l'utente di Dog non debba conoscere DogLeg, deve solo conoscere Legs.
Modifica:
class Dog : Animal
{
public override DogLeg GetLeg() {...}
}
A:
class Dog : Animal
{
public override Leg GetLeg() {...}
}
Non farlo:
if(a instanceof Dog){
DogLeg dl = (DogLeg)a.GetLeg();
vanifica lo scopo della programmazione di tipo astratto.
Il motivo per nascondere DogLeg è perché la funzione GetLeg nella classe astratta restituisce una gamba astratta.Se stai sovrascrivendo GetLeg devi restituire una Leg.Questo è il punto di avere un metodo in una classe astratta.Per propagare quel metodo ai suoi figli.Se vuoi che gli utenti di Dog conoscano DogLeg, crea un metodo chiamato GetDogLeg e restituisce un DogLeg.
Se POTRESTI fare quello che vuole chi ha posto la domanda, allora ogni utente di Animal dovrebbe conoscere TUTTI gli animali.
È un desiderio perfettamente valido che la firma di un metodo sottoposto a override abbia un tipo restituito che sia un sottotipo del tipo restituito nel metodo sottoposto a override (uff).Dopotutto, sono compatibili con il tipo di runtime.
Ma C# non supporta ancora i "tipi restituiti covarianti" nei metodi sovrascritti (a differenza di C++ [1998] e Java [2004]).
Dovrai aggirare il problema e arrangiarti per il prossimo futuro, come ha affermato Eric Lippert il suo blog[19 giugno 2008]:
Questo tipo di varianza è chiamata "covarianza del tipo di rendimento".
non abbiamo intenzione di implementare questo tipo di variazione in C#.
abstract class Animal
{
public virtual Leg GetLeg ()
}
abstract class Leg { }
class Dog : Animal
{
public override Leg GetLeg () { return new DogLeg(); }
}
class DogLeg : Leg { void Hump(); }
Fallo in questo modo, quindi potrai sfruttare l'astrazione nel tuo client:
Leg myleg = myDog.GetLeg();
Quindi, se necessario, puoi trasmetterlo:
if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }
Totalmente inventato, ma il punto è che puoi farlo:
foreach (Animal a in animals)
{
a.GetLeg().SomeMethodThatIsOnAllLegs();
}
Pur mantenendo la possibilità di avere uno speciale metodo Gobba su Doglegs.
È possibile utilizzare generici e interfacce per implementarlo in C#:
abstract class Leg { }
interface IAnimal { Leg GetLeg(); }
abstract class Animal<TLeg> : IAnimal where TLeg : Leg
{ public abstract TLeg GetLeg();
Leg IAnimal.GetLeg() { return this.GetLeg(); }
}
class Dog : Animal<Dog.DogLeg>
{ public class DogLeg : Leg { }
public override DogLeg GetLeg() { return new DogLeg();}
}
GetLeg() deve restituire Leg per essere un override.La tua classe Dog, tuttavia, può comunque restituire oggetti DogLeg poiché sono una classe figlia di Leg.i client possono quindi lanciarli e operare su di essi come dogleg.
public class ClientObj{
public void doStuff(){
Animal a=getAnimal();
if(a is Dog){
DogLeg dl = (DogLeg)a.GetLeg();
}
}
}
Il concetto che ti causa problemi è descritto in http://en.wikipedia.org/wiki/Covariance_and_contravariance_(informatica_scienza)
Non che sia molto utile, ma forse è interessante notare che Java supporta i rendimenti covarianti, e quindi funzionerebbe esattamente come speravi.Tranne ovviamente che Java non ha proprietà ;)
Forse è più semplice capire il problema con un esempio:
Animal dog = new Dog();
dog.SetLeg(new CatLeg());
Ora dovrebbe essere compilato se sei compilato da Dog, ma probabilmente non vogliamo un simile mutante.
Un problema correlato è Dog[] essere un Animal[] o IList<Dog> un IList<Animal>?
C# dispone di implementazioni di interfaccia esplicite per risolvere proprio questo problema:
abstract class Leg { }
class DogLeg : Leg { }
interface IAnimal
{
Leg GetLeg();
}
class Dog : IAnimal
{
public override DogLeg GetLeg() { /* */ }
Leg IAnimal.GetLeg() { return GetLeg(); }
}
Se hai un Dog tramite un riferimento di tipo Dog, la chiamata a GetLeg() restituirà un DogLeg.Se hai lo stesso oggetto, ma il riferimento è di tipo IAnimal, restituirà una Leg.
Giusto, capisco che posso semplicemente lanciare, ma ciò significa che il cliente deve sapere che i cani hanno DogLegs.Quello che mi chiedo è se ci sono ragioni tecniche per cui ciò non è possibile, dato che esiste una conversione implicita.
@Brian Leahy Ovviamente se stai opera solo come una gamba non c'è bisogno o motivo di lanciare.Ma se c'è qualche comportamento specifico di DogLeg o Dog, a volte ci sono ragioni per cui il cast è necessario.
Potresti anche restituire l'interfaccia ILeg implementata sia da Leg che da DogLeg.
La cosa importante da ricordare è che puoi usare un tipo derivato ogni volta che usi il tipo base (puoi passare Dog a qualsiasi metodo/proprietà/campo/variabile che si aspetta Animal)
Prendiamo questa funzione:
public void AddLeg(Animal a)
{
a.Leg = new Leg();
}
Una funzione perfettamente valida, ora chiamiamo la funzione in questo modo:
AddLeg(new Dog());
Se la proprietà Dog.Leg non è di tipo Leg, la funzione AddLeg contiene improvvisamente un errore e non può essere compilata.
@Luca
Penso che la tua eredità forse sia stata fraintesa.Dog.GetLeg() restituirà un oggetto DogLeg.
public class Dog{
public Leg GetLeg(){
DogLeg dl = new DogLeg(super.GetLeg());
//set dogleg specific properties
}
}
Animal a = getDog();
Leg l = a.GetLeg();
l.kick();
il metodo effettivamente chiamato sarà Dog.GetLeg();e DogLeg.Kick() (suppongo che esista un metodo Leg.kick()) per questo, il tipo restituito dichiarato essendo DogLeg non è necessario, perché questo è ciò che viene restituito, anche se il tipo restituito per Dog.GetLeg() è Gamba.
Puoi ottenere ciò che desideri utilizzando un generico con un vincolo appropriato, come il seguente:
abstract class Animal<LegType> where LegType : Leg
{
public abstract LegType GetLeg();
}
abstract class Leg { }
class Dog : Animal<DogLeg>
{
public override DogLeg GetLeg()
{
return new DogLeg();
}
}
class DogLeg : Leg { }