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

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

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, а не застревает в бесконечном цикле рекурсии?


person kasey    schedule 02.12.2009    source источник
comment
Вы не забыли `: MyClass` в своем определении MySubClass?   -  person Bruno Reis    schedule 02.12.2009
comment
1. Вы действительно хотите использовать MySubClass в качестве внутреннего класса? Почему? 2 - Не могли бы вы привести пример фактического вызова метода для обеспечения поведения, которое вы описываете?   -  person Joe    schedule 02.12.2009
comment
MySubClass не является внутренним классом, нет. Примером может быть эффективный mySubClassInstance.MyMethod(database, new AssetDetails(), ref msg); - это попадает в первый метод, затем передается второму   -  person kasey    schedule 02.12.2009
comment
Как объявляется mysubClassInstance? быть базового или потомкового типа? (независимо от того, какой тип вы в него вложили)   -  person Lasse V. Karlsen    schedule 02.12.2009
comment
mysubClassInstance был объявлен как тип-потомок.   -  person kasey    schedule 02.12.2009
comment
kasey, Вы запускали эту программу? Потому что, когда я это делал, он никогда не вызывает метод First MySubClass, если вы передаете AssetDetails при вызове MySubClass. Скорее он вызывает второй метод в этом вызове (имеется в виду окончательный метод MySubclass)   -  person Dhananjay    schedule 29.03.2012


Ответы (4)


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

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

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

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();

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

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

person Lasse V. Karlsen    schedule 02.12.2009
comment
Так как нет возможности попасть в этот бардак, не имея одного виртуального и одного невиртуального метода (у вас не может быть двух одинаковых методов в одном классе), или имея метод в базовом классе, вызывающий метод в классе-потомке не имея виртуального метода, я не думаю, что это влияет на это. Я до сих пор не уверен, почему это важно, как вы говорите, есть много таких тонких проблем, и не все из них связаны с виртуальными методами. Но, пожалуйста, просветите меня, и я соответствующим образом изменю свой ответ, мне только Джон Скит показал эту проблему, в любой момент он проснется и докажет, что мы все неправы... - person Lasse V. Karlsen; 02.12.2009
comment
Как я понял, у него из-за переопределения метода из базового класса, а также реализации его отдельно, в его классе-потомке два одинаковых метода, и он недоумевает, почему компилятор выбрал для своего вызова один, а не другой. - person Lasse V. Karlsen; 02.12.2009
comment
@Lasse, да, теперь я вижу, ты совершенно прав. - извините, я удалил свой комментарий как раз перед тем, как вы ответили. - person Charles Bretana; 02.12.2009
comment
Не волнуйтесь, эта часть компилятора - темный переулок, в который я все равно не осмелюсь зайти, опасаясь ограбления :) - person Lasse V. Karlsen; 02.12.2009
comment
Большое спасибо, благодаря вашему объяснению я смог предсказать, что выведет ваше приложение, и это было противоположно тому, что я ожидал раньше, так что я думаю, это означает, что вы ответили на мой вопрос! Это облегчение знать, что, наконец, есть веская причина для этого. - person kasey; 02.12.2009
comment
@Lasse, интересно посмотреть, кто ((какие типы) имеют доступ к двум реализациям. Объекты типа MySubClass имеют доступ только к невиртуальному методу. Объекты, производные от MySubClass (если он не был запечатан), будет иметь доступ только к виртуальному методу, если они были объявлены как их конкретный тип, и будет иметь доступ только к невиртуальному методу, если они будут объявлены как MySubClass, Учитывая, что он запечатан, а его база абстиракта, в его примере вообще не должно быть никакого доступа к виртуальному члену, не так ли? - person Charles Bretana; 02.12.2009
comment
Ну, вы можете внутренне, с помощью некоторых хитростей, позволить мету изменить мой ответ. - person Lasse V. Karlsen; 02.12.2009
comment
А с моими изменениями тоже в этом случае придется прибегнуть к гипсу, так что немного надумано. - person Lasse V. Karlsen; 02.12.2009

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

Раздел 7.3

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

Раздел 7.4.2:

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

person tvanfosson    schedule 02.12.2009
comment
Это лучший технический ответ на этот вопрос. - person Lasse V. Karlsen; 02.12.2009

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

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

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

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

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

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

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/

person Eric Lippert    schedule 02.12.2009
comment
Спасибо; очень интересно увидеть обоснование решения. - person kasey; 03.12.2009

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

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

person Charles Bretana    schedule 02.12.2009
comment
Извините, чтобы уточнить: под первым MyMethod я имел в виду первую реализацию, то есть первый MyMethod в MySubClass, но вторую лексическую инстанциацию. Что меня смущает, так это то, почему последнее вхождение MyMethod вызывается при передаче AssetDetails. - person kasey; 02.12.2009
comment
@kasey, да, вы правы в том, что я неправильно понял ваш вопрос ... ответ Лассе выше точно в цель ... - person Charles Bretana; 02.12.2009