Dlaczego potrzebujemy nowego słowa kluczowego i dlaczego domyślnym zachowaniem jest ukrywanie, a nie zastępowanie?

81

Przeglądałem ten post na blogu i miałem następujące pytania:

  • Dlaczego potrzebujemy newsłowa kluczowego, czy tylko po to, aby określić, że metoda klasy bazowej jest ukrywana. Mam na myśli, dlaczego tego potrzebujemy? Jeśli nie używamy overridesłowa kluczowego, czy nie ukrywamy metody klasy bazowej?
  • Dlaczego domyślnie w C # ukrywa się i nie zastępuje? Dlaczego projektanci zaimplementowali to w ten sposób?
Piaskownica
źródło
6
Nieco bardziej interesujące (dla mnie) pytanie, dlaczego ukrywanie się jest w ogóle legalne. Zwykle jest to błąd (stąd ostrzeżenie kompilatora), rzadko potrzebny i zawsze w najlepszym przypadku problematyczny.
Konrad Rudolph,

Odpowiedzi:

127

Dobre pytania. Pozwól mi je powtórzyć.

Dlaczego w ogóle ukrywanie metody inną metodą jest legalne?

Odpowiem na to pytanie na przykładzie. Masz interfejs z CLR v1:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Wspaniały. Teraz w CLR v2 masz generyczne i myślisz „stary, gdybyśmy tylko mieli generyczne w v1, zrobiłbym to jako ogólny interfejs. Ale nie zrobiłem. Powinienem stworzyć coś kompatybilnego z tym teraz, co jest ogólne, więc Korzystam z zalet leków generycznych bez utraty wstecznej zgodności z kodem, który oczekuje od IEnumerable. ”

interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> .... uh oh

Co zamierzasz wywołać metodę GetEnumerator IEnumerable<T>? Pamiętaj, chcesz , aby ukrywał GetEnumerator w nieogólnym interfejsie podstawowym. Ty nigdy chcesz, aby ta rzecz została wywołana, chyba że jesteś wyraźnie w sytuacji kompatybilności wstecznej.

Samo to uzasadnia ukrywanie metody. Więcej przemyśleń na temat uzasadnień ukrywania metod w moim artykule na ten temat .

Dlaczego ukrywanie się bez „nowego” powoduje ostrzeżenie?

Ponieważ chcemy zwrócić Twoją uwagę, że coś ukrywasz i możesz to zrobić przypadkowo. Pamiętaj, że możesz przypadkowo ukryć coś z powodu edycji klasy bazowej wykonanej przez kogoś innego, a nie przez edycję klasy pochodnej.

Dlaczego ukrywanie bez „nowego” jest raczej ostrzeżeniem niż błędem?

Ten sam powód. Być może przypadkowo coś ukrywasz, ponieważ właśnie wybrałeś nową wersję klasy bazowej. Dzieje się to cały czas. FooCorp tworzy klasę bazową B. BarCorp tworzy klasę pochodną D za pomocą metody Bar, ponieważ ich klienci lubią tę metodę. FooCorp widzi to i mówi hej, to dobry pomysł, możemy umieścić tę funkcjonalność w klasie bazowej. Robią to i wysyłają nową wersję Foo.DLL, a kiedy BarCorp odbierze nową wersję, byłoby miło, gdyby powiedziano im, że ich metoda ukrywa teraz metodę klasy bazowej.

Chcemy, aby ta sytuacja była ostrzeżeniem, a nie błędem, ponieważ popełnienie błędu oznacza, że jest to kolejna forma kruchego problemu z klasą bazową . Język C # został starannie zaprojektowany, aby gdy ktoś wprowadził zmianę w klasie bazowej, wpływ na kod, który używa klasy pochodnej, został zminimalizowany.

Dlaczego ukrywa się i nie zastępuje wartości domyślnej?

Ponieważ wirtualne nadpisanie jest niebezpieczne . Wirtualne przesłonięcie umożliwia klasom pochodnym zmianę zachowania kodu, który został skompilowany w celu użycia klas podstawowych. Robienie czegoś niebezpiecznego, jak dokonywanie zmian, powinno być czymś, co robisz świadomie i celowo , a nie przez przypadek.

Eric Lippert
źródło
1
FWIW, nie zgadzam się, że „[t] hat sam uzasadnia ukrywanie metody”. Czasami lepiej jest przełamać kompatybilność wsteczną. Python 3 pokazał, że to faktycznie działa bez niszczenia zbyt dużej ilości pracy. Należy zauważyć, że „wsteczna kompatybilność za wszelką cenę” nie jest jedyną rozsądną opcją. I ten powód prowadzi do drugiego punktu: zmiana klasy bazowej zawsze będzie krucha. W rzeczywistości lepsze może być zerwanie kompilacji zależnej. (Ale +1 za odpowiedź na pytanie z mojego komentarza poniżej rzeczywistego pytania.)
Konrad Rudolph
@Konrad: Zgadzam się, że kompatybilność wsteczna może zaszkodzić. Mono zdecydowało się porzucić wsparcie dla 1.1 w przyszłych wydaniach.
simendsjo
1
@Konrad: Zerwanie wstecznej kompatybilności z twoim językiem jest czymś zupełnie innym niż ułatwienie ludziom używającym twojego języka złamania wstecznej kompatybilności z własnym kodem. Przykład Erica to drugi przypadek. A osoba pisząca ten kod podjęła decyzję, że chce wstecznej kompatybilności. Jeśli to możliwe, język powinien wspierać tę decyzję.
Brian
Boże odpowiedz, Eric. Istnieje jednak bardziej szczegółowy powód, dla którego domyślne nadpisywanie jest złe. Jeśli klasa bazowa Base wprowadza nową metodę Duck () (która zwraca obiekt kaczki), która tak się składa, że ma taki sam podpis jak Twoja istniejąca metoda Duck () (która sprawia, że ​​kaczor Mario) w klasie pochodnej Derived (tj. Brak komunikacji między Ty i pisarz Base), nie chcesz, aby klienci obu klas robili kaczkę Mario, kiedy chcą tylko Kaczki.
jyoungdev
Podsumowując, ma to głównie związek ze zmianą klas bazowych i interfejsów.
jyoungdev
15

Jeśli metoda w klasie pochodnej jest poprzedzona słowem kluczowym new, metoda jest definiowana jako niezależna od metody w klasie bazowej

Jeśli jednak nie określisz ani new, ani zastąpień, wynikowe dane wyjściowe będą takie same, jak w przypadku określenia new, ale otrzymasz ostrzeżenie kompilatora (ponieważ możesz nie być świadomy, że ukrywasz metodę w metodzie klasy bazowej, a nawet mogłeś chcieć go zastąpić i po prostu zapomnieć o dołączeniu słowa kluczowego).

Pomaga więc uniknąć błędów i wyraźnie pokazuje, co chcesz zrobić, a także tworzy bardziej czytelny kod, dzięki czemu można go łatwo zrozumieć.

Incognito
źródło
12

Warto zauważyć, że jedyny efektnew w tym kontekście jest zniesienie ostrzeżenia. Nie ma zmiany w semantyce.

Zatem jedna odpowiedź brzmi: musimy newzasygnalizować kompilatorowi, że ukrywanie jest celowe i pozbyć się ostrzeżenia.

Kolejne pytanie brzmi: jeśli nie możesz / nie możesz zastąpić metody, dlaczego miałbyś wprowadzić inną metodę o tej samej nazwie? Ponieważ ukrywanie jest w istocie konfliktem nazw. I oczywiście w większości przypadków można by tego uniknąć.

Jedynym dobrym powodem, dla którego przychodzi mi do głowy celowe ukrywanie się, jest wymuszenie nazwy przez interfejs.

Henk Holterman
źródło
6

W języku C # elementy członkowskie są domyślnie zapieczętowane, co oznacza, że ​​nie można ich zastąpić (chyba że są oznaczone słowami kluczowymi virtuallub abstract), a to ze względu na wydajność . Nowy modyfikator stosuje się wyraźnie ukryć odziedziczony członka.

Darin Dimitrov
źródło
4
Ale kiedy naprawdę chcesz użyć nowego modyfikatora?
simendsjo
1
Gdy chcesz jawnie ukryć element członkowski odziedziczony z klasy bazowej. Ukrycie dziedziczonego elementu członkowskiego oznacza, że ​​wersja pochodna elementu członkowskiego zastępuje wersję klasy bazowej.
Darin Dimitrov
3
Ale wszelkie metody używające klasy bazowej nadal będą wywoływać podstawową implementację, a nie pochodne. Wydaje mi się to niebezpieczną sytuacją.
simendsjo
@simendsjo, @Darin Dimitrov: Trudno mi też myśleć o przypadku użycia, w którym nowe słowo kluczowe jest naprawdę potrzebne. Wydaje mi się, że zawsze są lepsze sposoby. Zastanawiam się też, czy ukrycie implementacji klasy bazowej nie jest błędem projektowym, który łatwo prowadzi do błędów.
Dirk Vollmar,
2

Jeśli zastępowanie było domyślne bez określania override słowa kluczowego, możesz przypadkowo przesłonić jakąś metodę swojej bazy tylko z powodu równości nazw.

Strategia kompilatora .Net polega na emitowaniu ostrzeżeń, jeśli coś może pójść nie tak, aby być bezpiecznym, więc w tym przypadku, gdyby zastępowanie było domyślne, musiałoby być ostrzeżenie dla każdej zastępowanej metody - coś w rodzaju `` ostrzeżenie: sprawdź, czy naprawdę chcesz przekroczyć'.

František Žiačik
źródło
0

Moje przypuszczenie wynikałoby głównie z dziedziczenia wielu interfejsów. Używając dyskretnych interfejsów, byłoby bardzo możliwe, że dwa różne interfejsy używają tej samej sygnatury metody. Zezwolenie na użycie newsłowa kluczowego umożliwiłoby utworzenie tych różnych implementacji z jedną klasą, zamiast konieczności tworzenia dwóch odrębnych klas.

Zaktualizowano ... Eric dał mi pomysł, jak ulepszyć ten przykład.

public interface IAction1
{
    int DoWork();
}
public interface IAction2
{
    string DoWork();
}

public class MyBase : IAction1
{
    public int DoWork() { return 0; }
}
public class MyClass : MyBase, IAction2
{
    public new string DoWork() { return "Hi"; }
}

class Program
{
    static void Main(string[] args)
    {
        var myClass = new MyClass();
        var ret0 = myClass.DoWork(); //Hi
        var ret1 = ((IAction1)myClass).DoWork(); //0
        var ret2 = ((IAction2)myClass).DoWork(); //Hi
        var ret3 = ((MyBase)myClass).DoWork(); //0
        var ret4 = ((MyClass)myClass).DoWork(); //Hi
    }
}
Matthew Whited
źródło
1
W przypadku korzystania z wielu interfejsów można jawnie nazwać metody interfejsu, aby uniknąć kolizji. Nie rozumiem, co masz na myśli, mówiąc „inna implementacja dla jednej klasy”, obie mają ten sam podpis ...
simendsjo
Słowo newkluczowe pozwala również zmienić podstawę dziedziczenia dla wszystkich dzieci z tego miejsca.
Matthew Whited
-1

Jak zauważono, ukrywanie metody / właściwości umożliwia zmianę rzeczy w metodzie lub właściwości, których w innym przypadku nie można by łatwo zmienić. Jedną z sytuacji, w których może to być przydatne, jest zezwolenie dziedziczonej klasie na posiadanie właściwości do odczytu i zapisu, które są tylko do odczytu w klasie bazowej. Na przykład załóżmy, że klasa bazowa ma kilka właściwości tylko do odczytu o nazwie Wartość1-Wartość40 (oczywiście prawdziwa klasa używałaby lepszych nazw). Zapieczętowany element potomny tej klasy ma konstruktora, który pobiera obiekt klasy bazowej i kopiuje stamtąd wartości; klasa nie pozwala na ich późniejszą zmianę. Inny, dziedziczony potomek deklaruje właściwości do odczytu i zapisu o nazwie Wartość1-Wartość40, które po odczytaniu zachowują się tak samo jak wersje klasy bazowej, ale po zapisaniu pozwalają na zapisanie wartości.

Jedną irytacją związaną z tym podejściem - być może ktoś może mi pomóc - jest to, że nie znam sposobu, aby zarówno przesłonić, jak i zastąpić określoną właściwość w ramach tej samej klasy. Czy któryś z języków CLR na to pozwala (używam VB 2005)? Byłoby użyteczne, gdyby obiekt klasy bazowej i jego właściwości były abstrakcyjne, ale wymagałoby to od klasy pośredniej zastąpienia właściwości Value1 do Value40, zanim klasa potomna będzie mogła je przesłonić.

supercat
źródło