W Javie nie istnieją virtual
, new
, override
kluczowe dla określenia metody. Tak więc działanie metody jest łatwe do zrozumienia. Ponieważ jeśli DerivedClass rozszerza BaseClass i ma metodę o tej samej nazwie i tej samej sygnaturze BaseClass, wówczas nadpisanie nastąpi w czasie polimorfizmu w czasie wykonywania (pod warunkiem, że metoda nie jest static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Przejdźmy teraz do C # nie może być tak wiele zamieszania i trudno zrozumieć, w jaki sposób new
lub virtual+derive
czy nowy wirtualny + korekcja działa.
Nie jestem w stanie zrozumieć, dlaczego na świecie dodam do siebie metodę DerivedClass
o tej samej nazwie i tej samej sygnaturze BaseClass
oraz zdefiniuję nowe zachowanie, ale przy polimorfizmie w czasie wykonywania BaseClass
metoda zostanie wywołana! (co nie jest nadrzędne, ale logicznie powinno być).
W przypadku, virtual + override
gdy logiczna implementacja jest poprawna, ale programista musi pomyśleć, którą metodą powinien zezwolić użytkownikowi na zastąpienie w czasie kodowania. Który ma pewne pro-cony (nie chodźmy tam teraz).
Więc dlaczego w C # jest tak dużo miejsca dla un -logical rozumowania i zamieszania. Czy mogę więc zmienić moje pytanie na pytanie, w jakim kontekście świata rzeczywistego powinienem pomyśleć o użyciu virtual + override
zamiast, new
a także o użyciu new
zamiast virtual + override
?
Po kilku bardzo dobrych odpowiedziach, szczególnie od Omara , dochodzę do wniosku, że projektanci C # kładli większy nacisk na programistów, którzy powinni pomyśleć, zanim opracują metodę, która jest dobra i radzi sobie z niektórymi błędami początkującymi w Javie.
Teraz mam na myśli pytanie. Jak w Javie, gdybym miał taki kod
Vehicle vehicle = new Car();
vehicle.accelerate();
a później tworzę nową klasę na SpaceShip
podstawie Vehicle
. Następnie chcę zmienić wszystko car
na SpaceShip
obiekt, po prostu muszę zmienić pojedynczy wiersz kodu
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
To nie złamie żadnej mojej logiki w żadnym punkcie kodu.
Ale w przypadku C #, jeśli SpaceShip
nie zastąpi Vehicle
klasy ' accelerate
i użyj, new
wtedy logika mojego kodu zostanie zepsuta. Czy to nie jest wada?
źródło
final
; jest odwrotnie).@Override
.Odpowiedzi:
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.
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:
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
ioverride
w C #, zobacz tę stronę MSDN ).Bardzo praktycznym scenariuszem jest:
Vehicle
.Vehicle
.Vehicle
klasa nie miała żadnej metodyPerformEngineCheck()
.Car
klasie dodaję metodęPerformEngineCheck()
.PerformEngineCheck()
.Więc kiedy ponownie skompiluję z twoim nowym API, C # ostrzega mnie przed tym problemem, np
Jeśli baza
PerformEngineCheck()
nie byłavirtual
:A jeśli podstawą
PerformEngineCheck()
byłovirtual
: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ę.
new
, nie łamię moich klientów, jeśli funkcjonalność metody podstawowej różni się od metody pochodnej. Jakikolwiek kod, do którego się odwołujeszVehicle
, nie będzieCar.PerformEngineCheck()
wywoływany, ale kod, który zawierał odwołanie,Car
będzie nadal widzieć tę samą funkcjonalność, którą oferowałemPerformEngineCheck()
.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łaniuPerformEngineCheck()
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 kluczowychnew
ioverride
).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
Kto decyduje, czy
SpaceShip
faktycznie zastępuje,Vehicle.accelerate()
czy też jest inny? To musi byćSpaceShip
programista. Więc jeśliSpaceShip
programista zdecyduje, że nie dotrzymuje umowy klasy podstawowej, to twoje wezwanie doVehicle.accelerate()
nie powinno iść doSpaceShip.accelerate()
, czy powinno? Wtedy to oznaczą jakonew
. Jeśli jednak zdecydują, że rzeczywiście dotrzymuje umowy, w rzeczywistości ją oznaczyoverride
. 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ć, czySpaceShip.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ę oVehicle.accelerate()
, wywołanie metody będzie nadal iść doSpaceShip.accelerate()
.źródło
Zostało to zrobione, ponieważ jest to właściwe. Faktem jest, że zezwolenie na zastąpienie wszystkich metod jest złe; prowadzi to do delikatnego problemu z klasą podstawową, w którym nie można stwierdzić, czy zmiana w klasie podstawowej spowoduje uszkodzenie podklas. Dlatego musisz albo umieścić na czarnej liście metody, których nie należy zastępować, albo umieścić na białej liście te, które mogą zostać zastąpione. Z dwóch, biała lista jest nie tylko bezpieczniejsze (ponieważ nie można utworzyć klasę bazową kruchy przypadkowo ), to również wymaga mniej pracy, ponieważ trzeba uniknąć dziedziczenia na rzecz kompozycji.
źródło
final
modyfikator, więc w czym problem?HashMap
?). Albo zaprojektuj dziedziczenie i wyjaśnij umowę, albo nie, i upewnij się, że nikt nie będzie mógł dziedziczyć klasy.@Override
został dodany jako bandaid, ponieważ było już za późno, aby zmienić zachowanie, ale nawet oryginalni deweloperzy Java (szczególnie Josh Bloch) zgadzają się, że była to zła decyzja.Jak powiedział Robert Harvey, wszystko zależy od tego, do czego jesteś przyzwyczajony. Uważam, że brak elastyczności Javy jest dziwny.
To powiedziawszy, dlaczego to w ogóle ma? Z tego samego powodu, że C # ma
public
,internal
(także „nic”)protected
,protected internal
iprivate
, ale tylko Java mapublic
,protected
nic, iprivate
. Zapewnia lepszą kontrolę ziarna nad zachowaniem tego, co kodujesz, kosztem posiadania większej liczby terminów i słów kluczowych do śledzenia.W przypadku
new
vs.virtual+override
wygląda to tak:abstract
iwoverride
podklasie.virtual
iwoverride
tej podklasie.new
w podklasie.sealed
w klasie podstawowej.Na przykład w świecie rzeczywistym: projekt, nad którym pracowałem nad przetworzonymi zamówieniami e-commerce z wielu różnych źródeł. Istniała podstawa,
OrderProcessor
która miała większość logiki, z pewnymiabstract
/virtual
metodami dla klas potomnych każdego źródła do przesłonięcia. Działało to dobrze, dopóki nie otrzymaliśmy nowego źródła, które miało zupełnie inny sposób przetwarzania zamówień, tak że musieliśmy zastąpić podstawową funkcję. W tym momencie mieliśmy dwie możliwości: 1) Dodajvirtual
metodę podstawową ioverride
dziecko; lub 2) Dodajnew
do dziecka.Chociaż jedno z nich mogłoby działać, pierwszy bardzo ułatwiłoby zastąpienie tej konkretnej metody w przyszłości. Na przykład pojawiłby się w autouzupełnianiu. Był to jednak wyjątkowy przypadek, dlatego zdecydowaliśmy się na jego użycie
new
. Dzięki temu zachowano standard „tej metody nie trzeba zastępować”, dopuszczając przy tym wyjątkowy przypadek. To różnica semantyczna, która ułatwia życie.Należy pamiętać jednak, że nie ma różnicy zachowanie wiąże się z tym, a nie tylko różnica semantyczna. Szczegółowe informacje można znaleźć w tym artykule . Jednak nigdy nie spotkałem się z sytuacją, w której musiałem skorzystać z tego zachowania.
źródło
new
tego sposobu jest błędem, który czeka. Jeśli wywołam metodę w jakiejś instancji, spodziewam się, że zawsze zrobi to samo, nawet jeśli przerzucę instancję do jej klasy podstawowej. Ale nie taknew
zachowują się metody ed.new
jest WebViewPage <TModel> w frameworku MVC. Ale od czasu do czasu rzuca mnie błądnew
, więc nie sądzę, żeby było to nierozsądne pytanie.Konstrukcja Java jest taka, że w przypadku jakiegokolwiek odwołania do obiektu, wywołanie określonej nazwy metody z określonymi typami parametrów, jeśli w ogóle jest dozwolone, zawsze wywołuje tę samą metodę. Możliwe jest, że na typ niejawnej konwersji może mieć wpływ typ odwołania, ale po rozwiązaniu wszystkich takich konwersji typ odwołania nie ma znaczenia.
Upraszcza to środowisko wykonawcze, ale może powodować niefortunne problemy. Załóżmy,
GrafBase
że nie implementujevoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
, aleGrafDerived
implementuje go jako metodę publiczną, która rysuje równoległobok, którego obliczony czwarty punkt jest przeciwny do pierwszego. Załóżmy ponadto, że późniejsza wersjaGrafBase
implementuje metodę publiczną o tej samej sygnaturze, ale jej obliczony czwarty punkt jest przeciwny do drugiego. Klienci, którzy otrzymają, oczekują,GrafBase
ale otrzymają odniesienie do,GrafDerived
będąDrawParallelogram
obliczać czwarty punkt zgodnie zeGrafBase
sposobem nowej metody, ale klienci, którzy używaliGrafDerived.DrawParallelogram
przed zmianą metody podstawowej, będą oczekiwać zachowania, które zostałoGrafDerived
pierwotnie zaimplementowane.W Javie autor nie miałby
GrafDerived
możliwości, aby ta klasa współistniała z klientami korzystającymi z nowejGrafBase.DrawParallelogram
metody (i mogą nie zdawać sobie sprawy, że wGrafDerived
ogóle istnieją) bez naruszenia kompatybilności z istniejącym kodem klienta, który byłGrafDerived.DrawParallelogram
wcześniejGrafBase
zdefiniowany. PonieważDrawParallelogram
nie jest w stanie powiedzieć, jakiego rodzaju klient go wywołuje, musi zachowywać się identycznie, gdy jest wywoływany przez rodzaj kodu klienta. Ponieważ oba rodzaje kodu klienta mają różne oczekiwania co do tego, jak powinien się zachowywać, nie można w żaden sposóbGrafDerived
uniknąć naruszenia uzasadnionych oczekiwań jednego z nich (tj. Złamania uzasadnionego kodu klienta).W języku C #, jeśli
GrafDerived
nie zostanie ponownie skompilowany, środowisko wykonawcze przyjmie, że kod, który wywołujeDrawParallelogram
metodę na podstawie referencji typu,GrafDerived
będzie oczekiwał zachowania, jakieGrafDerived.DrawParallelogram()
miał podczas ostatniej kompilacji, ale kod, który wywołuje metodę na podstawie referencji typu,GrafBase
będzie oczekiwałGrafBase.DrawParallelogram
(zachowanie, które zostało dodane). JeśliGrafDerived
później zostanie ponownie skompilowany w obecności rozszerzonegoGrafBase
, kompilator będzie skrzeczał, dopóki programista nie określi, czy jego metoda miała być prawidłowym zamiennikiem dziedziczonego elementuGrafBase
, czy też jego zachowanie musi być powiązane z referencjami typuGrafDerived
, ale nie powinno zastąpić zachowanie referencji typuGrafBase
.Można zasadnie argumentować, że posiadanie metody
GrafDerived
robienia czegoś innego niż członek,GrafBase
który ma taką samą sygnaturę, oznaczałoby zły projekt i jako taki nie powinien być obsługiwany. Niestety, ponieważ autor typu podstawowego nie ma możliwości dowiedzenia się, jakie metody można dodać do typów pochodnych, i odwrotnie, sytuacja, w której klienci klasy podstawowej i klasy pochodnej mają różne oczekiwania wobec metod o podobnych nazwach, jest zasadniczo nieunikniona, chyba że nikt nie może dodawać żadnych nazw, które ktoś mógłby również dodać. Pytanie nie dotyczy tego, czy takie zdublowanie nazwy powinno się zdarzyć, ale raczej, jak zminimalizować szkodę, kiedy to nastąpi.źródło
DerivedGraphics? A class using
GrafDerived`?GrafDerived
. Zacząłem używaćDerivedGraphics
, ale czułem, że to było trochę długie. NawetGrafDerived
jest jeszcze trochę długi, ale nie wiedziałem, jak najlepiej nazwać typy renderujące grafiki, które powinny mieć wyraźną relację podstawową / pochodną.Standardowa sytuacja:
Jesteś właścicielem klasy podstawowej, która jest używana w wielu projektach. Chcesz zmienić wspomnianą klasę podstawową, która rozbije między 1 i niezliczoną liczbę klas pochodnych, które znajdują się w projektach zapewniających rzeczywistą wartość (Framework zapewnia co najwyżej wartość przy jednym usunięciu, żadna prawdziwa istota ludzka nie chce Framework, chcą rzecz działająca na frameworku). Powodzenia dla zapracowanych właścicieli klas pochodnych; „no cóż, musisz się zmienić, nie powinieneś był zastąpić tej metody” bez uzyskania odpowiedzi jako „Framework: Delayer of Projects and Causer of Bugs” dla ludzi, którzy muszą zatwierdzać decyzje.
Zwłaszcza, że nie uznając, że nie można go zastąpić, domyślnie oświadczyłeś, że można robić rzeczy, które teraz uniemożliwiają zmianę.
A jeśli nie masz znacznej liczby klas pochodnych zapewniających rzeczywistą wartość poprzez przesłonięcie swojej klasy podstawowej, dlaczego jest to klasa podstawowa? Nadzieja jest silnym czynnikiem motywującym, ale także bardzo dobrym sposobem na uzyskanie kodu bez odniesień.
Rezultat końcowy: kod klasy bazowej frameworka staje się niewiarygodnie kruchy i statyczny, a tak naprawdę nie można wprowadzić niezbędnych zmian, aby zachować aktualność / wydajność. Alternatywnie, twój framework otrzymuje rep za niestabilność (klasy pochodne ciągle się psują) i ludzie w ogóle go nie używają, ponieważ głównym powodem korzystania z frameworka jest szybsze i bardziej niezawodne kodowanie.
Mówiąc najprościej, nie możesz prosić zajętych właścicieli projektów o opóźnienie ich projektu w celu naprawienia wprowadzanych przez ciebie błędów i oczekiwać czegoś lepszego niż „odejść”, chyba że zapewnisz im znaczące korzyści, nawet jeśli oryginalna „wina” było ich, co w najlepszym razie można argumentować.
Lepiej nie pozwól, aby zrobili coś złego w pierwszej kolejności, czyli tam, gdzie pojawia się „domyślnie nie-wirtualny”. A kiedy ktoś przychodzi do ciebie z bardzo wyraźnym powodem, dla którego potrzebuje tej konkretnej metody, aby ją zastąpić i dlaczego powinien być bezpieczny, możesz go „odblokować” bez ryzyka złamania kodu innej osoby.
źródło
Domyślnie nie-wirtualny zakłada, że programista klasy podstawowej jest idealny. Z mojego doświadczenia wynika, że programiści nie są doskonali. Jeśli twórca klasy podstawowej nie jest w stanie wyobrazić sobie przypadku użycia, w którym metoda może zostać zastąpiona lub zapomina o dodaniu wirtualnego, nie mogę skorzystać z polimorfizmu podczas rozszerzania klasy podstawowej bez modyfikacji klasy podstawowej. W świecie rzeczywistym modyfikowanie klasy bazowej często nie jest opcją.
W języku C # programista klasy podstawowej nie ufa programistowi podklasy. W Javie programista podklas nie ufa programistom klasy podstawowej. Twórca podklasy jest odpowiedzialny za podklasę i powinien (imho) otrzymać uprawnienia do rozszerzenia klasy bazowej według własnego uznania (wykluczając jawne zaprzeczanie, aw java mogą nawet pomylić się).
Jest to podstawowa właściwość definicji języka. To nie jest dobre ani złe, jest tym, czym jest i nie może się zmienić.
źródło