Почему foreach в C# ведет себя таким образом и есть ли способ исправить это в будущем?

long[] b = new long[1];
int i1 = b[0]; // compile error as it should

// no warning at all, large values gets converted to negative values silently
foreach (int i2 in b) 
{
}

class Customer : Person{}
Person[]p = new Person[]{mypers};

// no warning at all, throws typecastexception at runtime
foreach (Customer c in p) 
{
}

Я знаю, что они не могут просто исправить это, потому что это нарушит существующие программы.

Но почему они не делают параметр совместимости в компиляторе С#, где я могу включить генерацию блока foreach с безопасным типом, по крайней мере, для новых программ или программ, где я уверен, что это работает?


person codymanix    schedule 06.08.2010    source источник
comment
В будущем задавайте вопросы, не вешайте вопрос на очень спорную и подстрекательскую тираду.   -  person Nick Craver    schedule 06.08.2010
comment
Вы имеете в виду, что они должны добавить специальные правила преобразования типов для foreachспецифически? Проблема не имеет ничего общего с foreach. Именно система типов C# в целом допускает определенные преобразования. Это происходит потому, что в некоторых случаях они полезны.   -  person jalf    schedule 06.08.2010
comment
Согласен, можно ли исправить foreach без нарушения существующего кода, было бы вопросом.   -  person Henk Holterman    schedule 06.08.2010
comment
Для всех, кто интересуется моим последним комментарием, помимо вопроса, заголовок был ранее. Почему foreach в C # так сильно отстой?   -  person Nick Craver    schedule 06.08.2010
comment
foreach не работает в C#. Ваше понимание того, что он должен делать. Я мог бы вызвать такое же поведение в ваших одиночных присваиваниях, эмулируя то, что происходит в операторе foreach: int i1 = (int) b[0]; или Customer c = (Customer)p[0];. Указав тип в операторе foreach, вы говорите компилятору явно привести каждый элемент к желаемому типу, и он просто слушает вас.   -  person Justin Niessner    schedule 06.08.2010
comment
Я знаю, что foreach автоматически выполняет преобразование типов, что, по моему мнению, является плохим решением, которое следует исправить.   -  person codymanix    schedule 06.08.2010
comment
Где бэт-сигнал Эрика Липперта, когда он тебе нужен? :)   -  person Adam V    schedule 06.08.2010
comment
Это совсем не плохое решение, даже очень хорошее решение. Вы просто неправильно его используете   -  person Nealv    schedule 06.08.2010
comment
@codymanix - плохое решение в вашем коде, а не в компиляторе. Вы сказали в своем foreach, что хотите, чтобы тип longs был преобразован в int. Он сделал это. Я не вижу проблемы. Если вам нужны длинные строки в вашем foreach, вы должны были привести их к длинным. -1   -  person Joel Etherton    schedule 06.08.2010
comment
@Joel Etherton: Иногда вы делаете ошибки.   -  person Greg    schedule 06.08.2010
comment
@Greg - Верно, и я заработал больше, чем мог. Однако я никогда не жаловался, что языка программирования недостаточно из-за моих ошибок.   -  person Joel Etherton    schedule 06.08.2010
comment
Вот некоторый онлайн-код, демонстрирующий проблему. Компилируется нормально, но выдается исключение во время выполнения: dotnetfiddle.net/5zGgIZ.   -  person Colm Bhandal    schedule 19.08.2020


Ответы (5)


foreach (var c in p) 
{
}

красиво избегает проблемы, не так ли?

person jalf    schedule 06.08.2010
comment
foreach (Person c in p) также решит это, это не главное. - person codymanix; 06.08.2010
comment
Вам нужно будет добавить проверку для if (c is Customer) внутри в любом случае. - person Justin; 06.08.2010
comment
Нет. Смысл, видимо, был не в том, чтобы задать вопрос. Что заставляет меня задаться вопросом, почему вы разместили свою тираду на сайте вопросов и ответов. Я хочу сказать, что язык позволяет вам полностью опускать тип, и компилятор просто использует правильный. Если самое простое решение работает правильно, зачем тратить время на жалобы на то, что более сложное требует минимум размышлений, чтобы получить правильное решение? - person jalf; 06.08.2010
comment
@jalf Успокойся. Это законный вопрос, просто плохо сформулированный. - person Josh Stodola; 06.08.2010
comment
использование var везде не упростит чтение программ. - person codymanix; 06.08.2010
comment
@codymanix - Теперь я действительно запутался. Вы не хотите использовать вывод типа с помощью var, но вы также не хотите, чтобы компилятор пытался выполнить приведение типов даже после того, как вы явно укажете другой тип в цикле foreach. Что именно вы хотите, чтобы поведение было? (имейте в виду, что foreach может перебирать коллекции, которые также имеют разные типы). - person Justin Niessner; 06.08.2010
comment
@Josh: законные вопросы не помечены [rant], как это было. @codymanix: я думаю, что var прекрасно читается. - person jalf; 06.08.2010
comment
К ключевому слову [rant]: Печально видеть, что здесь нет никого с моим юмором... - person codymanix; 06.08.2010
comment
Мне нравится видеть типы, с которыми я работаю, и я не хочу просматривать коллекцию, чтобы увидеть, какой тип она на самом деле вернет. - person codymanix; 06.08.2010
comment
@codymanix, я начал использовать var некоторое время назад, и мой код по-прежнему очень читабелен без использования комментариев. Просто дайте своим переменным логические имена, такие как: personList и т. д. - person Nealv; 06.08.2010
comment
Ах, я понял. Вместо использования явной типизации вы используете var и делаете предполагаемый тип частью имени переменной. - person codymanix; 06.08.2010
comment
@jaff: В ответ на ваш первый комментарий, вот почему существует субъективная и аргументированная близость. - person Powerlord; 06.08.2010
comment
Я всегда набираю foreach( var f in foo ) и использую Resharper для явного указания типа, чтобы изменить var на любой другой тип. - person Greg; 06.08.2010
comment
@Р. Бернроуз: и именно поэтому я проголосовал за закрытие как Субъективное и Аргументативное, прежде чем оно было вновь открыто. ;) - person jalf; 06.08.2010

foreach ведет себя так, как может.

Когда foreach выполняет итерацию по коллекции, она не обязательно знает что-либо о типах, которые будут возвращены (это может быть набор реализаций интерфейса или просто объекты в случае более старых коллекций в стиле «мешок» .NET).

Рассмотрим этот пример:

var bag = new ArrayList();
bag.Add("Some string");
bag.Add(1);
bag.Add(2d);

Как будет вести себя foreach в этом случае (в коллекции есть строка, целое число и двойное число)? Вы можете заставить foreach перебирать наиболее совместимый тип, которым в данном случае является Object:

foreach(object o in bag)
{
    // Do work here
}

Интересно, что использование var сделает именно это:

foreach(var o in bag)
{
    // o is an object here
}

Но члены объекта не позволят вам сделать ничего полезного.

Единственный способ сделать что-то полезное — это положиться на то, что разработчик укажет точный тип, который он хочет использовать, а затем попытается выполнить приведение к этому типу.

person Justin Niessner    schedule 06.08.2010

Вы можете создать метод расширения для IEnumerable, который является типобезопасным:

public static void Each(this IEnumerable @this, Action action)
{
    if (@this == null) throw new ArgumentNullException("this");
    if (action == null) throw new ArgumentNullException("action");

    foreach (TElement element in @this)
    {
        action(element);
    }
}

Он немного отличается от цикла foreach и вызовет дополнительные накладные расходы, но обеспечит безопасность типов.

person Paul Ruane    schedule 06.08.2010
comment
Я никогда раньше не видел @, потенциально глупый вопрос, но что это значит? - person AndrewC; 06.08.2010
comment
@AndyC stackoverflow.com/questions/91817/ - person Justin; 06.08.2010
comment
@ позволяет использовать ключевое слово C# в качестве имени переменной. Таким образом, вы можете делать такие сомнительные вещи, как: int @foreach = 1; длинный @if = 2; строка @класс = foo; - person AlfredBr; 06.08.2010
comment
Вы можете использовать ключевые слова как обычные переменные со знаком @. обычно вы используете его при взаимодействии с другими языками, которые предоставляют элементы, являющиеся ключевыми словами на другом языке. - person codymanix; 06.08.2010
comment
Он просто добавлен, чтобы вы могли использовать зарезервированное слово в качестве имени переменной. - person JohannesH; 06.08.2010
comment
Привет, ребята, никогда не знал этого! - person AndrewC; 06.08.2010

Что касается почему:

Используя массивы, компилятор фактически переводит оператор foreach в нечто, похожее на следующее.

private static void OriginalCode(long[] elements)
{
    foreach (int element in elements)
    {
        Console.WriteLine(element);
    }
}

private static void TranslatedCode(long[] elements)
{
    int element;
    long[] tmp1 = elements;
    int tmp2 = 0;

    while (tmp2 < elements.Length)
    {
        // the cast avoids an error
        element = (int)elements[tmp2++];
        Console.WriteLine(element);
    }
}

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

Кстати, то же самое касается IEnumerable и foreach, что приводит к следующему коду, приводящему к такому же поведению и проблеме.

private static void OriginalCode(IEnumerable<long> elements)
{
    foreach (int element in elements)
    {
        Console.WriteLine(element);
    }
}

private static void TranslatedCode(IEnumerable<long> elements)
{
    int element;
    IEnumerator<long> tmp1 = elements.GetEnumerator();

    try
    {
        while (tmp1.MoveNext())
        {
            element = (int)tmp1.Current;
            Console.WriteLine(element);
        }
    }
    finally
    {
        (tmp1 as IDisposable).Dispose();
    }
}
person Roland Sommer    schedule 06.08.2010
comment
Я не верю, что foreach переводится в простой оператор if. Возможно, вы имеете в виду for-statement :) - person codymanix; 09.08.2010
comment
Я опубликовал ответ перед сравнением while, for, foreach и goto до такой степени, что все они будут скомпилированы в один и тот же IL. См. это здесь «c вызывается ли функция для каждой итерации цикла foreach»> stackoverflow.com/questions/2447559/ - person Matthew Whited; 09.08.2010
comment
@codymanix Опс, извините, должен быть цикл while :) исправлено - person Roland Sommer; 10.08.2010
comment
@Matthew Whited: Действительно, IL почти одинаков для многих блоков операторов. Я выбрал цикл while для демонстрации, потому что считаю, что это самый простой способ показать приведение. - person Roland Sommer; 10.08.2010

EDIT: уточнено и исправлено на основе комментария codymax.

На основании моей интерпретации раздела 8.8.4 в C# Spec foreach расширяется, как показано ниже. (См. Часть, начинающуюся со слов «Если тип выражения X является типом массива, то происходит неявное преобразование ссылки из X в интерфейс System.Collections.IEnumerable».

В первом случае значения выше int.MaxValue преобразуются в -1. Это связано с тем, что операции не вызывают переполнение по умолчанию . Поскольку в первом случае преобразование происходит в непроверенном контексте, в разделе 7.6.12 описывается, что должно произойти, т.е.

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

Второй, как и ожидалось, создает исключение InvalidCastException во время выполнения. Вероятно, это связано с тем, что тип, возвращаемый Enumerator.Current. Возможно, есть раздел, который описывает это, но я его не искал.

        long[] b = long[0] ;


        System.Collections.IEnumerator le = ((long[])(b)).GetEnumerator();

        int i2;
        while (le.MoveNext())
        {
            i2 = (int)(long)le.Current;

        }




        Person[] p = new Person[1] { mypers };

        System.Collections.IEnumerator e = ((Person[])(p)).GetEnumerator();

        Customer c;
           while (e.MoveNext())
           {
               c = (Customer)(Person)e.Current;

            }
person Conrad Frix    schedule 09.08.2010
comment
Во-первых, неверно, что значения выше int.MaxValue преобразуются в -1, вместо этого они преобразуются в соответствующее отрицательное значение. Во-вторых, foreach для массивов не создает вызов GetEnumerator, а создает простой цикл for. Кроме того, двойное приведение к Person, а затем к Customer излишне. - person codymanix; 11.08.2010
comment
@codymax Вы правы насчет преобразования, поэтому я обновил свой ответ. Как вы думаете, почему это не делает GetEnumerator? Двойной бросок может быть излишним в некоторых случаях, но, возможно, не во всех. - person Conrad Frix; 11.08.2010