Почему выбирается общий метод, когда существует не универсальный?

Программа ниже производит этот вывод:

Foo<T> called

Process is terminated due to StackOverflowException.

Итак, Foo(baz) вызывает общий Foo<T>, но Bar(baz) рекурсивно и не вызывает Bar<T>.

Я на С# 5.0 и Microsoft .NET. Компилятор, кажется, выбирает универсальный метод вместо рекурсии, когда неуниверсальным методом является override.

Где я могу найти объяснение этому правилу? (Я догадался, что компилятор выберет рекурсию в обоих случаях.)

Вот программа целиком:

using System;

namespace ConsoleApplication1 {
    class Baz { }

    abstract class Parent {
        public abstract void Foo(Baz baz);
    }

    class Child : Parent {
        void Bar<T>(T baz) {
            Console.WriteLine("Bar<T> called");
        }

        public void Bar(Baz baz) {
            Bar(baz);
        }

        void Foo<T>(T baz) {
            Console.WriteLine("Foo<T> called");
        }

        public override void Foo(Baz baz) {
            Foo(baz);
        }
    }

    class Program {
        static void Main(string[] args) {
            var child = new Child();
            child.Foo(null);
            child.Bar(null);
            Console.ReadLine();
        }
    }
}

person ken    schedule 30.06.2015    source источник
comment
Мне нравится рассматривать все подобные ситуации как способ создателей компилятора наказать вас за использование наследования...   -  person David Arno    schedule 01.07.2015
comment
Похоже, что приоритет отдается непереопределенному методу в дочернем классе по сравнению с переопределенным методом в дочернем классе; он может рассматривать переопределенный метод как часть родителя.   -  person adamdc78    schedule 01.07.2015


Ответы (3)


Согласно документам MSDN, приоритет отдается сигнатурам методов, которые не переопределяются. Поскольку неуниверсальная версия Foo переопределяется, она сразу уходит в конец приоритета выбора метода. В общих чертах, следующим шагом является выбор наиболее конкретного возможного метода и его выполнение. В случае методов Bar метод Bar(Baz baz) всегда будет наиболее специфичным в вашем случае.

Разрешение перегрузки — это механизм времени компиляции для выбора наилучшего члена функции для вызова с учетом списка аргументов и набора членов функции-кандидата. Разрешение перегрузки выбирает член функции для вызова в следующих различных контекстах в C#:

  • Вызов метода, названного в выражении вызова (раздел 7.5.5). Вызов конструктора экземпляра, названного в выражении создания объекта (раздел 7.5.10.1).
  • Вызов метода доступа индексатора через доступ к элементу (раздел 7.5.6). Вызов предопределенного или определяемого пользователем оператора, на который ссылается выражение (раздел 7.2.3 и раздел 7.2.4).

Каждый из этих контекстов определяет набор членов функции-кандидата и список аргументов по-своему, как подробно описано в разделах, перечисленных выше. Например, набор кандидатов для вызова метода не включает методы, помеченные как переопределение (раздел 7.3), и методы в базовом классе не являются кандидатами, если применим какой-либо метод в производном классе (раздел 7.5.5.1).

Разрешение перегрузки MSDN

Я выделил жирным шрифтом текст, который, как мне кажется, относится к вашему вопросу.

Вот еще один вопрос о переполнении стека это может помочь. Это говорит о разрешении метода в целом. Не затрагивает переопределенные методы, но помогает заполнить некоторые процессы, которые я не затронул.

person Nathan    schedule 30.06.2015
comment
хорошо, не глядя на msdn, я понял причину! - person Fredou; 01.07.2015

Разрешение перегрузки просматривает цепочки наследования, ища методы, определенные в каждой точке.

Child определяет void Foo<T>(T baz), но не определяет void Foo(Baz baz), поэтому выбирается void Foo<T>(T baz).

Обычно это имеет смысл; в реальном коде, если Foo<T>(T baz) не выполнял работу, очень похожую на то, что делал Foo(Baz baz) в базе при передаче Baz, тогда ваш дизайн сбивает с толку, и вам следует выбрать новое имя.

Вы можете немного запутаться, используя public new void Foo(Baz baz) или public new virtual void Foo(Baz baz), чтобы принудительно определить переопределение и в Child (хотя здесь должен быть промежуточный шаг в иерархии, чтобы абстрактный метод имел реализацию), который может вызвать base.Foo(baz) ( для вызова базовой реализации) и/или Foo<Baz>(baz) (для вызова универсальной версии)`, но таких трюков лучше избегать.

person Jon Hanna    schedule 30.06.2015

может быть, он ведет себя так, когда вы реализуете что-то вроде этого

    void myMethod(long? l) { }
    void myMethod(int? i) { }

вызов с помощью null будет использовать int?

добавление этого

    void myMethod(short? i) { }

и все еще вызывая его с помощью null, код переключится на short?

может быть, есть внутренний порядок/приоритет?

теперь с вашим кодом я даю это только для того, чтобы показать разницу между решением компилятора и программистом (явный вызов)

это общий Baz

.method private hidebysig 
    instance void Bar<T> (
        !!T baz
    ) cil managed 
{
    // Method begins at RVA 0x2060
    // Code size 13 (0xd)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldstr "Bar<T> called"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret
} // end of method Child::Bar

ваша реализация

    public void Bar(Baz baz) {
        Bar(baz);
    }

дай это

.method public hidebysig 
    instance void Bar (
        class ConsoleApplication1.Baz baz
    ) cil managed 
{
    // Method begins at RVA 0x206e
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldarg.1
    IL_0003: call instance void ConsoleApplication1.Child::Bar(class ConsoleApplication1.Baz)
    IL_0008: nop
    IL_0009: ret
} // end of method Child::Bar

Вот этот

    public void Bar(Baz baz)
    {
        Bar<Baz>(baz);
    }

дай это

.method public hidebysig 
    instance void Bar (
        class ConsoleApplication1.Baz baz
    ) cil managed 
{
    // Method begins at RVA 0x206e
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldarg.1
    IL_0003: call instance void ConsoleApplication1.Child::Bar<class ConsoleApplication1.Baz>(!!0)
    IL_0008: nop
    IL_0009: ret
} // end of method Child::Bar
person Fredou    schedule 30.06.2015