Разрешение перегрузки и виртуальные методы
Рассмотрим следующий код (он немного длинный, но, надеюсь, вы можете следовать):
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
является более производным классом?
Ответы
Ответ 1
Ответ указан в спецификации С# раздел 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 удаляется из набора.
-
Если 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)
.
Ответ 2
Почему компилятор выбирает версию, которая принимает A, когда B - это более производный класс?
Как отмечали другие, компилятор делает это, потому что это говорит о том, что говорит спецификация языка.
Это может быть неудовлетворительный ответ. Естественным продолжением было бы "какие принципы дизайна были в основном решением указать язык таким образом?"
Это часто задаваемый вопрос, как в StackOverflow, так и в моем почтовом ящике. Краткий ответ: "Этот проект смягчает семейство ошибок класса Brittle Base Class".
Описание функции и ее описание так, как описано в моей статье на эту тему:
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/
Здесь мой ответ на тот же вопрос с прошлой недели, который выглядит замечательно, как этот.
Почему объявления, объявленные в базовом классе, игнорируются?
И вот еще три актуальных или дублированных вопроса:
С# перегрузка разрешения?
Метод перегружает разрешение и мозговые тизеры Джона Скита
Почему это работает? Перегрузка метода + переопределение метода + полиморфизм
Ответ 3
Я думаю, что это связано с тем, что в случае не виртуального метода используется тип времени компиляции переменной, в которой используется метод.
У вас есть метод Foo, который не является виртуальным и, следовательно, этот метод вызывается.
Эта ссылка имеет очень хорошее объяснение http://msdn.microsoft.com/en-us/library/aa645767%28VS.71%29.aspx
Ответ 4
Итак, вот как это должно работать в соответствии со спецификацией (во время компиляции и при условии, что я правильно перевел документы):
Компилятор идентифицирует список методов сопоставления из типа 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)
Ответ 5
Я думаю, что при реализации другого класса он выглядит намного до дерева, чтобы получить надежную реализацию метода. Поскольку нет метода, называемого, он использует базовый класс.
public void Foo(A a){
Console.WriteLine("Foo(A)" + a.GetType().Name);
Console.WriteLine("Foo(A)" +a.GetType().BaseType );
}
Это предположение, что я не про в .Net