Подтвердить что ты не робот

Виртуальный метод, переопределяющий С# - почему это не вызывает бесконечную рекурсию?

Посмотрел какой-то код в нашей кодовой базе, и я не могу понять, как/почему это даже работает (и не вызывает переполнение стека из-за бесконечной рекурсии). Я вставил несколько эквивалентных кодов ниже: У нас есть виртуальный метод 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;
}

}

4b9b3361

Ответ 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.

Похоже, что не виртуальная версия предпочтительна, за исключением случаев, когда она находится внутри определения виртуального метода. Это правда?

Не совсем; как объяснялось выше, предпочтение отдается не для не виртуальных методов, а для методов, определенных в типе получателя (т.е. статический тип ссылки на объект, на который вызывается метод).