Dlaczego C # został stworzony przy użyciu słów kluczowych „new” i „virtual + override” w przeciwieństwie do Java?

61

W Javie nie istnieją virtual, new, overridekluczowe 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 newlub virtual+deriveczy nowy wirtualny + korekcja działa.

Nie jestem w stanie zrozumieć, dlaczego na świecie dodam do siebie metodę DerivedClasso tej samej nazwie i tej samej sygnaturze BaseClassoraz zdefiniuję nowe zachowanie, ale przy polimorfizmie w czasie wykonywania BaseClassmetoda zostanie wywołana! (co nie jest nadrzędne, ale logicznie powinno być).

W przypadku, virtual + overridegdy 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 + overridezamiast, newa także o użyciu newzamiast 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 SpaceShippodstawie Vehicle. Następnie chcę zmienić wszystko carna SpaceShipobiekt, 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 SpaceShipnie zastąpi Vehicleklasy ' acceleratei użyj, newwtedy logika mojego kodu zostanie zepsuta. Czy to nie jest wada?

Anirban Nag „tintinmj”
źródło
77
Jesteś przyzwyczajony do sposobu, w jaki robi to Java, i po prostu nie poświęciłeś czasu na zrozumienie słów kluczowych C # na własnych warunkach. Pracuję z C #, od razu zrozumiałem warunki i stwierdziłem, że Java robi to dziwnie.
Robert Harvey
12
IIRC, C # zrobił to, aby „sprzyjać przejrzystości”. Jeśli musisz wyraźnie powiedzieć „nowy” lub „zastąpienie”, staje się jasne i natychmiast widoczne, co się dzieje, a nie musisz rozglądać się, próbując dowiedzieć się, czy metoda zastępuje pewne zachowanie w klasie bazowej, czy nie. Uważam też, że bardzo przydatne jest określenie metod, które chcę określić jako wirtualne, a których nie. (Java robi to z final; jest odwrotnie).
Robert Harvey
15
„W Javie nie ma wirtualnych, nowych słów kluczowych zastępujących definicję metody.” Z wyjątkiem tego, że istnieje @Override.
svick
3
Adnotacja @svick i słowa kluczowe to nie to samo :)
Anirban Nag 'tintinmj'

Odpowiedzi:

86

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 newi overridew 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 Vehicleklasa nie miała żadnej metody PerformEngineCheck().
  • W mojej Carklasie 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, Carbę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ą virtualsłowa kluczowego), jak i od klasy pochodnej (za pomocą słów kluczowych newi 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 newnależ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 SpaceShipfaktycznie zastępuje, Vehicle.accelerate()czy też jest inny? To musi być SpaceShipprogramista. Więc jeśli SpaceShipprogramista 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().

Omer Iqbal
źródło
12
Punkt wydajności jest już całkowicie przestarzały. Na dowód zobaczyć mój punkt odniesienia pokazując, że dostęp do pola poprzez nieostatecznej ale nigdy przeciążona metoda przyjmuje jeden cykl.
maaartinus
7
Jasne, że tak może być. Pytanie brzmiało, że kiedy C # zdecydował się to zrobić, dlaczego zrobił to w TYM czasie i stąd ta odpowiedź jest prawidłowa. Jeśli pytanie brzmi, czy nadal ma to sens, to inna dyskusja, IMHO.
Omer Iqbal
1
Całkowicie się z Tobą zgadzam.
maaartinus
2
IMHO, choć zdecydowanie istnieją zastosowania do nie-wirtualnych funkcji, ryzyko niespodziewanych pułapek występuje, gdy robi to coś, co nie powinno przesłonić metody klasy podstawowej lub implementować interfejsu.
supercat
3
@Nilzor: Stąd argument dotyczący akademickiego kontra pragmatycznego. Pragmatycznie odpowiedzialność za złamanie czegoś spoczywa na ostatnim zmieniaczu. Jeśli zmienisz klasę podstawową, a istniejąca klasa pochodna zależy od tego, czy się nie zmieni, nie jest to ich problem („one” mogą już nie istnieć). Tak więc klasy podstawowe zostają zablokowane w zachowaniu klas pochodnych, nawet jeśli klasa ta nigdy nie powinna była być wirtualna . I jak mówisz, RhinoMock istnieje. Tak, jeśli poprawnie ukończysz wszystkie metody, wszystko jest równoważne. Tutaj wskazujemy na każdy system Java, jaki kiedykolwiek zbudowano.
deworde
94

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.

Doval
źródło
7
Zezwolenie na zastąpienie dowolnej metody jest niewłaściwe. Powinieneś być w stanie zastąpić tylko te metody, które projektant nadklasy określił jako bezpieczne do zastąpienia.
Doval
21
Eric Lippert szczegółowo omawia to w swoim poście, Metody wirtualne i Kruche klasy podstawowe .
Brian
12
Java ma finalmodyfikator, więc w czym problem?
Sarge Barszcz
13
@corsiKa „obiekty powinny być otwarte na rozszerzenie” - dlaczego? Jeśli twórca klasy podstawowej nigdy nie myślał o dziedziczeniu, jest prawie pewne, że w kodzie są subtelne zależności i założenia, które doprowadzą do błędów (pamiętasz HashMap?). Albo zaprojektuj dziedziczenie i wyjaśnij umowę, albo nie, i upewnij się, że nikt nie będzie mógł dziedziczyć klasy. @Overridezostał 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.
Voo
9
@jwenting „Opinia” to zabawne słowo; ludzie lubią dołączać to do faktów, aby mogli je zignorować. Ale niezależnie od tego, co jest warte, jest to również „opinia” Joshuy Blocha (patrz: Skuteczna Java , pozycja 17 ), „opinia” Jamesa Goslinga (patrz ten wywiad ), a jak wskazano w odpowiedzi Omer Iqbal , jest to również „opinia” Andersa Hejlsberga. (I chociaż nigdy nie zajmował się opowiadaniem się za rezygnacją z rezygnacji, Eric Lippert wyraźnie zgadza się, że dziedziczenie jest również niebezpieczne). Więc do kogo się odnosi?
Doval
33

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 internali private, ale tylko Java ma public, protectednic, i private. Zapewnia lepszą kontrolę ziarna nad zachowaniem tego, co kodujesz, kosztem posiadania większej liczby terminów i słów kluczowych do śledzenia.

W przypadku newvs. virtual+overridewygląda to tak:

  • Jeśli chcesz zmusić podklasy do wdrożenia tej metody, użyj abstractiw overridepodklasie.
  • Jeśli chcesz zapewnić funkcjonalność, ale pozwolić, aby podklasa ją zastąpiła, użyj virtualiw overridetej podklasie.
  • Jeśli chcesz zapewnić funkcjonalność, której podklasy nigdy nie powinny zastępować, nie używaj niczego.
    • Jeśli następnie mają szczególny przypadek podklasę, która nie trzeba zachowywać się inaczej, należy użyć neww podklasie.
    • Jeśli chcesz mieć pewność, że żadna podklasa nigdy nie zastąpi zachowania, użyj sealedw 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, OrderProcessorktóra miała większość logiki, z pewnymi abstract/ virtualmetodami 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) Dodaj virtualmetodę podstawową i overridedziecko; lub 2) Dodaj newdo 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.

Bobson
źródło
7
Myślę, że celowe użycie newtego 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 tak newzachowują się metody ed.
svick,
@svick - Potencjalnie tak. W naszym szczególnym scenariuszu tak się nigdy nie zdarzy, ale dlatego dodałem zastrzeżenie.
Bobson,
Jednym z doskonałych zastosowań newjest WebViewPage <TModel> w frameworku MVC. Ale od czasu do czasu rzuca mnie błąd new, więc nie sądzę, żeby było to nierozsądne pytanie.
pdr
1
@ C.Champagne: Każde narzędzie może być źle używane, a to jest szczególnie ostre narzędzie - możesz łatwo się skaleczyć. Ale to nie jest powód, aby usunąć narzędzie z przybornika i usunąć opcję z bardziej utalentowanego projektanta API.
pdr
1
@svick Correct, dlatego zwykle jest to ostrzeżenie kompilatora. Ale możliwość „nowego” uwzględnienia warunków brzegowych (takich jak ten podany), a nawet lepiej, sprawia, że naprawdę oczywiste jest , że robisz coś dziwnego, kiedy przychodzi zdiagnozować nieunikniony błąd. „Dlaczego ta klasa i tylko ta klasa są buggy… ach-hah,„ nowy ”, chodźmy przetestować, gdzie używana jest SuperClass”.
deworde
7

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 implementuje void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), ale GrafDerivedimplementuje 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 wersja GrafBaseimplementuje metodę publiczną o tej samej sygnaturze, ale jej obliczony czwarty punkt jest przeciwny do drugiego. Klienci, którzy otrzymają, oczekują, GrafBaseale otrzymają odniesienie do, GrafDerivedbędą DrawParallelogramobliczać czwarty punkt zgodnie ze GrafBasesposobem nowej metody, ale klienci, którzy używali GrafDerived.DrawParallelogramprzed zmianą metody podstawowej, będą oczekiwać zachowania, które zostało GrafDerivedpierwotnie zaimplementowane.

W Javie autor nie miałby GrafDerivedmożliwości, aby ta klasa współistniała z klientami korzystającymi z nowej GrafBase.DrawParallelogrammetody (i mogą nie zdawać sobie sprawy, że w GrafDerivedogóle istnieją) bez naruszenia kompatybilności z istniejącym kodem klienta, który był GrafDerived.DrawParallelogramwcześniej GrafBasezdefiniowany. Ponieważ DrawParallelogramnie 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ób GrafDeriveduniknąć naruszenia uzasadnionych oczekiwań jednego z nich (tj. Złamania uzasadnionego kodu klienta).

W języku C #, jeśli GrafDerivednie zostanie ponownie skompilowany, środowisko wykonawcze przyjmie, że kod, który wywołuje DrawParallelogrammetodę na podstawie referencji typu, GrafDerivedbędzie oczekiwał zachowania, jakie GrafDerived.DrawParallelogram()miał podczas ostatniej kompilacji, ale kod, który wywołuje metodę na podstawie referencji typu, GrafBasebędzie oczekiwał GrafBase.DrawParallelogram(zachowanie, które zostało dodane). Jeśli GrafDerivedpóźniej zostanie ponownie skompilowany w obecności rozszerzonego GrafBase, kompilator będzie skrzeczał, dopóki programista nie określi, czy jego metoda miała być prawidłowym zamiennikiem dziedziczonego elementu GrafBase, czy też jego zachowanie musi być powiązane z referencjami typu GrafDerived, ale nie powinno zastąpić zachowanie referencji typu GrafBase.

Można zasadnie argumentować, że posiadanie metody GrafDerivedrobienia czegoś innego niż członek, GrafBasektó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.

supercat
źródło
2
Myślę, że jest tutaj dobra odpowiedź, ale gubi się w ścianie tekstu. Proszę podzielić to na kilka kolejnych akapitów.
Bobson,
Co to jest DerivedGraphics? A class using GrafDerived`?
C.Champagne
@ C.Champagne: Miałem na myśli GrafDerived. Zacząłem używać DerivedGraphics, ale czułem, że to było trochę długie. Nawet GrafDerivedjest jeszcze trochę długi, ale nie wiedziałem, jak najlepiej nazwać typy renderujące grafiki, które powinny mieć wyraźną relację podstawową / pochodną.
supercat
1
@ Bobson: Lepiej?
supercat
6

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.

deworde
źródło
0

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ć.

clish
źródło
2
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 5 odpowiedziach
gnat