Я хочу построить ответ Ахратта . Для полноты, разница в том, что 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, и, несмотря на то, что он элегантен, он не всегда производит поведение, которое вы ожидаете, если исходите из фона на любом из старых языков.
Вот что нового: это становится сложным, когда вы объединяете два ключевых слова в длинной цепочке наследования. Учтите следующее:
class Foo { public virtual void DoFoo() { Console.WriteLine("Foo"); } }
class Bar:Foo { public override sealed void DoFoo() { Console.WriteLine("Bar"); } }
class Baz:Bar { public virtual void DoFoo() { Console.WriteLine("Baz"); } }
class Bai:Baz { public override void DoFoo() { Console.WriteLine("Bai"); } }
class Bat:Bai { public new void DoFoo() { Console.WriteLine("Bat"); } }
class Bak:Bat { }
Foo foo = new Foo();
Bar bar = new Bar();
Baz baz = new Baz();
Bai bai = new Bai();
Bat bat = new Bat();
foo.DoFoo();
bar.DoFoo();
baz.DoFoo();
bai.DoFoo();
bat.DoFoo();
Console.WriteLine("---");
Foo foo2 = bar;
Bar bar2 = baz;
Baz baz2 = bai;
Bai bai2 = bat;
Bat bat2 = new Bak();
foo2.DoFoo();
bar2.DoFoo();
baz2.DoFoo();
bai2.DoFoo();
Console.WriteLine("---");
Foo foo3 = bak;
Bar bar3 = bak;
Baz baz3 = bak;
Bai bai3 = bak;
Bat bat3 = bak;
foo3.DoFoo();
bar3.DoFoo();
baz3.DoFoo();
bai3.DoFoo();
bat3.DoFoo();
Вывод:
Foo
Bar
Baz
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
Первый набор из пяти вполне ожидаем; Поскольку каждый уровень имеет реализацию и на него ссылаются как на объект того же типа, который был создан, среда выполнения разрешает каждый вызов уровня наследования, на который ссылается тип переменной.
Второй набор из пяти является результатом назначения каждого экземпляра переменной непосредственного родительского типа. Теперь некоторые различия в поведении вытряхнуть; foo2
, который на самом деле Bar
приведен как Foo
, все равно найдет более производный метод фактического объекта типа Bar. bar2
это Baz
, но в отличие от foo2
, потому что Baz явно не переопределяет реализацию Bar (он не может; Bar sealed
это), он не виден во время выполнения при взгляде «сверху вниз», поэтому вместо этого вызывается реализация Bar. Обратите внимание, что Baz не должен использовать new
ключевое слово; вы получите предупреждение компилятора, если опустите ключевое слово, но подразумеваемое поведение в C # - скрыть родительский метод. baz2
это Bai
, который переопределяетBaz
«ыnew
реализация, поэтому его поведение похоже на , который не имеет ни одной из основных реализаций и просто использует реализацию своего родителя.foo2
«S; фактическая реализация типа объекта в Bai называется. bai2
is Bat
, который снова скрывает Bai
реализацию метода своего родителя , и ведет себя так же, как bar2
если бы реализация Bai не была запечатана, поэтому теоретически Bat мог бы переопределить, а не скрыть метод. Наконец, bat2
этоBak
Третий набор из пяти иллюстрирует полное поведение разрешения сверху вниз. На самом деле все ссылается на экземпляр самого производного класса в цепочке, Bak
но разрешение на каждом уровне типа переменной выполняется путем запуска на этом уровне цепочки наследования и перехода к самому производному явному переопределению метода, которое те , в Bar
, Bai
и Bat
. Скрытие метода, таким образом, «разрывает» основную цепочку наследования; Вы должны работать с объектом на уровне наследования, который скрывает метод, или ниже его уровня, чтобы использовать метод скрытия. В противном случае скрытый метод «раскрывается» и используется вместо него.