2016-02-11 4 views
34

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

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 (дважды) не тень или переопределить метод ABC (Int)

+0

Я подозреваю, что вы должны сказать 'A b = new B()' – pm100

+0

yup. вы должны сказать, что это тип 'A', потому что реализация« B »реализует тени« A » –

+0

@DarrenKopp - Нет, это не тень. Это перегрузка, и это прекрасно. – Enigmativity

ответ

52

Поскольку компилятор может неявно преобразовать Int удваивать, он выбирает метод B.Abc. Это объясняется в this post by Jon Skeet (поиск «неявное»):

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

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

More from Eric Lippert

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

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

Вещи становятся немного более интересными, если мы продолжим пример в вопросе с этим дополнительным классом, который нисходит от 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 помещается в байт), так что используется метод Abc C. Компилятор, однако, не может неявно использовать любой старый int для байта, поэтому c.Abc(i) не может использовать метод Ab c. В этом случае он должен использовать родительский класс.

This page on Implicit Numeric Conversions показывает компактную таблицу, в которой числовые типы имеют неявные преобразования в другие числовые типы.

+12

Jon Skeet отвечает на вопросы, прежде чем их даже попросят. – Danny

+0

Каждый раз, когда я слышал имя Джона, я думал: «Насколько сильны его знания?» –

12

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

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

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

Для получения более подробной информации вы можете ознакомиться с Overload resolution spec.

+1

Спасибо за ссылку на C# speck, не легко запомнить все подробности! –

7

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

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

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

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

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

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

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

Смежные вопросы