Виртуальный метод, переопределяющий С# - почему это не вызывает бесконечную рекурсию?
Посмотрел какой-то код в нашей кодовой базе, и я не могу понять, как/почему это даже работает (и не вызывает переполнение стека из-за бесконечной рекурсии). Я вставил несколько эквивалентных кодов ниже:
У нас есть виртуальный метод Foo (B), определенный в классе P1 и переопределенный в классе P2. P2 также определяет частный не виртуальный метод Foo (A). B происходит от A. P2:: Foo (B) имеет вызов в конце: Foo (b). Я ожидаю, что это закончится переполнением стека.
Однако выход:
P2:: Foo Virtual
P2:: Foo Частный не виртуальный
Похоже, второй вызов Foo в переопределенном методе - это выбор не виртуального метода Foo в этом случае. При выполнении аналогичных операций в P1 (код uncomment), мы в конечном итоге вызываем Foo бесконечное количество раз через рекурсию.
Вопросы: (наконец!)
1. Почему поведение отличается от исходного виртуального метода и переопределенного метода? Почему один называется сам, а другой вызывает другой метод?
2. Есть ли какой-то порядок предпочтений? Обратите внимание: если мы изменим частный модификатор на публичный, в обоих случаях мы вызываем не виртуальный метод (даже если мы создаем P2 следующим образом: P1 p2 = new P2(); вместо P2 p2 = new P2 ( );) Похоже, что не виртуальная версия является предпочтительной, за исключением случаев, когда она находится внутри определения виртуального метода. Это правда?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class P1
{
static void Main(string[] args)
{
B b = new B();
P2 p2 = new P2();
p2.Foo(b);
// Uncomment code to cause infinite recursion
//P1 p1 = new P1();
//p1.Foo(b);
}
private void Foo(A a)
{
Console.WriteLine("P1::Foo Private Non-Virtual");
}
public virtual void Foo(B b)
{
Console.WriteLine("Inside P1::Foo");
// Uncomment code to cause infinite recursion
// Foo(b);
}
}
public class P2 : P1
{
private void Foo(A a)
{
Console.WriteLine("P2::Foo Private Non-Virtual");
}
public override void Foo(B b)
{
Console.WriteLine("P2::Foo Virtual");
Foo(b);
}
}
public class A
{
public int a = 10;
}
public class B : A
{
public int b = 20;
}
}
Ответы
Ответ 1
Это связано с тем, что разрешение перегрузки просматривает только унаследованные элементы, если он не может выбрать перегрузку, определенную для производного типа. Из спецификации (версия 4):
Например, набор кандидатов для вызова метода не включает переопределенные методы (§7.4), а методы в базовом классе не являются кандидатами, если применим какой-либо метод в производном классе (§7.6.5.1).
Чтобы конкретно задать свои вопросы:
Почему поведение отличается от исходного виртуального метода и переопределенного метода?
Поскольку переопределенный метод определен в производном классе, а применимая перегрузка существует в этом классе, виртуальный метод не рассматривается. Метод переопределения не учитывается, поскольку переопределения никогда не рассматриваются.
Почему один вызов сам, а другой вызывает другой метод?
Поведение в производном классе объясняется выше. В базовом классе лучшим кандидатом на разрешение перегрузки является сам виртуальный метод, поскольку он более конкретный (B получен из A).
Есть ли порядок предпочтений, указанный где-то?
Да, в С# Language Specification (ссылка на страницу MSDN для версии спецификации Visual Studio 2012).
Обратите внимание, что если мы изменим частный модификатор на общедоступный, в обоих случаях мы вызываем не виртуальный метод (даже если мы создаем P2 следующим образом: P1 p2 = new P2(); вместо P2 p2 = новый P2();)
Доступность не является существенной проблемой в этом случае. Тип переменной p2
также не имеет значения, так как разрешение перегрузки, о котором вы спрашиваете, касается сайта вызова в переопределении p2
виртуального метода. Виртуальная отправка гарантирует, что вызов в Main()
вызывает переопределение, независимо от статического типа переменной. На сайте вызова в p2
override void Foo(B b)
приемник неявно this
, который всегда имеет статический тип p2
.
Похоже, что не виртуальная версия предпочтительна, за исключением случаев, когда она находится внутри определения виртуального метода. Это правда?
Не совсем; как объяснялось выше, предпочтение отдается не для не виртуальных методов, а для методов, определенных в типе получателя (т.е. статический тип ссылки на объект, на который вызывается метод).
Ответ 2
Это часто неправильно понимаемая функция С#: методы в базовом классе не являются кандидатами, если применим какой-либо метод в производном классе