2013-07-30 10 views
10

Sono bloccato in questa situazione in cui:È possibile sovrascrivere un metodo con un parametro derivato anziché uno di base?

  1. devo una classe astratta denominata Ammo, con AmmoBox e Clip come i bambini.
  2. Ho una classe astratta denominata Weapon, con Firearm e Melee come bambini.
  3. Firearm è astratto, con ClipWeapon e ShellWeapon come bambini.
  4. All'interno Firearm, c'è una void Reload(Ammo ammo);

Il problema è che, una ClipWeapon potrebbe utilizzare sia un Clip ed un AmmoBox Per ricaricare:

public override void Reload(Ammo ammo) 
{ 
    if (ammo is Clip) 
    { 
     SwapClips(ammo as Clip); 
    } 
    else if (ammo is AmmoBox) 
    { 
     var ammoBox = ammo as AmmoBox; 
     // AddBullets returns how many bullets has left from its parameter 
     ammoBox.Set(clip.AddBullets(ammoBox.nBullets)); 
    } 
} 

Ma un ShellWeapon, potrebbe utilizzare solo una AmmoBox a ricaricare. Ho potuto fare questo:

public override void Reload(Ammo ammo) 
{ 
    if (ammo is AmmoBox) 
    { 
     // reload... 
    } 
} 

Ma questo è un male perché, anche se sto controllando per assicurarsi che sia di tipo AmmoBox, dall'esterno, appare come un ShellWeapon potrebbe prendere un Clip così, dal momento che un Clip è Ammo pure.

Oppure, potrei rimuovere Reload da Firearm, e metterlo sia ClipWeapon e ShellWeapon con i params specifici di cui ho bisogno, ma in questo modo io perdo i benefici del polimorfismo, che non è quello che voglio.

Non sarebbe ottimale, se potessi ignorare Reload all'interno ShellWeapon come questo:

public override void Reload(AmmoBox ammoBox) 
{ 
    // reload ... 
} 

Naturalmente ho provato e non ha funzionato, ho ottenuto un errore dicendo che la firma deve corrispondere o qualcosa del genere, ma non dovrebbe essere valido 'logicamente'? dal AmmoBox è un Ammo?

Come dovrei aggirare questo? E in generale, il mio progetto è corretto? (Nota Stavo usando le interfacce IClipWeapon e IShellWeapon ma mi sono imbattuto in problemi, quindi mi sono trasferito a utilizzare invece le classi)

Grazie in anticipo.

risposta

13

but shouldn't this be valid 'logically'?

No. L'interfaccia dice che il chiamante può passare in qualsiasi Ammo - dove si sta limitando a richiedere una AmmoBox, che è più specifica.

Cosa ci si può aspettare che accada se qualcuno dovesse scrivere:

Firearm firearm = new ShellWeapon(); 
firearm.Reload(new Ammo()); 

? Dovrebbe essere un codice interamente valido, quindi vuoi farlo esplodere al momento dell'esecuzione? Metà del punto di tipizzazione statica è evitare questo tipo di problema.

Si potrebbe fare Firearm generica nel tipo di munizioni è usi:

public abstract class Firearm<TAmmo> : Weapon where TAmmo : Ammo 
{ 
    public abstract void Reload(TAmmo ammo); 
} 

Poi:

public class ShellWeapon : Firearm<AmmoBox> 

che può o non può essere un modo utile per fare le cose, ma è almeno vale la pena considerarlo.

+0

Come si può riscrivere la definizione generica che hai scritto, ma per arma da fuoco, invece di Arma? (perché le armi Shell e Clip ereditano direttamente da Firearm e non da Weapon) Ho provato qualcosa del genere ma ovviamente non è così: classe astratta pubblica Firearm dove AmmoType: Munizioni: Arma XD – vexe

+0

@VeXe: Devi mettere il ': Weapon 'prima di' dove AmmoType: Munizioni'. Aggiornerò la mia risposta per mostrarlo. Vorrei * fortemente * consigliare di utilizzare il prefisso 'T' per i parametri del tipo. –

+0

Penso che sia meglio, ma posso chiederti perché hai detto che potrebbe non essere un modo utile di fare le cose? – vexe

1

Penso che sia perfettamente corretto verificare se il numero Ammo è valido. La situazione simile è, quando la funzione accetta uno Stream, ma controlla internamente, se è ricercabile o scrivibile - a seconda delle sue esigenze.

3

Il problema con cui si sta lottando deriva dalla necessità di chiamare un'implementazione diversa in base ai tipi di runtime delle munizioni e dell'arma. Essenzialmente, l'azione di ricaricare deve essere "virtuale" rispetto a due, non a un solo oggetto. Questo problema si chiama double dispatch.

Un modo per affrontarlo sarebbe la creazione di un visitatore come costrutto:

abstract class Ammo { 
    public virtual void AddToShellWeapon(ShellWeapon weapon) { 
     throw new ApplicationException("Ammo cannot be added to shell weapon."); 
    } 
    public virtual void AddToClipWeapon(ClipWeapon weapon) { 
     throw new ApplicationException("Ammo cannot be added to clip weapon."); 
    } 
} 
class AmmoBox : Ammo { 
    public override void AddToShellWeapon(ShellWeapon weapon) { 
     ... 
    } 
    public override void AddToClipWeapon(ClipWeapon weapon) { 
     ... 
    } 
} 
class Clip : Ammo { 
    public override void AddToClipWeapon(ClipWeapon weapon) { 
     ... 
    } 
} 
abstract class Weapon { 
    public abstract void Reload(Ammo ammo); 
} 
class ShellWeapon : Weapon { 
    public void Reload(Ammo ammo) { 
     ammo.AddToShellWeapon(this); 
    } 
} 
class ClipWeapon : Weapon { 
    public void Reload(Ammo ammo) { 
     ammo.AddToClipWeapon(this); 
    } 
} 

"La magia" avviene nelle implementazioni di Reload delle sottoclassi di armi: piuttosto che decidere che tipo di munizioni che ricevono, lasciano che la munizione faccia "la seconda parte" del doppio invio e chiama qualsiasi metodo appropriato, perché i loro metodi AddTo...Weapon conoscono sia il proprio tipo sia il tipo di arma in cui vengono ricaricati.

+0

Il problema è che, sia AddToShellWeapon che AddToClipWeapon sono entrambi astratti, devono essere definiti in chiunque stia ereditando Munizioni, quindi all'interno di Clip c'è AddToShellWeapon, che è ridondante. Correggimi se sbaglio. – vexe

+0

@VeXe Non è necessario renderli astratti - puoi renderli virtuali e farli lanciare un'eccezione di default, dicendo "questo tipo di amo non può essere aggiunto a quel tipo di arma". Ho modificato la risposta per illustrare l'approccio. – dasblinkenlight

+0

È una buona idea, andando dall'altra parte, ma se dovessi farlo, allora perché non dovrei usare la mia prima soluzione e aggiungere un altro se (ammo è Clip) -> lanciare un'eccezione? Che cosa offre di più il tuo metodo, in modo che lo consideri al posto del mio? – vexe

2

È possibile utilizzare la composizione con le estensioni di interfaccia invece di multiple-eredità:

class Ammo {} 
class Clip : Ammo {} 
class AmmoBox : Ammo {} 

class Firearm {} 
interface IClipReloadable {} 
interface IAmmoBoxReloadable {} 

class ClipWeapon : Firearm, IClipReloadable, IAmmoBoxReloadable {} 
class AmmoBoxWeapon : Firearm, IAmmoBoxReloadable {} 

static class IClipReloadExtension { 
    public static void Reload(this IClipReloadable firearm, Clip ammo) {} 
} 

static class IAmmoBoxReloadExtension { 
    public static void Reload(this IAmmoBoxReloadable firearm, AmmoBox ammo) {} 
} 

Così che si avrà 2 definizioni di Reload() con la clip e AmmoBox come argomenti a ClipWeapon e solo 1 Reload() metodo nella classe AmmoBoxWeapon con argomento AmmoBox.

var ammoBox = new AmmoBox(); 
var clip = new Clip(); 

var clipWeapon = new ClipWeapon(); 
clipWeapon.Reload(ammoBox); 
clipWeapon.Reload(clip); 

var ammoBoxWeapon = new AmmoBoxWeapon(); 
ammoBoxWeapon.Reload(ammoBox); 

E se si tenta passaggio clip per AmmoBoxWeapon.Reload si otterrà un errore:

ammoBoxWeapon.Reload(clip); // <- ERROR at compile time 
+0

Questo è ESATTAMENTE quello che sto cercando di fare, ma il modo in cui lo stai facendo è un po 'fuori dalla mia conoscenza. Non sono riuscito a capire cosa succede con le estensioni e cosa sta facendo il "this" ... forse vado a controllare alcuni tutorial su questa estensione dell'interfaccia e dare nuovamente un'occhiata alla tua soluzione. Cosa dovrebbe contenere all'interno delle interfacce? Scusa ma al momento è un po 'sfocato. – vexe

+0

@VeXe Se è necessario solo il metodo Ricarica, è possibile lasciare le interfacce vuote, l'estensione farà tutto ciò che serve. Date un'occhiata a loro è piuttosto utile caratteristica di C# per l'estensione funzionale delle classi. –

+0

@Rinold è bellissimo, grazie – user25064