Мысли о foreach с Enumerable.Range и традиционным циклом for

В С# 3.0 мне нравится этот стиль:

// Write the numbers 1 thru 7
foreach (int index in Enumerable.Range( 1, 7 ))
{
    Console.WriteLine(index);
}

над традиционным циклом for:

// Write the numbers 1 thru 7
for (int index = 1; index <= 7; index++)
{
    Console.WriteLine( index );
}

Предполагая, что «n» мало, поэтому производительность не является проблемой, кто-нибудь возражает против нового стиля по сравнению с традиционным стилем?


person Marcel Lamothe    schedule 27.05.2009    source источник
comment
Теперь этот вопрос с включенным РЕЗЮМЕ ИЗ МНОГИХ ОТВЕТОВ ДЕЙСТВИТЕЛЬНО актуален, почти модель передовой практики для вопросов в SO. Это должно быть оформлено!   -  person heltonbiker    schedule 08.03.2013
comment
@heltonbiker Неправильно — на самом деле включение сводки не соответствует модели SO. Аннотация должна быть удалена. SO как сайт вопросов и ответов разделяет вопросы и ответы, а не имеет общее представление о сообщении.   -  person Keith Pinson    schedule 05.06.2013
comment
Поведение обоих различается в зависимости от того, какую версию .Net вы компилируете. Версия цикла for может не всегда сохранять контекст выполнения, особенно если у вас есть инструкция yield.   -  person Surya Pratap    schedule 07.12.2014
comment
Есть еще одно преимущество использования Range(): вы можете изменить значение индекса внутри цикла, и это не нарушит ваш цикл.   -  person Vincent    schedule 05.05.2016
comment
Этот пример вводит в заблуждение, потому что он представлен как Min-Max, тогда как Enumerable.Range на самом деле Min+Count. Таким образом, Enumerable.Range(3,9) на самом деле будет изменяться от 3 до 11.   -  person Triynko    schedule 04.06.2016
comment
С нетерпением ждем добавления новых возможностей C# 8.0 в эту тему! docs.microsoft.com/ en-us/dotnet/csharp/whats-new/   -  person Andrew    schedule 04.10.2019


Ответы (18)


Я считаю, что последний формат «минимум-максимум» намного понятнее, чем стиль «минимум-количество» Range для этой цели. Кроме того, я не думаю, что это действительно хорошая практика - вносить подобные изменения в норму, которая не быстрее, не короче, не более знакома и не явно яснее.

Тем не менее, я не против идеи в целом. Если бы вы пришли ко мне с синтаксисом, похожим на foreach (int x from 1 to 8), то я, вероятно, согласился бы, что это было бы улучшением по сравнению с циклом for. Однако Enumerable.Range довольно неуклюжий.

person mqp    schedule 27.05.2009
comment
Благодаря функции именованных аргументов C# 4 мы могли бы добавить метод расширения и назвать его следующим образом (сейчас у меня нет для него подходящего имени): foreach (int x in Enumerable.Range2(from = 1, to = 8) ) {} Это лучше или хуже ;) - person Marcel Lamothe; 27.05.2009
comment
Извините, это (с: 1 по: 8) - person Marcel Lamothe; 27.05.2009
comment
Лично для меня это было бы слишком многословно и неуклюже (в отличие от цикла for), но я понимаю, что это в какой-то степени дело вкуса. - person mqp; 27.05.2009
comment
@mquander Это очень похоже на синтаксис VB: For i = 1 To 8 - person MEMark; 19.03.2012
comment
Как насчет того, чтобы украсть foreach(var x in (1 to 8)) у Scala? Это перечислимый тип. - person Mateen Ulhaq; 03.04.2016
comment
Теперь, когда в C# 9 есть foreach GetEnumerator методы расширения и Range, как насчет foreach (var x in 1..8)? - person NetMage; 04.12.2020

Это просто для удовольствия. (Я бы просто использовал стандартный формат цикла «for (int i = 1; i <= 10; i++)».)

foreach (int i in 1.To(10))
{
    Console.WriteLine(i);    // 1,2,3,4,5,6,7,8,9,10
}

// ...

public static IEnumerable<int> To(this int from, int to)
{
    if (from < to)
    {
        while (from <= to)
        {
            yield return from++;
        }
    }
    else
    {
        while (from >= to)
        {
            yield return from--;
        }
    }
}

Вы также можете добавить метод расширения Step:

foreach (int i in 5.To(-9).Step(2))
{
    Console.WriteLine(i);    // 5,3,1,-1,-3,-5,-7,-9
}

// ...

public static IEnumerable<T> Step<T>(this IEnumerable<T> source, int step)
{
    if (step == 0)
    {
        throw new ArgumentOutOfRangeException("step", "Param cannot be zero.");
    }

    return source.Where((x, i) => (i % step) == 0);
}
person LukeH    schedule 27.05.2009
comment
public static IEnumerable‹int› To(this int from, int to) { return Enumerable.Range(from, to); } - person THX-1138; 27.05.2009
comment
@unknown, Enumerable.Range может считать только вперед, моя версия тоже считает назад. Ваш код также создает последовательность, отличную от моей: попробуйте foreach (int i in 5.To(15)). - person LukeH; 27.05.2009
comment
Очень хорошо, это то, что я хотел услышать, когда задавал вопрос! - person Marcel Lamothe; 27.05.2009
comment
Step() выглядит неправильно. 5.To(10).Step(2) приведет к [ 6, 8, 10 ], когда ожидается [ 5, 7, 9 ]. Кроме того, Step() ужасен с точки зрения производительности. А если шаг 10? - person Vincent; 05.05.2016
comment
@wooohoh: метод Step работает правильно: i - это индекс, а не значение. Вы правы в том, что производительность невысока, но это демо-версия для проверки концепции, а не производственный код. - person LukeH; 05.05.2016
comment
@LukeH Это моя вина. Я проглядел (x,i). Кроме того, мне жаль, что я был слишком негативен. Мне действительно понравилась идея, реализованная (без шага), протестированная ее производительность, сделанный вывод, что она нуждается в поддержке на уровне компилятора, чтобы работать достаточно быстро в кодах, чувствительных к производительности. To() был примерно в 4 раза медленнее, чем for в тесте. Тем не менее, это все еще хорошо для большинства случаев использования, когда производительность не критична. - person Vincent; 09.05.2016
comment
@wooohoh: нужно сделать несколько простых оптимизаций. Например, вы можете напрямую обрабатывать объект перечислителя, поддерживать свой собственный внутренний счетчик и возвращать результат только на каждом n-м шаге. Производительность, вероятно, все еще не будет фантастической, но вы бы вообще удалили вызов делегата. - person LukeH; 09.05.2016

В C# 6.0 с использованием

using static System.Linq.Enumerable;

вы можете упростить его до

foreach (var index in Range(1, 7))
{
    Console.WriteLine(index);
}
person Mike Tsayper    schedule 13.04.2016
comment
Я думаю, что 7 - это count:public static IEnumerable‹int› Range(int start,int count) - person user3717478; 10.10.2017
comment
сначала я не понял, что такое using static. Стоит упомянуть, что читабельнее писать: Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); - person Yitzchak; 30.07.2018

На самом деле вы можете сделать это на С# (предоставив To и Do в качестве методов расширения для int и IEnumerable<T> соответственно):

1.To(7).Do(Console.WriteLine);

SmallTalk навсегда!

person THX-1138    schedule 27.05.2009
comment
Хорошо — я предполагаю, что вы просто создадите пару методов расширения для To и Do? - person Marcel Lamothe; 27.05.2009
comment
Я думал, что синтаксис C++ плохой LOL - person Artyom; 27.05.2009
comment
Хммм... To и Do у меня не работают? Это часть какой библиотеки? Я на .Net 4.0. - person BlueVoodoo; 29.03.2012
comment
@BlueVoodoo это методы расширения, которые вам нужно определить. - person Brian Rasmussen; 17.07.2012
comment
Мне не нравится метод расширения Do. Будет яснее, если это произойдет внутри цикла foreach, как предлагали другие. - person Arturo Torres Sánchez; 01.12.2014
comment
Do будет методом расширения ForEach в C#, который MS решила не использовать. - person NetMage; 04.12.2020

Мне нравится идея. Это очень похоже на Python. Вот моя версия в несколько строк:

static class Extensions
{
    public static IEnumerable<int> To(this int from, int to, int step = 1) {
        if (step == 0)
            throw new ArgumentOutOfRangeException("step", "step cannot be zero");
        // stop if next `step` reaches or oversteps `to`, in either +/- direction
        while (!(step > 0 ^ from < to) && from != to) {
            yield return from;
            from += step;
        }
    }
}

Он работает как Python:

  • 0.To(4)[ 0, 1, 2, 3 ]
  • 4.To(0)[ 4, 3, 2, 1 ]
  • 4.To(4)[ ]
  • 7.To(-3, -3)[ 7, 4, 1, -2 ]
person Kache    schedule 10.10.2012
comment
Таким образом, диапазон Python является эксклюзивным для конца, как диапазоны C++ STL и D? Мне это нравится. Как новичок, я предпочитал first..last (конец-включительно), пока не начал работать с более сложными вложенными циклами и изображениями, наконец, осознав, насколько элегантным был end-exclusive (начало-конец), убрав все те +1 и -1, которые загрязняют код. Однако на обычном английском языке to имеет тенденцию означать включение, поэтому наименование ваших параметров start/end может быть более понятным, чем from/to. - person Dwayne Robinson; 27.01.2016
comment
!(step > 0 ^ from < to) можно было бы переписать в step > 0 == from < to. Или еще короче: используйте цикл do-while с конечным условием from += step != to. - person EriF89; 08.01.2018

Я думаю, что foreach + Enumerable.Range менее подвержен ошибкам (у вас меньше контроля и меньше способов сделать это неправильно, например, уменьшить индекс внутри тела, чтобы цикл никогда не заканчивался и т. д.)

Проблема удобочитаемости связана с семантикой функции диапазона, которая может меняться от одного языка к другому (например, если задан только один параметр, он будет начинаться с 0 или 1, или конец включен или исключен, или второй параметр является счетчиком, а не концом ценность).

Что касается производительности, я думаю, что компилятор должен быть достаточно умен, чтобы оптимизировать оба цикла, чтобы они выполнялись с одинаковой скоростью, даже с большими диапазонами (я полагаю, что Range создает не коллекцию, а, конечно, итератор).

person fortran    schedule 27.05.2009

Это кажется довольно длинным подходом к проблеме, которая уже решена. За Enumerable.Range стоит целая машина состояний, которая на самом деле не нужна.

Традиционный формат является основополагающим для развития и знаком всем. Я не вижу никаких преимуществ в твоем новом стиле.

person spender    schedule 27.05.2009
comment
В какой-то степени. Enumerable.Range становится таким же нечитаемым, когда вы хотите перейти от минимума к максимуму, потому что второй параметр — это диапазон, и его необходимо вычислить. - person spender; 27.05.2009
comment
Ах, хорошая мысль, но это другой случай (с которым я еще не сталкивался). Возможно, в этом сценарии поможет метод расширения с (start, end) вместо (start, count). - person Marcel Lamothe; 27.05.2009

Я думаю, что Range полезен для работы с некоторыми встроенными диапазонами:

var squares = Enumerable.Range(1, 7).Select(i => i * i);

Вы можете каждый над. Требуется преобразование в список, но сохраняет компактность, когда это то, что вам нужно.

Enumerable.Range(1, 7).ToList().ForEach(i => Console.WriteLine(i));

Но кроме чего-то подобного, я бы использовал традиционный цикл for.

person mcNux    schedule 29.05.2014
comment
Например, я только что сделал это в своем коде, чтобы создать набор динамических параметров для sql var paramList = String.Join(",", Enumerable.Range(0, values.Length).Select(i => "@myparam" + i)); - person mcNux; 29.05.2014
comment
Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); ЛУЧШЕ - person Yitzchak; 30.07.2018

Я хотел бы иметь синтаксис некоторых других языков, таких как Python, Haskell и т. д.

// Write the numbers 1 thru 7
foreach (int index in [1..7])
{
    Console.WriteLine(index);
}

К счастью, теперь у нас есть F# :)

Что касается C#, мне придется придерживаться метода Enumerable.Range.

person Thomas Danecker    schedule 30.05.2009
comment
В C# уже некоторое время существует диапазон docs.microsoft.com/en-us/dotnet/csharp/language-reference/ - person user1496062; 06.07.2020

@Luke: я повторно реализовал ваш метод расширения To() и использовал для этого метод Enumerable.Range(). Таким образом, получается немного короче и используется как можно больше инфраструктуры, предоставленной нам .NET:

public static IEnumerable<int> To(this int from, int to)
{ 
    return from < to 
            ? Enumerable.Range(from, to - from + 1) 
            : Enumerable.Range(to, from - to + 1).Reverse();
}
person Thorsten Lorenz    schedule 21.12.2009
comment
Я думаю, что .Reverse() в конечном итоге загрузит всю коллекцию в память при чтении первого элемента. (Если только Reverse не проверит базовый тип итератора и не скажет: «А, это объект Enumerator.Range. Я выдам объект с обратным счетом вместо своего обычного поведения».) - person billpg; 19.09.2013

Как использовать новый синтаксис сегодня

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

using static Enumerizer;

// prints: 0 1 2 3 4 5 6 7 8 9
foreach (int i in 0 <= i < 10)
    Console.Write(i + " ");

Не разница между <= и <.

Я также создал репозиторий для подтверждения концепции на GitHub с еще большей функциональностью ( обратная итерация, пользовательский размер шага).

Минимальная и очень ограниченная реализация вышеуказанного цикла будет выглядеть примерно так:

public readonly struct Enumerizer
{
    public static readonly Enumerizer i = default;

    public Enumerizer(int start) =>
        Start = start;

    public readonly int Start;

    public static Enumerizer operator <(int start, Enumerizer _) =>
        new Enumerizer(start);

    public static Enumerizer operator >(int _, Enumerizer __) =>
        throw new NotImplementedException();

    public static IEnumerable<int> operator <=(Enumerizer start, int end)
    {
        for (int i = start.Start; i < end; i++)
            yield return i;
    }

    public static IEnumerable<int> operator >=(Enumerizer _, int __) =>
        throw new NotImplementedException();
}
person Bruno Zell    schedule 03.09.2019

Я уверен, что у каждого есть свои личные предпочтения (многие предпочли бы последнее просто потому, что оно знакомо почти всем языкам программирования), но я, как и вы, начинаю любить foreach все больше и больше, особенно теперь, когда вы можете определить диапазон .

person TheTXI    schedule 27.05.2009

На мой взгляд, способ Enumerable.Range() более декларативен. Новое и незнакомое людям? Безусловно. Но я думаю, что этот декларативный подход дает те же преимущества, что и большинство других языковых возможностей, связанных с LINQ.

person MEMark    schedule 19.03.2012

Я предполагаю, что могут быть сценарии, в которых Enumerable.Range(index, count) более понятен при работе с выражениями для параметров, особенно если некоторые значения в этом выражении изменяются в цикле. В случае for выражение будет оцениваться на основе состояния после текущей итерации, тогда как Enumerable.Range() оценивается заранее.

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

person jerryjvl    schedule 27.05.2009

Я согласен с тем, что во многих (или даже в большинстве случаев) foreach намного читабельнее, чем стандартный цикл for при простом переборе коллекции. Однако ваш выбор использования Enumerable.Range(index, count) не является убедительным примером ценности foreach для.

Для простого диапазона, начинающегося с 1, Enumerable.Range(index, count), выглядит вполне читаемо. Однако, если диапазон начинается с другого индекса, он становится менее читаемым, потому что вам нужно правильно выполнить index + count - 1, чтобы определить, каким будет последний элемент. Например…

// Write the numbers 2 thru 8
foreach (var index in Enumerable.Range( 2, 7 ))
{
    Console.WriteLine(index);
}

В данном случае я предпочитаю второй пример.

// Write the numbers 2 thru 8
for (int index = 2; index <= 8; index++)
{
    Console.WriteLine(index);
}
person Dustin Campbell    schedule 27.05.2009
comment
Я согласен - похоже на комментарий Спендера об итерации от минимума к максимуму. - person Marcel Lamothe; 27.05.2009

Строго говоря, вы неправильно используете перечисление.

Enumerator предоставляет средства для доступа ко всем объектам в контейнере один за другим, но не гарантирует порядок.

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

Изменить: я ошибаюсь. Как отметил Люк (см. комментарии), безопасно полагаться на порядок при перечислении массива в С#. Это отличается, например, от использования for in для перечисления массива в Javascript.

person buti-oxa    schedule 27.05.2009
comment
Не уверен - я имею в виду, что метод Range построен для возврата возрастающей последовательности, поэтому я не уверен, как я полагаюсь на детали реализации. Можете ли вы (или кто-либо еще) уточнить? - person Marcel Lamothe; 27.05.2009
comment
Я не думаю, что обещано, что возвращаемая последовательность увеличивается. Конечно, это всегда на практике. Вы можете перечислить любую коллекцию. Для многих смысловых заказов много, и вы можете получить любой из них. - person buti-oxa; 27.05.2009
comment
@Marcel, @buti-oxa: процесс перечисления (например, с помощью foreach) сам по себе не гарантирует какого-либо конкретного порядка, но порядок определяется используемым перечислителем: встроенный перечислитель для Array, List‹› и т. д. всегда будет выполнять итерацию в порядке элементов; для SortedDictionary‹›, SortedList‹› и т. д. он всегда будет повторяться в порядке сравнения ключей; а для Dictionary‹›, HashSet‹› и т. д. нет гарантированного порядка. Я почти уверен, что вы можете положиться на то, что Enumerable.Range всегда выполняет итерацию в порядке возрастания. - person LukeH; 28.05.2009
comment
Конечно, перечислитель знает порядок, который он обеспечивает, поэтому, если я пишу перечислитель, я могу полагаться на порядок. Если компилятор/библиотека для меня, как я могу знать. Говорится ли где-нибудь в спецификации/документации, что встроенный перечислитель для массива всегда выполняет итерацию от 1 до N? Я не мог найти это обещание. - person buti-oxa; 28.05.2009
comment
@buti-oxa, взгляните на раздел 8.8.4 (стр. 240) спецификации C#3: для одномерных массивов элементы просматриваются в порядке возрастания индекса, начиная с индекса 0 и заканчивая индексом Длина — 1. ( скачать. microsoft.com/download/3/8/8/) - person LukeH; 29.05.2009

Мне нравится подход foreach + Enumerable.Range, и я иногда его использую.

// does anyone object to the new style over the traditional style?
foreach (var index in Enumerable.Range(1, 7))

Я возражаю против var злоупотреблений в вашем предложении. Я ценю var, но, черт возьми, просто напишите int в этом случае! ;-)

person xyz    schedule 27.05.2009
comment
Хех, а я все думал, когда же меня за это забанят :) - person Marcel Lamothe; 27.05.2009
comment
Я обновил свой пример, чтобы не отвлекать от основного вопроса. - person Marcel Lamothe; 27.05.2009

Просто бросаю свою шляпу на ринг.

Я определяю это...

namespace CustomRanges {

    public record IntRange(int From, int Thru, int step = 1) : IEnumerable<int> {

        public IEnumerator<int> GetEnumerator() {
            for (var i = From; i <= Thru; i += step)
                yield return i;
        }

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
    };

    public static class Definitions {

        public static IntRange FromTo(int from, int to, int step = 1)
            => new IntRange(from, to - 1, step);

        public static IntRange FromThru(int from, int thru, int step = 1)
            => new IntRange(from, thru, step);

        public static IntRange CountFrom(int from, int count)
            => new IntRange(from, from + count - 1);

        public static IntRange Count(int count)
            => new IntRange(0, count);

        // Add more to suit your needs. For instance, you could add in reversing ranges, etc.
    }
}

Затем везде, где я хочу его использовать, я добавляю это вверху файла...

using static CustomRanges.Definitions;

И использовать его так...

foreach(var index in FromTo(1, 4))
    Debug.WriteLine(index);
// Prints 1, 2, 3

foreach(var index in FromThru(1, 4))
    Debug.WriteLine(index);
// Prints 1, 2, 3, 4

foreach(var index in FromThru(2, 10, 2))
    Debug.WriteLine(index);
// Prints 2, 4, 6, 8, 10

foreach(var index in CountFrom(7, 4))
    Debug.WriteLine(index);
// Prints 7, 8, 9, 10

foreach(var index in Count(5))
    Debug.WriteLine(index);
// Prints 0, 1, 2, 3, 4

foreach(var _ in Count(4))
    Debug.WriteLine("A");
// Prints A, A, A, A

В этом подходе хорошо то, что по именам вы точно знаете, включен ли конец или нет.

person Mark A. Donohoe    schedule 10.01.2021