Страницы

Поиск по вопросам

пятница, 14 февраля 2020 г.

Порядок вызова методов со схожей сигнатурой

#c_sharp #language_lawyer


Почему при выполнении этой программы this(x,y) будет ссылаться на A(float x, byte
y)? Как в общем случае производится выполнение программы, при схожих методах и с (не)явным
приведением? 

Что, если будет еще один public A(byte x, int y)?

class A
{
    public A(float x, short y) { Console.Write(1); }
    public A(float x, byte y) { Console.Write(2); }
    public A(double x, int y) { Console.Write(3); }
    public A(byte x, byte y, byte z):this(x,y) { Console.Write(4); }
}

static void Main()
{
    byte x = 1, y = 1, z = 1;
    A f = new A(x, y, z);
}

    


Ответы

Ответ 1



Смотрите, то что вас интересует — это порядок разрешения перегруженных имён (overload resolution). Окончательную информацию по этому поводу лучше всего смотреть в стандарте языка. В частности, за overload resolution отвечает раздел 7.5.3. Я перескажу релевантные части из стандарта. Для начала, создаётся список подходящих кандидатов. Затем, из этого списка выбирается один наилучшим образом подходящий кандидат. Если среди кандидатов нету такого кандидата, который подходит больше всех других, вызов функции-члена считается неоднозначным, и возникает ошибка привязки имён. Если наилучший подходящий кандидат не может быть использован (например потому, что он нестатический, а вызов происходит из статического контекста), возникает ошибка привязки имён. Подходящим кандидатом по отношению к данном списку аргументов A называется такая функция-член, что каждый аргумент из A соответствует параметру в декларации функции-члена, как описано в разделе Соответствующие параметры, а каждый параметр без соответствия можно поставить в соответствие необязательному параметру. Режим передачи аргумента (out, ref) соответствует режиму соответствующего параметра, а выражение-аргумент неявно приводимо к типу параметра, либо (для out/ref) совпадает с ним. Для аргумента в списке аргументов, соответствующим параметром является параметр с тем же индексом для позиционных, и с тем же именем для именованных аргументов. Итак, для нашего случая вызова this(x, y) все три конструктора с двумя параметрами являются подходящими: byte неявно приводим к всем типам double, float, short, byte. Далее, выбор наилучшего подходящего кандидата. Кандидат с типами параметров {P1, P2, ..., Pn} считается лучшим кандидатом, чем кандидат с типами параметров {Q1, Q2, ..., Qn} для нашего списка аргументов {E1, E2, ..., En}, если для каждого аргумента неявное преобразование Ex к Qx не лучше преобразования Ex к Px, а для хотя бы одного аргумента строго хуже. (Если все преобразования эквивалентны, то в игру вступают дополнительные правила, которые, например, предпочитают необобщённую функцию обобщённой.) Сравниваются преобразования так. Если тип Px совпадает с типом выражения (плюс хитрости для dynamic, плюс специальное рассмотрение для лямбд), а тип Qx не совпадает, то первое преобразование однозначно лучше. В противном случае, первое преобразование лучше, если либо нет неявного преобразования Qx к Px, а неявное преобразование Px к Qx есть, libo Px — знаковый числовой тип (или, возможно, Nullable), a Qx — беззнаковый (или его Nullable-собрат). В частности, sbyte — лучший целевой тип, чем byte, ushort, uint и ulong; short — лучший, чем ushort, uint и ulong; int — лучший, чем uint и ulong; long -- лучший, чем ulong. [Специальное рассмотрение для лямбд и Task пропущено.] Посмотрим, что это даёт для нас. У нас есть типы аргументов [byte, byte], и наборы типов параметров: [float, short], [float, byte], [double, int]. Второй список лучше, чем первый, т. к. преобразования из float в byte одинаковы, а преобразование из byte в byte лучше, чем из short. Второй список также лучше, чем третий, т. к. преобразования из double в byte хуже, чем из float в byte (т. к. float неявно конвертируется в double), и преобразование из int в byte хуже, чем из byte в byte. Таким образом, у нас второй список однозначно лучше всех. Если переставить параметры: public A(float x, short y) { Console.Write(1); } public A(double x, byte y) { Console.Write(2); } — код не скомпилируется. Как мы помним, преобразование из float лучше, чем из double, а из short хуже, чем из byte. Поэтому ни один из списков не лучше другого. Однако, можно заставить этот код скомпилироваться, используя именованные параметры: public A(float a, short b) { Console.Write(1); } public A(double x, byte y) { Console.Write(2); } public A(byte x, byte y, byte z) : this(a: x, b: y) { ... } Здесь в списке подходящих кандидатов оказывается только первый конструктор. Если вам в реальном коде понадобилось выяснять такие вот тонкости — скорее всего, вы злоупотребляете перегрузкой функций. В нормальных случаях правила работают очевидно правильным образом, а вот в сложных случаях может оказаться что-то неочевидным. Если вам такое встретилось, попробуйте дать функциям разные имена, а для конструкторов используйте идиому именованного конструктора (публичная статическая функция, возвращающая сконструированный объект).

Комментариев нет:

Отправить комментарий