Рассмотрим этот код:
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");
}
}
Когда я запускаю этот код, выводится следующее:
Я человек
Тем не менее, вы можете видеть, что это пример Teacher
, а не из Person
. Почему код делает это?
c#
class
derived-class
голубоватый
источник
источник
Ответы:
Есть разница между
new
иvirtual
/override
.Вы можете себе представить, что класс, когда создается его экземпляр, является не чем иным, как таблицей указателей, указывающих на фактическую реализацию его методов. Следующее изображение должно визуализировать это довольно хорошо:
Теперь есть разные способы, метод может быть определен. Каждый ведет себя по-разному, когда он используется с наследованием. Стандартный способ всегда работает так, как показано на рисунке выше. Если вы хотите изменить это поведение, вы можете прикрепить к своему методу разные ключевые слова.
1. Абстрактные занятия
Первый
abstract
.abstract
методы просто указывают на никуда:Если ваш класс содержит абстрактные члены, его также нужно пометить как
abstract
, иначе компилятор не скомпилирует ваше приложение. Вы не можете создавать экземплярыabstract
классов, но вы можете наследовать от них и создавать экземпляры ваших унаследованных классов и получать к ним доступ, используя определение базового класса. В вашем примере это будет выглядеть так:Если вызывается, поведение
ShowInfo
меняется в зависимости от реализации:И
Student
s, иTeacher
s - этоPerson
s, но они ведут себя по-разному, когда их просят сообщить информацию о себе. Тем не менее, способ попросить их предложить свою информацию, тот же: использованиеPerson
интерфейса класса.Так что же происходит за кулисами, когда вы наследуете
Person
? При реализацииShowInfo
указатель больше не указывает на никуда , теперь он указывает на фактическую реализацию! При созданииStudent
экземпляра он указывает наStudent
sShowInfo
:2. Виртуальные методы
Второй способ заключается в использовании
virtual
методов. Поведение такое же, за исключением того, что вы предоставляете необязательную реализацию по умолчанию в базовом классе. Классы сvirtual
членами могут быть созданы, однако унаследованные классы могут обеспечивать различные реализации. Вот как на самом деле должен выглядеть ваш код:Основное отличие состоит в том, что базовый элемент
Person.ShowInfo
больше не указывает никуда . Это также причина, по которой вы можете создавать экземплярыPerson
(и, следовательно, их не нужно больше отмечатьabstract
):Вы должны заметить, что на данный момент это не выглядит иначе, чем на первом изображении. Это потому, что
virtual
метод указывает на реализацию " стандартным способом ". Используяvirtual
, вы можете сказатьPersons
, что они могут (не должны ) предоставлять другую реализацию дляShowInfo
. Если вы предоставите другую реализацию (используяoverride
), как я сделал дляTeacher
вышеизложенного, изображение будет выглядеть так же, как дляabstract
. Представьте себе, мы не предоставили пользовательскую реализацию дляStudent
s:Код будет называться так:
И изображение для
Student
будет выглядеть так:3. Волшебное `новое` ключевое слово aka" Shadowing "
new
больше взломать вокруг этого. Вы можете предоставить методы в обобщенных классах, которые имеют те же имена, что и методы в базовом классе / интерфейсе. Оба указывают на свою собственную, индивидуальную реализацию:Реализация выглядит как та, что вы предоставили. Поведение отличается в зависимости от способа доступа к методу:
Такое поведение можно желать, но в вашем случае оно вводит в заблуждение.
Я надеюсь, что это сделает вещи понятнее для вас!
источник
new
который нарушает наследование функции и делает новую функцию отдельной от функции суперклассаPerson
, а неStudent
;)Полиморфизм подтипов в C # использует явную виртуальность, похожую на C ++, но в отличие от Java. Это означает, что вы явно должны пометить методы как переопределяемые (т.е.
virtual
). В C # вы также должны явно пометить переопределяющие методы как переопределяющие (то естьoverride
), чтобы предотвратить опечатки.В коде в вашем вопросе, вы используете
new
, который не слежка , а не перекрывая. Затенение просто влияет на семантику времени компиляции, а не на семантику времени выполнения, а следовательно, на непреднамеренный вывод.источник
Вы должны сделать метод виртуальным и переопределить функцию в дочернем классе, чтобы вызвать метод объекта класса, который вы поместили в ссылку на родительский класс.
Виртуальные методы
Использование New для Shadowing
Вы используете новое ключевое слово вместо переопределения, это то, что делает новый
Если методу в производном классе не предшествуют ключевые слова new или override, компилятор выдаст предупреждение, и метод будет вести себя так, как если бы присутствовало новое ключевое слово.
Если методу в производном классе предшествует ключевое слово new, метод определяется как независимый от метода в базовом классе. Эта статья MSDN объясняет это очень хорошо.
Раннее связывание В.С. Позднее связывание
У нас есть раннее связывание во время компиляции для обычного метода (не виртуального), что является текущим случаем, когда компилятор будет связывать вызов метода базового класса, который является методом ссылочного типа (базовый класс), вместо объекта, который содержится в ссылке на базовый класс. класс т.е. объект производного класса . Это потому, что
ShowInfo
это не виртуальный метод. Позднее связывание выполняется во время выполнения для (виртуальный / переопределенный метод) с использованием таблицы виртуальных методов (vtable).источник
Я хочу построить ответ Ахратта . Для полноты, разница в том, что OP ожидает, что
new
ключевое слово в методе производного класса переопределит метод базового класса. На самом деле он скрывает метод базового класса.В C #, как упоминалось в другом ответе, переопределение традиционного метода должно быть явным; метод базового класса должен быть помечен как,
virtual
а производный класс должен, в частности,override
метод базового класса. Если это сделано, тогда не имеет значения, рассматривается ли объект как экземпляр базового класса или производного класса; производный метод найден и вызван. Это делается аналогично C ++; метод, помеченный как «виртуальный» или «переопределить», при компиляции разрешается «поздно» (во время выполнения) путем определения фактического типа ссылочного объекта и обхода иерархии объектов вниз по дереву от типа переменной к фактическому типу объекта, найти наиболее производную реализацию метода, определенного типом переменной.Это отличается от Java, который допускает «неявные переопределения»; например, методы экземпляра (нестатические), простое определение метода с той же сигнатурой (имя и число / тип параметров) приведет к тому, что подкласс переопределит суперкласс.
Поскольку часто полезно расширить или переопределить функциональность не виртуального метода, которым вы не управляете, C # также включает
new
ключевое слово contextual.new
Ключевое слово «шкура» родительский метод не переопределяет его. Любой наследуемый метод может быть скрыт независимо от того, виртуальный он или нет; это позволяет вам, разработчику, использовать элементы, которые вы хотите унаследовать от родителя, без необходимости обходить тех, которые у вас нет, и в то же время позволяет вам предоставлять тот же «интерфейс» потребителям вашего кода.Сокрытие работает аналогично переопределению с точки зрения человека, использующего ваш объект на уровне наследования или ниже, на котором определен метод сокрытия. Из примера вопроса, кодер, создающий Учителя и сохраняющий эту ссылку в переменной типа Учитель, увидит поведение реализации ShowInfo () от Учителя, которая скрывает его от Person. Однако кто-то, работающий с вашим объектом в коллекции записей Person (как и вы), увидит поведение реализации Person в ShowInfo (); Поскольку метод Учителя не переопределяет его родителя (что также потребует, чтобы Person.ShowInfo () был виртуальным), код, работающий на уровне абстракции Person, не найдет реализацию Учителя и не будет использовать ее.
Кроме того, не только
new
ключевое слово будет делать это явно, C # позволяет скрытно скрывать методы; простое определение метода с той же сигнатурой, что и у метода родительского класса, безoverride
илиnew
, приведет к его скрытию (хотя это вызовет предупреждение компилятора или жалобу от некоторых помощников по рефакторингу, таких как ReSharper или CodeRush). Это компромисс, который разработчики C # придумали между явными переопределениями C ++ по сравнению с неявными переопределениями Java, и, несмотря на то, что он элегантен, он не всегда производит поведение, которое вы ожидаете, если исходите из фона на любом из старых языков.Вот что нового: это становится сложным, когда вы объединяете два ключевых слова в длинной цепочке наследования. Учтите следующее:
Вывод:
Первый набор из пяти вполне ожидаем; Поскольку каждый уровень имеет реализацию и на него ссылаются как на объект того же типа, который был создан, среда выполнения разрешает каждый вызов уровня наследования, на который ссылается тип переменной.
Второй набор из пяти является результатом назначения каждого экземпляра переменной непосредственного родительского типа. Теперь некоторые различия в поведении вытряхнуть;
foo2
, который на самом делеBar
приведен какFoo
, все равно найдет более производный метод фактического объекта типа Bar.bar2
этоBaz
, но в отличие отfoo2
, потому что Baz явно не переопределяет реализацию Bar (он не может; Barsealed
это), он не виден во время выполнения при взгляде «сверху вниз», поэтому вместо этого вызывается реализация Bar. Обратите внимание, что Baz не должен использоватьnew
ключевое слово; вы получите предупреждение компилятора, если опустите ключевое слово, но подразумеваемое поведение в C # - скрыть родительский метод.baz2
этоBai
, который переопределяетBaz
«ыnew
реализация, поэтому его поведение похоже на , который не имеет ни одной из основных реализаций и просто использует реализацию своего родителя.foo2
«S; фактическая реализация типа объекта в Bai называется.bai2
isBat
, который снова скрываетBai
реализацию метода своего родителя , и ведет себя так же, какbar2
если бы реализация Bai не была запечатана, поэтому теоретически Bat мог бы переопределить, а не скрыть метод. Наконец,bat2
этоBak
Третий набор из пяти иллюстрирует полное поведение разрешения сверху вниз. На самом деле все ссылается на экземпляр самого производного класса в цепочке,
Bak
но разрешение на каждом уровне типа переменной выполняется путем запуска на этом уровне цепочки наследования и перехода к самому производному явному переопределению метода, которое те , вBar
,Bai
иBat
. Скрытие метода, таким образом, «разрывает» основную цепочку наследования; Вы должны работать с объектом на уровне наследования, который скрывает метод, или ниже его уровня, чтобы использовать метод скрытия. В противном случае скрытый метод «раскрывается» и используется вместо него.источник
Пожалуйста, прочитайте о полиморфизме в C #: Полиморфизм (Руководство по программированию в C #)
Это пример оттуда:
источник
Вы должны сделать это
virtual
и затем переопределить эту функцию вTeacher
. Поскольку вы наследуете и используете базовый указатель для ссылки на производный класс, вам необходимо переопределить его, используяvirtual
.new
предназначен для сокрытияbase
метода класса на производной ссылке на класс, а не наbase
ссылку на класс.источник
Я хотел бы добавить еще пару примеров, чтобы расширить информацию об этом. Надеюсь, это тоже поможет:
Вот пример кода, который проясняет, что происходит, когда производный тип назначается базовому типу. Какие методы доступны и разница между переопределенными и скрытыми методами в этом контексте.
Другая небольшая аномалия заключается в том, что для следующей строки кода:
Компилятор VS (intellisense) будет показывать a.foo () как A.foo ().
Следовательно, ясно, что когда более производный тип назначается базовому типу, переменная «базовый тип» действует как базовый тип до тех пор, пока не будет указан метод, который переопределен в производном типе. Это может стать немного нелогичным со скрытыми методами или методами с одинаковым именем (но не переопределенными) между родительским и дочерним типами.
Этот пример кода должен помочь определить эти предостережения!
источник
C # отличается от Java в поведении переопределения родительского / дочернего класса. По умолчанию в Java все методы являются виртуальными, поэтому требуемое поведение поддерживается сразу после установки.
В C # вы должны пометить метод как виртуальный в базовом классе, тогда вы получите то, что хотите.
источник
Новое ключевое слово сказать , что метод в текущем классе будет работать только если у вас есть экземпляр класса Учителя , хранящегося в переменной типа Учителя. Или вы можете запустить его с помощью приведений: ((Teacher) Person) .ShowInfo ()
источник
Тип переменной'acher 'здесь такой,
typeof(Person)
и этот тип ничего не знает о классе Teacher и не пытается искать какие-либо методы в производных типах. Для вызова метода класса учителя вы должны бросить переменную:(person as Teacher).ShowInfo()
.Чтобы вызвать конкретный метод на основе типа значения, вы должны использовать ключевое слово «virtual» в базовом классе и переопределить виртуальные методы в производных классах. Этот подход позволяет реализовать производные классы с или без переопределения виртуальных методов. Методы базового класса будут вызываться для типов без избыточных виртуалов.
источник
Возможно, будет слишком поздно ... Но вопрос прост и ответ должен иметь такой же уровень сложности.
В вашем коде переменная персона ничего не знает о Teacher.ShowInfo (). Невозможно вызвать последний метод из ссылки на базовый класс, потому что он не виртуальный.
Есть полезный подход к наследованию - попытайтесь представить, что вы хотите сказать с помощью вашей иерархии кода. Также постарайтесь представить, что тот или иной инструмент говорит о себе. Например, если вы добавляете виртуальную функцию в базовый класс, вы предполагаете: 1. она может иметь реализацию по умолчанию; 2. это может быть переопределено в производном классе. Если вы добавляете абстрактную функцию, это означает только одно - подкласс должен создавать реализацию. Но если у вас есть простая функция - вы не ожидаете, что кто-нибудь изменит ее реализацию.
источник
Компилятор делает это, потому что он не знает, что это
Teacher
. Все, что он знает, это то, что онPerson
или что-то происходит от него. Так что все, что он может сделать, это вызватьPerson.ShowInfo()
метод.источник
Просто хотел дать краткий ответ -
Вы должны использовать
virtual
иoverride
в классах, которые могут быть переопределены. Используйтеvirtual
для методов, которые могут быть переопределены дочерними классами, и используйтеoverride
для методов, которые должны переопределять такиеvirtual
методы.источник
Я написал тот же код, который вы упомянули выше в Java, за исключением некоторых изменений, и он работал нормально, как исключено. Метод базового класса переопределяется, поэтому выводится «Я учитель».
Причина: поскольку мы создаем ссылку на базовый класс (который может иметь ссылку на экземпляр производного класса), который фактически содержит ссылку на производный класс. И поскольку мы знаем, что экземпляр всегда сначала смотрит на свои методы, если он находит его там, он выполняет его, и если он не находит там определения, он поднимается в иерархии.
источник
Опираясь на отличную демонстрацию Кейта С. и качественные ответы всех остальных, а также для полноты изложения, давайте продолжим и добавим явные реализации интерфейса, чтобы продемонстрировать, как это работает. Рассмотрим ниже:
пространство имен LinqConsoleApp {
}
Вот вывод:
человек: я человек == LinqConsoleApp.Teacher
учитель: я учитель == LinqConsoleApp.Teacher
person1: я учитель == LinqConsoleApp.Teacher
person2: я учитель == LinqConsoleApp.Teacher
teacher1: я учитель == LinqConsoleApp.Teacher
person4: я человек == LinqConsoleApp.Person
person3: я интерфейс Person == LinqConsoleApp.Person
Обратите внимание на две вещи:
метод Teacher.ShowInfo () опускает ключевое слово new. Когда новый опущен, поведение метода такое же, как если бы ключевое слово new было определено явно.
Вы можете использовать ключевое слово override только вместе с виртуальным ключевым словом. Метод базового класса должен быть виртуальным. Или абстрактный, в этом случае класс также должен быть абстрактным.
person получает базовую реализацию ShowInfo, потому что класс Teacher не может переопределить базовую реализацию (без виртуального объявления), а person - .GetType (Teacher), поэтому он скрывает реализацию класса Teacher.
Учитель получает производную реализацию Учителя от ShowInfo, потому что учитель, потому что это Typeof (Учитель), а не на уровне наследования Person.
person1 получает производную реализацию Teacher, потому что это .GetType (Teacher), а подразумеваемое новое ключевое слово скрывает базовую реализацию.
person2 также получает производную реализацию Учителя, даже если она реализует IPerson и получает явное приведение к IPerson. Это опять-таки потому, что класс Teacher явно не реализует метод IPerson.ShowInfo ().
teacher1 также получает производную реализацию Учителя, потому что это .GetType (Учитель).
Только person3 получает реализацию ShowInfo для IPerson, потому что только класс Person явно реализует метод, а person3 является экземпляром типа IPerson.
Чтобы явным образом реализовать интерфейс, вы должны объявить экземпляр var целевого типа интерфейса, а класс должен явно реализовать (полностью квалифицировать) член (ы) интерфейса.
Обратите внимание, что даже person4 не получает реализацию IPerson.ShowInfo. Это потому, что, хотя person4 является .GetType (Person) и хотя Person реализует IPerson, person4 не является экземпляром IPerson.
источник
Пример LinQPad для запуска вслепую и уменьшения дублирования кода. Я думаю, это то, что вы пытались сделать.
источник