Почему это работает? Перегрузка метода + переопределение метода + полиморфизм
В следующем коде:
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, вместо того, чтобы застревать в бесконечном цикле рекурсии?
Ответы
Ответ 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
, так что вызывается реализация, независимо от того, что код, который ее вызывает, находится в базовом классе.
Для не виртуальных участников/методов справедливо обратное.