建议17:多数情况下使用foreach进行循环遍历

由于本建议涉及集合的遍历,所以在开始讲解本建议之前,我们不妨来设想一下如何对结合进行遍历。假设存在一个数组,其遍历模式可以采用依据索引来进行遍历的方法;又假设存在一个HashTable,其遍历模式可能是按照键值来进行遍历。无论是哪个集合,如果他们的遍历没有一个公共的接口,那么客户端在进行遍历时,相当于是对具体类型进行了编码。这样一来,当需求发生变化时,必须修改我们的代码。而且,由于客户端代码过多地关注了集合内部的实现,代码的可移植性就会变得很差,这直接违反了面向对象的开闭原则。于是,迭代器模式就诞生了。现在,不要管FCL中如何实现该模式的,我们先来实现一个自己的迭代器模式。

  1.      /// <summary>
  2. /// 要求所有的迭代器全部实现该接口
  3. /// </summary>
  4. interface IMyEnumerator
  5. {
  6. bool MoveNext();
  7. object Current { get; }
  8. }
  9.  
  10. /// <summary>
  11. /// 要求所有的集合实现该接口
  12. /// 这样一来,客户端就可以针对该接口编码,
  13. /// 而无须关注具体的实现
  14. /// </summary>
  15. interface IMyEnumerable
  16. {
  17. IMyEnumerator GetEnumerator();
  18. int Count { get; }
  19. }
  20.  
  21. class MyList : IMyEnumerable
  22. {
  23. object[] items = new object[];
  24. IMyEnumerator myEnumerator;
  25.  
  26. public object this[int i]
  27. {
  28. get { return items[i]; }
  29. set { this.items[i] = value; }
  30. }
  31.  
  32. public int Count
  33. {
  34. get { return items.Length; }
  35. }
  36.  
  37. public IMyEnumerator GetEnumerator()
  38. {
  39. if (myEnumerator == null)
  40. {
  41. myEnumerator = new MyEnumerator(this);
  42. }
  43. return myEnumerator;
  44. }
  45. }
  46.  
  47. class MyEnumerator : IMyEnumerator
  48. {
  49. int index = ;
  50. MyList myList;
  51. public MyEnumerator(MyList myList)
  52. {
  53. this.myList = myList;
  54. }
  55.  
  56. public bool MoveNext()
  57. {
  58. if (index + > myList.Count)
  59. {
  60. index = ;
  61. return false;
  62. }
  63. else
  64. {
  65. index++;
  66. return true;
  67. }
  68. }
  69.  
  70. public object Current
  71. {
  72. get { return myList[index - ]; }
  73. }
  74. }
  1. static void Main(string[] args)
  2. {
  3. //使用接口IMyEnumerable代替MyList
  4. IMyEnumerable list = new MyList();
  5. //得到迭代器,在循环中针对迭代器编码,而不是集合MyList
  6. IMyEnumerator enumerator = list.GetEnumerator();
  7. for (int i = ; i < list.Count; i++)
  8. {
  9. object current = enumerator.Current;
  10. enumerator.MoveNext();
  11. }
  12. while (enumerator.MoveNext())
  13. {
  14. object current = enumerator.Current;
  15. }
  16. }

MyList模拟了一个集合类,它继承了接口IMyEnumerable,这样,在客户端调用的时候,我们就可以直接调用IMyEnumerable来声明变量,如代码中的一下语句:

IMyEnumerable list=new MyList();

如果未来我们新增了其他的集合类,那么针对list的编码即使不做修改也能运行良好。在IMyEnumerable中声明了GetEnumerator方法返回一个继承了IMyEnumerator的对象。在MyList的内部,默认返回MyEnumerator,MyEnumerator就是迭代器的一个实现,如果对于迭代的需求有变化,可以重新开发一个迭代器(如下所示),然后在客户端迭代的时候使用该迭代器。

  1. //使用接口IMyEnumerable代替MyList
  2. IMyEnumerable list = new MyList();
  3. //得到迭代器,在循环中针对迭代器编码,而不是集合MyList
  4. IMyEnumerator enumerator2 = new MyEnumerator(list);
           //for调用
  5. for (int i = ; i < list.Count; i++)
  6. {
  7. object current = enumerator2.Current;
  8. enumerator.MoveNext();
  9. }
           //while调用
  10. while (enumerator.MoveNext())
  11. {
  12. object current = enumerator2.Current;
  13. }

在客户端的代码中,我们在迭代的过程中分别演示了for循环和while循环,到那时因为使用了迭代器的缘故,两个循环都没有针对MyList编码,而是实现了对迭代器的编码。

理解了自己实现的迭代器模式,相当于理解了FCL中提供的对应模式。以上代码中,在接口和类型中都加入了“My”字样,其实,FCL中有与之相对应的接口和类型,只不过为了演示需要,增加了其中部分内容,但是大致思路是一样的。使用FCL中相应类型进行客户端代码编写,大致应该下面这样:

  1. ICollection<object> list = new List<object>();
  2. IEnumerator enumerator = list.GetEnumerator();

  3. for (int i = ; i < list.Count; i++)
  4. {
  5. object current = enumerator.Current;
  6. enumerator.MoveNext();
  7. }
  8. while (enumerator.MoveNext())
  9. {
  10. object current = enumerator.Current;
  11. }

但是,无论是for循环还是while循环,都有些啰嗦,于是,foreach就出现了。

  1. foreach (var current in list)
  2. {
  3. //省略了 object current = enumerator.Current;
  4. }

可以看到,采用foreach最大限度地简化了代码。它用于遍历一个继承了IEnumerable或IEnumerable<T>接口的集合元素。借助IL代码,我们查看使用foreach到底发生了什么事情:

  1. .method private hidebysig static void Main(string[] args) cil managed
  2. {
  3. .entrypoint
  4. // 代码大小 62 (0x3e)
  5. .maxstack
  6. .locals init ([] class [mscorlib]System.Collections.Generic.ICollection`<object> list,
  7. [] object current,
  8. [] class [mscorlib]System.Collections.Generic.IEnumerator`<object> CS$$,
  9. [] bool CS$$)
  10. IL_0000: nop
  11. IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`<object>::.ctor()
  12. IL_0006: stloc.0
  13. IL_0007: nop
  14. IL_0008: ldloc.0
  15. IL_0009: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`<!> class [mscorlib]System.Collections.Generic.IEnumerable`<object>::GetEnumerator()
  16. IL_000e: stloc.2
  17. .try
  18. {
  19. IL_000f: br.s IL_001a
  20. IL_0011: ldloc.2
  21. IL_0012: callvirt instance ! class [mscorlib]System.Collections.Generic.IEnumerator`<object>::get_Current()
  22. IL_0017: stloc.1
  23. IL_0018: nop
  24. IL_0019: nop
  25. IL_001a: ldloc.2
  26. IL_001b: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
  27. IL_0020: stloc.3
  28. IL_0021: ldloc.3
  29. IL_0022: brtrue.s IL_0011
  30. IL_0024: leave.s IL_0036
  31. } // end .try
  32. finally
  33. {
  34. IL_0026: ldloc.2
  35. IL_0027: ldnull
  36. IL_0028: ceq
  37. IL_002a: stloc.3
  38. IL_002b: ldloc.3
  39. IL_002c: brtrue.s IL_0035
  40. IL_002e: ldloc.2
  41. IL_002f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
  42. IL_0034: nop
  43. IL_0035: endfinally
  44. } // end handler
  45. IL_0036: nop
  46. IL_0037: call int32 [mscorlib]System.Console::Read()
  47. IL_003c: pop
  48. IL_003d: ret
  49. } // end of method Program::Main

查看IL代码就可以看出,运行时还是会调用get_Current()和MoveNext()方法。

在调用完MoveNext()方法后,如果结果是true,跳转到循环开始处。实际上foreach循环和while循环是一样的:

  1. while (enumerator.MoveNext())
  2. {
  3. object current = enumerator.Current;
  4. }

foreach循环除了可以提供简化的语法外,还有另外两个优势:

  • 自动将代码置入try finally块
  • 若类型实现了IDisposable接口,它会在循环结束后自动调用Dispose方法。

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

编写高质量代码改善C#程序的157个建议——建议17:多数情况下使用foreach进行循环遍历的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. Tensorflow的采样方法:candidate sampling(zhuan)

    zhuanzi:https://blog.csdn.net/u010223750/article/details/69948463 采样介绍 假如我们有一个多分类任务或者多标签分类任务,给定训练集(x ...

  2. Android开发SQLite数据库的创建

    package com.example.db; import android.content.Context; import android.database.sqlite.SQLiteDatabas ...

  3. 如何设计并使用FireMonkeyStyle

    如何设计并使用FireMonkeyStyle FireMonkey使用Style来控制控件的显示方式. 每个控件都有一个StyleLookup属性,FireMonkey就是通过控件的这个属性来在当前窗 ...

  4. cowboy的例子

    大体参考的这里,非常感谢他的例子 开发的时候先下载好cowboy的库,放到~/.erlang里面 code:add_pathz("/Users/mmc/erlang/3rd_libs/cow ...

  5. unittest框架+ HTMLTestRunner 出报告时,展示控制台信息 不同展示的参数写法 加verbosity

    加verbosity参数 没有加的时候展示: 参考: 来源:  https://www.cnblogs.com/tomweng/p/6609937.html 介绍: HTMLTestRunner 是 ...

  6. Java-Runoob:Java switch case

    ylbtech-Java-Runoob:Java switch case 1.返回顶部 1. Java switch case 语句 switch case 语句判断一个变量与一系列值中某个值是否相等 ...

  7. 微信小程序之目录结构

    小程序,功能不会太多,页面不会太多. 正常情况下,会包含首页,分类页面,个人中心页面,导航页面,其他页面等等. 我们首先要把页面结构布置好,把架子搭建好. 剩下的就是配置一些内容,小程序的基本信息,接 ...

  8. Docker构建ssh镜像

    FROM ubuntu MAINTAINER ggzone xxx@live.com ENV REFRESHED_AT 2015-10-21 RUN apt-get -qqy update & ...

  9. python's sixteenth day for me 员工信息表

    import os user_dic = { 'username':None, 'password':None, 'login':True } flag = False name_list = ['i ...

  10. [原创]Spring JdbcTemplate 使用总结与经验分享

    引言 近期开发的几个项目,均是基于Spring boot框架的web后端项目,使用JdbcTemplate执行数据库操作,实际开发过程中,掌握了一些有效的开发经验,踩过一些坑,在此做个记录及总结,与各 ...