小解c# foreach原理
【本篇文章首发于51CTO,https://developer.51cto.com/art/202010/628737.htm】
作为开发人员我们经常会在程序中编写 foreach 语句实现对类型的遍历,但是并不是所有的类型都可以遍历,这个知识点是绝大部分开发成员所知晓的。但是类型可以被 foreach 遍历的依据是什么部分程序员并不清楚,下面我就通过举例的方式来具体讲解 foreach 原理。
在这里我们首先自定义一个类型 Cat 并遍历这个类型:
//定义 Cat 类型
class Cat
{
}
//遍历 Cat
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat();
foreach(var item in cat)
{
//more code
}
}
}
我们运行上述代码后编译器会提示错误 “Cat” 不包含 “GetEnumerator” 的公共定义,因此 foreach 语句不能作用于 “Cat” 类型的变量,由此错误提示我们可以得知如果 Cat 类型可以被 foreach 遍历,那么 Cat 类就必须实现 GetEnumerator 方法。下面我们就在 Cat 类中加入 GetEnumerator 方法。
class Cat
{
//加入 GetEnumerator 方法的实现
public object GetEnumerator()
{
return null;
}
}
我们再次运行代码,这时程序出现如下两个错误提示:
- foreach 要求 “Cat.GetEnumerator()”的返回类型 “object”必须具有适当的公共 MoveNext 方法和公共 Current 属性;
- object 并不包含 “MoveNext” 的定义。
根据上述错误提示我们可以推断出 GetEnumerator 方法的返回值必须要有 MoveNext 方法和 Current 属性。但是我们目前并不知道 GetEnumerator 方法的返回值类型和 Current 属性是否是只读的,这种情况我们该怎么办呢?此时我们可以查看已经支持 foreach 遍历的类型是怎么做的,下面的代码段展示了 string 类型是如何实现的(只列出了关键代码)。
//more code
public CharEnumerator GetEnumerator();
//more code
pubic sealed class CharEnumerator:ICloneabe,IEnumerator<char>,IEnumerator,IDisposable
{
public char Current {get;}
//more code
public bool MoveNext();
//more code
}
根据上述代码段我们仿写如下:
class Cat
{
public CatEnumerator GetEnumerator()
{
return new CatEnumerator();
}
}
class CatEnumerator
{
public char Current {get;}
public bool MoveNext()
{
return true;
}
}
这时我们编译发现原来的错误已经消失了,程序编译通过了。但是不要以为到这里就完了,Cat 类仅仅包含这些是没有任何意义的,这些内容只是为了让程序通过编译而已,在实际开发中我们遍历的对象是一个序列,那么我们现在就在 Cat 类中添加一个固定的序列:
class Cat
{
string[] datas=new string[]{"波斯猫","狸花猫","无毛猫","虎斑猫"};
public CatEnumerator GetEnumerator()
{
return new CatEnumerator();
}
}
我们已经添加了数据对象,那么 foreach 是如何访问到这个数据的呢?这时我们可以将数据对象通过 GetEnumerator 方法作为迭代计数器对象(CatEnumerator)构造函数的参数传递进去,然后迭代计数器对象提供一个属性将这些数据存储起来。
class Cat
{
string[] datas=new string[]{"波斯猫","狸花猫","无毛猫","虎斑猫"};
public CatEnumerator GetEnumerator()
{
return new CatEnumerator(datas);
}
}
class CatEnumerator
{
//存储数据
private string[] datas;
//带参构造函数
public CatEnumerator(string[] datas)
{
this.datas=datas;
}
public char Current {get;}
public bool MoveNext()
{
return true;
}
}
到目前为止我们已经设置了遍历的数据,如果要将数据遍历出来还需要一个下标索引来读取数组中的每个元素,并将每次读取出来的元素值赋值给 Current 属性。我们可以在迭代计数器对象中定义一个 index 整型私有属性作为下标索引属性,这里需要注意的是我们 index 这个属性的默认值为 -1 ,这一点是很多新手开发人员比较容易出错的地方。既然有下标了,我们在遍历的时候下标就必须是递增变化,不断指向下一个元素的位置直到到达数组的末端为止。这时我们就需要在 MoveNext 方法中进行执行下标递增的操作了,MoveNext 方法是一个返回值为 bool 类型的方法,其目的是告知 foreach 但钱遍历的数据对象是否存在还未遍历到的元素,如果存在就返回 true 反之返回 false 遍历结束。下面我们针对这一段所说的内容进行代码编写。
class CatEnumerator
{
//存储数据
private string[] datas;
//带参构造函数
public CatEnumerator(string[] datas)
{
this.datas=datas;
}
//数组下标
private int index=-1;
//遍历当前元素
public char Current
{
get
{
return datas[index];
}
}
public bool MoveNext()
{
index++;
return index < datas.Length;
return true;
}
}
到目前为止我们就编写了一个可以通过 foreach 遍历的类型,这里有三点很重要:
- GetEnumerator 方法的作用是 foreach 调用当前需要遍历的类型的迭代计数器对象,该方法的返回类型为用于foreach 遍历的迭代计数器对象;
- Current 属性就是当前遍历到的对象;
- MoveNext 方法促使迭代计数器对象的计数移动到下一位。
通过前面所述的内容,我们可知 foreach 遍历主要有三个步骤:
- foreach 调用当前可遍历类型的 GetEnumerator 方法创建一个迭代计数器对象,并将要遍历的数据传递给迭代计数器对象的构造函数中;
- 迭代计数器对象调用它 MoveNext 方法将所以小标递增 1 ,若下标大于数据长度则迭代完成;
- MoveNext 方法返回 true 并返回 Current 属性中存储的数据。
以上三个步骤总结起来就是 获取迭代计数器对象 >> 调用 MoveNext 方法 >> 获取 Current 属性。
小技巧:在 c# 中如果要查看某个类型是否支持 foreach 我们可以查看还类型和该类型的迭代计数器是否都实现了 IEnumerable 接口,因为 IEnumerable 接口中的就包含了 foreach 实现的原理和必须调用的成员。