Почему C # не выполняет простой вывод типов для универсальных шаблонов?

Просто любопытно: конечно, все мы знаем, что общий случай вывода типов для дженериков неразрешим. Таким образом, C # вообще не будет использовать какой-либо подтип: если Foo ‹T› является общим, Foo ‹int› не является подтипом Foo ‹T› или Foo ‹Object› или что-нибудь еще, что вы можете придумать. И, конечно же, мы все пытаемся обойти это с помощью уродливых интерфейсов или определений абстрактных классов.

Но ... если вы не можете решить общую проблему, почему бы просто не ограничить решение простыми случаями. Например, в моем списке выше ОЧЕРЕДНО, что Foo ‹int› является подтипом Foo ‹T›, и проверить это было бы тривиально. То же самое и с проверкой на Foo ‹Object›.

Так есть ли какой-нибудь другой глубокий ужас, который выползет из бездны, если они просто скажут: черт возьми, мы сделаем все, что в наших силах? Или это просто своего рода религиозная чистота со стороны языковых ребят из Microsoft?


Обновлять:

Это очень старая ветка. В наши дни в C # есть var, который решает половину того, на что я жаловался, а затем, используя стиль анонимных делегатов Linq, имеет отличную нотацию, позволяющую не вводить одно и то же дважды. Таким образом, каждый аспект того, против чего я возражал, был решен более свежими изменениями в C # (или, возможно, мне просто потребовалось время, чтобы узнать о вещах, которые только что были представлены, когда я опубликовал ветку ...) Я использую эти новые теперь в системе Isis2 есть функции для надежных облачных вычислений (isis2.codeplex.com), и я думаю, что в результате библиотека имеет очень чистый внешний вид. Проверьте это и дайте мне знать, что вы думаете). - Кен Бирман (июль 2014 г.)


person Ken Birman    schedule 21.12.2010    source источник
comment
Это было больно. Используйте обратную косую черту `, а не обратную косую черту.   -  person Hans Passant    schedule 21.12.2010
comment
Этот вопрос крайне сбивает с толку. Можете прояснить вопрос? Начните с тщательного определения того, что вы имеете в виду под подтипом, потому что мне совсем не ясно, что вы под этим подразумеваете. Помог бы простой, реалистичный пример конверсии, которая, по вашему мнению, должна быть законной и тривиальной для проверки.   -  person Eric Lippert    schedule 22.12.2010
comment
Например: Foo ‹T› имеет метод Bar: public T Bar(T value){/*...*/}. Для Foo<object> foo = new Foo<object>(); это будет законным: object result = foo.Bar(0);. Так будет: object result = foo.Bar("XYZ"); Однако для Foo<object> foo = new Foo<int>(); только первый будет законным.   -  person phoog    schedule 22.12.2010
comment
Прости, Ганс; потребовалась минута, чтобы вычислить escape-последовательность.   -  person Ken Birman    schedule 22.12.2010
comment
Эрик, я пытаюсь создать Список объектов, каждый из которых специализируется на определенном мною универсальном, но с разными типами. Затем я хочу иметь возможность вызывать методы в этих объектах. Например: мой общий шаблон требует, чтобы пользователь предоставил метод Aggregate (KeyType key, ValueType value), и я планирую вызывать его с объектами соответствующего типа (полученными от пользователя). Мой код представляет собой довольно сложный распределенный протокол; думайте об этом как о MapReduce. Они специализируются на моей логике.   -  person Ken Birman    schedule 22.12.2010


Ответы (6)


Они уже решили это для многих «простых» случаев: C # 4.0 поддерживает ковариация и контравариантность для параметров универсального типа в интерфейсах и делегатах. Но, к сожалению, не занятия.

Это ограничение довольно легко обойти:

List<Foo> foos = bars.Select(bar => (Foo)bar).ToList();
person Mark Byers    schedule 21.12.2010
comment
+1 За указание на условия контра / ковариации и предоставление ссылки. - person ; 21.12.2010
comment
Я знаю о дополнениях ковариации и контравариантности. Трес классный, но, к сожалению, не очень подходит для моего предполагаемого использования. - person Ken Birman; 22.12.2010

ОЧЕВИДНО, что Foo<int> является подтипом Foo<T>

Может быть, тебе, но не мне.

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

Тот факт, что массивы ковариантны, даже несмотря на то, что это, как известно, нарушает типобезопасность, достаточно плохо, теперь вы хотите сломать его для всего?

Это лежит в основе того, что такое система типов. Все, что делает система типов, - это отклоняет программы. А из-за теоремы Райса отвергнутые программы включают в себя отлично типизированные и безопасные по типу программы.

Это огромная цена. Убирают выразительность, мешают писать полезные программы. Чтобы оправдать эту стоимость, система шрифтов лучше окупится большим сроком. По сути, у него есть два способа сделать это: вернуть выразительность на уровне типов, отобранных на уровне программ, и безопасность типов.

Первое отсутствует просто потому, что система типов C # недостаточно мощна, чтобы позволить мне выразить что-либо даже отдаленно интересное. Остается только последнее, и это уже довольно шаткое положение из-за null, ковариантных массивов, неограниченных побочных эффектов unsafe и так далее. Делая универсальные типы автоматически ковариантными, вы более или менее полностью убираете последний остаток типобезопасности.

Лишь очень мало случаев, когда S <: T ⇒ G<S> <: G<T> действительно типобезопасен. (IEnumerable - один из таких примеров.) И, вероятно, одинаково много случаев, когда только S <: T ⇒ G<T> <: G<S> является типобезопасным. (IObservable, IComparer, IComparable, IEqualityComparer.) Как правило, ни G<S> <: G<T>, ни G<T> <: G<S> не являются типобезопасными.

person Jörg W Mittag    schedule 21.12.2010
comment
Не верьте вашему утверждению, что существует несколько интересных случаев, когда простой механизм может быть достаточным и полезным. Я думаю, что, вероятно, есть много случаев, когда вывод можно сделать легко и безопасно по типу. У меня есть такие случаи; Тема номер один для дженериков здесь, в потоках C #, а также в Java. Потому что очевидные вещи кажутся сломанными. Нет проблем со сложными случаями, которые не работают: C # мог бы просто сказать, что не может доказать отношение подтипа, если он не может сделать вывод о безопасности за несколько мс, не уверен и хочет выдать красивое сообщение об ошибке ... - person Ken Birman; 22.12.2010
comment
@ Кен Бирман: Я не понимаю, что вы имеете в виду. Я конкретно отвечал на ваше утверждение о том, что очевидно, что Foo<int> является подтипом Foo<object>, и я привел четыре примера случаев, когда это не только не < / i> очевидно, но просто не верно. - person Jörg W Mittag; 22.12.2010
comment
Йорг, твои примеры не касаются. Я не возражаю, что вывод типа для дженериков - неразрешимый вопрос; Я сам указал на это, задавая свой вопрос. Вы не понимаете мою мысль о том, что иногда существуют простые подмножества этих проблем, которые можно решить, даже если общий случай не разрешим. Примеры, которые я предложил, очевидны, потому что типы напрямую производятся от тех, из которых они подтипируются, и, следовательно, любой крошечный объем истории вообще выявит точное совпадение и, следовательно, отношение подтипов, даже если алгоритм вывода был бы трудным. - person Ken Birman; 22.12.2010
comment
Можно подумать об этом: многие проблемы являются NP-завершенными. Тем не менее, сегодня мы постоянно решаем такие проблемы, потому что мы часто обнаруживаем, что мир создает предвзятый набор экземпляров, которые имеют своего рода причуду, делающую эти экземпляры простыми, несмотря на сложность общего случая. У меня есть несколько примеров из моей области распределенных систем; хороший вариант предполагает оптимальное назначение ресурсов многоадресной IP-рассылки в облачных вычислениях с учетом ограничений. Поэтому я предлагаю переосмыслить неразрешимость выводов о родовых подтипах, нацеленных на то же понимание. - person Ken Birman; 22.12.2010
comment
... что, в частности, означает понимание того, что у некоторых сложных проблем есть простые, но полезные подслучаи. Их решение важно, даже если вы оставите наиболее общий пример проблемы открытым - даже если она никогда не будет решена. - person Ken Birman; 22.12.2010

Дело в том, что вы не можете сделать это для всех случаев, поэтому вы не делаете этого ни для каких. Проблема в том, где провести черту. Если вы этого не делаете, то каждый, кто использует C #, знает, что он этого не делает. Если вы делаете это время от времени, тогда это усложняется. Это может превратиться в игру в угадывание того, как будет вести себя ваш код. Все крайние случаи того, что легко, а что нет, становятся сложными для программистов и могут вызвать ошибки в коде.

Вот сценарий, который абсолютно вызовет хаос. Предположим, что вы можете сделать вывод, что в сценарии A. Кто-то другой приходит и меняет часть базового типа, и это больше не так. Сделав это либо всегда, либо никогда, вы никогда не попадете в эту ситуацию. В сложной среде отслеживание такой проблемы может быть абсолютным кошмаром, особенно если учесть это, может быть невозможно уловить во время компиляции (отражение, DLR и т. Д.). Намного проще написать код, чтобы вручную обрабатывать преобразование заранее, чем предполагать, что он будет работать в вашем сценарии, когда есть вероятность, что когда-нибудь в будущем этого просто не будет (не считая обновления до новой версии).

C # 4.0 кое-что из этого исправляет, поскольку позволяет делать выводы о том, что, по их мнению, «безопасно» для программистов.

person kemiller2002    schedule 21.12.2010
comment
Да, я это понимаю. Общий случай не разрешим. Но многие полезные частные случаи были бы легко разрешимы. Может быть, все полезные кейсы. - person Ken Birman; 21.12.2010

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

person jaydel    schedule 21.12.2010
comment
Не просить ничего даже отдаленно такого фантастического. Просто хочу объявить Isis Aggregator ‹KeyType, ValueType› generic; мой пользователь создает IntAggregator: IsisAggregator ‹int, int›, а компилятор C # понимает, что IntAggregator является IsisAggregator, следовательно, позволяет мне вызывать некоторые методы без странного, написанного вручную отражения. Что да, я понимаю, как писать, и да, представляет собой один из возможных обходных путей. - person Ken Birman; 22.12.2010

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

class Base<T> 
{
 public T Method() { return default(T);}
}

Если вы предполагаете, что Base и Base, то у них обоих есть «Метод» с одинаковой сигнатурой (исходящий из базового класса), и он может быть вызван с использованием ссылки на базовый класс. Не могли бы вы объяснить, какое очевидное возвращаемое значение "Метод" будет иметь в вашей системе, если указать на Base<int> или Base<string> объект, указав на базовый класс?

person Alexei Levenkov    schedule 22.12.2010
comment
Я бы привел его к объекту, так что в конечном итоге у меня будет объект по умолчанию в этом случае типа T. Позже мог бы передать этот INTO какой-нибудь другой метод в том же определяемом пользователем производном классе. Мое конкретное использование сосредоточено на классах агрегатора, которые принимают некоторый тип ввода (например, int или double) и вычисляют некоторую операцию агрегирования (скажем, max или min). У них есть стандартные методы, но специализированные для типов, используемых (или определенных) программистом с помощью моей масштабируемой библиотеки распределенной агрегации (воспринимайте это как вариант MapReduce для облачных вычислений). - person Ken Birman; 22.12.2010

Итак, я нашел элегантный способ решения моей реальной проблемы (хотя я считаю, что C # чрезмерно ограничивает).

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

Например, мне нужно составить список объектов, которые вы определите в следующем году, и перебрать элементы списка, обращаясь к методу «Aggregate‹ KeyType, ValueType ›» каждого объекта. Мое разочарование заключалось в том, что, хотя вы, безусловно, можете зарегистрировать свой класс или объект в своем классе, проверка типов C # не позволяла мне вызывать методы, потому что я не знаю типы (я могу получить типы через GetType (), но могу не использовать их как типы в выражениях). Итак, я начал использовать отражение, чтобы вручную скомпилировать желаемое поведение, что некрасиво и может нарушить безопасность типов.

И ответ ... анонимные классы. Поскольку анонимный класс подобен замыканию, я могу определить свои обратные вызовы внутри процедуры «регистрации» (которая имеет доступ к типам: вы можете передавать их, когда регистрируете свои объекты). Я создам небольшой анонимный метод обратного вызова прямо в строке и сохраню для него делегата. Метод обратного вызова может вызывать ваш агрегатор, поскольку C # считает, что он «знает» типы параметров. И все же в моем списке могут быть перечислены методы с известными типами сигнатур на момент компиляции моей библиотеки, а именно завтра.

Очень чистый, и C # не сводит меня с ума. Тем не менее, C # действительно недостаточно старается вывести отношения подтипов (извините, Эрик: «какими должны быть отношения подтипов»). Обобщения - это не макросы в C. Однако C # слишком близок к тому, чтобы рассматривать их так, как если бы они были таковыми!

И это отвечает на мой собственный (настоящий) вопрос.

person Ken Birman    schedule 22.12.2010
comment
Отображение нескольких строк кода поможет нам гораздо лучше понять все с одного взгляда. Я думаю, что мы, программисты, достигли точки, когда язык программирования легче понять, чем нормальный человеческий язык: / - person nawfal; 10.07.2014
comment
Nawfal, это очень старая ветка. В наши дни var неплохо справляется с большинством проблем вывода типов, а функциональный стиль делегата решает главное, на что я жаловался. Нам больше не нужно набирать все дважды, и с моей точки зрения это отличный результат. Посмотрите мою систему Isis2 (isis2.codeplex.com), и вы увидите, как все выглядит в наши дни - я думаю, это дает очень чистое и элегантное решение. Ребята, работающие с C #, действительно тщательно продумали свой язык и проделали отличную работу. - person Ken Birman; 11.07.2014