专栏首页喵叔's 专栏小解c# foreach原理
原创

小解c# foreach原理

原创声明:本文首发于 51CTO,如需转载请联系我

作为开发人员我们经常会在程序中编写 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 实现的原理和必须调用的成员。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 小解c# foreach原理

    【本篇文章首发于51CTO,https://developer.51cto.com/art/202010/628737.htm】 作为开发人员我们经常会在程序...

    喵叔
  • Entity Framework Core 捕获数据库变动

    在实际项目中我们往往需要记录存储在数据库中数据的变动(例如修改数据前记录下数据的原始值),这样一来在发生误操作时可以将数据恢复到变动前的状态,也可以追溯到数据的...

    喵叔
  • SQL Server 每日一题--解析老N的收入

    首先去除数据中相同的工资,然后将工资从大到小排序,接着利用 row_number 函数给每行数据加上行号,最后过滤出行号为N的工资,因为需要根据指定的排名查询,...

    喵叔
  • 小解c# foreach原理

    【本篇文章首发于51CTO,https://developer.51cto.com/art/202010/628737.htm】 作为开发人员我们经常会在程序...

    喵叔
  • PHP中的Iterator迭代对象属性详解

    foreach用法和之前的数组遍历是一样的,只不过这里遍历的key是属性名,value是属性值。在类外部遍历时,只能遍历到public属性的,因为其它的都是受保...

    砸漏
  • 详解一道高频面试题:接雨水

    接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。

    帅地
  • 双指针的妙用,巧解一道高频面试题:接雨水

    接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。

    五分钟学算法
  • 分布式协调神器 ZooKeeper 之整体概述

    ZooKeeper 最早起源于雅虎研究院的一个研究小组。当时,雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点...

    美的让人心动
  • 针对不同创业阶段的创业者适合参加哪些创业赛事活动呢?

    创业是一场异常艰辛的马拉松,坚持是唯一的捷径,借力是加速的方法之一。如何借力?创业者可以通过投资机构,媒体等,获得资金和宣传。想要快速的接触到投资机构和媒体,参...

    用户2158343
  • 【行业】从自动驾驶到语音识别,算法已经“侵入”我们的生活了

    在2018年,算法将越来越多地影响我们生活的方方面面,从语音识别到自动驾驶汽车等。但是人类很难理解这种抽象。伦敦艺术与技术工作室FIELD的创意总监Marcus...

    AiTechYun

扫码关注云+社区

领取腾讯云代金券