关于带有Enumerable.Range vs传统for循环的foreach的思考


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模型。摘要应删除。因此,作为问答站点将问题和答案分开,而不是具有“发布”的一般概念。
Keith Pinson

两者的行为都不同,具体取决于您所编译的.Net版本。for循环版本可能并不总是保留执行上下文,尤其是在您具有yield语句的情况下。
Surya Pratap 2014年

使用Range()的另一个好处是:您可以在循环内更改index的值,并且不会破坏循环。
文森特

1
期待为此线程增加新的C#8.0功能! docs.microsoft.com/en-us/dotnet/csharp/whats-new/…–
安德鲁(Andrew)

Answers:


58

我发现后者的“最小到最大”格式比Range“最小计数”样式更清晰。此外,我认为从规范中进行这样的更改(不是更快,更短,更不熟悉,也不很明显)并不是真正的好习惯。

也就是说,我总体上并不反对这个想法。如果您提出的语法看起来像是foreach (int x from 1 to 8)我,那么我可能会同意这将是对for循环的改进。但是,Enumerable.Range是很笨重的。


使用C#4的命名参数功能,我们可以添加一个扩展方法并像这样调用它(我现在没有一个好名字):foreach(Enumerable.Range2中的int x(从= 1到= 8)) ){}是好还是坏;)
马塞尔·拉莫特

抱歉,这是“(从:1到:8)”
Marcel Lamothe

5
就我个人而言,这对于我来说太冗长和笨拙了(而不是for循环),但我知道这在某种程度上是个品味问题。
mqp,2009年

1
@mquander看起来很像VB的语法:For i = 1 To 8
MEMark 2012年

1
foreach(var x in (1 to 8))从Scala偷东西怎么样?这是一个Enumerable类型。
Mateen Ulhaq '16

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){返回Enumerable.Range(from,to); }
THX-1138,2009年

2
@ unknown,Enumerable.Range只能向前计数,我的版本也可以向后计数。您的代码还会产生不同的序列:请尝试“ foreach(5.To(15)中的int i)”。
路加福音

很好,这是我发布问题时想要听到的事情!
马塞尔·拉莫特

Step()看起来不对。5.To(10)。如果期望[5、7、9],则步骤(2)将导致[6、8、10]。此外,Step()是明智的性能选择。如果步长是10,该怎么办?
文森特

1
@wooohoh:该Step方法正常工作:i是索引,而不是值。没错,性能不是很好,但这是概念验证的演示,而不是生产代码。
路加福音

21

在C#6.0中配合使用

using static System.Linq.Enumerable;

您可以简化为

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

我认为7是count:public static IEnumerable <int> Range(int start,int count)
user3717478

1
起初我不知道是什么using static。应该提到的是更易读的内容:Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine);
Yitzchak

9

您实际上可以在C#中执行此操作(分别在和上提供ToDo作为扩展方法):intIEnumerable<T>

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

永远的SmallTalk!


很好-我假设您会为“ To”和“ Do”创建几个扩展方法?
马塞尔·拉莫特

1
我以为C ++语法不好,哈哈
Artyom

嗯... To and Do对我不起作用?这是哪个库的一部分?我在.Net 4.0上。
BlueVoodoo 2012年

2
@BlueVoodoo它们是您需要定义的扩展方法。
Brian Rasmussen 2012年

1
我不喜欢Do扩展方法。更清楚的是,是否foreach像其他人所建议的那样进入循环。
Arturo TorresSánchez2014年

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(包括结尾),直到处理更复杂的嵌套循环和图像为止,最后我通过消除所有弄脏的+1和-1来意识到结尾(排除结尾)的优雅程度。编码。不过,在普通英语中,“至”往往意味着包含性,因此将参数命名为“开始” /“结束”可能比“从” /“至”更清晰。
Dwayne Robinson

!(step > 0 ^ from < to)可以改写成step > 0 == from < to。甚至更简洁:使用带有结束条件的do-while循环from += step != to
EriF89 '18八月

7

我认为foreach + Enumerable.Range不太容易出错(您的控制较少,错误处理的方式也较少,例如减少体内的索引,使循环永远不会结束,等等)。

可读性问题与范围函数语义有关,它可以从一种语言更改为另一种语言(例如,如果仅给出一个参数,它将从0或1开始,或者是包含或排除的结尾,还是第二个参数是计数而不是结尾)值)。

关于性能,我认为编译器应该足够聪明来优化两个循环,以便即使在较大范围内,它们也能以相似的速度执行(我想Range不会创建集合,而是创建迭代器)。


7

对于已经解决的问题,似乎采用了漫长的方法。背后有整个状态机Enumerable.Range真正不需要。

传统格式是开发的基础,也是所有人都熟悉的格式。我真的看不到您的新样式有任何优势。


2
在某些方面。当您要从最小值迭代到最大值时,Enumerable.Range变得不可读,因为第二个参数是一个范围,需要计算。
支出者

嗯,很好,但这是另一种情况(我还没有遇到)。在这种情况下,使用(开始,结束)而不是(开始,计数)的扩展方法也许会有所帮助。
马塞尔·拉莫特

6

我认为Range对于使用某些范围内联很有用:

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

你们都可以结束。需要转换为列表,但在需要时保持紧凑。

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

但是除了类似这样的事情,我会使用传统的for循环。


例如,我只是在我的代码中这样做,为sql建立了一组动态参数var paramList = String.Join(",", Enumerable.Range(0, values.Length).Select(i => "@myparam" + i));
mcNux 2014年

Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); 更好
Yitzchak '18


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

我同意-与Spender关于从最小迭代到最大迭代的评论类似。
马塞尔·拉莫特

1

严格来说,您滥用枚举。

枚举器提供了一种访问容器中所有对象的方法,但是不能保证顺序。

可以使用枚举查找数组中的最大数字。如果要使用它来查找第一个非零元素,那么您将依赖您不应该知道的实现细节。在您的示例中,顺序似乎对您很重要。

编辑:我错了。正如Luke指出的那样(请参见注释),在C#中枚举数组时可以依赖顺序。这与例如使用“ for in”枚举Javascript中的数组不同。


不确定-我的意思是Range方法是为返回递增序列而构建的,因此我不确定我如何依赖实现细节。您(或其他任何人)可以详细说明吗?
Marcel Lamothe

我不认为返回的序列会递增。当然,它总是在实践中。您可以枚举任何集合。对于许多人来说,有很多有意义的订单,您可能会得到其中的任何一个。
buti-oxa

2
@Marcel,@ buti-oxa:枚举过程(例如,使用foreach)本身并不保证任何特定的顺序,但是该顺序由所使用的枚举确定:Array的内置枚举器List <> etc总是按元素顺序进行迭代;对于SortedDictionary <>,SortedList <>等,它将始终按键比较顺序进行迭代;对于Dictionary <>,HashSet <>等,没有保证的顺序。我非常有信心您可以依靠Enumerable.Range始终以升序进行迭代。
路加福音

当然,枚举器知道它提供的顺序,因此,如果我编写枚举器,则可以依靠顺序。如果编译器/库适合我,我怎么知道。它是否在spec / documentation中某个地方说数组的内置枚举器总是从1迭代到N?我找不到这个承诺。
buti-oxa

2
@ buti-oxa,请看C#3规范的8.8.4节(第240页):“对于一维数组,元素以递增的索引顺序遍历,从索引0开始,以索引Length – 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在这种情况下,请写信!;-)


嘿,我想知道我什么时候该死:)
Marcel Lamothe

我更新了示例,以免影响主要问题。
马塞尔·拉莫特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.