Почему это работает? Перегрузка метода + переопределение метода + полиморфизм - PullRequest
12 голосов
/ 02 декабря 2009

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

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

Ответы [ 4 ]

10 голосов
/ 02 декабря 2009

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

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

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

5 голосов
/ 02 декабря 2009

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

Раздел 7.3

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

Раздел 7.4.2:

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

4 голосов
/ 02 декабря 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/

1 голос
/ 02 декабря 2009

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

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

...