Разрешение перегрузки и виртуальные методы - PullRequest
21 голосов
/ 09 сентября 2010

Рассмотрим следующий код (он немного длинный, но, надеюсь, вы сможете следовать):

class A
{
}

class B : A
{
}

class C
{
    public virtual void Foo(B b)
    {
        Console.WriteLine("base.Foo(B)");
    }
}

class D: C
{
    public override void Foo(B b)
    {
        Console.WriteLine("Foo(B)");
    }

    public void Foo(A a)
    {
        Console.WriteLine("Foo(A)");
    }
}

class Program
{
    public static void Main()
    {
        B b = new B();
        D d = new D ();
        d.Foo(b);
    }
}

Если вы думаете, что вывод этой программы «Foo (B)», то вы окажетесь в той же лодке, что и я: совершенно неправильно! Фактически он выдает «Foo (A)»

Если я удаляю виртуальный метод из класса C, то он работает как положено: «Foo (B)» - это выход.

Почему компилятор выбирает версию, которая принимает A, когда B является производным классом?

Ответы [ 5 ]

14 голосов
/ 09 сентября 2010

Ответ находится в спецификации C # раздел 7.3 и раздел 7.5.5.1

Я разбил шаги, используемые для выбора метода для вызова.

  • Сначала создается набор всех доступных членов с именем N (N=Foo), объявленных в T (T=class D), и базовых типов T (class C). Объявления, содержащие модификатор переопределения, исключаются из набора ( D.Foo (B) исключается )

    S = { C.Foo(B) ; D.Foo(A) }
    
  • Построен набор методов-кандидатов для вызова метода. Начиная с набора методов, связанных с M, которые были найдены предыдущим поиском члена, набор сокращается до тех методов, которые применимы в отношении списка аргументов AL (AL=B). Сокращение набора состоит в применении следующих правил к каждому методу T.N в наборе, где T (T=class D) - это тип, в котором объявлен метод N (N=Foo):

    • Если N не применимо по отношению к AL ( Раздел 7.4.2.1 ), то N удаляется из набора.

      • C.Foo(B) применимо в отношении AL
      • D.Foo(A) применимо в отношении AL

        S = { C.Foo(B) ; D.Foo(A) }
        
    • Если N применимо по отношению к AL (раздел 7.4.2.1), то все методы, объявленные в базовом типе T, удаляются из набора . C.Foo(B) удалено из набора

          S = { D.Foo(A) }
      

В конце победитель D.Foo(A).


Если абстрактный метод удален из C

Если абстрактный метод удален из C, начальный набор будет S = { D.Foo(B) ; D.Foo(A) }, а правило разрешения перегрузки должно использоваться для выбора лучшего элемента функции в этом наборе.

В этом случае победителем становится D.Foo(B).

9 голосов
/ 09 сентября 2010

Почему компилятор выбирает версию, которая принимает A, когда B является более производным классом?

Как уже отмечали другие, компилятор делает это, потому что вот чтоспецификация языка говорит, что делать.

Это может быть неудовлетворительным ответом.Естественным продолжением было бы «какие принципы проектирования лежат в основе решения указать язык таким образом?»

Это часто задаваемый вопрос как в StackOverflow, так и в моем почтовом ящике.Краткий ответ: «Этот дизайн смягчает семейство ошибок хрупкого базового класса».

Описание функции и причины ее разработки см. В моей статье на эту тему:

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

Дополнительные статьи по темео том, как разные языки решают проблему с хрупким базовым классом, см. в моем архиве статей на эту тему:

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

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

Почему подписи, объявленные в базовом классе, игнорируются?

И вот еще три соответствующих или дублированных вопроса:

Разрешение перегрузки C #?

Метод перегрузки разрешает и тизеры мозга Джона Скита

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

1 голос
/ 09 сентября 2010

Итак, вот как это должно работать в соответствии со спецификацией (во время компиляции и с учетом правильной навигации по документам):

Компилятор определяет список подходящих методовиз типа D и его базовых типов на основе имени метода и списка аргументов.Это означает, что любой метод с именем Foo, принимающий один параметр типа, в который происходит неявное преобразование из B, является допустимым кандидатом.Это приведет к следующему списку:

C.Foo(B) (public virtual)
D.Foo(B) (public override)
D.Foo(A) (public)

Из этого списка любые объявления, содержащие модификатор переопределения, исключаются.Это означает, что список теперь содержит следующие методы:

C.Foo(B) (public virtual)
D.Foo(A) (public)

На данный момент у нас есть список подходящих кандидатов, и теперь компилятор должен решить, что вызывать.В документе 7.5.5.1 Вызовы методов мы находим следующий текст:

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

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

D.Foo(A) (public)
1 голос
/ 09 сентября 2010

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

У вас есть метод Foo, который не виртуален и, следовательно,этот метод называется.

Эта ссылка имеет очень хорошее объяснение http://msdn.microsoft.com/en-us/library/aa645767%28VS.71%29.aspx

0 голосов
/ 09 сентября 2010

Я думаю, что при реализации другого класса он смотрит вверх по дереву, чтобы получить надежную реализацию метода. Поскольку нет вызываемого метода, он использует базовый класс. public void Foo(A a){ Console.WriteLine("Foo(A)" + a.GetType().Name); Console.WriteLine("Foo(A)" +a.GetType().BaseType ); }

это предположение, что я не профессионал в .Net

...