对传统的循环范围与枚举范围的思考

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

63223 次浏览

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

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

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

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

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

为此,我发现后者的“从最小到最大”格式比 Range的“最小计数”格式要清楚得多。此外,我不认为这是一个真正的好做法,使这样的变化从标准,不是更快,不是更短,不是更熟悉,不明显更清楚。

总的来说,我并不反对这个想法。如果你来找我的语法看起来像 foreach (int x from 1 to 8),那么我可能会同意,这将是一个改进的 for循环。然而,Enumerable.Range相当笨重。

我同意,在许多情况下(甚至大多数情况下) ,在简单地迭代一个集合时,foreach比标准的 for循环更具可读性。但是,选择使用 Enumerable.Range(index, count)并不能很好地说明 foreach over 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);
}

我认为 foreach + Enumable。范围是较少的错误倾向(你有更少的控制和更少的方法来做它错误,像减少体内的索引,所以循环永远不会结束,等等)

可读性问题与 Range 函数语义有关,它可以从一种语言转换到另一种语言(例如,如果只给出一个参数,它将从0或1开始,或者结束是包含或排除的,或者第二个参数是一个计数而不是一个结束值)。

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

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

这只是为了好玩。(我只是使用标准的“ 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);
}

实际上,您可以在 C # 中完成这项工作(分别在 intIEnumerable<T>上提供 ToDo作为扩展方法) :

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

SmallTalk 永远!

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

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

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

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

我希望有一些其他语言的语法,比如 Python、 Haskell 等等。

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

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

至于 C # ,我必须坚持使用 Enumerable.Range方法。

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

在我看来,Enumerable.Range()的方式更具有陈述性。新的和陌生的人?当然可以。但是,我认为这种声明性方法与大多数其他与 LINQ 相关的语言特性产生了相同的好处。

我有点喜欢这个主意。它非常像 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 ]

我认为 Range 对于处理一些 Range 内联是有用的:

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

你可以每个。需要转换为列表,但保持紧凑的东西,当这是你想要的。

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

但是除了像这样的东西,我会使用传统的 for 循环。

在 C # 6.0中使用

using static System.Linq.Enumerable;

你可以把它简化成

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

今天如何使用一种新的语法

因为这个问题,我尝试了一些东西来提出一个不错的语法,而不需要等待一流的语言支持。这就是我所知道的:

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

我只是想参加比赛。

我定义这个..。

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

这种方法的好处是通过名称,您可以准确地知道是否包含目标。

正如 Nick Chapsas 在他出色的 YouTube 视频。中指出的那样,传统迭代和范围迭代之间没有显著的性能差异,甚至基准测试也表明,对于少量迭代,在纳秒之间存在一些差异。随着循环变得越来越大,差异几乎消失了。

这里有一种优雅的方法,可以根据他的内容在范围循环中进行迭代:

private static void Test()
{
foreach (var i in 1..5)
{


}
}

使用这个扩展:

public static class Extension
{
public static CustomIntEnumerator GetEnumerator(this Range range)
{
return new CustomIntEnumerator(range);
}
public static CustomIntEnumerator GetEnumerator(this int number)
{
return new CustomIntEnumerator(new Range(0, number));
}
}


public ref struct CustomIntEnumerator
{
private int _current;
private readonly int _end;


public CustomIntEnumerator(Range range)
{
if (range.End.IsFromEnd)
{
throw new NotSupportedException();
}


_current = range.Start.Value - 1;
_end = range.End.Value;
}


public int Current => _current;


public bool MoveNext()
{
_current++;
return _current <= _end;
}
}

基准结果: Benchmark result

我喜欢这种实现方式。但是,这种方法最大的问题是它不能在异步方法中使用它。