Думки про foreach з Enumerable.Range проти традиційного для циклу


75

У C # 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" мало, тож продуктивність не є проблемою, чи хтось заперечує проти нового стилю порівняно з традиційним?


1
Зараз це питання, включаючи РЕЗЮМЕ ВІД БАГАТО ВІДПОВІДЕЙ, ДІЙСНО актуальне, майже модель передової практики для питань у SO. Це має бути оформлено!
heltonbiker

4
@heltonbiker Неправильно --- насправді включення резюме не відповідає моделі SO. Короткий зміст слід видалити. ТАК як сайт із запитаннями та відповідями відокремлює питання та відповіді, а не має загальне уявлення про "публікацію".
Кіт Пінсон,

Поведінка обох відрізняється залежно від того, з якою версією .Net ви компілюєте. Версія циклу for може не завжди зберігати контекст виконання, особливо якщо у вас є оператор yield.
Суря Пратап,

Є ще одна перевага використання Range (): ви можете змінити значення індексу всередині циклу, і це не порушить ваш цикл.
Вінсент

1
З нетерпінням чекаємо на додавання нових функцій C # 8.0 до цієї теми! docs.microsoft.com/en-us/dotnet/csharp/whats-new/…
Ендрю

Відповіді:


58

Я вважаю, що формат "від мінімального до максимального" останнього набагато зрозуміліший, ніж Rangeстиль "мінімальний рахунок" для цієї мети. Крім того, я вважаю, що насправді не є гарною практикою вносити подібні зміни до норми, яка не швидша, не коротша, не більш звична і не очевидно чіткіша.

Тим не менш, я не проти ідеї загалом. Якби ви підійшли до мене із синтаксисом, який виглядав приблизно так, foreach (int x from 1 to 8)то я б, напевно, погодився, що це було б покращенням у порівнянні з forциклом. Однак Enumerable.Rangeце досить незграбно.


За допомогою іменованої функції аргументів C # 4 ми могли б додати метод розширення і викликати його так (зараз у мене немає хорошого імені для нього): foreach (int x в Enumerable.Range2 (від = 1, до = 8) ) {} Це краще чи гірше;)
Марсель Ламоте,

Вибачте, це "(від: 1, до: 8)"
Марсель Ламоте,

5
Особисто для мене це було б занадто багатослівно і незграбно - на відміну від циклу for), але я розумію, що це певною мірою справа смаку.
mqp

1
@mquander Це дуже схоже на синтаксис VB:For i = 1 To 8
ПАМ'ЯТКА,

1
Як щодо крадіжки foreach(var x in (1 to 8))у Scala? Це незліченний тип.
Mateen Ulhaq

44

Це просто для розваги. (Я б просто використовував стандартний 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);
}

загальнодоступний статичний IEnumerable <int> To (this int from, int to) {return Enumerable.Range (from, to); }
THX-1138,

2
@unknown, Enumerable.Range може рахувати лише вперед, моя версія також рахується назад. Ваш код також видає іншу послідовність, ніж моя: спробуйте "foreach (int i in 5.To (15))".
LukeH

Дуже добре, це те, що я хотів почути, коли розмістив запитання!
Марсель Ламоте,

Step () виглядає неправильно. 5.До (10). Крок (2) призведе до [6, 8, 10], коли очікується [5, 7, 9]. Крім того, Step () - це жахлива робота. Що робити, якщо крок 10?
Вінсент

1
@wooohoh: StepМетод працює коректно: iце індекс, а не значення. Ви маєте рацію, що продуктивність не є чудовою, але це демонстрація концепції, а не виробничий код.
LukeH

21

У C # 6.0 з використанням

using static System.Linq.Enumerable;

ви можете спростити це до

foreach (var index in Range(1, 7))
{
    Console.WriteLine(index);
}

Я думаю, що 7 - це кількість: public static IEnumerable <int> Range (int start, int count)
user3717478

1
спочатку я не розумів, що таке using static. Слід зазначити, що читабельніше писати:Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine);
Іцхак

9

Ви можете насправді зробити це в C # (надавши Toі Doяк методи розширення, intі IEnumerable<T>відповідно):

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

SmallTalk назавжди!


Приємно - я припускаю, ви просто створили б кілька методів розширення для "Кому" та "Зробити"?
Марсель Ламоте,

1
Я думав, що синтаксис C ++ поганий LOL
Артем

Хм-м-м ... "А" чи "Мені" не працює у мене? До якої бібліотеки це частина? Я працюю в .Net 4.0.
BlueVoodoo

2
@BlueVoodoo - це методи розширення, які вам потрібно визначити.
Брайан Расмуссен,

1
Мені не подобається Doметод розширення. Ясніше, якщо це входить у foreachцикл, як пропонували інші.
Артуро Торрес Санчес

8

Мені така ідея подобається. Це дуже схоже на 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 ]

Отже, діапазон Python є ексклюзивним, як діапазони C ++ STL та D? Мені це подобається. Як новачок, я віддавав перевагу first..last (end-inclusive), поки не працював із більш складними вкладеними циклами та зображеннями, нарешті зрозумівши, наскільки елегантним був end-exclusive (begin end), усунувши всі ті +1 і -1, які забруднилися код. У звичайній англійській мові, "to" має тенденцію означати включно, тому називання параметрів "start" / "end" може бути зрозумілішим, ніж "від" / "до".
Дуейн Робінсон

!(step > 0 ^ from < to)може бути переписаний на step > 0 == from < to. Або ще більш лаконічно: використовуйте цикл виконуваної роботи з кінцевим умовоюfrom += step != to
EriF89,

7

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

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

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


7

Здається, досить тривалий підхід до проблеми, яка вже вирішена. За цілою машиною стоїть ціла державна машина, Enumerable.Rangeяка насправді не потрібна.

Традиційний формат є фундаментальним для розробки і знайомий всім. Я насправді не бачу жодної переваги у вашому новому стилі.


2
Певним чином. Enumerable.Range стає настільки ж нечитабельним, коли ви хочете виконати ітерацію від хв до макс., Оскільки другий параметр є діапазоном і його потрібно обчислити.
Спендер

Ах, добре, але це інший випадок (на який я ще не натрапляв). Можливо, метод розширення з (start, end) замість (start, count) допоможе в цьому сценарії.
Марсель Ламоте,

6

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

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

Ви можете кожен. Потрібно перетворення в список, але зберігає компактність, коли це те, що ви хочете.

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

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


Наприклад, я щойно зробив це у своєму коді, щоб створити набір динамічних параметрів для sqlvar paramList = String.Join(",", Enumerable.Range(0, values.Length).Select(i => "@myparam" + i));
mcNux

Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); КРАЩЕ
Іцхак

6

Я хотів би мати синтаксис деяких інших мов, таких як Python, Haskell тощо.

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

На жаль, ми отримали F # зараз :)

Що стосується C #, мені доведеться дотримуватися Enumerable.Rangeметоду.


У C # вже деякий час існує діапазон docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
user1496062

5

@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();
}

1
Я думаю, що .Reverse () закінчить завантаження всієї колекції в пам’ять при читанні першого елемента. (Якщо Reverse не перевірить базовий тип ітератора і не скаже "Ах, це об'єкт Enumerator.Range. Я випромінюю об'єкт із зворотним рахунком замість моєї нормальної поведінки")
billpg

3

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


3

На мій погляд, Enumerable.Range()спосіб є більш декларативним. Новий і незнайомий людям? Звичайно. Але я думаю, що цей декларативний підхід приносить ті самі переваги, що й більшість інших мовних функцій, пов'язаних з LINQ.


3

Як використовувати новий синтаксис сьогодні

Через це запитання я спробував деякі речі, щоб придумати гарний синтаксис, не чекаючи першокласної мовної підтримки. Ось що я маю:

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();
}

2

Я думаю, що можуть бути сценарії, де Enumerable.Range(index, count)зрозуміліше, коли йдеться про вирази для параметрів, особливо якщо деякі значення у цьому виразі змінюються в циклі. У випадку forвиразу буде обчислюватися на основі стану після поточної ітерації, тоді Enumerable.Range()як обчислюється вперед.

Окрім цього, я б погодився, що дотримуватися for, як правило, було б краще (більш звичним / читабельним для більшої кількості людей ... читабельність - дуже важлива цінність у коді, яку потрібно підтримувати).


2

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

Для простого діапазону, починаючи з 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);
}

Я погоджуюсь - схоже на коментар Спендера щодо ітерації від хв до макс.
Марсель Ламоте

1

Власне кажучи, ви зловживаєте переліком.

Перерахувач надає засоби доступу до всіх об’єктів у контейнері по одному, але це не гарантує порядок.

Добре використовувати перерахування, щоб знайти найбільше число в масиві. Якщо ви використовуєте його для пошуку, скажімо, першого ненульового елемента, ви покладаєтесь на деталі реалізації, про які ви не повинні знати. У вашому прикладі порядок здається вам важливим.

Редагувати : я помиляюся. Як зазначив Лука (див. Коментарі), безпечно покладатися на порядок при перерахуванні масиву в C #. Це відрізняється від, наприклад, використання "for in" для перерахування масиву в Javascript.


Не впевнений - я маю на увазі, що метод Range побудований для повернення послідовності, що збільшується, тому я не впевнений, як я покладаюся на деталь реалізації. Чи можете ви (або хтось інший) пояснити?
Марсель Ламоте

Я не думаю, що обіцяно, що повернута послідовність збільшується. Звичайно, це завжди є на практиці. Ви можете перерахувати будь-яку колекцію. Для багатьох існує багато замовлень, які мають сенс, і ви можете отримати будь-яке з них.
buti-oxa

2
@Marcel, @ Буті-окс: Процес підрахунку (наприклад, з Еогеасп) не гарантує яку - або конкретна впорядкованості самого по собі, але порядку буде визначатися лічильнику використовується: Вбудований Нумератор для Array, List <> etc завжди буде повторювати порядок елементів; для SortedDictionary <>, SortedList <> тощо він завжди буде повторюватись у порядку порівняння ключів; а для Dictionary <>, HashSet <> тощо немає гарантованого впорядкування. Я впевнений, що ви можете покластися на Enumerable.Range завжди ітерації у порядку зростання.
LukeH

Звичайно, перелічувач знає порядок, який він надає, тому, якщо я напишу перерахувач, я можу покластися на замовлення. Якщо компілятор / бібліотека це для мене, як я можу це знати. Чи сказано десь у специфікації / документації, що вбудований перечислювач для масиву завжди повторює від 1 до N? Я не міг знайти цю обіцянку.
buti-oxa

2
@ buti-oxa, Погляньте на розділ 8.8.4 (сторінка 240) специфікації C # 3: "Для одновимірних масивів елементи обводяться у зростаючому порядку індексу, починаючи з індексу 0 і закінчуючи довжиною індексу - 1. " ( download.microsoft.com/download/3/8/8/… )
LukeH

1

Мені подобається підхід 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у цьому випадку! ;-)


Хех, мені було цікаво, коли я збирався за це поспішати :)
Марсель Ламоте

Я оновив свій приклад, щоб не стримувати головне питання.
Марсель Ламоте,

0

Просто кидаю мій капелюх на ринг.

Я визначаю це ...

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

Приємним у цьому підході є імена, ви точно знаєте, включений кінець чи ні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.