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

Почему компилятор С# использует недопустимую перегрузку метода?

Меня смутил следующий код

class A
{
    public void Abc(int q)
    {
        Console.Write("A");
    }
}

class B : A
{
    public void Abc(double p)
    {
        Console.Write("B");
    }
}

    ...
    var b = new B();
    b.Abc((int)1);

Результат выполнения кода - "B", записанный на консоль.

Фактически класс B содержит две перегрузки метода Abc, первый для параметра int, второй для double. Почему компилятор использует двойную версию для целочисленного аргумента?

Будьте осторожны, метод abc (double) не затеняет или не отменяет метод abc (int)

4b9b3361

Ответ 1

Поскольку компилятор может неявно преобразовать int в double, он выбирает метод B.Abc. Это объясняется в этом посте Jon Skeet (поиск "неявный" ):

Цель вызова метода - выражение типа Child, поэтому компилятор сначала смотрит на класс Child. Там только один метод там, и он применим (там неявное преобразование из int в double), так что тот, который выбирается. Компилятор не рассмотрите метод "Родитель".

Причиной этого является снижение риска хрупкого базового класса проблема...

Больше от Эрика Липперта

Как гласит стандарт, "методы в базовом классе не являются кандидатами, если применим какой-либо метод в производном классе".

Иными словами, алгоритм разрешения перегрузки начинается с поиска класс применимого метода. Если он найдет один, то все остальные применимые методы в более глубоких базовых классах удаляются из кандидата для разрешения перегрузки. Поскольку Delta.Frob(float) является Charlie.Frob(int) никогда не считается кандидатом. Только если не найдены подходящие кандидаты в самом производном типе мы начинаем смотреть на его базовый класс.

Все становится интереснее, если мы расширим пример в вашем вопросе с помощью этого дополнительного класса, который происходит от A:

class C : A {
    public void Abc(byte b) {
        Console.Write("C");
    }
}

Если мы выполним следующий код

int i = 1;
b.Abc((int)1);
b.Abc(i);
c.Abc((int)1);
c.Abc(i);

результаты BBCA. Это связано с тем, что в случае класса B компилятор знает, что он может неявно использовать любой int для удвоения. В случае класса C компилятор знает, что он может передавать литерал int 1 в байт (поскольку значение 1 соответствует байту), поэтому используется метод C Abc. Компилятор, однако, не может неявно использовать любой старый int для байта, поэтому c.Abc(i) не может использовать метод C Abc. Он должен использовать родительский класс в этом случае.

На этой странице по неявным числовым преобразованиям показана компактная таблица, у которой числовые типы имеют неявные преобразования для других числовых типов.

Ответ 2

Вы получаете ту же функциональность, даже если вы определяете B как:

class B : A
{
    public void Abc(object p)
    {
        Console.Write("B");
    }
}

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

Подробное объяснение можно найти в Спецификации разрешения перегрузки.

Ответ 3

Различные языки (например, С++, Java или С#) имеют совершенно разные правила разрешения перегрузки. В С# перегрузка была правильно выбрана в соответствии с спецификацией языка. Если вы хотите, чтобы другая перегрузка была выбрана, у вас есть выбор. Помните об этом:

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

В чем преимущество языкового дизайна, требующего этого упражнения?

Представьте, что вы используете стороннюю библиотеку (скажем, платформу .NET) и выходите из одного из ее классов. В какой-то момент вы вводите частный метод под названием Abc (новое, уникальное имя, а не перегрузка чего-либо). Через два года вы обновите версию сторонней библиотеки, не заметив, что они также добавили доступный вам метод и вызвали, к сожалению, Abc, за исключением того, что он имеет другой тип параметра где-то (так что обновление не предупреждает вас ошибка времени компиляции), и она ведет себя по-разному или, может быть, даже имеет совсем другую цель. Вы действительно хотите, чтобы одна половина ваших частных звонков на Abc была тихо перенаправлена ​​на стороннюю сторону Abc? На Java это может случиться. В С# или С++ этого не произойдет.

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

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

Недостатком способа С# является то, что он разрезает дыру в философии ООП переопределяющих методов, когда-либо изменяющих только реализацию, но не API класса.