Rozważ ten kod:
class Program
{
static void Main(string[] args)
{
Person person = new Teacher();
person.ShowInfo();
Console.ReadLine();
}
}
public class Person
{
public void ShowInfo()
{
Console.WriteLine("I am Person");
}
}
public class Teacher : Person
{
public new void ShowInfo()
{
Console.WriteLine("I am Teacher");
}
}
Kiedy uruchamiam ten kod, wyprowadzane jest następujące polecenie:
Jestem Osobą
Jednak widać, że jest to przykład Teacher
, a nie Person
. Dlaczego kod to robi?
c#
class
derived-class
niebieskawy
źródło
źródło
Odpowiedzi:
Istnieje różnica między
new
ivirtual
/override
.Można sobie wyobrazić, że klasa po utworzeniu jest niczym innym jak tabelą wskaźników wskazujących na rzeczywistą implementację jej metod. Poniższy obraz powinien to całkiem dobrze zobrazować:
Teraz można zdefiniować metodę na różne sposoby. Każdy zachowuje się inaczej, gdy jest używany z dziedziczeniem. Standardowy sposób zawsze działa tak, jak pokazano na powyższym obrazku. Jeśli chcesz zmienić to zachowanie, możesz dołączyć różne słowa kluczowe do swojej metody.
1. Klasy abstrakcyjne
Pierwszy to
abstract
.abstract
metody po prostu wskazują donikąd:Jeśli Twoja klasa zawiera abstrakcyjne elementy członkowskie, również musi być oznaczona jako
abstract
, w przeciwnym razie kompilator nie skompiluje aplikacji. Nie można tworzyć instancjiabstract
klas, ale można po nich dziedziczyć i tworzyć instancje dziedziczonych klas oraz uzyskiwać do nich dostęp przy użyciu definicji klasy bazowej. W Twoim przykładzie wyglądałoby to następująco:W przypadku wywołania zachowanie
ShowInfo
różni się w zależności od implementacji:Zarówno
Student
s, jak iTeacher
s sąPerson
s, ale zachowują się inaczej, gdy są proszeni o podpowiedzenie informacji o sobie. Jednak sposób poproszenia ich o podanie informacji jest taki sam: za pomocąPerson
interfejsu klasowego.Więc co dzieje się za kulisami, kiedy dziedziczysz po
Person
? Podczas implementacjiShowInfo
wskaźnik nie wskazuje już nigdzie , teraz wskazuje rzeczywistą implementację! Podczas tworzeniaStudent
instancji wskazuje naStudent
sShowInfo
:2. Metody wirtualne
Drugim sposobem jest użycie
virtual
metod. Zachowanie jest takie samo, z wyjątkiem tego, że podajesz opcjonalną domyślną implementację w swojej klasie bazowej. Klasy zvirtual
członkami mogą być instancjonowane, jednak klasy dziedziczone mogą zapewniać różne implementacje. Oto jak powinien wyglądać Twój kod, aby działał:Kluczowa różnica polega na tym, że element bazowy
Person.ShowInfo
nie wskazuje już nigdzie . Jest to również powód, dla którego możesz tworzyć instancjePerson
(i dlatego nie musi być już oznaczane jakoabstract
):Należy zauważyć, że na razie nie wygląda to inaczej niż na pierwszym obrazku. Dzieje się tak, ponieważ
virtual
metoda wskazuje na implementację „ w standardowy sposób ”. Używającvirtual
można powiedziećPersons
, że oni mogą (nie muszą ) zapewniać innej implementacji dlaShowInfo
. Jeśli podasz inną implementację (używającoverride
), tak jak zrobiłem dlaTeacher
powyższego, obraz będzie wyglądał tak samo jak w przypadkuabstract
. Wyobraź sobie, że nie dostarczyliśmy niestandardowej implementacji dlaStudent
s:Kod zostałby nazwany następująco:
Obrazek
Student
wyglądałby tak:3. Magiczne słowo kluczowe „nowe”, znane również jako „Cieniowanie”
new
to bardziej hack wokół tego. Możesz udostępniać metody w klasach uogólnionych, które mają takie same nazwy jak metody w klasie bazowej / interfejsie. Obie wskazują na własną, niestandardową implementację:Implementacja wygląda jak ta, którą podałeś. Zachowanie różni się w zależności od sposobu uzyskiwania dostępu do metody:
Takie zachowanie może być pożądane, ale w twoim przypadku jest mylące.
Mam nadzieję, że dzięki temu wszystko będzie dla Ciebie bardziej zrozumiałe!
źródło
new
co przerywa dziedziczenie funkcji i sprawia, że nowa funkcja jest oddzielna od funkcji nadklasyPerson
, nieStudent
;)Polimorfizm podtypów w C # wykorzystuje jawną wirtualność, podobnie jak w C ++, ale w przeciwieństwie do Javy. Oznacza to, że musisz jawnie oznaczyć metody jako zastępowalne (tj
virtual
.). W C # musisz również jawnie oznaczyć metody zastępowania jako zastępujące (tj.override
), Aby zapobiec literówkom.W kodzie w swoim pytaniu używasz
new
, co powoduje cieniowanie zamiast nadpisywania. Cieniowanie wpływa jedynie na semantykę czasu kompilacji, a nie na semantykę środowiska wykonawczego, stąd niezamierzone wyniki.źródło
Musisz uczynić tę metodę wirtualną i musisz przesłonić funkcję w klasie potomnej, aby wywołać metodę obiektu klasy, którą umieściłeś w odwołaniu do klasy nadrzędnej.
Metody wirtualne
Używanie nowego do cieniowania
Używasz nowego słowa kluczowego zamiast zastępowania, oto co robi new
Jeśli metoda w klasie pochodnej nie jest poprzedzona słowami kluczowymi new lub override, kompilator wygeneruje ostrzeżenie, a metoda będzie zachowywać się tak, jakby było obecne słowo kluczowe new.
Jeśli metoda w klasie pochodnej jest poprzedzona słowem kluczowym new, metoda jest definiowana jako niezależna od metody w klasie bazowej. Ten artykuł w witrynie MSDN wyjaśnia to bardzo dobrze.
Wiązanie wczesne VS Wiązanie późne
Mamy wczesne wiązanie w czasie kompilacji dla metody normalnej (nie wirtualnej), która jest obecnym przypadkiem, w którym kompilator połączy wywołanie metody klasy bazowej, która jest metodą typu referencyjnego (klasa bazowa) zamiast obiektu jest przechowywany w referencji bazy class, czyli obiekt klasy pochodnej . Dzieje się tak, ponieważ
ShowInfo
nie jest to metoda wirtualna. Późne wiązanie jest wykonywane w czasie wykonywania dla (metoda wirtualna / nadpisana) przy użyciu tabeli metod wirtualnych (vtable).źródło
Chcę wykorzystać odpowiedź Achratta . Aby uzyskać kompletność, różnica polega na tym, że OP oczekuje, że
new
słowo kluczowe w metodzie klasy pochodnej zastąpi metodę klasy bazowej. Właściwie to się ukrywa metodę klasy bazowej.W języku C #, jak wspomniano w innej odpowiedzi, zastępowanie tradycyjnej metody musi być jawne; metoda klasy bazowej musi być oznaczona jako,
virtual
a klasa pochodna musi być konkretnieoverride
metodą klasy bazowej. Jeśli tak się stanie, nie ma znaczenia, czy obiekt jest traktowany jako instancja klasy bazowej czy pochodnej; metoda pochodna zostanie znaleziona i wywołana. Odbywa się to w podobny sposób jak w C ++; metoda oznaczona jako „wirtualna” lub „nadpisanie” podczas kompilacji jest rozwiązywana „późno” (w czasie wykonywania) przez określenie rzeczywistego typu obiektu, do którego się odwołuje, i przechodzenie hierarchii obiektów w dół wzdłuż drzewa od typu zmiennej do rzeczywistego typu obiektu, aby znaleźć najbardziej pochodną implementację metody zdefiniowanej przez typ zmiennej.Różni się to od języka Java, który umożliwia „niejawne przesłonięcia”; na przykład metody (niestatyczne), po prostu zdefiniowanie metody o tej samej sygnaturze (nazwa i liczba / typ parametrów) spowoduje, że podklasa zastąpi nadklasę.
Ponieważ często warto rozszerzyć lub zastąpić funkcjonalność metody niewirtualnej, której nie kontrolujesz, C # zawiera również
new
kontekstowe słowo kluczowe. Słowonew
kluczowe „ukrywa” metodę nadrzędną zamiast ją przesłonić. Każda dziedziczna metoda może zostać ukryta, niezależnie od tego, czy jest wirtualna, czy nie; pozwala to programistom na wykorzystanie członków, których chcesz odziedziczyć po rodzicach, bez konieczności obchodzenia się z tymi, których nie masz, a jednocześnie umożliwia prezentowanie tego samego „interfejsu” konsumentom Twojego kodu.Ukrywanie działa podobnie do nadpisywania z perspektywy osoby korzystającej z Twojego obiektu na poziomie dziedziczenia, na którym zdefiniowana jest metoda ukrywania lub poniżej niego. Z przykładu pytania, programista tworzący Nauczyciela i przechowujący to odniesienie w zmiennej typu Nauczyciel zobaczy zachowanie implementacji ShowInfo () od Nauczyciela, która ukrywa to przed Person. Jednak osoba pracująca z Twoim obiektem w kolekcji rekordów Person (tak jak Ty) zobaczy zachowanie implementacji Person metody ShowInfo (); ponieważ metoda Nauczyciela nie przesłania swojego rodzica (co również wymagałoby wirtualizacji Person.ShowInfo ()), kod działający na poziomie abstrakcji Person nie znajdzie implementacji Nauczyciela i nie będzie jej używał.
Ponadto
new
słowo kluczowe nie tylko zrobi to jawnie, ale C # umożliwia niejawne ukrywanie metod; po prostu zdefiniowanie metody z taką samą sygnaturą jak metoda klasy nadrzędnej, bezoverride
lubnew
, spowoduje jej ukrycie (chociaż spowoduje to ostrzeżenie kompilatora lub skargę od niektórych asystentów refaktoryzacji, takich jak ReSharper lub CodeRush). Jest to kompromis, jaki wymyślili projektanci C #, między jawnymi przesłonięciami w C ++ a niejawnymi przesłonięciami Javy, i chociaż jest elegancki, nie zawsze daje takie zachowanie, jakiego można by się spodziewać, jeśli pochodzisz z tła w którymkolwiek ze starszych języków.Oto nowe rzeczy: staje się to skomplikowane, gdy połączysz dwa słowa kluczowe w długim łańcuchu dziedziczenia. Rozważ następujące:
Wynik:
Można się spodziewać pierwszego zestawu pięciu; ponieważ każdy poziom ma implementację i odwołuje się do obiektu tego samego typu, do którego została utworzona instancja, środowisko wykonawcze rozpoznaje każde wywołanie poziomu dziedziczenia, do którego odwołuje się typ zmiennej.
Drugi zestaw pięciu jest wynikiem przypisania każdej instancji do zmiennej bezpośredniego typu nadrzędnego. Otóż, pewne różnice w zachowaniu się trzęsą;
foo2
, który w rzeczywistości jestBar
rzutowaniem jako aFoo
, nadal znajdzie bardziej pochodną metodę rzeczywistego typu obiektu Bar.bar2
jest aBaz
, ale w przeciwieństwie do withfoo2
, ponieważ Baz nie zastępuje jawnie implementacji Bara (nie może; Barsealed
it), nie jest widziany przez środowisko wykonawcze, gdy wygląda „z góry na dół”, więc zamiast tego wywoływana jest implementacja Bar. Zauważ, że Baz nie musi używaćnew
słowa kluczowego; otrzymasz ostrzeżenie kompilatora, jeśli pominiesz słowo kluczowe, ale niejawnym zachowaniem w C # jest ukrycie metody nadrzędnej.baz2
jest aBai
, który zastępujeBaz
snew
implementacja, więc jej zachowanie jest podobne dofoo2
's; wywoływana jest rzeczywista implementacja typu obiektu w Bai.bai2
jest aBat
, który ponownie ukrywaBai
implementację metody swojego rodzica i zachowuje się tak samo,bar2
jakby implementacja Baia nie została zapieczętowana, więc teoretycznie Bat mógł przesłonić metodę zamiast ją ukryć. Wreszciebat2
jest to aBak
, które nie ma nadrzędnej implementacji żadnego rodzaju i po prostu używa implementacji swojego rodzica.Trzeci zestaw pięciu ilustruje zachowanie pełnej rozdzielczości z góry na dół. Wszystko faktycznie odwołuje się do wystąpienia najbardziej pochodnej klasy w łańcuchu,
Bak
ale rozpoznawanie na każdym poziomie typu zmiennej jest wykonywane przez rozpoczęcie od tego poziomu łańcucha dziedziczenia i drążenie w dół do najbardziej pochodnego jawnego zastąpienia metody, które są tych wBar
,Bai
iBat
. W ten sposób metoda ukrywania „przerywa” nadrzędny łańcuch dziedziczenia; musisz pracować z obiektem na poziomie dziedziczenia lub poniżej tego poziomu, który ukrywa metodę, aby można było użyć metody ukrywania. W przeciwnym razie metoda ukryta jest „odkrywana” i używana zamiast niej.źródło
Przeczytaj o polimorfizmie w C #: Polimorphism (Przewodnik programowania w języku C #)
Oto przykład stamtąd:
źródło
Musisz to zrobić,
virtual
a następnie zastąpić tę funkcję wTeacher
. Ponieważ dziedziczysz i używasz wskaźnika podstawowego do odwoływania się do klasy pochodnej, musisz zastąpić go za pomocąvirtual
.new
służy do ukrywaniabase
metody klasy w odwołaniu do klasy pochodnej, a nie wbase
odwołaniu do klasy.źródło
Chciałbym dodać kilka więcej przykładów, aby rozwinąć informacje na ten temat. Mam nadzieję, że to też pomoże:
Oto przykład kodu, który oczyszcza powietrze wokół tego, co się dzieje, gdy typ pochodny zostanie przypisany do typu podstawowego. Jakie metody są dostępne i jaka jest różnica między metodami nadpisanymi i ukrytymi w tym kontekście.
Inną małą anomalią jest to, że dla następującego wiersza kodu:
Kompilator VS (intellisense) pokaże a.foo () jako A.foo ().
W związku z tym jasne jest, że gdy typ bardziej pochodny jest przypisany do typu podstawowego, zmienna „typ podstawowy” działa jako typ podstawowy do momentu odwołania do metody, która jest zastępowana w typie pochodnym. Może to stać się trochę sprzeczne z intuicją w przypadku ukrytych metod lub metod o tej samej nazwie (ale nie nadpisanej) między typami rodzica i dziecka.
Ten przykładowy kod powinien pomóc w określeniu tych zastrzeżeń!
źródło
C # różni się od języka Java w zachowaniu przesłaniania klasy nadrzędnej / podrzędnej. Domyślnie w Javie wszystkie metody są wirtualne, więc pożądane zachowanie jest obsługiwane po wyjęciu z pudełka.
W C # musisz oznaczyć metodę jako wirtualną w klasie bazowej, wtedy otrzymasz to, czego chcesz.
źródło
Nowy kluczowe stwierdzić, że metoda w obecnej klasie będzie tylko praca, jeśli masz instancję Wychowawca przechowywane w zmiennej typu Nauczyciela. Lub możesz go wywołać za pomocą rzutowania: ((Nauczyciel) Osoba) .ShowInfo ()
źródło
Typ zmiennej „nauczyciel” jest tutaj
typeof(Person)
i ten typ nic nie wie o klasie nauczyciela i nie próbuje szukać żadnych metod w typach pochodnych. Aby wywołać metodę Wychowawca oddasz zmienną:(person as Teacher).ShowInfo()
.Aby wywołać określoną metodę opartą na typie wartości, należy użyć słowa kluczowego „virtual” w swojej klasie bazowej i przesłonić metody wirtualne w klasach pochodnych. Takie podejście pozwala na implementację klas pochodnych z lub bez zastępowania metod wirtualnych. Metody klasy bazowej będą wywoływane dla typów bez overided virtuals.
źródło
Może być za późno ... Ale pytanie jest proste, a odpowiedź powinna mieć ten sam poziom złożoności.
W twojej zmiennej kodu osoba nie wie nic o Teacher.ShowInfo (). Nie ma możliwości wywołania ostatniej metody z odwołania do klasy bazowej, ponieważ nie jest ona wirtualna.
Istnieje przydatne podejście do dziedziczenia - spróbuj wyobrazić sobie, co chcesz powiedzieć o hierarchii kodu. Spróbuj także wyobrazić sobie, co mówi o sobie jedno lub drugie narzędzie. Np. Jeśli dodasz funkcję wirtualną do klasy bazowej, przypuszczasz: 1. może mieć domyślną implementację; 2. może zostać ponownie zaimplementowany w klasie pochodnej. Dodanie funkcji abstrakcyjnej oznacza tylko jedno - podklasa musi stworzyć implementację. Ale jeśli masz zwykłą funkcję - nie oczekujesz, że ktoś zmieni jej implementację.
źródło
Kompilator robi to, ponieważ nie wie, że jest to plik
Teacher
. Wie tylko, że jest toPerson
coś, co się z niego wywodzi. Więc wszystko, co może zrobić, to wywołaćPerson.ShowInfo()
metodę.źródło
Chciałem tylko udzielić krótkiej odpowiedzi -
Należy używać
virtual
ioverride
w klasach, które można przesłonić. Użyjvirtual
dla metod, które mogą być przesłonięte przez klasy potomne i użyjoverride
dla metod, które powinny przesłonić takievirtual
metody.źródło
Napisałem ten sam kod, o którym wspomniałeś powyżej w Javie, z wyjątkiem pewnych zmian i działał dobrze, z wyjątkiem. Metoda klasy bazowej jest nadpisywana i wyświetlany jest komunikat „Jestem nauczycielem”.
Powód: ponieważ tworzymy odwołanie do klasy bazowej (która może mieć odwołanie do instancji klasy pochodnej), która w rzeczywistości zawiera odwołanie do klasy pochodnej. A ponieważ wiemy, że instancja zawsze najpierw patrzy na swoje metody, jeśli ją tam znajdzie, wykonuje ją, a jeśli nie znajduje tam definicji, przechodzi w górę w hierarchii.
źródło
Opierając się na doskonałej demonstracji Keitha S. i odpowiedziach wszystkich innych, a ze względu na kompletną kompletność, wrzućmy jawne implementacje interfejsu, aby pokazać, jak to działa. Rozważ poniższe kwestie:
przestrzeń nazw LinqConsoleApp {
}
Oto wynik:
osoba: Jestem Person == LinqConsoleApp.Teacher
nauczyciel: jestem nauczycielem == LinqConsoleApp.Teacher
person1: Jestem nauczycielem == LinqConsoleApp.Teacher
person2: Jestem nauczycielem == LinqConsoleApp.Teacher
nauczyciel1: Jestem nauczycielem == LinqConsoleApp.Teacher
person4: Jestem Person == LinqConsoleApp.Person
person3: Jestem interfejsem Person == LinqConsoleApp.Person
Dwie rzeczy do zapamiętania:
Metoda Teacher.ShowInfo () pomija słowo kluczowe new. W przypadku pominięcia new zachowanie metody jest takie samo, jak w przypadku jawnego zdefiniowania słowa kluczowego new.
Słowa kluczowego override można używać tylko w połączeniu z wirtualnym słowem kluczowym. Metoda klasy bazowej musi być wirtualna. Lub abstrakcyjny, w którym to przypadku klasa musi być również abstrakcyjna.
person pobiera podstawową implementację ShowInfo, ponieważ klasa Teacher nie może przesłonić podstawowej implementacji (bez deklaracji wirtualnej), a person to .GetType (Teacher), więc ukrywa implementację klasy Teacher.
nauczyciel pobiera pochodną implementację narzędzia ShowInfo dla nauczyciela, ponieważ nauczyciel, ponieważ jest to typ (Nauczyciel), a nie jest na poziomie dziedziczenia Osoba.
person1 pobiera pochodną implementację Teacher, ponieważ jest to .GetType (Teacher), a implikowane nowe słowo kluczowe ukrywa podstawową implementację.
person2 pobiera również pochodną implementację Nauczyciela, mimo że implementuje IPerson i otrzymuje jawne rzutowanie na IPerson. Dzieje się tak ponownie, ponieważ klasa Teacher nie implementuje jawnie metody IPerson.ShowInfo ().
Teacher1 również pobiera pochodną implementację Teacher, ponieważ jest to .GetType (Teacher).
Tylko person3 pobiera implementację IPerson ShowInfo, ponieważ tylko klasa Person jawnie implementuje metodę, a person3 jest instancją typu IPerson.
Aby jawnie zaimplementować interfejs, należy zadeklarować instancję var typu docelowego interfejsu, a klasa musi jawnie implementować (w pełni kwalifikować) elementy składowe interfejsu.
Zauważ, że nawet person4 nie otrzymuje implementacji IPerson.ShowInfo. Dzieje się tak, ponieważ mimo że person4 to .GetType (Person) i mimo że Person implementuje IPerson, person4 nie jest instancją IPerson.
źródło
Próbka LinQPad, aby uruchomić się na ślepo i zredukować powielanie kodu. Myślę, że to jest to, co próbujesz zrobić.
źródło