Методы перегрузки в унаследованных классах

Я начал понимать, что я не понимаю, что происходит. В С# существует следующее поведение:

public class Base
{
    public void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }
}

public class Derived: Base
{
    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }
}

public class B { }

public class D: B { }

class Program
{
    static void Main(string[] args)
    {
        Derived derived = new Derived();
        D d = new D();

        derived.Method(d);
    }
}

Он напечатает

public void Method(B a)

вместо

public void Method(D a)

Это удивительно. Я полагаю, что причиной такого поведения является реализация таблицы методов. CLR не ищет методы в базовом классе, если находит соответствующий метод в текущем типе. Я думаю, что они пытались улучшить производительность.

Но я был полностью разочарован следующим кодом:

public class Base
{
    public virtual void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }
}

public class Derived: Base
{
    public override void Method(D a)
    {
        Console.WriteLine("public override void Method(D a)");
    }

    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }

}

public class B { }

public class D: B { }

class Program
{
    static void Main(string[] args)
    {
        Derived derived = new Derived();
        D d = new D();

        derived.Method(d);
    }
}

и он напечатает

public void Method(B a)

вместо

public override void Method(D a)

Это ужасно и очень непредсказуемо.

Кто-нибудь может это объяснить?

Я полагаю, что таблица методов имеет методы, которые были реализованы только в текущем типе (исключая методы переопределения), и CLR перестает искать соответствующий метод, как только будет найден любой метод, который может быть вызван. Я прав?

Ответы

Ответ 1

Я начал понимать, что я не понимаю, что происходит.

Таким образом, начинается мудрость.

Это ужасно и очень непредсказуемо.

Это не так. Напротив, эта функция предназначена для уменьшения непредсказуемости, устраняя причину отказа хрупкого базового класса.

Кто-нибудь может объяснить это?

См. мою статью за 2007 год.

https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/

Короткий вариант: метод в производном классе всегда лучше метода в базовом классе; человек, который написал производный класс, знает больше о семантике объекта и обрабатывает более конкретный случай, чем человек, который написал базовый класс.

Servy указывает, что я не использовал вторую точку. Почему переопределение в производном классе не делает метод "D" "в производном классе"?

Виртуальные методы считаются методами класса, в котором они были объявлены, а не класса, в котором они были в последнее время переопределены. Зачем? Поскольку выбор переопределять или нет, это деталь реализации класса, а не часть области публичной поверхности.

Я имею в виду, подумайте об этом. Вы пишете какой-то код, он работает, а затем вы говорите, эй, я собираюсь сделать новый (или удалить старый!) Переопределить этот метод где-то в этой иерархии типов, и вдруг это изменит разрешение перегрузки где-то еще? Это точно такая хрупкость, которую С# пытается устранить.

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

Ответ 2

Я вижу здесь путаницу. На примере Эрика лучше было бы проиллюстрировать пример. Если вы изменили код следующим образом:

public class Base
{
    public virtual void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }

    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }
}

public class Derived : Base
{
    public override void Method(D a)
    {
        Console.WriteLine("public override void Method(D a)");
    }

}

public class B { }

public class D : B { }

Выход становится "public override void Method (D a)".

Почему?

Как сказано в статье Эрика...

в базовом классе не являются кандидатами, если какой-либо метод в производном класс применим

Поскольку оба метода (D) и метод (B) теперь находятся в базовом классе, метод (D) стал самым близким совпадением, которое переопределяется в вашем производном классе, следовательно, выход.

Довольно запутанный для новичка эй? Ну, довольно запутанный для опытного разработчика. Я думаю, что ключевым моментом здесь является то, что у вас здесь плохой дизайн, я не говорю о компиляторе CLR или С#, он сделал все возможное, чтобы помочь вам здесь, я говорю о классах в вашем примере.

Я думаю, что распространенная ошибка многих людей, новых для ООП, заключается в том, чтобы злоупотреблять новой игрушкой, которая является наследованием. Наследование часто не является предпочтительным отношением, используемым в OOD. Когда вы начинаете изучать шаблоны проектирования ООП, вы заметите, что многие шаблоны не используют наследование.

Почему? Поскольку ваш пример иллюстрирует ту путаницу, которая может быть создана при использовании наследования, она называется Fragile Inheritance. Шаблоны проектирования чаще используют агрегацию и состав, чтобы разделить проблемное пространство на более работоспособное, расширяемое и управляемое решение.

Наследование - это мощная конструкция, как и в случае с самыми мощными вещами, ее следует использовать экономно и с осторожностью.