Ponieważ zapytałeś, dlaczego C # zrobił to w ten sposób, najlepiej zapytaj twórców C #. Anders Hejlsberg, główny architekt C #, odpowiedział, dlaczego nie zdecydował się na domyślną wirtualność (jak w Javie) w wywiadzie , odpowiednie fragmenty są poniżej.
Należy pamiętać, że Java ma domyślnie wirtualny z końcowym słowem kluczowym, aby oznaczyć metodę jako niewirtualną. Nadal dwie koncepcje do nauczenia się, ale wielu ludzi nie wie o ostatecznym słowie kluczowym lub nie używa proaktywnie. C # zmusza do korzystania z wirtualnego i nowego / zastępowania, aby świadomie podejmować te decyzje.
Jest kilka powodów. Jednym z nich jest wydajność . Możemy zaobserwować, że kiedy ludzie piszą kod w Javie, zapominają oznaczyć swoje metody jako ostateczne. Dlatego te metody są wirtualne. Ponieważ są wirtualne, nie działają tak dobrze. Istnieje tylko narzut związany z wydajnością związany z byciem metodą wirtualną. To jeden problem.
Ważniejszym problemem jest przechowywanie wersji . Istnieją dwie szkoły myślenia o metodach wirtualnych. Akademicka szkoła myślenia mówi: „Wszystko powinno być wirtualne, bo może kiedyś zechcę to zmienić”. Pragmatyczna szkoła myślenia, która pochodzi z budowania prawdziwych aplikacji działających w prawdziwym świecie, mówi: „Musimy być bardzo ostrożni w kwestii tego, co robimy wirtualnie”.
Kiedy tworzymy coś wirtualnego na platformie, składamy strasznie wiele obietnic dotyczących tego, jak ewoluuje ona w przyszłości. W przypadku metody innej niż wirtualna obiecujemy, że po wywołaniu tej metody nastąpi x i y. Kiedy publikujemy wirtualną metodę w interfejsie API, nie tylko obiecujemy, że gdy wywołasz tę metodę, nastąpi x i y. Obiecujemy również, że jeśli zastąpicie tę metodę, nazwiemy ją w tej konkretnej kolejności w odniesieniu do tych innych, a stan będzie w tym i tamtym niezmiennym.
Za każdym razem, gdy mówisz wirtualnie w interfejsie API, tworzysz hak oddzwaniania. Jako projektant systemu operacyjnego lub interfejsu API musisz być bardzo ostrożny. Nie chcesz, aby użytkownicy zastępowali i przechwytywali w dowolnym dowolnym punkcie interfejsu API, ponieważ niekoniecznie możesz składać te obietnice. I ludzie mogą nie rozumieć w pełni obietnic, które składają, gdy tworzą coś wirtualnego.
Wywiad ma więcej dyskusji na temat tego, jak programiści myślą o projektowaniu dziedziczenia klas i jak to doprowadziło do ich decyzji.
Teraz przejdź do następującego pytania:
Nie jestem w stanie zrozumieć, dlaczego na świecie dodam metodę do mojej DerivedClass o tej samej nazwie i tym samym podpisie co BaseClass i zdefiniuję nowe zachowanie, ale w polimorfizmie w czasie wykonywania zostanie wywołana metoda BaseClass! (co nie jest nadrzędne, ale logicznie powinno być).
Dzieje się tak, gdy klasa pochodna chce zadeklarować, że nie przestrzega kontraktu klasy podstawowej, ale ma metodę o tej samej nazwie. (Dla każdego, kto nie zna różnicy między new
i override
w C #, zobacz tę stronę MSDN ).
Bardzo praktycznym scenariuszem jest:
- Utworzono interfejs API, który ma klasę o nazwie
Vehicle
.
- Zacząłem używać twojego API i wyprowadziłem
Vehicle
.
- Twoja
Vehicle
klasa nie miała żadnej metody PerformEngineCheck()
.
- W mojej
Car
klasie dodaję metodę PerformEngineCheck()
.
- Wydałeś nową wersję swojego API i dodałeś
PerformEngineCheck()
.
- Nie mogę zmienić nazwy mojej metody, ponieważ moi klienci są zależni od mojego interfejsu API, co spowodowałoby ich uszkodzenie.
Więc kiedy ponownie skompiluję z twoim nowym API, C # ostrzega mnie przed tym problemem, np
Jeśli baza PerformEngineCheck()
nie była virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
A jeśli podstawą PerformEngineCheck()
było virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
Teraz muszę wyraźnie zdecydować, czy moja klasa faktycznie przedłuża umowę klasy podstawowej, czy też jest to inna umowa, ale okazuje się, że ma taką samą nazwę.
- Robiąc to
new
, nie łamię moich klientów, jeśli funkcjonalność metody podstawowej różni się od metody pochodnej. Jakikolwiek kod, do którego się odwołujesz Vehicle
, nie będzie Car.PerformEngineCheck()
wywoływany, ale kod, który zawierał odwołanie, Car
będzie nadal widzieć tę samą funkcjonalność, którą oferowałem PerformEngineCheck()
.
Podobny przykład jest, gdy inna metoda w klasie bazowej może wywoływać PerformEngineCheck()
(szczególnie w nowszej wersji), w jaki sposób można zapobiec wywołaniu PerformEngineCheck()
klasy pochodnej? W Javie ta decyzja spoczywałaby na klasie podstawowej, ale nie wie ona nic o klasie pochodnej. W języku C # decyzja ta zależy zarówno od klasy podstawowej (za pomocą virtual
słowa kluczowego), jak i od klasy pochodnej (za pomocą słów kluczowych new
i override
).
Oczywiście błędy, które generuje kompilator, zapewniają również przydatne narzędzie dla programistów, które nie mogą nieoczekiwanie popełnić błędów (tj. Albo przesłonić, albo udostępnić nową funkcjonalność, nie zdając sobie z tego sprawy).
Jak powiedział Anders, prawdziwy świat zmusza nas do takich problemów, do których, gdybyśmy mieli zacząć od zera, nigdy nie chcielibyśmy się w to wdawać.
EDYCJA: Dodano przykład, gdzie new
należy zastosować, aby zapewnić kompatybilność interfejsu.
EDYCJA: Przeglądając komentarze, natknąłem się również na napisane przez Erica Lipperta (wówczas jednego z członków komitetu projektowego C #) na inne przykładowe scenariusze (wspomniane przez Briana).
CZĘŚĆ 2: Na podstawie zaktualizowanego pytania
Ale w przypadku C #, jeśli SpaceShip nie zastąpi przyspieszenia klasy Vehicle i używa nowego, logika mojego kodu zostanie zepsuta. Czy to nie jest wada?
Kto decyduje, czy SpaceShip
faktycznie zastępuje, Vehicle.accelerate()
czy też jest inny? To musi być SpaceShip
programista. Więc jeśli SpaceShip
programista zdecyduje, że nie dotrzymuje umowy klasy podstawowej, to twoje wezwanie do Vehicle.accelerate()
nie powinno iść do SpaceShip.accelerate()
, czy powinno? Wtedy to oznaczą jako new
. Jeśli jednak zdecydują, że rzeczywiście dotrzymuje umowy, w rzeczywistości ją oznaczy override
. W obu przypadkach kod będzie działał poprawnie, wywołując poprawną metodę na podstawie umowy . W jaki sposób Twój kod może decydować, czy SpaceShip.accelerate()
faktycznie zastępuje, Vehicle.accelerate()
czy też jest kolizją nazw? (Zobacz mój przykład powyżej).
Jednak w przypadku domniemanego dziedziczenia, nawet jeśli SpaceShip.accelerate()
nie utrzymać umowę o Vehicle.accelerate()
, wywołanie metody będzie nadal iść do SpaceShip.accelerate()
.