高效的数据处理:C# 的 foreach 循环

作者:微信公众号:【架构师老卢】
5-3 16:33
16

概述:在现代软件开发的动态环境中,高效的数据处理是创建高性能应用程序的核心。作为开发人员,我们在编写代码时不断努力平衡可读性、可维护性和速度。在 C#(一种以其多功能性和健壮性而闻名的编程语言)领域,循环成为无缝导航集合的强大工具。foreachforeach 是 C# 中的语句,用于生成遍历集合项所需的代码。语法非常简单:foreach(var item in source) {     Console.WriteLine(item);  }本文将重点介绍编译器在给定各种类型的集合的情况下生成的代码,以便您可以准确了解应用程序在使用此类循环时正在执行的操作。最低要求要使语句接受集合作为其源,

在现代软件开发的动态环境中,高效的数据处理是创建高性能应用程序的核心。作为开发人员,我们在编写代码时不断努力平衡可读性、可维护性和速度。在 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() {}  
    }  
}

您可以在 SharpLab 中看到它的工作

注意:A 无法实现接口。在这种情况下,将调用该方法(如果存在)。不需要枚举器实现 .ref structforeachDispose()IDisposable

GetEnumerator() 扩展方法

从 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

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

唯一的区别是:

  • 该集合派生自 。IEnumerable
  • GetEnumerator()必须返回.IEnumerator
  • 枚举器派生自 。IEnumerator
  • 枚举器必须具有方法。Reset()

请注意,该属性需要返回类型 。我希望它返回,因为它是此集合的项目类型。可以同时实现两种实现,一种是公共的,另一种是使用显式接口实现。仅当枚举器被强制转换为 时,才使用显式实现属性。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();  
    }  
}

需要注意的几件事:

  • 枚举器以 type 的形式返回,该类型是 reference-type。IEnumerator
  • 返回的值必须强制转换为,因为它使用的是显式实现。Currentint
  • 尽管枚举器没有实现,但它会添加代码以在运行时检查它是否实现。IDispose

我在上面提到过,枚举器应该有一个值类型的枚举器以获得更好的性能。我们在这里看到,通过返回 ,枚举器是盒装的,这会将其转换为引用类型。解决此问题的方法是也对以下方法使用显式接口实现: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>

正如我在上一篇文章中所解释的,并扩展接口对并指定属性返回的项的类型。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();  
};

区别在于:

  • 枚举器是值类型。
  • 返回的值不需要强制转换。这提高了每个项目的性能。Current
  • Dispose()即使它是空的,也会被调用。这就是强制性的。IEnumerator<T>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() {}  
    }  
}

改变的事情:

  • public 返回值类型枚举器的实例,而其他枚举器返回引用类型枚举器的实例。GetEnumerator()
  • 值类型枚举器仅实现最低要求。
  • 引用类型枚举器声明为专用,因为它仅在内部使用。
  • 引用类型枚举器声明为类。这避免了从值类型转换为引用类型的装箱性能损失。

您可以在 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# 编译器将生成的代码调整为集合类型。

相关留言评论
昵称:
邮箱:
阅读排行