Dlaczego C # domyślnie implementuje metody jako niewirtualne?

105

W przeciwieństwie do języka Java, dlaczego C # domyślnie traktuje metody jako funkcje niewirtualne? Czy bardziej prawdopodobne jest, że będzie to problem z wydajnością, a nie inne możliwe wyniki?

Przypomina mi się przeczytanie akapitu Andersa Hejlsberga o kilku zaletach, jakie przynosi istniejąca architektura. Ale co z efektami ubocznymi? Czy to naprawdę dobry kompromis, aby domyślnie mieć metody niewirtualne?

Burcu Dogan
źródło
1
Odpowiedzi, które wspominają o powodach wydajności, pomijają fakt, że kompilator C # głównie kompiluje wywołania metod do callvirt, a nie call . Dlatego w C # nie można mieć metody, która zachowuje się inaczej, jeśli thisodwołanie ma wartość null. Więcej informacji znajdziesz tutaj .
Andy
Prawdziwe! Instrukcja call IL jest przeznaczona głównie dla wywołań metod statycznych.
RBT
1
Architekt C #, Anders Hejlsberg, myśli tu i tutaj .
RBT

Odpowiedzi:

98

Klasy powinny być zaprojektowane do dziedziczenia, aby móc z niego skorzystać. Posiadanie metod virtualdomyślnie oznacza, że ​​każdą funkcję w klasie można odłączyć i zastąpić inną, co nie jest dobrą rzeczą. Wiele osób uważa nawet, że zajęcia powinny być sealeddomyślne.

virtualmetody mogą mieć również niewielki wpływ na wydajność. Jednak prawdopodobnie nie jest to główny powód.

Mehrdad Afshari
źródło
7
Osobiście wątpię w tę część dotyczącą wydajności. Nawet w przypadku funkcji wirtualnych kompilator jest bardzo w stanie dowiedzieć się, gdzie zastąpić callvirtprostą callw kodzie IL (lub nawet dalej w dół, gdy JITting). Java HotSpot robi to samo. Reszta odpowiedzi jest trafna.
Konrad Rudolph
5
Zgadzam się, że wydajność nie jest najważniejsza, ale może mieć również wpływ na wydajność. Prawdopodobnie jest bardzo mały. Z pewnością nie jest to jednak powód takiego wyboru. Chciałem tylko o tym wspomnieć.
Mehrdad Afshari
71
Zamknięte zajęcia sprawiają, że mam ochotę bić dzieci.
mxmissile
6
@mxmissile: ja też, ale dobry projekt interfejsu API i tak jest trudny. Myślę, że domyślne zapieczętowanie ma sens w przypadku kodu, który posiadasz. Najczęściej inny programista w twoim zespole nie rozważał dziedziczenia, kiedy implementował tę klasę, której potrzebujesz, i domyślnie zapieczętowany przekazałby tę wiadomość całkiem dobrze.
Roman Starkov
49
Wiele osób też jest psychopatami, co nie oznacza, że ​​powinniśmy ich słuchać. Wirtualny powinien być domyślny w C #, kod powinien być rozszerzalny bez konieczności zmiany implementacji lub całkowitego ponownego pisania. Ludzie, którzy nadmiernie chronią swoje interfejsy API, często kończą z martwymi lub częściowo używanymi interfejsami API, co jest znacznie gorsze niż scenariusz, w którym ktoś ich nadużywa.
Chris Nicola,
89

Dziwię się, że wydaje się, że istnieje taki konsensus, że domyślnie niewirtualny jest właściwym sposobem robienia rzeczy. Zejdę po drugiej - myślę pragmatycznie - stronie ogrodzenia.

Większość uzasadnień brzmi dla mnie jak stary argument „Jeśli damy ci siłę, możesz się skrzywdzić”. Od programistów ?!

Wydaje mi się, że programista, który nie wiedział wystarczająco dużo (lub nie miał wystarczająco dużo czasu), aby zaprojektować swoją bibliotekę pod kątem dziedziczenia i / lub rozszerzalności, jest koderem, który stworzył dokładnie taką bibliotekę, którą prawdopodobnie będę musiał naprawić lub ulepszyć - dokładnie biblioteka, w której najbardziej przydatna byłaby możliwość nadpisania.

Ile razy musiałem napisać brzydki, desperacki kod obejścia (lub zrezygnować z użycia i wprowadzić własne alternatywne rozwiązanie), ponieważ nie mogę nadpisać daleko, znacznie przewyższa liczbę razy, kiedy byłem kiedykolwiek ugryziony ( np. w Javie), nadpisując, gdy projektant mógł nie rozważyć, że mogę.

Domyślnie niewirtualny utrudnia mi życie.

AKTUALIZACJA: Wskazano [całkiem poprawnie], że tak naprawdę nie odpowiedziałem na pytanie. A więc - i przepraszam za spóźnienie ....

Chciałem móc napisać coś zwięzłego, np. „C # domyślnie implementuje metody jako niewirtualne, ponieważ podjęto złą decyzję, która ceniła programy bardziej niż programiści”. (Myślę, że można to nieco uzasadnić na podstawie niektórych innych odpowiedzi na to pytanie - takich jak wydajność (przedwczesna optymalizacja, ktoś?) Lub gwarantowanie zachowania klas).

Jednak zdaję sobie sprawę, że przedstawiłbym tylko swoją opinię, a nie ostateczną odpowiedź, której pragnie Stack Overflow. Z pewnością, pomyślałem, na najwyższym poziomie ostateczna (ale nieprzydatna) odpowiedź brzmi:

Domyślnie nie są wirtualne, ponieważ projektanci języka musieli podjąć decyzję i właśnie to wybrali.

Teraz domyślam się, że z dokładnego powodu, dla którego podjęli tę decyzję, nigdy ... och, czekaj! Zapis rozmowy!

Wydawałoby się więc, że odpowiedzi i komentarze tutaj dotyczące niebezpieczeństw związanych z zastępowaniem interfejsów API i potrzeby jawnego projektowania dziedziczenia są na właściwej drodze, ale wszystkim brakuje ważnego aspektu czasowego: głównym zmartwieniem Andersa było utrzymanie niejawnych klas lub API kontrakt między wersjami . Myślę, że bardziej interesuje go pozwolenie platformie .Net / C # na zmianę pod kodem, niż zmiana kodu użytkownika na platformie. (A jego „pragmatyczny” punkt widzenia jest dokładnym przeciwieństwem mojego, ponieważ patrzy z drugiej strony).

(Ale czy nie mogli po prostu wybrać domyślnie wirtualnego, a następnie posypać „ostatecznym” kodem? Może to nie to samo ... a Anders jest wyraźnie mądrzejszy ode mnie, więc pozwolę mu kłamać.)

mwardm
źródło
2
Nie mogłam zgodzić się na więcej. Bardzo frustrujące podczas korzystania z interfejsu API innej firmy i chęci zastąpienia niektórych zachowań, a nie możliwości.
Andy
3
Z entuzjazmem się zgadzam. Jeśli zamierzasz opublikować API (czy to wewnętrznie w firmie, czy w świecie zewnętrznym), naprawdę możesz i powinieneś upewnić się, że Twój kod jest przeznaczony do dziedziczenia. Powinieneś sprawić, by API było dobre i dopracowane, jeśli publikujesz go do użytku przez wiele osób. W porównaniu z wysiłkiem potrzebnym do dobrego ogólnego projektu (dobra treść, jasne przypadki użycia, testowanie, dokumentacja), projektowanie pod kątem dziedziczenia naprawdę nie jest takie złe. A jeśli nie publikujesz, wirtualizacja domyślnie jest mniej czasochłonna i zawsze możesz naprawić niewielką część przypadków, w których jest to problematyczne.
Gravity,
2
Gdyby teraz w programie Visual Studio była tylko funkcja edytora, która automatycznie oznaczałaby wszystkie metody / właściwości jako virtual… dodatek programu Visual Studio?
kevinarpe
1
Chociaż całkowicie się z tobą zgadzam, to nie jest do końca odpowiedź.
Chris Morgan,
2
Biorąc pod uwagę nowoczesne praktyki programistyczne, takie jak iniekcja zależności, mocking frameworks i ORM, wydaje się jasne, że nasi projektanci C # trochę przegapili znak. Testowanie zależności, gdy nie można domyślnie przesłonić właściwości, jest bardzo frustrujące.
Jeremy Holovacs
17

Ponieważ zbyt łatwo jest zapomnieć, że metoda może zostać zastąpiona, a nie zaprojektowana pod tym kątem. C # sprawia, że ​​myślisz, zanim zrobisz to wirtualnie. Myślę, że to świetna decyzja projektowa. Niektórzy ludzie (na przykład Jon Skeet) powiedzieli nawet, że klasy powinny być domyślnie zapieczętowane.

Zifre
źródło
12

Podsumowując to, co powiedzieli inni, jest kilka powodów:

1 - W języku C # jest wiele elementów składni i semantyki, które pochodzą bezpośrednio z C ++. Fakt, że metody domyślnie nie były wirtualne w C ++, wpłynął na C #.

2- Posiadanie każdej metody jako domyślnej wirtualnej jest problemem wydajnościowym, ponieważ każde wywołanie metody musi używać wirtualnej tabeli obiektu. Co więcej, to mocno ogranicza zdolność kompilatora Just-In-Time do wbudowywania metod i wykonywania innych rodzajów optymalizacji.

3- Co najważniejsze, jeśli metody domyślnie nie są wirtualne, możesz zagwarantować zachowanie swoich klas. Gdy są one domyślnie wirtualne, na przykład w Javie, nie można nawet zagwarantować, że prosta metoda pobierająca będzie działać zgodnie z przeznaczeniem, ponieważ można ją przesłonić w celu wykonania czegokolwiek w klasie pochodnej (oczywiście można i należy metoda i / lub finał klasy).

Można by się zastanawiać, jak wspomniał Zifre, dlaczego język C # nie poszedł o krok dalej i domyślnie zapieczętował klasy. To część całej debaty o problemach dziedziczenia implementacji, co jest bardzo interesującym tematem.

Trillian
źródło
1
Wolałbym też domyślnie zapieczętowane klasy. Wtedy byłoby to spójne.
Andy
9

C # jest pod wpływem języka C ++ (i nie tylko). C ++ domyślnie nie włącza dynamicznego wysyłania (funkcji wirtualnych). Jednym (dobrym?) Argumentem przemawiającym za tym jest pytanie: „Jak często implementujesz klasy, które są członkami hierarchii klas?”. Innym powodem, dla którego należy unikać domyślnego włączania dynamicznego wysyłania, jest zużycie pamięci. Klasa bez wskaźnika wirtualnego (vpointer) wskazującego na wirtualną tabelę jest oczywiście mniejsza niż odpowiadająca jej klasa z włączonym późnym wiązaniem.

Kwestia wydajności nie jest łatwa do powiedzenia „tak” lub „nie”. Powodem tego jest kompilacja Just In Time (JIT), która jest optymalizacją czasu wykonywania w języku C #.

Kolejne, podobne pytanie dotyczące „ szybkości połączeń wirtualnych

Schildmeijer
źródło
Trochę wątpię, czy metody wirtualne mają wpływ na wydajność dla języka C #, ze względu na kompilator JIT. To jeden z obszarów, w którym JIT może być lepszy niż kompilacja offline, ponieważ mogą
wbudować
1
Właściwie myślę, że większy wpływ na nią ma Java niż C ++, który robi to domyślnie.
Mehrdad Afshari
5

Prosty powód to koszt projektu i utrzymania, oprócz kosztów wydajności. Metoda wirtualna ma dodatkowy koszt w porównaniu z metodą niewirtualną, ponieważ projektant klasy musi zaplanować, co się stanie, gdy metoda zostanie zastąpiona przez inną klasę. Ma to duży wpływ, jeśli spodziewasz się, że określona metoda zaktualizuje stan wewnętrzny lub będzie miało określone zachowanie. Teraz musisz zaplanować, co się stanie, gdy klasa pochodna zmieni to zachowanie. W takiej sytuacji znacznie trudniej jest napisać niezawodny kod.

Dzięki metodzie niewirtualnej masz pełną kontrolę. Wszystko, co idzie nie tak, jest winą oryginalnego autora. Kod jest znacznie łatwiejszy do zrozumienia.

JaredPar
źródło
To naprawdę stary post, ale dla początkującego, który pisze swój pierwszy projekt, nieustannie martwię się niezamierzonymi / nieznanymi konsekwencjami kodu, który piszę. To bardzo pocieszające, wiedząc, że metody niewirtualne są całkowicie MOJĄ winą.
trevorc
2

Gdyby wszystkie metody C # były wirtualne, to vtbl byłby znacznie większy.

Obiekty C # mają tylko metody wirtualne, jeśli klasa ma zdefiniowane metody wirtualne. Prawdą jest, że wszystkie obiekty mają informacje o typie, które zawierają odpowiednik vtbl, ale jeśli nie zdefiniowano żadnych metod wirtualnych, będą obecne tylko podstawowe metody Object.

@Tom Hawtin: Prawdopodobnie dokładniej jest powiedzieć, że C ++, C # i Java pochodzą z rodziny języków C :)

Thomas Bratt
źródło
1
Dlaczego stół vtable miałby być dużo większy? Jest tylko 1 tabela vtable na klasę (a nie na instancję), więc jej rozmiar nie robi dużej różnicy.
Gravity,
1

Pochodząc z tła perla, myślę, że C # przypieczętował zagładę każdego programisty, który mógł chcieć rozszerzyć i zmodyfikować zachowanie klasy bazowej za pomocą metody niewirtualnej bez zmuszania wszystkich użytkowników nowej klasy do zdawania sobie sprawy z potencjalnie za kulisami Detale.

Rozważmy metodę Add klasy List. Co by się stało, gdyby programista chciał zaktualizować jedną z kilku potencjalnych baz danych za każdym razem, gdy dana lista jest dodawana do? Gdyby opcja „Add” była domyślnie wirtualna, programista mógłby opracować klasę „BackedList”, która zastąpiłaby metodę „Add” bez wymuszania na całym kodzie klienta, że ​​jest to „BackedList” zamiast zwykłej „Listy”. Ze względów praktycznych „BackedList” można traktować jako kolejną „Listę” z kodu klienta.

Ma to sens z punktu widzenia dużej klasy głównej, która może zapewniać dostęp do jednego lub więcej składników listy, które same są obsługiwane przez jeden lub więcej schematów w bazie danych. Biorąc pod uwagę, że metody C # nie są domyślnie wirtualne, lista dostarczona przez główną klasę nie może być prostym IEnumerable lub ICollection ani nawet wystąpieniem List, ale zamiast tego musi być anonsowana klientowi jako „BackedList”, aby zapewnić, że nowa wersja operacji „Dodaj” jest wywoływana w celu zaktualizowania poprawnego schematu.

Travis
źródło
To prawda, że ​​metody wirtualne domyślnie ułatwiają pracę, ale czy ma to sens? Jest wiele rzeczy, które możesz zastosować, aby ułatwić życie. Dlaczego nie tylko publiczne pola na zajęciach? Zmiana jego zachowania jest zbyt łatwa. Moim zdaniem wszystko w języku powinno być ściśle zamknięte, sztywne i domyślnie odporne na zmiany. Zmień to tylko w razie potrzeby. Po prostu będąc lil 'filozoficznymi decyzjami projektowymi. Ciąg dalszy ..
nawfal
... Aby porozmawiać na bieżący temat, model dziedziczenia powinien być używany tylko wtedy, gdy Bjest A. Jeśli Bwymaga czegoś innego, Ato nie jest A. Uważam, że nadrzędność jako umiejętność w samym języku jest wadą konstrukcyjną. Jeśli potrzebujesz innej Addmetody, twoja klasa kolekcji nie jest List. Próba stwierdzenia, że ​​to udawanie. Właściwe podejście to kompozycja (a nie udawanie). To prawda, że ​​cały framework jest zbudowany na nadrzędnych możliwościach, ale po prostu mi się to nie podoba.
nawfal
Myślę, że rozumiem twój punkt widzenia, ale w podanym konkretnym przykładzie: „BackedList” może po prostu zaimplementować interfejs „IList”, a klient wie tylko o interfejsie. dobrze? brakuje mi czegoś jednak rozumiem szerszą kwestię, którą próbujesz poruszyć.
Vetras
0

Z pewnością nie jest to kwestia wydajności. Interpreter Javy firmy Sun używa tego samego kodu do wysyłania ( invokevirtualkodu bajtowego), a HotSpot generuje dokładnie ten sam kod niezależnie od tego, finalczy nie. Uważam, że wszystkie obiekty C # (ale nie struktury) mają metody wirtualne, więc zawsze będziesz potrzebować vtblidentyfikacji klasy / runtime. C # to dialekt „języków podobnych do języka Java”. Sugerowanie, że pochodzi z C ++, nie jest do końca uczciwe.

Istnieje pomysł, że powinieneś „zaprojektować do dziedziczenia albo tego zabronić”. To brzmi jak świetny pomysł, aż do momentu, gdy masz poważne uzasadnienie biznesowe, które trzeba szybko naprawić. Być może dziedziczenie po kodzie, nad którym nie masz kontroli.

Tom Hawtin - haczyk
źródło
Hotspot jest zmuszony dołożyć wszelkich starań, aby dokonać tej optymalizacji, właśnie dlatego, że wszystkie metody są domyślnie wirtualne, co ma ogromny wpływ na wydajność. CoreCLR jest w stanie osiągnąć podobną wydajność, będąc znacznie prostszym
Yair Halberstadt
@YairHalberstadt Hotspot musi mieć możliwość wycofania skompilowanego kodu z wielu innych powodów. Minęły lata, odkąd zajrzałem do źródła, ale różnica między metodami finalskutecznymi finaljest banalna. Warto również zauważyć, że może wykonywać bimorficzne inlining, czyli metody inline z dwiema różnymi implementacjami.
Tom Hawtin - tackline