Метод С# переопределяет неоднозначность разрешения

Рассмотрим следующий фрагмент кода:

using System;

class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}

class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }

    public void Foo(object o)
    {
        Console.WriteLine("Derived.Foo(object)");
    }
}

public class Program
{
    public static void Main()
    {
        Derived d = new Derived();
        int i = 10;
        d.Foo(i);
    }
}

И удивительный результат:

Derived.Foo(object)

Я бы ожидал, что он выберет переопределенный метод Foo(int x), поскольку он более конкретный. Однако компилятор С# выбирает не унаследованную версию Foo(object o). Это также приводит к боксерской операции.

В чем причина такого поведения?

Ответы

Ответ 1

Это правило, и оно тебе может не понравиться...

Цитата от Эрика Липперта

если какой-либо метод в более производном классе является подходящим кандидатом, он автоматически лучше, чем любой метод в менее производном классе, даже если метод с меньшим производным имеет лучшее совпадение сигнатуры.

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


Примечание: это довольно сложная/углубленная часть спецификаций С#, и она прыгает повсюду. Тем не менее, основные части проблемы, с которой вы столкнулись, написаны следующим образом

Обновить

И именно поэтому мне нравится stackoverflow, это такое прекрасное место для изучения.

Я цитировал раздел об обработке во время выполнения вызова метода. Где, поскольку речь идет о разрешении перегрузки времени компиляции, и должно быть.

7.6.5.1 Вызовы методов

...

Набор методов-кандидатов сокращен, чтобы содержать только методы из наиболее производных типов: для каждого метода CF в наборе, где C - это тип, в котором объявлен метод F, все методы, объявленные в базовом типе C, удаляются из набор. Кроме того, если C является типом класса, отличным от объекта, все методы, объявленные в типе интерфейса, удаляются из набора. (Это последнее правило влияет только тогда, когда группа методов была результатом поиска члена по параметру типа, имеющему эффективный базовый класс, отличный от объекта, и непустой эффективный набор интерфейсов.)

Пожалуйста, ознакомьтесь с ответом Эрика на fooobar.com/questions/16554502/... чтобы получить полную информацию о том, что здесь происходит, и соответствующую часть спецификаций.

оригинал

Спецификация языка С# версия 5.0

7.5.5 Вызов члена функции

...

Обработка во время выполнения вызова члена функции состоит из следующих шагов, где M - это член функции, а если M - это элемент экземпляра, E - выражение экземпляра:

...

Если M является членом функции экземпляра, объявленным в ссылочном типе:

  • E оценивается. Если эта оценка вызывает исключение, дальнейшие шаги не выполняются.
  • Список аргументов оценивается, как описано в §7.5.1.
  • Если тип E является типом значения, для преобразования E в объект типа выполняется преобразование в бокс (§4.3.1), и E рассматривается как объект типа на следующих этапах. В этом случае M может быть только членом System.Object.
  • Значение E проверяется, чтобы быть действительным. Если значение E равно нулю, выдается исключение System.NullReferenceException и дальнейшие шаги не выполняются.
  • Реализация функции-члена для вызова определяется:
    • Если тип времени привязки E является интерфейсом, то вызываемый элемент функции является реализацией M, предоставленной типом времени выполнения экземпляра, на который ссылается E. Этот член функции определяется путем применения правил отображения интерфейса (§13.4.4) для определения реализации M, предоставляемой типом времени выполнения экземпляра, на который ссылается E.
    • В противном случае, если M является членом виртуальной функции, вызываемый элемент функции является реализацией M, предоставленной типом времени выполнения экземпляра, на который ссылается E. Этот член функции определяется путем применения правил для определения наиболее производной реализации ( §10.6.3) M относительно типа времени выполнения экземпляра, на который ссылается E.
    • В противном случае M - это не виртуальный член функции, а вызываемый член функции - это сам M.

После прочтения спецификаций, что интересно, если вы используете интерфейс, описывающий метод, компилятор выберет сигнатуру перегрузки, работая в свою очередь, как и ожидалось

  public interface ITest
  {
     void Foo(int x);
  }

Который может быть показан здесь

Что касается интерфейса, то он имеет смысл при рассмотрении поведения перегрузки для защиты от базового класса хрупкого


Дополнительные ресурсы

Эрик Липперт, чем ближе, тем лучше

Аспект разрешения перегрузки в С#, о котором я хочу поговорить сегодня, действительно является фундаментальным правилом, согласно которому одна потенциальная перегрузка оценивается как лучшая, чем другая для данного сайта вызовов: ближе всегда лучше, чем дальше. Есть несколько способов охарактеризовать "близость" в С#. Давайте начнем с ближайшего и двинемся дальше:

  • Метод, впервые объявленный в производном классе, ближе, чем метод, впервые объявленный в базовом классе.
  • Метод во вложенном классе ближе, чем метод в содержащем классе.
  • Любой метод принимающего типа ближе, чем любой метод расширения.
  • Метод расширения, найденный в классе во вложенном пространстве имен, ближе, чем метод расширения, найденный в классе во внешнем пространстве имен.
  • Метод расширения, найденный в классе в текущем пространстве имен, ближе, чем метод расширения, найденный в классе в пространстве имен, упомянутом в директиве using.
  • Метод расширения, найденный в классе в пространстве имен, упомянутом в директиве using, где директива находится во вложенном пространстве имен, ближе, чем метод расширения, найденный в классе в пространстве имен, упомянутом в директиве using, где директива находится во внешнем пространстве имен.

Ответ 2

Принятый ответ правильный (за исключением того, что он цитирует неправильный раздел спецификации), но он объясняет вещи с точки зрения спецификации, а не дает обоснование того, почему спецификация хороша.

Предположим, что у нас есть базовый класс B и производный класс D. B имеет метод M, принимающий Жираф. Теперь, помните, по предположению, автор D знает все о B общественных и защищенных членах. Иными словами: автор D должен знать больше, чем автор B, потому что D был написан после B, а D был написан, чтобы продлить B до сценария, который уже не обрабатывается B. Поэтому мы должны полагать, что автор D лучше выполнять всю функциональность D, чем автор B.

Если автор D совершает перегрузку M, которая берет Animal, они говорят, что я знаю лучше, чем автор B, как бороться с животными, и это включает жирафов. Мы должны ожидать разрешения перегрузки при вызове DM (Жираф), чтобы вызвать DM (Animal), а не BM (Жираф).

Иначе говоря: нам даны два возможных оправдания:

  • Призыв к DM (Жираф) должен пойти в BM (Жираф), потому что Жираф более специфичен, чем Животное
  • Призыв к DM (Жираф) должен перейти в DM (Animal), потому что D более специфичен, чем B

Оба оправдания касаются специфики, и какое оправдание лучше? Мы не называем какой-либо метод для животных ! Мы вызываем метод на D, так что определенность должна быть той, которая побеждает. Специфика приемника гораздо важнее, чем специфика любого из его параметров. Типы параметров для разрыва связи. Важно то, что мы выбираем самый конкретный приемник, потому что этот метод был написан позже кем-то, у которого больше знаний о сценарии, который D предназначен для обработки.

Теперь вы можете сказать, что, если автор D также переопределил BM (Жираф)? Есть два аргумента, почему вызов DM (Жираф) должен вызвать DM (Animal) в этом случае.

Во-первых, автор D должен знать, что DM (Animal) можно вызвать с Жирафом, и он должен быть написан правильно. Поэтому с точки зрения пользователя не имеет значения, разрешен ли вызов DM (Animal) или BM (Giraffe), потому что D был правильно написан, чтобы делать правильные вещи.

Во-вторых, был ли автор D переопределен методом B или нет, является деталью реализации D, а не частью области публичной поверхности. Иными словами: было бы очень странно, если бы изменил, был ли метод переопределен, изменяется какой метод выбран. Представьте, если вы вызываете метод в каком-либо базовом классе в одной версии, а затем в следующей версии автор базового класса делает незначительное изменение в том, переопределен или нет метод; вы не ожидаете изменения разрешения перегрузки в производном классе. С# был разработан для предотвращения такого рода сбоев.