.Net наследование и перегрузка метода

Вот пример кода:

class Program
{
    static void Main(string[] args)
    {
        var obj = new DerivedClass();
        obj.SomeMethod(5);
    }
}

class BaseClass
{
    internal void SomeMethod(int a) { }
}

class DerivedClass : BaseClass
{
    internal void SomeMethod(long a) { }
}

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

Спасибо.

Ответ 1

Точная формулировка и расположение различаются в разных версиях спецификации, но, например, здесь, можно прочитать:

Создан набор методов-кандидатов для вызова метода. Начиная с набора методов, связанных с M, которые были найдены предыдущим поиском члена (§7.3), набор сводится к тем методам, которые применимы относительно списка аргументов A. Сокращение набора состоит из применения следующих правил каждому методу TN в множестве, где T - тип, в котором объявлен метод N:

Если N не применимо относительно A (§7.4.2.1), то N удаляется из множества.

Если N применимо относительно A (§7.4.2.1), то все методы, объявленные в базовом типе T, удаляются из набора.

Итак, учитывая, что мы имеем obj типа DerivedClass, тогда набор методов-членов содержит void SomeMethod(long) из DerivedClass и void SomeMethod(int) из BaseClass.

Оба этих метода применимы, и действительно void SomeMethod(int) является лучшим соответствием перегрузки, но из-за правила в последнем предложении, приведенном выше, после того, как будет найдено, что void SomeMethod(long) применимо, все методы из базовых классов удаляется из набора кандидатов, что означает, что void SomeMethod(int) больше не рассматривается.

Хорошо, что техническая причина с точки зрения спецификации. Какова причина дизайна, заключающаяся в том, что в первую очередь входит в спецификацию?

Итак, представьте, что начало BaseClass определено как:

public class BaseClass
{
}

Если остальная часть кода была одинаковой, то довольно очевидно, что вызов obj.SomeMethod(5) должен вызывать единственный так называемый метод, который существовал.

Теперь рассмотрим, был ли после написан код, метод void SomeMethod(int) был добавлен в BaseClass. И подумайте, что это может быть в другой сборке DerivedClass и отдельным автором.

Теперь значение вызова SomeMethod() изменилось. Хуже того, оно изменилось или не зависело от того, какие обновления на данной машине были или не были применены. (И что еще хуже, поскольку тип возврата не используется в разрешении перегрузки С#, он изменился таким образом, что может привести к ошибке компиляции в уже скомпилированном коде: полное изменение смены).

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

Следствие этого, что может быть удивительным для некоторых, хотя находится в:

class Program
{
    static void Main(string[] args)
    {
        var obj = new DerivedClass();
        obj.SomeMethod(5);
    }
}
class BaseClass
{
    public virtual void SomeMethod(int a) { Console.WriteLine("Base"); }
}
class DerivedClass : BaseClass
{
    public override void SomeMethod(int a) { Console.WriteLine("Defined in Base, overriden in Derived"); }
    public void SomeMethod(long a) { Console.WriteLine("Derived"); }
}

Это выводит Derived, потому что это правило применяется в соответствии с тем, где объявлен метод, даже если есть реализация из переопределения.

(Другая причина для правила, действующего как это происходит, заключается в том, что при его преобразовании в CIL вызов будет содержать информацию о классе, в котором он был объявлен. Правило здесь - это самый простой способ сделать вещи: 1) Подобная логика, применяемая в дизайне CIL и 2) выше, сделала это особенностью CIL для людей С#, с которыми нужно работать, а не с ней работать).

Ответ 2

var obj = new DerivedClass();

var ключевое слово - это просто синтаксический сахар в С#; это по существу то же самое, что:

DerivedClass obj = new DerivedClass();

следовательно, вы вызываете DerrivedClass.SomeMethod, а это только то поведение, которое вы испытываете. Вы заметили бы разницу, если бы вы определили свою переменную следующим образом:

BaseClass obj = new DerivedClass();

Ответ 3

Редактировать после комментариев: Правда, я, вероятно, не ответил на правильный вопрос, поэтому позвольте мне попробовать сейчас:

Вызов метода в исходном коде соответствует сигнатуре обоих методов (как в базе, так и в производном классе), поскольку в этом случае параметр 5 может быть либо int, либо long. Однако базовый метод не помечен как virtual (что позволит переопределить), а метод "производный" на самом деле не получен, поскольку он не помечен override.

ЗАМЕЧАНИЕ, что даже если вы отметили его как override, вы получите ошибку, так как на самом деле две сигнатуры метода не эквивалентны: один принимает int, а другой принимает значение long тип. Это приведет к ошибке времени компиляции с messsage: "подходящий метод не найден для переопределения".

Остальное, надеюсь, станет ясным, если вы прочтете остальную часть моего первоначального ответа ниже.


Оригинальный ответ:

Здесь есть несколько вещей:

1) Ваши методы имеют разные подписи; один занимает длинный, а другой принимает int

2) Вы не отметили свои методы virtual или override.

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

internal class Program
{
    private static void Main(string[] args)
    {

        var obj = new DerivedClass();
        // That is the same as:
        //DerivedClass obj = new DerivedClass();

        // Will call the base method, since that now matches the
        // signature (takes an int parameter). DerivedClass simply
        // does not HAVE a method with that signature on it own:
        obj.SomeMethod(5); // will output "base with int"

        // Now call the other method, which IS defined in DerivedClass, 
        // by appending an "l", to mark this as a Long:
        obj.SomeMethod(5l); // Will output "derived"

        // This would call the base method directly
        var obj2 = new BaseClass();
        obj2.SomeMethod(5l); 

        Console.ReadKey();
    }
}

internal class BaseClass
{
    internal void SomeMethod(int a)
    {
        Console.WriteLine("base with int");
    }

    // Added method for the example:
    // Note that "virtual" allows it to be overridden
    internal virtual void SomeMethod(long a)
    {
        Console.WriteLine("base with long");
    }
}

internal class DerivedClass : BaseClass
{
    // Note: Overrides the base method now
    internal override void SomeMethod(long a)
    {
        Console.WriteLine("derived");
    }
}

Ответ 4

Из ссылка на С# язык:

7.5.5 Вызов функции-члена

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

Для описания процесса вызова функции делятся на две категории:

  • Элементы статической функции. <snip>
  • Элементы функции экземпляра. Это методы экземпляра, аксессоры свойств экземпляра и аксессоры индексатора. Элементы функции экземпляра либо не виртуальные, либо виртуальные, и всегда вызывается на конкретный случай. Экземпляр вычисляется экземпляром выражение, и оно становится доступным внутри члена функции как это (§7.6.7). Обработка времени выполнения вызова функции-члена состоит из следующих этапов, где M - член функции и, если M является членом экземпляра, E является выражением экземпляра:
    • Если M является статическим членом функции: <snip>
    • Если M - член функции экземпляра, объявленный в типе значения: <snip>
    • Если M - член функции экземпляра, объявленный в ссылочном типе:
      • E оценивается. Если эта оценка вызывает исключение, то дальнейшие шаги не выполняются.
      • Список аргументов оценивается, как описано в п. 7.5.1.
      • Если тип E является типом значений, <snip>
      • Значение E проверено как действительное. Если значение E равно null, генерируется исключение System.NullReferenceException и дальнейшие шаги выполняются.
      • Выполняется реализация функции-члена для вызова:
        • Если тип привязки E является интерфейсом, <snip>
        • В противном случае, если M является членом виртуальной функции, <snip>
        • В противном случае M является не виртуальным членом функции, а для вызова функции является сам M.
      • Вызывается реализация члена элемента, определенная на шаге выше. Объект, на который ссылается E, ссылается на объект этим.

Что еще в 1.6.6.4 Виртуальные, переопределенные и абстрактные методы, мы имеем

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

Итак, что происходит, так это то, что при компиляции кода тип используемой переменной определяет, какой метод вызывается.

public class A { public void WhoAreYou() { Console.WriteLine("A"); } }

public class B : A  { public void WhoAreYou() { Console.WriteLine("B"); } }

internal class Program
{

private static void Main(string[] args)
{
    (new B() as A).WhoAreYou(); // "A"
    (new B()).WhoAreYou(); // "B"

    Console.ReadLine();
}

Обратите внимание, что компилятор предупредит вас о потенциальной проблеме, так как метод, который будет вызываться, отличается в зависимости от типа, который вы используете для определения экземпляра класса.

Ответ 5

Я понимаю, что, поскольку переопределение/скрытие не применяется, метод производного класса вызывается, поскольку он создается в main().

В переопределении метода: Базовая ссылочная переменная класса, указывающая на объект дочернего класса, вызывается переопределенным методом в дочернем классе. Ключевое слово "Override" используется в сигнатуре метода производного класса.

В скрытии метода: Базовая ссылочная переменная класса, указывающая на объект дочернего класса, вызовет скрытый метод в базовом классе. Ключевое слово "Новое" используется в сигнатуре метода производного класса.