在现代软件开发的动态环境中,高效的数据处理是创建高性能应用程序的核心。作为开发人员,我们在编写代码时不断努力平衡可读性、可维护性和速度。在 C#(一种以其多功能性和健壮性而闻名的编程语言)领域,循环成为无缝导航集合的强大工具。foreach
foreach 是 C# 中的语句,用于生成遍历集合项所需的代码。语法非常简单:
foreach(var item in source)
{
Console.WriteLine(item);
}
本文将重点介绍编译器在给定各种类型的集合的情况下生成的代码,以便您可以准确了解应用程序在使用此类循环时正在执行的操作。
要使语句接受集合作为其源,该集合必须提供一个名为“返回枚举器对象的新实例”的公共无参数方法。然后,枚举器必须提供一个名为 的公共无参数方法,该方法返回 a 和一个名为 的公共属性。foreachGetEnumerator()MoveNext()boolCurrent
枚举器必须维护枚举的状态。如果下一个项目可用,则该方法应返回;否则。该属性应返回当前位置的项。MoveNext()truefalseCurrent
例如,下面是一个集合,该集合将其项存储在内部数组中,并使用语句实现遍历所需的最小值:foreach
class MyCollection
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public Enumerator GetEnumerator()
=> new Enumerator(this);
public struct Enumerator
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
public bool MoveNext()
=> ++index < source.Length;
}
}
枚举器应始终是结构体(值类型)以提高性能!查看我的另一篇文章“C# 中值类型与引用类型枚举器的性能”以了解原因。
编译器将 C# 代码转换为中间语言 (IL)。使用 SharpLab,您可以看到在这种情况下,foreach 被转换为等效于以下内容的内容:foreach
MyCollection.Enumerator enumerator = new MyCollection(array).GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
它首先调用以获取枚举器的新实例。然后,它用作循环中的延续条件,并且每次返回 时,都会使用 获取项。GetEnumerator()MoveNext()whiletrueCurrent
如果枚举器必须在项遍历结束时释放任何资源,则必须实现 。您可以在 SharpLab 中看到,在这种情况下,生成的代码将确保 被调用:IDisposableDispose()
MyCollection.Enumerator enumerator = new MyCollection(array).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
finally
{
((IDisposable)enumerator).Dispose();
}
该语句支持按引用返回项目。通过引用传递项可提高遍历包含大型结构的集合时的性能,因为它不会复制每个项。它只会传递对每个它们的引用。foreach
例如,我们可以将属性更改为返回:Currentref int
class MyCollection
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public Enumerator GetEnumerator()
=> new Enumerator(this);
public struct Enumerator
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public ref int Current // return by reference
=> ref source[index];
public bool MoveNext()
=> ++index < source.Length;
public void Dispose() {}
}
}
在本例中,返回对存储项位置的引用。这样,您既可以使用 初始化实例的值,也可以将其项目列到控制台:CurrentMyCollectionforeach
var source = new MyCollection(new int[10]);
// initialize all to ones
foreach(ref var item in source)
item = 1;
// output to console
foreach(ref readonly var item in source)
Console.WriteLine(item);
请注意第一个关键字,以便可以更改项目值。第二个使用关键字,以便无法为项目分配新值。您可以在 SharpLab 中看到它的工作。refforeachforeachref readonly
可以将属性更改为返回,从而无法为项赋值。Currentref readonly
如果希望枚举器包含 span 字段,则可以将枚举器声明为 .示例集合可以按如下方式实现:ref struct
class MyCollection
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public Enumerator GetEnumerator()
=> new Enumerator(this);
public ref struct Enumerator // ref struct enumerator
{
readonly ReadOnlySpan<int> source; // readonly span field
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public ref readonly int Current // return by reference
=> ref source[index];
public bool MoveNext()
=> ++index < source.Length;
public void Dispose() {}
}
}
注意:A 无法实现接口。在这种情况下,将调用该方法(如果存在)。不需要枚举器实现 .ref structforeachDispose()IDisposable
从 C# 6 开始,还支持将 用作扩展方法。foreachGetEnumerator()
假设您想在第三方开发的以下集合上使用:foreach
public class MyCollection
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public int Count
=> source.Length;
public int this[int index]
=> source[index];
}
此集合提供一个索引器和一个返回项数的属性。如果尝试使用它,编译将失败。Countforeach
然后,可以为集合定义以下扩展方法:
public static class MyExtension
{
public static Enumerator GetEnumerator(this MyCollection source)
=> new Enumerator(source);
public struct Enumerator
{
readonly MyCollection source;
int index;
public Enumerator(MyCollection source)
{
this.source = source;
index = -1;
}
public int Current
=> source[index];
public bool MoveNext()
=> ++index < source.Count;
}
}
您可以在 SharpLab 中看到该 语句已编译。您还可以看到生成的代码非常相似。唯一的区别是它使用了扩展方法:foreach
MyExtensions.Enumerator enumerator = MyExtensions.GetEnumerator(new MyCollection(array))
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
};
IEnumerable是在命名空间中定义的接口,它实际强制执行语句所需的模式。因此,可以使用该语句遍历实现的任何类型。System.CollectionsforeachIEnumerableforeach
例如,下面是现在正在实现的示例集合:IEnumerable
class MyCollection : IEnumerable
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public IEnumerator GetEnumerator()
=> new Enumerator(this);
public struct Enumerator : IEnumerator
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
// public property
public int Current
=> source[index];
// explicit IEnumerator implementation
object IEnumerator.Current
=> Current;
public bool MoveNext()
=> ++index < source.Length;
public void Reset()
=> index = -1;
}
}
唯一的区别是:
请注意,该属性需要返回类型 。我希望它返回,因为它是此集合的项目类型。可以同时实现两种实现,一种是公共的,另一种是使用显式接口实现。仅当枚举器被强制转换为 时,才使用显式实现属性。IEnumeratorCurrentobjectintIEnumerator
注意:如果枚举器不支持重置,则应引发 NotSupportedException。
您可以在 SharpLab 中看到 foreach 语句生成的代码如下:
IEnumerator enumerator = new MyCollection(array).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
Console.WriteLine((int)enumerator.Current);
}
}
finally
{
IDisposable disposable = enumerator as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
需要注意的几件事:
我在上面提到过,枚举器应该有一个值类型的枚举器以获得更好的性能。我们在这里看到,通过返回 ,枚举器是盒装的,这会将其转换为引用类型。解决此问题的方法是也对以下方法使用显式接口实现:IEnumeratorGetEnumerator()
class MyCollection : IEnumerable
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
// public method
public Enumerator GetEnumerator()
=> new Enumerator(this);
// explicit IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public struct Enumerator : IEnumerator
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
object IEnumerator.Current
=> Current;
public bool MoveNext()
=> ++index < source.Length;
public void Reset()
=> index = -1;
}
}
您可以在 SharpLab 中看到,现在为语句生成的代码如下:foreach
MyCollection.Enumerator enumerator = new MyCollection(array).GetEnumerator()
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
};
它使用值类型枚举器。仅当集合被强制转换为 时,它才会使用引用类型枚举器。IEnumerable
注意:.NET 提供的所有集合都提供值类型枚举器。如果实现自己的集合,则应执行相同的操作。
正如我在上一篇文章中所解释的,并扩展接口对并指定属性返回的项的类型。IEnumerable<T>IEnumerator<T>IEnumerableIEnumeratorCurrent
因为派生自 ,并且派生自 和 ,所以示例集合应按如下方式实现:IEnumerable<T>IEnumerableIEnumerator<T>IEnumeratorIDispose
class MyCollection : IEnumerable<int>
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
// public method
public Enumerator GetEnumerator()
=> new Enumerator(this);
// explicit IEnumerable<T> implementation
IEnumerator<int> IEnumerable<int>.GetEnumerator()
=> GetEnumerator();
// explicit IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public struct Enumerator : IEnumerator<int>
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
object IEnumerator.Current
=> Current;
public bool MoveNext()
=> ++index < source.Length;
public void Reset()
=> index = -1;
public void Dispose() {}
}
}
您可以在 SharpLab 中看到,现在为语句生成的代码如下:foreach
MyCollection.Enumerator enumerator = new MyCollection(array).GetEnumerator()
try
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
finally
{
((IDisposable)enumerator).Dispose();
};
区别在于:
可以通过为集合声明两个枚举器来避免调用:Dispose()
class MyCollection : IEnumerable<int>
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public Enumerator GetEnumerator()
=> new Enumerator(this);
IEnumerator<int> IEnumerable<int>.GetEnumerator()
=> new ReferenceEnumerator(this);
IEnumerator IEnumerable.GetEnumerator()
=> new ReferenceEnumerator(this);
// value type enumerator
public struct Enumerator
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
public bool MoveNext()
=> ++index < source.Length;
}
// reference type enumerator
class ReferenceEnumerator : IEnumerator<int>
{
readonly int[] source;
int index;
public ReferenceEnumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
object IEnumerator.Current
=> Current;
public bool MoveNext()
=> ++index < source.Length;
public void Reset()
=> index = -1;
public void Dispose() {}
}
}
改变的事情:
您可以在 SharpLab 中看到为 foreach 语句生成的代码,如果满足以下条件:
MyCollection.Enumerator enumerator = new MyCollection(array).GetEnumerator()
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
};
注意:请查看我的另一篇文章“C#中的数组迭代性能”,其中我更详细地分析了这种情况。
在数组上使用的唯一问题是它只允许完全遍历。如果只想遍历数组的一部分,则可以创建 ArraySegment<T> 或 Span<T> 的实例并用于遍历它。foreachforeach
注意:查看我的另一篇文章“C# 中的数组迭代性能 — ArraySegment<T>”,了解 和 之间的区别。ArraySegment<T>Span<T>
两者都支持通过引用传递项目。在使用 . 遍历这些类型时,不要忘记使用关键字。Span<T>ReadOnlySpan<T>refforeach
注意:我已经实现了一个 Roslyn 分析器,其中包含一个规则,该规则会警告您何时 使用以及 何时可以使用。安装它以获得此以及与使用 foreach 相关的许多其他规则。refref readonly
foreach具有非常简单明了的语法。我们在这里看到 C# 编译器将生成的代码调整为集合类型。