【问题标题】:Thoughts on foreach with Enumerable.Range vs traditional for loop关于 Enumerable.Range 与传统 for 循环的 foreach 的思考
【发布时间】:2010-10-29 06:45:07
【问题描述】:

在 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”很小,所以性能不是问题,有人反对新风格而不是传统风格吗?

【问题讨论】:

  • 现在这个问题,包括来自许多答案的总结,真的很相关,几乎是 SO 中问题的良好实践模型。应该装框!
  • @heltonbiker 错误——实际上包含摘要不适合 SO 模型。应删除摘要。 SO 作为问答网站将问题和答案分开,而不是有一个“帖子”的一般概念。
  • 两者的行为会有所不同,具体取决于您正在编译的 .Net 版本。 for 循环版本可能并不总是保留执行上下文,特别是如果您有一个 yield 语句。
  • 使用 Range() 还有一个好处:你可以在循环内改变 index 的值,而且不会破坏你的循环。
  • 期待在此线程中添加新的 C# 8.0 功能! docs.microsoft.com/en-us/dotnet/csharp/whats-new/…

标签: c# .net c#-3.0 for-loop


【解决方案1】:

只是把我的帽子扔进戒指。

我定义这个...

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

这种方法的好处在于名称,您可以确切地知道是否包含结尾。

【讨论】:

    【解决方案2】:

    今天如何使用新语法

    由于这个问题,我尝试了一些方法来提出一个好的语法,而无需等待一流的语言支持。这是我所拥有的:

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

    不是&lt;=&lt; 之间的区别。

    我还创建了一个proof of concept repository on 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();
    }
    

    【讨论】:

      【解决方案3】:

      对于已经解决的问题,这似乎是一种冗长的方法。 Enumerable.Range 后面有一个不需要的完整状态机。

      传统格式是开发的基础,并且为所有人所熟悉。我真的看不出你的新风格有什么好处。

      【讨论】:

      • 在某些方面。当您想从最小值迭代到最大值时,Enumerable.Range 变得同样不可读,因为第二个参数是一个范围,需要计算。
      • 啊,好点,但那是另一种情况(我还没有遇到过)。在这种情况下,也许使用 (start, end) 而不是 (start, count) 的扩展方法会有所帮助。
      【解决方案4】:

      在我看来,Enumerable.Range() 方式更具声明性。对人们来说是新的和不熟悉的?当然。但我认为这种声明性方法与大多数其他与 LINQ 相关的语言功能具有相同的好处。

      【讨论】:

        【解决方案5】:

        我同意在许多(甚至大多数情况下)foreach 在简单地迭代集合时比标准的for-loop 更具可读性。但是,您选择使用 Enumerable.Range(index, count) 并不是 foreach 价值超过 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 关于从最小值迭代到最大值的评论。
        【解决方案6】:

        我确实喜欢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! ;-)

        【讨论】:

        • 嘿,我想知道我什么时候会被那个 :)
        • 我更新了我的示例,以免影响主要问题。
        【解决方案7】:

        @卢克: 我重新实现了您的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();
        }
        

        【讨论】:

        • 我认为 .Reverse() 最终会在读取第一项时将整个集合加载到内存中。 (除非 Reverse 检查迭代器的底层类型并说“啊,这是一个 Enumerator.Range 对象。我将发出一个向后计数的对象,而不是我的正常行为。”)
        【解决方案8】:

        我想要一些其他语言的语法,例如 Python、Haskell 等。

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

        幸运的是,我们现在得到了 F# :)

        至于 C#,我将不得不坚持使用 Enumerable.Range 方法。

        【讨论】:

        【解决方案9】:

        我有点喜欢这个主意。它非常像 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),直到使用更复杂的嵌套循环和图像,最终通过消除所有那些脏的 +1 和 -1 来意识到 end-exclusive(begin end)是多么优雅代码。不过,在普通英语中,“to”往往意味着包容,因此将参数命名为“start”/“end”可能比“from”/“to”更清晰。
        • !(step &gt; 0 ^ from &lt; to) 可以重写为step &gt; 0 == from &lt; to。或者更简洁:使用带有 endcondition from += step != to 的 do-while 循环
        【解决方案10】:

        在 C# 6.0 中使用

        using static System.Linq.Enumerable;
        

        你可以把它简化为

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

        【讨论】:

        • 我认为 7 是计数:public static IEnumerable Range(int start,int count)
        • 一开始我不明白using static是什么。应该提到写起来更具可读性:Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine);
        【解决方案11】:

        我认为 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 =&gt; "@myparam" + i));建立一组动态参数
        • Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); 更好
        【解决方案12】:

        您实际上可以在 C# 中执行此操作(通过分别在 intIEnumerable&lt;T&gt; 上提供 ToDo 作为扩展方法):

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

        SmallTalk 永远!

        【讨论】:

        • 很好 - 我假设您只需为“To”和“Do”创建几个扩展方法?
        • 我认为 C++ 语法不好 LOL
        • 嗯... To and Do 对我不起作用?这是哪个图书馆的一部分?我在 .Net 4.0 上。
        • @BlueVoodoo 它们是您需要定义的扩展方法。
        • 我不喜欢Do 扩展方法。如果它像其他人建议的那样进入foreach 循环,那就更清楚了。
        【解决方案13】:

        严格来说,你滥用了枚举。

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

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

        编辑:我错了。正如 Luke 指出的(参见 cmets),在 C# 中枚举数组时依赖顺序是安全的。这与例如在 Javascript 中使用“for in”枚举数组不同。

        【讨论】:

        • 不确定 - 我的意思是,Range 方法被构建为返回一个递增序列,所以我不确定我是如何依赖实现细节的。你(或其他任何人)能详细说明吗?
        • 我不认为返回的序列是递增的。当然,它总是在实践中。您可以枚举任何集合。对许多人来说,有很多订单是有意义的,你可能会得到其中任何一个。
        • @Marcel, @buti-oxa:枚举过程(例如,使用 foreach)本身并不保证任何特定的排序,但排序决定使用的枚举器:Array、List 等的内置枚举器将始终按元素顺序迭代;对于 SortedDictionary、SortedList 等,它将始终以键比较顺序进行迭代;对于 Dictionary、HashSet 等,没有保证顺序。我非常有信心您可以依赖 Enumerable.Range 始终按升序迭代。
        • 当然,枚举器知道它提供的顺序,所以如果我写枚举器,我可以依赖顺序。如果编译器/库对我有用,我怎么知道。它是否在规范/文档中的某处说数组的内置枚举器总是从 1 迭代到 N?我找不到这个承诺。
        • @buti-oxa,查看 C#3 规范的第 8.8.4 节(第 240 页):“对于单维数组,元素以递增的索引顺序遍历,从索引 0 开始并以索引长度 - 1 结尾。” (download.microsoft.com/download/3/8/8/…)
        【解决方案14】:

        这只是为了好玩。 (我自己会使用标准的“for (int i = 1; i &lt;= 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);
        }
        

        【讨论】:

        • public static IEnumerable To(this int from, int to) { return Enumerable.Range(from, to); }
        • @unknown, Enumerable.Range 只能向前计数,我的版本也向后计数。您的代码还会生成一个不同的序列来挖掘:尝试“foreach (int i in 5.To(15))”。
        • 很好,这是我发布问题时想听到的内容!
        • Step() 看起来不对。 5.To(10).Step(2) 在预期为 [ 5, 7, 9 ] 时将导致 [ 6, 8, 10 ]。另外, Step() 在性能方面非常糟糕。如果步长是 10 怎么办?
        • @wooohoh:Step 方法工作正常:i 是索引,而不是值。你是对的,性能不是很好,但这是一个概念验证演示,而不是生产代码。
        【解决方案15】:

        我认为 foreach + Enumerable.Range 不太容易出错(您的控制权和出错的方法更少,比如减少主体内的索引,这样循环就永远不会结束,等等)

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

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

        【讨论】:

          【解决方案16】:

          为此,我发现后者的“最小到最大”格式比Range 的“最小计数”样式要清晰得多。另外,我认为从不更快、不更短、不更熟悉、不明显更清晰的规范进行这样的改变并不是一个好习惯。

          也就是说,总的来说,我并不反对这个想法。如果您向我提出类似于foreach (int x from 1 to 8) 的语法,那么我可能会同意这将是对for 循环的改进。但是,Enumerable.Range 相当笨拙。

          【讨论】:

          • 使用 C# 4 的命名参数功能,我们可以添加一个扩展方法并像这样调用它(我现在没有一个好名字): foreach (int x in Enumerable.Range2( from = 1, to = 8)) {} 好还是坏 ;)
          • 对不起,这是“(从:1,到:8)”
          • 就我个人而言,这对我来说太冗长和笨拙了(与 for 循环相反),但我知道这在某种程度上是一个品味问题。
          • @mquander 这看起来很像 VB 的语法:For i = 1 To 8
          • 从 Scala 窃取foreach(var x in (1 to 8)) 怎么样?它是一个 Enumerable 类型。
          【解决方案17】:

          我想在处理参数表达式时,Enumerable.Range(index, count) 可能会更清晰,特别是如果该表达式中的某些值在循环内被更改。在 for 的情况下,表达式将根据当前迭代后的状态进行评估,而 Enumerable.Range() 是预先评估的。

          除此之外,我同意坚持使用for 通常会更好(对更多人来说更熟悉/更易读......可读性是需要维护的代码中一个非常重要的值)。

          【讨论】:

            【解决方案18】:

            我相信每个人都有自己的个人喜好(很多人更喜欢后者,因为它比几乎所有编程语言都熟悉),但我和你一样,开始越来越喜欢 foreach,尤其是现在你可以定义一个范围。

            【讨论】:

              猜你喜欢
              • 2017-03-01
              • 2015-07-25
              • 1970-01-01
              • 2012-12-05
              • 2018-08-10
              • 1970-01-01
              • 1970-01-01
              • 2016-02-26
              相关资源
              最近更新 更多