Aby upewnić się, że jesteśmy na tej samej stronie, metoda „ukrywa się” polega na tym, że klasa pochodna definiuje element członkowski o tej samej nazwie co element klasy podstawowej (który, jeśli jest metodą / właściwością, nie jest oznaczony jako wirtualny / nadpisywalny) ), a po wywołaniu z instancji klasy pochodnej w „kontekście pochodnym” używany jest element pochodny, natomiast jeśli jest wywoływany przez to samo wystąpienie w kontekście swojej klasy podstawowej, używany jest element klasy podstawowej. Różni się to od abstrakcji / zastępowania elementu, gdy element klasy podstawowej oczekuje, że klasa pochodna zdefiniuje zamiennik, oraz od modyfikatorów zakresu / widoczności, które „ukrywają” członka przed konsumentami poza pożądanym zakresem.
Krótka odpowiedź na pytanie, dlaczego jest to dozwolone, jest taka, że nie zmuszałoby to deweloperów do łamania kilku kluczowych założeń projektowania obiektowego.
Oto dłuższa odpowiedź; po pierwsze, rozważ następującą strukturę klas w alternatywnym wszechświecie, w którym C # nie pozwala na ukrywanie członków:
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";}}
}
Chcemy anulować komentarz członka w pasku, a tym samym pozwolić Barowi na dostarczenie innego MyFooString. Nie możemy tego jednak zrobić, ponieważ stanowiłoby to naruszenie zakazu ukrywania członków w rzeczywistości alternatywnej. Ten konkretny przykład byłby powszechny dla błędów i jest doskonałym przykładem tego, dlaczego możesz chcieć go zakazać; na przykład, jakie dane wyjściowe konsoli uzyskałbyś, gdybyś zrobił następujące rzeczy?
Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;
Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);
Z czubka mojej głowy tak naprawdę nie jestem pewien, czy dostaniesz „Foo” czy „Bar” w tej ostatniej linii. Zdecydowanie dostaniesz „Foo” dla pierwszego wiersza i „Bar” dla drugiego, mimo że wszystkie trzy zmienne odnoszą się dokładnie do tego samego wystąpienia z dokładnie tym samym stanem.
Tak więc projektanci języka, w naszym alternatywnym wszechświecie, zniechęcają do tego oczywiście złego kodu, uniemożliwiając ukrywanie własności. Teraz, jako programista, naprawdę musisz dokładnie to zrobić. Jak obejść to ograniczenie? Jednym ze sposobów jest inna nazwa właściwości Bar:
public class Bar:Foo
{
public string MyBarString {get{return "Bar";}}
}
Idealnie legalne, ale nie takie zachowanie chcemy. Instancja Bar zawsze będzie produkować „Foo” dla właściwości MyFooString, gdy chcemy, aby produkowała „Bar”. Musimy nie tylko wiedzieć, że nasz IFoo to konkretnie Bar, musimy także wiedzieć, jak korzystać z innego akcesorium.
Możemy również, całkiem prawdopodobne, zapomnieć o relacji rodzic-dziecko i wdrożyć interfejs bezpośrednio:
public class Bar:IFoo
{
public string MyFooString {get{return "Bar";}}
public int FooMethod() {...}
}
W tym prostym przykładzie jest to idealna odpowiedź, o ile zależy ci tylko na tym, że Foo i Bar to IFoos. Kod użycia, o którym mowa w kilku przykładach, nie zostałby skompilowany, ponieważ pasek nie jest Foo i nie może być przypisany jako taki. Jednakże, jeśli Foo miał jakąś przydatną metodę „FooMethod”, której potrzebował Bar, teraz nie można dziedziczyć tej metody; musisz albo sklonować jego kod w Bar, albo uzyskać kreatywność:
public class Bar:IFoo
{
public string MyFooString {get{return "Bar";}}
private readonly theFoo = new Foo();
public int FooMethod(){return theFoo.FooMethod();}
}
To oczywisty hack i choć niektóre implementacje specyfikacji języka OO stanowią niewiele więcej, to jest to błędne koncepcyjnie; jeśli konsumenci Bar konieczności narazić funkcjonalność foo, bar powinien być Foo, nie mają do Foo.
Oczywiście, jeśli kontrolujemy Foo, możemy uczynić go wirtualnym, a następnie zastąpić. Jest to najlepsza praktyka konceptualna w naszym obecnym wszechświecie, gdy oczekuje się, że członek zostanie zastąpiony, i obejmowałby każdy alternatywny wszechświat, który nie pozwalałby na ukrywanie:
public class Foo:IFoo
{
public virtual string MyFooString {get{return "Foo";}}
//...
}
public class Bar:Foo
{
public override string MyFooString {get{return "Bar";}}
}
Problem polega na tym, że dostęp do elementów wirtualnych jest, pod maską, stosunkowo droższy w wykonywaniu, dlatego zazwyczaj chcesz to zrobić tylko wtedy, gdy jest to konieczne. Jednak brak ukrywania zmusza cię do pesymizmu co do elementów, które inny koder, który nie kontroluje twojego kodu źródłowego, może chcieć ponownie wdrożyć; „najlepszą praktyką” dla każdej niezamkniętej klasy byłoby uczynienie wszystkiego wirtualnym, chyba że specjalnie tego nie chcesz. Jest też jeszcze nie daje dokładnie zachowanie ukryciu; ciąg zawsze będzie miał wartość „Bar”, jeśli instancja to Bar. Czasami naprawdę przydatne jest wykorzystanie warstw danych o stanie ukrytym na podstawie poziomu dziedziczenia, na którym pracujesz.
Podsumowując, umożliwienie ukrywania się członków to mniejsze zło. Nie posiadanie go generalnie prowadzi do gorszych okrucieństw popełnianych wbrew zasadom obiektowym niż pozwala na to.