Подтвердить что ты не робот

Почему это работает? Перегрузка метода + переопределение метода + полиморфизм

В следующем коде:

public abstract class MyClass
{
public abstract bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage);
}

public sealed class MySubClass : MyClass
{
    public override bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage)
    {
        return MyMethod(database, asset, ref errorMessage);
    }

    public bool MyMethod(
        Database database,
        AssetBase asset,
        ref string errorMessage)
    {
    // work is done here
}
}

где AssetDetails является подклассом AssetBase.

Почему первый MyMethod вызывает второй во время выполнения, когда передал AssetDetails, вместо того, чтобы застревать в бесконечном цикле рекурсии?

4b9b3361

Ответ 1

С# разрешит ваш вызов другой реализации, потому что вызовы метода для объекта, где класс для этого объекта имеет свою собственную реализацию, будут предпочтительнее переопределенного или унаследованного.

Это может привести к тонким и труднодоступным проблемам, например, показанным здесь.

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

using System;

namespace ConsoleApplication9
{
    public class Base
    {
        public virtual void Test(String s)
        {
            Console.Out.WriteLine("Base.Test(String=" + s + ")");
        }
    }

    public class Descendant : Base
    {
        public override void Test(String s)
        {
            Console.Out.WriteLine("Descendant.Test(String=" + s + ")");
        }

        public void Test(Object s)
        {
            Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Descendant d = new Descendant();
            d.Test("Test");
            Console.In.ReadLine();
        }
    }
}

Обратите внимание: если вы укажете тип переменной типа Base вместо Descendant, вызов перейдет к другому методу, попробуйте изменить эту строку:

Descendant d = new Descendant();

и снова запустите:

Base d = new Descendant();

Итак, как вы могли бы называть Descendant.Test(String) тогда?

Моя первая попытка выглядит так:

public void Test(Object s)
{
    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Test((String)s);
}

Это не помогло мне, и вместо этого просто вызывал Test(Object) снова и снова для возможного.

Но следующие работы. Поскольку, когда мы объявляем переменную d типом Base, мы в конечном итоге вызываем правильный виртуальный метод, мы также можем прибегнуть к этой обманке:

public void Test(Object s)
{
    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Base b = this;
    b.Test((String)s);
}

Это напечатает:

Descendant.Test(Object=Test)
Descendant.Test(String=Test)

вы также можете сделать это извне:

Descendant d = new Descendant();
d.Test("Test");
Base b = d;
b.Test("Test");
Console.In.ReadLine();

распечатает то же самое.

Но сначала вы должны знать о проблеме, что совсем другое.

Ответ 2

См. раздел Спецификация языка С# на Поиск участника и Разрешение перегрузки. Метод переопределения производного класса не является кандидатом из-за правил поиска Member, а метод базового класса не является лучшим совпадением на основе правил разрешения перегрузки.

Раздел 7.3

Сначала создается множество всех доступных (раздел 3.5) элементов с именем N, объявленным в T, и базовыми типами (раздел 7.3.1) T. Объявления, содержащие модификатор переопределения, исключаются из набора. Если ни один из членов с именем N не существует и не доступен, тогда поиск не будет соответствовать, и следующие шаги не будут оцениваться.

Раздел 7.4.2:

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

Ответ 3

Как правильно отметили другие, когда задан выбор между двумя применимыми методами кандидата в классе, компилятор всегда выбирает тот, который изначально был объявлен "ближе" к классу, который содержит сайт вызова при исследовании иерархии базового класса.

Это кажется противоречивым. Разумеется, если в базовом классе указано точное соответствие, это лучше совпадение, чем неточное соответствие, объявленное в производном классе, да?

Нет. Есть две причины выбирать более производный метод всегда по менее производному методу.

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

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

Подробное объяснение того, как это правило защищает вас от отказов хрупкого базового класса, см. в моей статье на эту тему:

http://blogs.msdn.com/ericlippert/archive/2007/09/04/future-breaking-changes-part-three.aspx

И для статей о других способах, которыми языки взаимодействуют с ситуациями с хрупким базовым классом, см.:

http://blogs.msdn.com/b/ericlippert/archive/tags/brittle+base+classes/

Ответ 4

Потому что так определяется язык. Для виртуальных членов реализация, которая вызывается во время выполнения, когда метод существует как в базовом классе, так и в производном классе, основывается на конкретном типе объекта, к которому вызывается метод, а не объявленный тип переменной, которая содержит ссылку на объект. Ваш первый MyMethod находится в абстрактном классе. Таким образом, он может никогда вызываться из объекта типа MyClass - потому что такой объект никогда не может существовать. Все, что вы можете установить, - это производный класс MySubClass. Конкретный тип MySubClass, так что вызывается реализация, независимо от того, что код, который ее вызывает, находится в базовом классе.

Для не виртуальных участников/методов справедливо обратное.