Solo per essere sicuri che siamo sulla stessa pagina, il metodo "nascondere" è quando una classe derivata definisce un membro con lo stesso nome di uno nella classe base (che, se si tratta di un metodo / proprietà, non è contrassegnato come virtuale / sostituibile ) e quando viene invocato da un'istanza della classe derivata in "contesto derivato", viene utilizzato il membro derivato, mentre se invocato dalla stessa istanza nel contesto della sua classe base, viene utilizzato il membro della classe base. Ciò è diverso dall'astrazione / sostituzione del membro in cui il membro della classe base si aspetta che la classe derivata definisca una sostituzione e dai modificatori di ambito / visibilità che "nascondono" un membro dai consumatori al di fuori dell'ambito desiderato.
La risposta breve al perché è consentito è che non farlo costringerebbe gli sviluppatori a violare diversi principi chiave della progettazione orientata agli oggetti.
Ecco la risposta più lunga; innanzitutto, considera la seguente struttura di classe in un universo alternativo in cui C # non consente il nascondimento dei membri:
public interface IFoo
{
string MyFooString {get;}
int FooMethod();
}
public class Foo:IFoo
{
public string MyFooString {get{return "Foo";}}
public int FooMethod() {//incredibly useful code here};
}
public class Bar:Foo
{
//public new string MyFooString {get{return "Bar";}}
}
Vogliamo decommentare il membro in Bar e, in tal modo, consentire a Bar di fornire un MyFooString diverso. Tuttavia, non possiamo farlo perché violerebbe il divieto della realtà alternativa di nascondere i membri. Questo esempio particolare sarebbe diffuso per i bug ed è un ottimo esempio del motivo per cui potresti voler vietarlo; ad esempio, quale output di console otterresti se avessi fatto quanto segue?
Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;
Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);
In cima alla mia testa, in realtà non sono sicuro se avresti trovato "Foo" o "Bar" su quest'ultima riga. Otterresti sicuramente "Foo" per la prima riga e "Bar" per la seconda, anche se tutte e tre le variabili fanno riferimento esattamente alla stessa istanza con esattamente lo stesso stato.
Quindi, i progettisti del linguaggio, nel nostro universo alternativo, scoraggiano questo codice ovviamente negativo impedendo il nascondimento delle proprietà. Ora, come programmatore, hai davvero bisogno di fare esattamente questo. Come aggirare la limitazione? Bene, un modo è quello di nominare la proprietà di Bar in modo diverso:
public class Bar:Foo
{
public string MyBarString {get{return "Bar";}}
}
Perfettamente legale, ma non è il comportamento che vogliamo. Un'istanza di Bar produrrà sempre "Foo" per la proprietà MyFooString, quando volevamo che producesse "Bar". Non solo dobbiamo sapere che il nostro IFoo è specificamente un bar, ma dobbiamo anche sapere di utilizzare i diversi accessori.
Potremmo anche, abbastanza plausibilmente, dimenticare la relazione genitore-figlio e implementare direttamente l'interfaccia:
public class Bar:IFoo
{
public string MyFooString {get{return "Bar";}}
public int FooMethod() {...}
}
Per questo semplice esempio è una risposta perfetta, purché ti interessi solo che Foo e Bar siano entrambi IFoos. Il codice di utilizzo che un paio di esempi non riuscirebbe a compilare perché una barra non è un Foo e non può essere assegnata come tale. Tuttavia, se Foo aveva qualche metodo utile "FooMethod" di cui aveva bisogno Bar, ora non puoi ereditare quel metodo; dovresti clonare il suo codice nella barra o essere creativo:
public class Bar:IFoo
{
public string MyFooString {get{return "Bar";}}
private readonly theFoo = new Foo();
public int FooMethod(){return theFoo.FooMethod();}
}
Questo è un trucco evidente, e mentre alcune implementazioni delle specifiche del linguaggio OO ammontano a poco più di questo, concettualmente è sbagliato; se i consumatori di Bar necessità di esporre la funzionalità di Foo, Bar dovrebbe essere un Foo, non hanno un Foo.
Ovviamente, se abbiamo controllato Foo, possiamo renderlo virtuale, quindi sovrascriverlo. Questa è la migliore pratica concettuale nel nostro universo attuale quando ci si aspetta che un membro venga sovrascritto, e si reggerebbe in qualsiasi universo alternativo che non permettesse di nascondersi:
public class Foo:IFoo
{
public virtual string MyFooString {get{return "Foo";}}
//...
}
public class Bar:Foo
{
public override string MyFooString {get{return "Bar";}}
}
Il problema è che l'accesso virtuale ai membri è, sotto il cofano, relativamente più costoso da eseguire, e quindi in genere si desidera farlo solo quando è necessario. La mancanza di nascondimento, tuttavia, ti costringe a essere pessimista sui membri che un altro programmatore che non controlla il tuo codice sorgente potrebbe voler reimplementare; la "migliore pratica" per qualsiasi classe non sigillata sarebbe quella di rendere tutto virtuale a meno che tu non lo volessi specificamente. Inoltre non ti dà ancora il comportamento esatto di nasconderti; la stringa sarà sempre "Bar" se l'istanza è una barra. A volte è davvero utile sfruttare i livelli di dati di stato nascosti, in base al livello di eredità a cui stai lavorando.
In sintesi, consentire il nascondimento dei membri è il minore di questi mali. Non averlo in genere porterebbe a peggiori atrocità commesse contro principi orientati agli oggetti di quanto non lo consenta.