Зачем нам нужно ключевое слово new и почему по умолчанию скрывается, а не отменяется?

81

Я просматривал это сообщение в блоге и у меня возникли следующие вопросы:

  • Зачем нам нужно newключевое слово, просто чтобы указать, что метод базового класса скрывается. Я имею ввиду, зачем нам это нужно? Если мы не используем overrideключевое слово, разве мы не скрываем метод базового класса?
  • Почему в C # по умолчанию используется скрытие, а не переопределение? Почему дизайнеры реализовали это именно так?
Песочница
источник
6
Немного более интересный вопрос (для меня): почему сокрытие вообще законно. Обычно это ошибка (отсюда и предупреждение компилятора), которая редко требуется и в лучшем случае всегда проблематична.
Конрад Рудольф

Ответы:

127

Хорошие вопросы. Позвольте мне их переформулировать.

Почему вообще законно скрывать метод с помощью другого метода?

Позвольте мне ответить на этот вопрос примером. У вас есть интерфейс из CLR v1:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Супер. Теперь в CLR v2 у вас есть дженерики, и вы думаете: «Блин, если бы у нас были дженерики в v1, я бы сделал его универсальным интерфейсом. Но я этого не сделал. Я должен сделать что-то совместимое с ним сейчас, что является универсальным, чтобы Я получаю преимущества универсальных шаблонов без потери обратной совместимости с кодом, ожидающим IEnumerable ».

interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> .... uh oh

Для чего вы собираетесь вызывать метод GetEnumerator IEnumerable<T>? Помните, что вы хотите, чтобы GetEnumerator был скрыт в неуниверсальном базовом интерфейсе. Вы никогда не захотите, чтобы эта вещь вызывалась, если вы явно не находитесь в ситуации обратной совместимости.

Одно это оправдывает сокрытие метода. Дополнительные мысли об обосновании скрытия метода см. В моей статье на эту тему .

Почему скрытие без «нового» вызывает предупреждение?

Потому что мы хотим обратить ваше внимание на то, что вы что-то скрываете и, возможно, делаете это случайно. Помните, что вы могли случайно что-то скрыть из-за изменения базового класса, сделанного кем-то другим, а не в результате редактирования производного класса.

Почему скрытие без «нового» является предупреждением, а не ошибкой?

Той же причине. Возможно, вы случайно что-то скрываете, потому что только что приобрели новую версию базового класса. Это происходит все время. FooCorp создает базовый класс B. BarCorp создает производный класс D с методом Bar, потому что их клиентам нравится этот метод. FooCorp видит это и говорит: «Эй, это хорошая идея, мы можем добавить эту функциональность в базовый класс». Они поступают так и выпускают новую версию Foo.DLL, и когда BarCorp подберет новую версию, было бы неплохо, если бы им сказали, что их метод теперь скрывает метод базового класса.

Мы хотим, чтобы эта ситуация была предупреждением, а не ошибкой, потому что создание ошибки означает, что это еще одна форма проблемы хрупкого базового класса . C # был тщательно разработан, чтобы при изменении базового класса влияние на код, использующий производный класс, сводилось к минимуму.

Почему скрывается, а не отменяется значение по умолчанию?

Потому что виртуальное переопределение опасно . Виртуальное переопределение позволяет производным классам изменять поведение кода, который был скомпилирован для использования базовых классов. Совершать что-то опасное, например, отменять, следует делать сознательно и намеренно , а не случайно.

Эрик Липперт
источник
1
FWIW, я не согласен с тем, что «одна только эта вещь оправдывает сокрытие метода». Иногда лучше нарушить обратную совместимость. Python 3 показал, что это действительно работает, не разрушая слишком много работы. Важно отметить, что «обратная совместимость любой ценой» - не единственный разумный вариант. И эта причина переносится на другой момент: изменение базового класса всегда будет непростым делом. На самом деле может быть предпочтительнее прервать зависимую сборку. (Но +1 за ответ на вопрос из моего комментария под фактическим вопросом.)
Конрад Рудольф
@Konrad: Я согласен, что обратная совместимость может повредить. Mono решила отказаться от поддержки 1.1 в будущих выпусках.
simendsjo
1
@Konrad: Нарушение обратной совместимости с вашим языком сильно отличается от того, чтобы облегчить людям, использующим ваш язык, нарушение обратной совместимости с их собственным кодом. Пример Эрика - второй случай. И человек, пишущий этот код, принял решение, что ему нужна обратная совместимость. Если возможно, язык должен поддерживать это решение.
Брайан
Боже, ответь, Эрик. Однако есть более конкретная причина, по которой переопределение по умолчанию - это плохо. Если базовый класс Base вводит новый метод Duck () (который возвращает объект утки), который просто случайно имеет ту же сигнатуру, что и ваш существующий метод Duck () (который делает утку Марио) в производном классе Derived (т.е. нет связи между вы и автор Base), вы не хотите, чтобы клиенты обоих классов готовили утку Марио, когда им просто нужна утка.
jyoungdev
Таким образом, в основном это связано с изменением базовых классов и интерфейсов.
jyoungdev
15

Если методу в производном классе предшествует ключевое слово new, метод определяется как независимый от метода в базовом классе.

Однако, если вы не укажете ни new, ни переопределения, результат будет таким же, как если бы вы указали new, но вы получите предупреждение компилятора (поскольку вы можете не знать, что скрываете метод в методе базового класса, или, возможно, вы хотели его переопределить и просто забыли включить ключевое слово).

Таким образом, это помогает вам избежать ошибок и явно показать, что вы хотите сделать, и делает код более читаемым, чтобы вы могли легко понять ваш код.

Инкогнито
источник
12

Стоит отметить, что единственный эффект newв этом контексте - подавление предупреждения. В семантике нет изменений.

Итак, один ответ: нам нужно newсообщить компилятору, что скрытие является преднамеренным, и избавиться от предупреждения.

Следующий вопрос: если вы не хотите / не можете переопределить метод, зачем вам вводить другой метод с тем же именем? Потому что сокрытие - это, по сути, конфликт имен. И вы, конечно, в большинстве случаев этого избегаете.

Единственная веская причина, о которой я могу думать для намеренного сокрытия, - это когда имя навязывается вам интерфейсом.

Хенк Холтерман
источник
6

В C # члены по умолчанию запечатаны, что означает, что вы не можете переопределить их (если они не отмечены ключевыми словами virtualили abstract), и это по причинам производительности . Новый модификатор используется для явного скрыть унаследованный элемент.

Дарин Димитров
источник
4
Но когда вы действительно хотите использовать новый модификатор?
simendsjo
1
Если вы хотите явно скрыть член, унаследованный от базового класса. Скрытие унаследованного члена означает, что производная версия члена заменяет версию базового класса.
Дарин Димитров
3
Но любые методы, использующие базовый класс, по-прежнему будут вызывать базовую реализацию, а не производную. Мне это кажется опасной ситуацией.
simendsjo
@simendsjo, @ Дарин Димитров: Мне также трудно думать о сценарии использования, когда действительно необходимо ключевое слово new. Мне кажется, что всегда есть способы получше. Я также задаюсь вопросом, не является ли скрытие реализации базового класса недостатком дизайна, который легко приводит к ошибкам.
Dirk Vollmar
2

Если переопределение было по умолчанию без указания overrideключевого слова, вы могли случайно переопределить какой-либо метод своей базы только из-за равенства имен.

Стратегия компилятора .Net состоит в том, чтобы на всякий случай выдавать предупреждения, если что-то может пойти не так, поэтому в этом случае, если переопределение было по умолчанию, должно быть предупреждение для каждого метода переопределения - что-то вроде `` предупреждение: проверьте, действительно ли вы хотите переопределить'.

Франтишек Жячик
источник
0

Я предполагаю, что в основном это связано с наследованием множественных интерфейсов. При использовании дискретных интерфейсов очень возможно, что два разных интерфейса будут использовать одну и ту же сигнатуру метода. Разрешение использования newключевого слова позволит вам создавать эти разные реализации с одним классом, вместо того, чтобы создавать два разных класса.

Обновлено ... Эрик дал мне идею, как улучшить этот пример.

public interface IAction1
{
    int DoWork();
}
public interface IAction2
{
    string DoWork();
}

public class MyBase : IAction1
{
    public int DoWork() { return 0; }
}
public class MyClass : MyBase, IAction2
{
    public new string DoWork() { return "Hi"; }
}

class Program
{
    static void Main(string[] args)
    {
        var myClass = new MyClass();
        var ret0 = myClass.DoWork(); //Hi
        var ret1 = ((IAction1)myClass).DoWork(); //0
        var ret2 = ((IAction2)myClass).DoWork(); //Hi
        var ret3 = ((MyBase)myClass).DoWork(); //0
        var ret4 = ((MyClass)myClass).DoWork(); //Hi
    }
}
Мэтью Уайтед
источник
1
При использовании нескольких интерфейсов вы можете явно назвать методы интерфейса, чтобы избежать коллизий. Я не понимаю, что вы имеете в виду под «разными реализациями для одного класса», оба имеют одинаковую сигнатуру ...
simendsjo
newКлючевое слово позволяет также изменить базу наследования для всех детей от этой точки.
Matthew Whited
-1

Как уже отмечалось, скрытие метода / свойства позволяет изменить некоторые аспекты метода или свойства, которые иначе нельзя было бы легко изменить. Одна из ситуаций, когда это может быть полезно, - это разрешение унаследованному классу иметь свойства чтения и записи, которые доступны только для чтения в базовом классе. Например, предположим, что базовый класс имеет набор свойств только для чтения, называемых Value1-Value40 (конечно, настоящий класс будет использовать более удачные имена). Запечатанный потомок этого класса имеет конструктор, который принимает объект базового класса и копирует оттуда значения; после этого класс не позволяет их изменять. Другой наследуемый потомок объявляет свойства чтения-записи с именем Value1-Value40, которые при чтении ведут себя так же, как версии базового класса, но при записи позволяют записывать значения.

Одно раздражение в этом подходе - возможно, кто-то может мне помочь - заключается в том, что я не знаю способа как затенять, так и переопределять конкретное свойство в одном классе. Допускает ли это какой-либо из языков CLR (я использую vb 2005)? Было бы полезно, если бы объект базового класса и его свойства могли быть абстрактными, но это потребовало бы, чтобы промежуточный класс переопределил свойства Value1 до Value40, прежде чем класс-потомок сможет их затенять.

суперкар
источник