Здравствуйте, Димчанский, Вы писали:
Д>А можно ознакомиться с пунктом стандарта?
Ну давайте проанализируем. Надо начинать с 7.5.5.1 Method invocations. Сначала строим множество применимых (applicable) методов. Применимость определяется согласно 7.4.3.1 Applicable function member. Ну, очевидно, оно будет состоять из двух методов Foo, причем второй метод будет применим только в expanded form, то есть как будто он имеет сигнатуру Foo(ulong? x, long y). Далее множество урезается только до методов из наиболее производных классов (для нас это не дает никакого эффекта, так как оба метода определены в одном классе). Теперь наступает самое интересное — нам нужно выделить лучший (best) метод согласно 7.4.3 Overload resolution. Лучший метод — это метод, который лучше, чем все остальные методы в построенном множестве, по отношению к данному списку аргументов. То есть надо сравнить все методы попарно (в данном случае только два метода). Чтобы узнать, какой из двух методов лучше, надо воспользоваться 7.4.3.2 Better function member. Если есть два метода A и B, то возможны три исхода: метод A лучше, метод B лучше, ни один метод не лучше.
Чтобы метод A был лучше метода B надо:
* чтобы хотя бы для одного аргумента неявное преобразование от этого аргумента к типу соответствующего параметра метода A было лучше, чем неявное преобразование от этого же аргумента к типу соответствующего параметра метода B И
* ни для одного аргумента неявное преобразование от этого аргумента к типу соответствующего параметра метода B не было бы лучше, чем неявное преобразование от этого же аргумента к типу соответствующего параметра метода A.
ИЛИ
* типы соответствующих параметров методов A и B были одинаковы И
* tie-breaking rules выбирали бы метод A.
Впрочем, второй блок условий нам не понадобится. Таким образом, нам надо ответить на два вопроса:
1) Какое из неявных преобразований: 1 -> long, 1 -> Nullable<ulong> лучше?
2) Какое из неявных преобразований: 1 -> ulong, 1 -> long лучше?
Так как 1 — это выражение, имеющие тип int, то согласно 7.4.3.3 Better conversion from expression, нам надо ответить на следующие вопросы:
1) Какое из преобразований: int -> long, int -> Nullable<ulong> лучше?
2) Какое из преобразований: int -> ulong, int -> long лучше?
Заметьте исчезновение слова "неявных". Теперь мы смотрим на 7.4.3.4 Better conversion from type. Если у нас есть преобразование от типа C к типу A, и преобразование от типа C к типу B, то первое преобразование считается лучшим, если типы A и B различны и выполняется хотя бы одно из следующих условий:
* Тип C совпадает с типом A.
* Существует неявное преобразование от типа A к типу B, но не существует неявного преобразовани от типа B к типу A.
* A — целочисленный тип со знаком, а B — целочисленный тип без знака, но не существует неявного преобразования от типа B к типу A (Ключевой момент: здесь ничего не говорится о nullable типах, сконструированных из целочисленных типов без знака).
Таким образом мы получаем следующие ответы на наши вопросы:
1) Ни одно из преобразований не лучше.
2) Преобразовани int -> long лучше, чем преобразование int -> ulong (согласно третьему пункту).
Таким образом, получается что метод Foo(ulong? x, params long[] y) лучше.
Парадокс состоит еще в том, что если бы мы попарно сравнивали 3 преобразования:
1) int -> long
2) int -> ulong
3) int -> ulong?
то первое преобразование было бы лучше второго, а второе — лучше третьего. Тем не менее, как мы уже убедилсь, первое преобразование оказывается не лучше чем третье. То есть отношение "лучше чем" оказывается не транзитивным.
Здравствуйте, VladD2, Вы писали:
VD>Вот как бы с Шарпом так же не стало. Он конечно несравнимо более интуитивен, но вот вместо того чтобы выявлять проблемы и устранять их (или хотя бы обращать внимание разработчиков и общественности) ты радостно объясняешь всем, что мол, так оно и надо.
Поздно, Влад, уже что-то менять в этой части C#. Поломается обратная совместимось. Можно только сообщать людям, где есть подводные камни, чтобы они их избегали. Ну и чтобы разработчики новых языков могли учесть это. Собственно, я это и делаю (сообщаю).
Здравствуйте, nikov, Вы писали:
N>Поздно, Влад, уже что-то менять в этой части C#. Поломается обратная совместимось. Можно только сообщать людям, где есть подводные камни, чтобы они их избегали. Ну и чтобы разработчики новых языков могли учесть это. Собственно, я это и делаю (сообщаю).
C# не Святой Грааль. В С++ меняли и все пережили. Схема может быть проста как три копейки — в следующей верссии вводятся предупреждения, а в следующей за ней меняется код.
На самом деле проблем это особых не создаст, так как, как правильно заметил Хвост, за такой код надо чем-то тяжелым и острым... Другими словами подобный код большая редкость и напорются на несоответствие единицы. Зато миллионам будет жить проще.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, xvost, Вы писали:
X>Честно говоря, увидев такие оверлоады в реальном production коде, рука потянется за чем-нибудь тяжелым и острым....
Тем не менее, РеШарпер всегда справлялся с этим примером нормально.
Здравствуйте, nikov, Вы писали:
N>А какой путь был бы легким в данном случае? N>Очень трудно придумать правила, которые бы не имели парадоксальных последствий в граничных случаях.
Здравствуйте, nikov, Вы писали:
N>Тогда получим путь F#. В нем неявные преобразования сведены до минимума.
Да, мне это в F# тоже в частности понравилось. Так же как и неизменность объектов (ну если не говорить про умышленные ref, mutable).
Но F# еще не готовый блин, хотя и уже вкусный.
Здравствуйте, nikov, Вы писали:
N>А какой путь был бы легким в данном случае? N>Очень трудно придумать правила, которые бы не имели парадоксальных последствий в граничных случаях.
Элементарное. При прочих равных выбирать метод без params. Это банальное правило позволило бы поднять производительность за счет выбора более специализированных функций.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
N>>Очень трудно придумать правила, которые бы не имели парадоксальных последствий в граничных случаях.
VD>Элементарное. При прочих равных выбирать метод без params. Это банальное правило позволило бы поднять производительность за счет выбора более специализированных функций.
А такое правило в C# есть. Это второе из tie-breaking rules, которые применяются при совпадении списка параметров (7.4.3.2 Better function member).
В исходном примере типы параметров различны, и действует более сильное правило. Правда, несколько парадоксальным образом.
Здравствуйте, nikov, Вы писали:
N>А теперь покажи мне спецификацию, которая это объясняет.
Ну, то есть тебе шашечки, а не ехать?
С С++ тоже было все очень похоже. Спрашиваешь у людей "почему из виртуального деструктора не вызываются методы переопределенные в потомках?", а тебе отвечают "по спецификации!". "Но ведь это же неинтуитивно!" — говорю я — "А нам по фигу" — говорят они.
Вот как бы с Шарпом так же не стало. Он конечно несравнимо более интуитивен, но вот вместо того чтобы выявлять проблемы и устранять их (или хотя бы обращать внимание разработчиков и общественности) ты радостно объясняешь всем, что мол, так оно и надо.
А я вот не считаю, что так оно надо. Надо так как удобнее (если конечно это не вызывает других проблем).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, nikov, Вы писали:
N>А такое правило в C# есть. Это второе из tie-breaking rules, которые применяются при совпадении списка параметров (7.4.3.2 Better function member). N>В исходном примере типы параметров различны, и действует более сильное правило. Правда, несколько парадоксальным образом.
Значит правила надо местами переставить. Стандарт вообще штука недетерминированная. Его ведь по разному можно интерпретировать. Я бы вот счел приведение константы к long и ulong неразличимым и применил бы правило для params.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
А вообще, характерно, то что большинство найденных тобой странностей C# не обнаруживается в Немерле. Это говорит о том, что алгоритмы в нем по разумнее. В прочем немерловый компилятор и думает существенно дольше.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, nikov, Вы писали:
N>Поздно, Влад, уже что-то менять в этой части C#. Поломается обратная совместимось. Можно только сообщать людям, где есть подводные камни, чтобы они их избегали. Ну и чтобы разработчики новых языков могли учесть это. Собственно, я это и делаю (сообщаю).
"Мотороллер не мой, я просто разместил объяву."
P.S.:
Прошу простить меня за шутку, конец дня, мозги ни на что больше не способны.
Здравствуйте, VladD2, Вы писали:
VD>А вообще, характерно, то что большинство найденных тобой странностей C# не обнаруживается в Немерле. Это говорит о том, что алгоритмы в нем по разумнее.
Оно может быть и так. Но я уверен, что если поставить цель эти алгоритмы документировать и потом протестировать реализацию на соответствие этой документации (на уровне объема тестирования C#), то это будет стоить неимоверную кучу бабла. И еще ошибки найдутся.
#pragma indent
using System;
using System.Console;
module A
Foo[T](_ : T) : void where T : struct
WriteLine(1);
Foo(params _ : array[object]) : void
WriteLine(2);
Main() : void
Foo(1); // Error: the type 'System.Object' must be a value type in order to use it as type parameter 'T' in method A.Foo(_N_u1710 : T.922) : void
_ = ReadKey()
N>Почему взять S с потолка — это лучше, чем развернуть params?
На мой взгляд, этот пример работает наилучим образом. Если есть альтернативы без params, то нужно предпочитать их.
Единственный варос который у меня возникает: не надо ли здесь сообщять о неопределенности?
Как, кстати, в подобном случае ведет себя C#?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
С точки зрения алгоритма вывода типов — это обычное поведение. object в Немереле считается самым базовым типом для всех типов. Параметр типов без ограничений также выводится как object (как и любая другая переменная типа, есть такое понятие в компиляторе).
Другой вопрос насколько удобен именно выбор object-а? Ведь в данном случае можно просто сообщить о неоднозначности.
Однако пример может быть и несколько другой:
В нем выбор специализированного параметра будет 100%-но предпочтительнее. Возможно выбор объекта тоже основывается на предпочтении "более специализированного" типа, что в данном случае подводит.
N>И главное непонятно: то ли это баг в реализации, то ли косяк проектировщика. Спросить не с кого.
Можно спросить у Камила и Михаля. Они ведь не умерли. Но важнее не это, а то "какое поведение будет лучше?". Ведь цель не найти ответственного за проблемы, а минимизировать их количество.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
N>#pragma indent
N>using System;
N>using System.Console;
N>module A
N> Foo[T](_ : T) : void where T : struct
N> WriteLine(1);
N> Foo(params _ : array[object]) : void
N> WriteLine(2);
N> Main() : void
N> Foo(1); // Error: the type 'System.Object' must be a value type in order to use it as type parameter 'T' in method A.Foo(_N_u1710 : T.922) : void
N> _ = ReadKey()
N>
Здравствуйте, VladD2, Вы писали:
VD>Параметр типов без ограничений также выводится как object (как и любая другая переменная типа, есть такое понятие в компиляторе).
С чего бы вдруг? Это же тип-параметр метода, он должен выводиться из аргументов при вызове. Иначе, зачем вообще нужны типы-параметры без ограничений?
Здравствуйте, VladD2, Вы писали:
N>>Почему взять S с потолка — это лучше, чем развернуть params?
VD>На мой взгляд, этот пример работает наилучим образом. Если есть альтернативы без params, то нужно предпочитать их.
Почему? params не так уж и плох — он позволяет захватить строго типизированные аргументы, и обработать их специфичным для данного типа способом. Поэтому он должен рассматриваться как лучший вариант по сравнению с приведением к базовому типу. И хуже, только если есть метод, содержащий только фиксированные параметры с теми же типами (потому что это позволяет избежать создания массива), или хотя бы большее количество фиксированных параметров (потому что мы можем предположить, что параметры, различимые по имени, метод обрабатывает более специфичным образом).
VD>Единственный варос который у меня возникает: не надо ли здесь сообщять о неопределенности? VD>Как, кстати, в подобном случае ведет себя C#?
Ну, в C# тип-параметр не выводится, если он не фигурирует в типах параметров метода. Поэтому первый вариант просто не будет участвовать в overload resolution. Если же убрать тип-параметр, то будет неоднозначность:
using System;
static class A
{
static void Foo(IConvertible x){}
static void Foo(params IComparable[] x){}
static void Main()
{
Foo(1); // error CS0121: The call is ambiguous between the following methods or properties: 'A.Foo(System.IConvertible)' and 'A.Foo(params System.IComparable[])'
}
}
потому что params в C# может быть средством разрешения неоднозначности только если раскрытые сигнатуры совпадают.
Здравствуйте, VladD2, Вы писали:
N>>Почему взять S с потолка — это лучше, чем развернуть params? VD>На мой взгляд, этот пример работает наилучим образом. Если есть альтернативы без params, то нужно предпочитать их.
А на мой взгляд тут явная бага ибо тип S неоткуда вывести.
... << RSDN@Home 1.2.0 alpha rev. 745>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, HotDog, Вы писали:
HD>... если бы твои этюды использовались в качестве теста при приеме на работу, то количество подходящих кандидатур стремилось бы к нулю
Их надо использовать если кандидат Вам крайне не симпотичен.
Здравствуйте, HotDog, Вы писали:
HD>... если бы твои этюды использовались в качестве теста при приеме на работу, то количество подходящих кандидатур стремилось бы к нулю
Что забавно, во многих случаях происходит именно так — интервьюирующий спрашивает подобного рода спецзнания, хорошо известные ему самому. Каюсь, сам грешен был, но всё же...
Здравствуйте, nikov, Вы писали:
N>Здравствуйте, VladD2, Вы писали:
N>>>Очень трудно придумать правила, которые бы не имели парадоксальных последствий в граничных случаях.
VD>>Элементарное. При прочих равных выбирать метод без params. Это банальное правило позволило бы поднять производительность за счет выбора более специализированных функций.
N>А такое правило в C# есть. Это второе из tie-breaking rules, которые применяются при совпадении списка параметров (7.4.3.2 Better function member). N>В исходном примере типы параметров различны, и действует более сильное правило. Правда, несколько парадоксальным образом.
Жаль, что не учитывается такой фактор, как "длина" цепочки преобразований, т.к. для приведения 1 к ulong? требуется на самом деле два преобразования. Без учёта этого фактора, как ты правильно заметил, потерялась транзитивность при попарном сравнении на лучшее преобразование. А раз вместо строгой иерархии преобразований имеем неупорядоченные локальные отношения "лучше/хуже", то привет грабли.
Здравствуйте, WolfHound, Вы писали:
N>>>Почему взять S с потолка — это лучше, чем развернуть params? VD>>На мой взгляд, этот пример работает наилучим образом. Если есть альтернативы без params, то нужно предпочитать их. WH>А на мой взгляд тут явная бага ибо тип S неоткуда вывести.
А он похоже и не учитывается. Вот код метода отвечающего за выбор лучшей перегрузки (комментарии мои):
GetBestOverloads (parms : list [OverloadPossibility]) : list [OverloadPossibility]
{
match (parms) {
| [] | [_] => parms // если есть только одна перегрузка, то возвращаем ее и ничего не делаем.
| _ =>
def res = RemoveExtensionMethods (parms); // Первым делом отбрасываем методы-расширения.
def res = GetMinimal (res, IsBetterOverload); // Далее выбираем "лучшую перегрузку" исходя из выведенных типов методов.
def res = // отбрасываем params-методы (методы с переменным числом параметров) если в списке есть не params-методы.if (List.Exists (res, AintVarArgs))
List.RevFilter (res, AintVarArgs)
else res;
def res = // повбывал бы за такие имена. В общем, что-то там отбрасывают...if (List.Exists (res, DidntMamboJumbo))
List.RevFilter (res, DidntMamboJumbo)
else res;
def res = // отбрасываем джереник-методы если есть не дженерик-методы.if (List.Exists (res, AintGeneric))
List.RevFilter (res, AintGeneric)
else res;
// Message.Debug ($"gbo: $parms ---> $res");
res
}
}
Таким образом "Foo[S](_x : IConvertible) : void" выбирается исходя из правила отбрасывать params-методы если есть другие:
if (List.Exists (res, AintVarArgs))
List.RevFilter (res, AintVarArgs)
else res;
Интересно, что по логике метода GetBestOverloads отбросит "Foo[S](_x : IConvertible) : void" и предпочтет скажем "Foo(_x : IComparable) : void" просто на основании, что первый вариант — это дженерик-метод.
В данном примере это конечно бессмысленно, но на практике от этого есть толк, так как просто так параметры типов никто в методы не пихает, и с большой вероятностью обобщенный метод будет более медленным.
ЗЫ
Кстати, правило предпочтения "Foo(_x : object) : void" вместо "Foo[T](_x : T) : void" основывается на том, что IsBetterOverload выводит T как object (ведь никаких доп-констрейнов нет), что приводит к тому, что после вызова "GetMinimal (res, IsBetterOverload)" в списке оказывается два метода, но обобщенный вариант отбрасывается в виду правила предпочтения не обобщенного варианта.
На мой взгляд нужно вставлять проверку которая пытается обработать ситуация "T vs. object" и предпочитать обобшенный вариант. Однако это легче сказать нежели сделать, ведь тип может быть вычислен до сравнения и сравниваться будут два object-а, что опять таки приведет к тому что в списке окажутся оба кандидата.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>На мой взгляд нужно вставлять проверку которая пытается обработать ситуация "T vs. object" и предпочитать обобшенный вариант. Однако это легче сказать нежели сделать, ведь тип может быть вычислен до сравнения и сравниваться будут два object-а, что опять таки приведет к тому что в списке окажутся оба кандидата.
Интересно, что в C# правила overload resolution могут учитывать и типы, полученные в результате подстановки типов-параметров, а при их совпадении — и исходные типы, указанные в декларации метода.