Явная ошибка компилятора в параметрах C # (C # 5.0)

Это продолжение thread Я думал, что вчера было решено. Вчера у меня были проблемы с моим кодом в следующем случае:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication3
{
    class Program
    {
        class Bar
        {
            int v;

            public Bar(int v) { this.v = v; }
            public override string ToString() { return v.ToString(); }
        }

        static void Main(string[] args)
        {
            Foo(1, 2, 3);
            Foo(new int[] { 1, 2, 3 });
            Foo(new Bar(1), new Bar(2), new Bar(3));
            Foo(new Bar[] { new Bar(1), new Bar(2), new Bar(3) });
            System.Threading.Thread.Sleep(20000);
        }

        static void Foo(params object[] objs)
        {
            Console.WriteLine("New call to Foo: ");
            foreach(object o in objs)
                Console.WriteLine("Type = " + o.GetType() + ", value = "+o.ToString());
        }
    }
}

Если вы запустите это, вы можете увидеть проблему с последним вызовом Foo. То, что аргумент является вектором, «потеряно».

Итак .... кто-нибудь знает, как сообщить об ошибке компилятора C #? Или это будет считаться ошибкой отражения?

(Какое облегчение: я был обескуражен, подумав, что зря потратил здесь время на собственную ошибку. На самом деле, это все-таки ошибка C #, и я оправдан! И как часто мы можем видеть настоящие ошибки компилятора C # в наши дни? Нечасто ...)


person Ken Birman    schedule 14.03.2012    source источник
comment
Я думаю, что C # 5 находится в стадии бета-версии ...   -  person Senad Meškin    schedule 15.03.2012
comment
Не могли бы вы опубликовать полный исходный код вашего теста?   -  person Adriano Repetti    schedule 15.03.2012
comment
Я связал то, что похоже на вчерашний вопрос, о котором вы говорите. Если это не тот, укажите ссылку на правильный.   -  person M.Babcock    schedule 15.03.2012
comment
В вашем коде ошибка, одна правая скобка слишком велика для new Foobar(3)), не так ли?   -  person Tim Schmelter    schedule 15.03.2012
comment
Сделанный. Если вы запустите этот пример, вы увидите, что последний вызов Foo работает неправильно, как я описал. Кстати: почему мой код искажается? Может кто поправить?   -  person Ken Birman    schedule 15.03.2012
comment
Кен, чтобы создать блок кода в своем сообщении, вам нужно сделать отступ для каждой строки кода как минимум четырьмя пробелами. Вы можете использовать кнопку {}, чтобы сделать это с большим блоком текста.   -  person phoog    schedule 15.03.2012
comment
Спасибо, Фог. В следующий раз сделаю это.   -  person Ken Birman    schedule 15.03.2012
comment
Довольно неясно, что вы считаете ошибкой, но в более ранних версиях компилятора она не работает иначе. Попробуй. Если вы хотите, чтобы был передан только один аргумент, вам придется ударить компилятор по голове с помощью Foo (new object [] {new FooBar [] {...}});   -  person Hans Passant    schedule 15.03.2012
comment
Это не ошибка компилятора C #.   -  person Eric Lippert    schedule 15.03.2012
comment
@HansPassant Проще просто привести к object, как в Foo((object)new Bar[] { new Bar(1), new Bar(2), new Bar(3) });. Насколько мне известно, всегда можно устранить неоднозначность между нормальной формой и расширенной формой метода params с помощью простого приведения, поэтому никогда не требуется явно создавать самый внешний массив.   -  person Jeppe Stig Nielsen    schedule 03.04.2013


Ответы (5)


Я ожидал, что эти два вызова будут работать одинаково - аргумент params представляет собой массив в вызываемом методе. Пример Джона Скита в предыдущем вопросе работает, потому что массив int не ковариантен массиву объектов (и поэтому обрабатывается как new Object[] { new Int[] {1,2,3} }), но в этом примере массив FooBars равен < / strong> ковариантен массиву объектов, поэтому ваш параметр расширяется до аргумента objs.

Википедия всего описывает этот конкретный случай: Ковариация и контравариантность (информатика) < / а>

Извините, но я уверен, что это не ошибка компилятора.

РЕДАКТИРОВАТЬ:

Вы можете добиться желаемого таким образом:

Foo(new Object[] { new Bar[] { new Bar(1), new Bar(2), new Bar(3) } });

НОВОЕ РЕДАКТИРОВАНИЕ (другой автор):

Или просто используйте:

Foo((Object)new Bar[] { new Bar(1), new Bar(2), new Bar(3) });
person Chris Shain    schedule 14.03.2012
comment
Я люблю людей PL. Но в данном конкретном случае они явно уговорили себя совершить ошибку! - person Ken Birman; 15.03.2012
comment
Извините, я не понимаю последний комментарий - person Chris Shain; 15.03.2012
comment
Ясно, что должен быть способ использовать params, чтобы точно выяснить, что это за аргумент метода. Приняв это определение, разработчики C # 5.0 создали кейс, который нельзя правильно обработать с помощью параметров. Итак, хотя вы явно правы (просто прочтите страницу в Википедии), используемое определение явно неверно! Во всяком случае, есть ли способ сделать то, что я на самом деле пытаюсь сделать? - person Ken Birman; 15.03.2012
comment
@KenBirman - Я думаю, это действительно сводится к тому, почему вам нужно точно знать, как это было вызвано? Я не могу вспомнить многих практических ситуаций, когда это так. В тех случаях, когда это необходимо, вы можете сделать это через пространство имен Dynamic, как указал Джон Скит. - person John; 15.03.2012
comment
В Isis2 люди создают параллельные приложения. Предположим, кто-то запрашивает набор серверов, и каждый из них возвращает, скажем, URL-адреса, соответствующие некоторому шаблону (каждый выполняет поиск в отдельном корпусе). Некоторые находят 2 совпадения, некоторые 4. Я не могу извлечь сигнатуру типа, следовательно, не могу вернуть разумные списки ответов для вызывающего. Фактически, я не могу отличить g.Reply (url1, url2) от g.Reply (vector-of-urls); Конечно, я поддерживаю общий случай, а не только строки. - person Ken Birman; 15.03.2012
comment
Я более внимательно смотрю на то, что предложил Джон Скит. Если это решит мою проблему, я в порядке. - person Ken Birman; 15.03.2012
comment
Хорошо, я понимаю, как работает Джон. Заставляет меня делать группы Isis2 динамическими и заставляет моих пользователей использовать ключевое слово dynamic, следовательно, глупо и нежелательно. Еще более нежелательным является приведенное выше предложение, хотя да, оно работает. Пользователям C # не нужно понимать ковариацию массивов объектов, чтобы использовать язык! Я еще говорю, что это ошибка (ошибка в определении языка) - person Ken Birman; 15.03.2012
comment
О, и Крис, поскольку я упорядочиваю сообщения, все мои вещи будут иметь ценность - я беру то, что они предоставляют, копирую это в сообщение и отправляю парню, который сделал g.Query. Это выглядит очень элегантно, и на самом деле Isis2 НАМНОГО проще в использовании, чем большинство других подобных пакетов. Вот почему оригинальная Isis была так популярна и в старые времена. Мои пользователи - это люди, которые программируют обработчики щелчков мыши (и это модель API, которую я принял). - person Ken Birman; 15.03.2012
comment
Пользователям C # СЛЕДУЕТ понимать ковариацию ссылочного массива и связанные с этим проблемы с безопасностью типов. Если вы этого не понимаете, используйте вместо этого ArrayList или List ‹T›. Оба варианта несовместимы (по понятным причинам ArrayList). Вот почему рекомендуется не использовать массивы [] как часть API. - person Michael Graczyk; 07.07.2012

Спецификация C # 4.0 является довольно ясно в том, как все это разыгрывается. В 7.5.3.1 говорится, что если функция с params может применяться либо в нормальной форме (игнорируя ключевое слово params), либо в развернутой форме (с использованием ключевого слова params), тогда нормальная форма выигрывает.

Предполагая, что Foo был объявлен как Foo(params object[] args), тогда вызов Foo(new Foobar[] {new Foobar(1), new Foobar(2), new Foobar(3)) }); применим в нормальной форме, поскольку Foobar[] неявно конвертируется в object[] (6.1.6, пункт 5). Поэтому используется нормальная форма, а развернутая игнорируется.

(Я предполагаю, что C # 5.0 не изменил эту часть языка.)

person Raymond Chen    schedule 14.03.2012

См. Раздел 7.5.3.1 спецификации C #:

7.5.3.1 Применимый функциональный член

Функциональный член считается применимым функциональным членом по отношению к списку аргументов A, если выполняются все следующие условия:

  • Каждый аргумент в A соответствует параметру в объявлении члена функции, как описано в §7.5.1.1, и любой параметр, которому не соответствует ни один аргумент, является необязательным параметром.
  • For each argument in A, the parameter passing mode of the argument (i.e., value, ref, or out) is identical to the parameter passing mode of the corresponding parameter, and
    • for a value parameter or a parameter array, an implicit conversion (§6.1) exists from the argument to the type of the corresponding parameter, or
    • [... некоторый не относящийся к делу материал, касающийся параметров ref и out ...]

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

Поскольку переданный вами массив может быть неявно преобразован в object[], и поскольку при разрешении перегрузки предпочтительнее «нормальная» форма, а не «развернутая», наблюдаемое вами поведение соответствует спецификации, и ошибки нет.

В дополнение к обходному пути, описанному Крисом Шейном, вы также можете изменить Bar с класса на структуру; что делает массив больше не конвертируемым в object[] неявно, поэтому вы получите желаемое поведение.

person phoog    schedule 14.03.2012
comment
Я согласен с тем, что компилятор реализует спецификацию. Но в спецификации явно есть ошибки, поскольку этот пример иллюстрирует все большую и большую ясность. Как может быть так, что g.Reply (vector-of-Foos) неотличим от g.Reply (Foo, Foo '...), но при этом g.Reply (3, vector-of-foos) работает так, как и следовало ожидать? Да, я понимаю, почему это работает, любой из нас в этой ветке это видит. Но почему в глубоком философском смысле это правильное решение о том, как вывести тип? У них было две тропы в лесу ... и они пошли не по той. Затем сделал это частью спецификации. Все еще ошибка! - person Ken Birman; 15.03.2012
comment
@KenBirman мне кажется, что решение вашей проблемы состоит в том, чтобы отказаться от использования params и просто взять единственный аргумент object[]; требовать от пользователей упаковки аргументов в массив, несмотря ни на что. Тогда нет двусмысленности. Кроме того, я бы поставил под сомнение использование здесь buggy; если разработчики языка сделали плохой выбор, то во что бы то ни стало назвать его плохим выбором. Безусловно, система в том виде, в котором она работает, теперь имеет несколько полезных свойств. - person phoog; 15.03.2012
comment
@KenBirman: Дизайн - это всегда процесс компромисса между множеством несовместимых целей. То, что вам кажется ошибкой, безусловно, является прискорбным следствием этого процесса проектирования, но вы не принимаете во внимание все соображения, которые должны были учесть дизайнеры. По сути, проблема возникает из-за того, что небезопасная ковариация массива работает только с ссылочными типами. Это прискорбно, да, но это не баг или ошибка; это было специально разработано в языке разумными людьми, которым пришлось идти на жесткие компромиссы. - person Eric Lippert; 15.03.2012
comment
@EricLippert - хорошо, я бы сказал, что в ретроспективе ковариация массивов - это ошибка в CLR. С точки зрения C # это более оправданно, поскольку базовая платформа поддерживает его (и сопоставление с платформой имеет некоторые преимущества). - person kvb; 15.03.2012
comment
Эрик, см. Ниже. К настоящему времени я считаю, что спецификация верна и тип соответствует, но это скучная ошибка компилятора. Я думаю, что просто нет способа проанализировать часть params сигнатуры типа, в которой даже возникает эта проблема ковариации. Компилятор ошибается, игнорируя параметры; на самом деле, если вы думаете о том, что он делал, это явно было ошибочным поведением! (Но см. Ниже; заканчивается место для комментариев по этому конкретному предлагаемому ответу) - person Ken Birman; 15.03.2012
comment
@kvb: Я с тобой согласен. Однако разработчики среды CLR v1.0 также жили в мире с противоречивыми целями и должны были выбрать разумный компромиссный вариант. Я прав, желая, чтобы это решение было по-другому; Я считаю, что преимущества этой функции не оправдывают боли, которую она также вызывает. Но то, что я эгоистично желаю, чтобы мы сделали другой выбор более десяти лет назад, не делает это решение ошибкой или ошибкой. Решение было принято с полным пониманием последствий. - person Eric Lippert; 15.03.2012
comment
@kvb это правда, но, насколько я понимаю, CLR поддерживает это, потому что это желательная функция для C #; это была желанная функция для C #, потому что это была функция Java. Если это правда, то основная ошибка действительно была связана с дизайном языка C #. - person phoog; 15.03.2012
comment
@KenBirman: Ваш анализ неверен; это не ошибка компилятора C #. - person Eric Lippert; 15.03.2012
comment
@phoog: Нет, вы получаете это задом наперед. CLR поддерживает это, потому что CLR хотела иметь возможность быть средой выполнения для любого возможного Java-подобного языка, поэтому она поддерживает расширенный набор функций Java. Учитывая, что система общих типов CLR поддерживает небезопасную ковариацию массивов, для C # естественно поддерживать и ее; иначе было бы немного странно. (Однако C # не поддерживает все функции ковариации общей системы типов; например, приведение int [] к uint [] недопустимо в C #, даже если среда CLR допускает такое преобразование.) - person Eric Lippert; 15.03.2012
comment
@EricLippert, спасибо, что прояснили это. - person phoog; 15.03.2012
comment
@phoog: Пожалуйста. Интересным следствием несоответствия между системой типов C # и системой типов CLR является то, что оператор is ведет себя довольно странно в некоторых из этих угловых случаев. См. Например, stackoverflow.com/questions/593730/. - person Eric Lippert; 15.03.2012
comment
@EricLippert - Я, конечно, согласен с тем, что несправедливо оглядываться на решения, принятые в прошлом, и критиковать их, как если бы они были приняты сегодня, и я понимаю, что в контексте .NET 1.0 ковариация массивов могла иметь смысл. Возможно, я просто спорю о семантике, но я бы все же классифицировал это как ошибку (задним числом) и не согласился бы с тем, что это было сделано с полным пониманием последствий (или, иначе, почему мы сегодня по-другому чувствуем?) . - person kvb; 15.03.2012

Я считаю, что вы ошибаетесь, Кен. Это потому, что int [] не относится к типу object [], поэтому компилятор предполагает, что int [] является лишь одним из аргументов, переданных методу.

вот как:

new Foobar[] { } is object[]; // true
new int[] { } is object[]; // false

update: вы можете сделать метод универсальным, чтобы компилятор знал, что тип структуры / объекта передается как параметры:

void Foo<T>(params T[] objs)
{
    foreach (T o in objs)
        Console.WriteLine(o.GetType());
}
person Kamyar Nazeri    schedule 14.03.2012
comment
Params делает вывод типов наоборот, Камар! Если ваше объяснение выдержано, то 3-й, а не 4-й вызов будет тем, который выводит нежелательный результат. - person Ken Birman; 15.03.2012
comment
вот как: в C # экземпляр структуры является объектом, однако массив структуры не имеет типа object [] - person Kamyar Nazeri; 15.03.2012
comment
Лучше всего сделать метод универсальным, в этом случае компилятор определит тип параметра (структура / объект) при вызове метода! проверить обновление - person Kamyar Nazeri; 15.03.2012
comment
Предполагает (ошибочно), что все ответы одного типа. Я часто вижу, например, g.Reply (качество, URL-список, ...). Но это сработает правильно. На самом деле проблема возникает ТОЛЬКО с одним аргументом, который представляет собой вектор определяемых пользователем объектов, и для меня это подчеркивает абсурдность утверждения, что C # правильно использует это определение. - person Ken Birman; 15.03.2012

Итак, я собираюсь предположить, что лучший ответ - я прав и что это действительно ошибка компилятора. Хотя я прекрасно понимаю суть дела, ваше объяснение игнорирует ключевое слово "params". Фактически, чтобы использовать ковариацию таким образом, ДОЛЖНО игнорировать ключевое слово params, как будто оно неважно. Я постулирую, что просто не существует правдоподобного объяснения компиляции кода таким образом с Params, присутствующим в качестве ключевого слова в сигнатуре типа Foo: чтобы вызвать ваше объяснение, вам нужно убедить меня, что myClass [] должен соответствовать типу object [] , но мы не должны даже задавать этот вопрос, учитывая конструкцию params. Фактически, чем больше вы думаете об этом, тем яснее, что это настоящая ошибка компилятора C # 5.0: компилятор не применяет ключевое слово params. Спецификация языка на самом деле вообще не нуждается в изменении. Я должен получить какой-то странный значок, имхо!

person Ken Birman    schedule 14.03.2012
comment
Ваш анализ неверен; это не ошибка компилятора. Все остальные анализы верны; Ответ Раймонда Чена, на мой взгляд, наиболее ясен из всех. - person Eric Lippert; 15.03.2012
comment
Я не уверен, что понимаю ваш аргумент. С одной стороны, вы находите абсурдным игнорирование ключевого слова params, но с другой стороны, соглашаетесь с тем, что спецификация верна. Но эти моменты расходятся, поскольку в спецификации (относительно) ясно, что взаимодействие между params и массивом ссылочного типа в точности производит компилятор. Так как это может быть ошибкой компилятора? Как вы упомянули ранее, такое поведение может быть нежелательным (хотя я считаю, что в большинстве случаев это не должно представлять реальной проблемы), но компилятор реализован в соответствии со спецификацией. - person dlev; 15.03.2012
comment
Можете ли вы сказать нам, какая часть спецификации, по вашему мнению, нарушается поведением компилятора? - person phoog; 15.03.2012
comment
Фог, чтобы дать хороший ответ на ваш вопрос, у меня уйдет немного времени. Но, опять же, я хочу сказать, что для применения интерпретации Раймонда или других предложенных интерпретаций нам нужно сделать вид, что вызов Foo был идентичен вызову метода, не использующего ключевое слово params. Но при наличии параметров мы берем следующий аргумент типа для Foo (object []) и сопоставляем аргументы с типом object. То есть не против объекта []. Неправильно C # сопоставляется с объектом [] и применяет ковариацию. - person Ken Birman; 15.03.2012
comment
@KenBirman: Это идентично вызову метода, не использующего ключевое слово params. Как правильно сказал Раймонд, если метод применим как в его нормальной (игнорировать параметры), так и в его расширенной форме (использовать параметры), тогда нормальная форма выигрывает . (Также будьте осторожны. Я подозреваю, что аргумент типа не означает то, что вы думаете.) - person Eric Lippert; 15.03.2012
comment
@KenBirman: Возможно, оправдание поможет. int M(bool b, params string[] p) { return b ? N(p) : O(p); } int N(params object[] q) {...} int O(params object[] r) {...}. Вызовы N и O применимы в их нормальной форме. Очевидно, здесь было бы ошибкой передавать new object[] { p } в качестве аргумента N или O. Если мы не разрешаем вам вызывать N или O в нормальной форме, тогда становится невозможным написать метод М. - person Eric Lippert; 15.03.2012
comment
Подожди ... подумай о твоем примере - person Ken Birman; 15.03.2012
comment
Эрик, этот пример помогает мне понять, почему C # делает то, что делает. Позвольте мне больше подумать о том, согласен ли я с тем, что невозможно разрешить проблемы таким образом, чтобы этот пример работал, И дать мне способ извлечь фактическую сигнатуру типа из списка аргументов без этой абсурдности - той, которая, на мой взгляд, такая же проблема, как и ваш пример невозможности. Возможно, более того: N и O на самом деле не нуждаются в ключевом слове params здесь. По сути, вы предоставили ярлык (нет необходимости делать вторую версию N и O, если требуются прямые вызовы параметров), который полностью нарушает мой код (выхода нет) - person Ken Birman; 15.03.2012
comment
Те из вас, кто проголосовал против, просто не задумываются над этим вопросом. Эрик действительно понимает это (и, возможно, только Эрик), но мы с ним просто не согласны с тем, как C # должен с этим справиться. - person Ken Birman; 15.03.2012
comment
Кен, в примере @EricLippert, N и O могут быть частью общедоступной поверхности класса и по этой причине могут нуждаться в params, или могут быть библиотечными методами вне контроля программиста. Выходом было бы рассматривать список аргументов (params object[] x) как отличный от (object[] x) с целью разрешения перегрузки. Я подозреваю, что это принесет больше затрат, чем пользы. Во-первых, версия params каждого такого метода будет выглядеть одинаково: T M(params object[] a) { return M(a); } T M(object[] a) { // ... real logic here ... - person phoog; 15.03.2012
comment
Кен, размышляя дальше: с такой парой перегрузок, как вы решите, какую из них вызывать, когда сайт вызова выглядит как T result = M(new object[0]);? - person phoog; 15.03.2012
comment
@KenBirman, вы можете не согласиться с тем, как C # должен себя вести. Но это не делает текущее поведение ошибкой. - person svick; 03.04.2012