连载,LINQ兵法十四章,5(1of1)

hi20140509 2014-05-13 09:04:18
加精
上一篇

第五章 foreach、IEnumerable<T>类型和yield return

首先提一个问题:
这是一段再简单不过的用for循环累加的代码:
int sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
Console.WriteLine(sum);

我们能不能用while循环改写它呢?
很容易改写:
int sum = 0;
int i = 1;
while (i <= 100)
{
sum += i;
i++;
}
Console.WriteLine(sum);

我们发现,将for循环用while循环改写,是一个机械的过程。任何for循环,都可以不假思索地改写成while循环。甚至不用理解这个for循环干了什么。

那么思考下,既然有了while循环,我们为什么需要for循环?它简化了什么?

本质上说,for循环将循环初始化和改变循环状态变量两部分提到循环表达式中,而不是放在循环体和别的地方,因此for循环对于表达具有初始状态和每次通过相同方法改变循环状态的循环表达力更强。

下面我们编写一个程序,输出一个字符串中的单词。为了简化起见,我们假设单词就是以空格分隔的子串,并且字符串不会出现连续的空格。比如输入”hello world”,则可以输出”hello”和”world”。

我们将这个过程分为2个步骤,分别是,从字符串获取一个单词;判断循环是否结束。

下面是一个代码框架,请思考下如何补全它:
class LetterEnumerator
{
private string str = "";

private int lastScaned = 0;

private int startPos = 0;

public LetterEnumerator(string input) { str = input.Trim(); }

/// <summary>
/// 将游标指向下一个位置,如果遍历到结尾了,就返回false,否则返回true
/// </summary>
/// <returns></returns>
public bool MoveNext()
{
在此填写代码
}

/// <summary>
/// 返回当前位置的单词
/// </summary>
public string Current
{
get
{
在此填写代码
}
}
}

下面是一个参考答案
class LetterEnumerator
{
private string str = "";

private int lastScaned = 0;

private int startPos = 0;

public LetterEnumerator(string input) { str = input.Trim(); }

/// <summary>
/// 将游标指向下一个位置,如果遍历到结尾了,就返回false,否则返回true
/// </summary>
/// <returns></returns>
public bool MoveNext()
{
startPos = lastScaned++;
for (int i = startPos; i < str.Length; i++)
{
if (str[i] == ' ') break;
lastScaned++;
}
return startPos < str.Length;
}

/// <summary>
/// 返回当前位置的单词
/// </summary>
public string Current
{
get
{
return str.Substring(startPos, lastScaned - startPos - 1);
}
}
}

我们调用下它:
var le = new LetterEnumerator("hello world");
while (le.MoveNext())
Console.WriteLine(le.Current);

以上代码可以简化,方法是,在LetterEnumerator中添加一个方法:

public LetterEnumerator GetEnumerator() { return this; }

我们用foreach调用它

var le = new LetterEnumerator("hello world");
foreach (string s in le)
Console.WriteLine(s);


很明显,我们看到foreach也会调用Current和MoveNext两个方法。

那么foreach语句是什么呢?和for循环简化了while循环一样,foreach也是一种特殊的简化写法,它将while (obj.MoveNext()) { object item = obj.Current; … } 简化成 foreach (object item in obj) { … },obj是一个具有GetEnumerator方法的对象,而返回的Enumerator则包含了Current和MoveNext方法。

为什么要增加GetEnumerator呢?因为foreach允许你将包含Current和MoveNext的遍历对象和包含数据以及GetEnumerator方法的数据对象分离,你可以对相同的数据使用不同的方法遍历。

上面,我们构造了一个LetterEnumerator,它包含了Current和MoveNext两个方法,以及GetEnumerator方法,使得我们可以用foreach来遍历。事实上,这个LetterEnumerator完全可以由编译器自动构造。
虽然编译器遇到foreach会自动调用GetEnumerator寻找包含MoveNext和Current的对象的这两个方法/属性。但是如果编译器自动产生的匿名类,我们用什么表示呢?很遗憾,函数不能返回var类型。

于是.NET提供了IEnumerable<T>和IEnumerator<T>两个泛型类型(出于向前兼容和别的原因,当然它们也有非泛型的版本,这里不表,有兴趣可以自己研究)。
IEnumerable<T>接口表示这个类可以拥有IEnumerator<T> GetEnumerator()方法,而IEnumerator<T>接口表示它拥有T Current属性和bool MoveNext()方法。另外,它还包括了Reset方法,可以用它重置迭代过程,不过对于foreach,它并不是必须的。

看下面的代码:

static IEnumerable<string> BuildALetterEnumerator(string str)
{
int lastScaned = 0;
int startPos = 0;
while (true)
{
startPos = lastScaned++;
for (int i = startPos; i < str.Length; i++)
{
if (str[i] == ' ') break;
lastScaned++;
}
if (!(startPos < str.Length)) break;
yield return str.Substring(startPos, lastScaned - startPos - 1);
}
}

这个函数会返回一个匿名类型的对象实例,我们说了,它是IEnumerable<string>类型,它和我们手写的LetterEnumerator一样,拥有GetEnumerator方法,返回同样支持IEnumerator<T>的匿名类型,而这个类型也包含了string Current属性和bool MoveNext方法。类的初始化代码被放在了这个函数的前面,MoveNext方法和Current属性被用一个循环包住,使得它可以连续迭代。Current被yield return语句代替,yield return会暂停这个循环,并且把它的返回值作为Current属性。当MoveNext再次被调用,循环会继续。如果这个函数执行完了,但是没有再遇到yield return,那么这次MoveNext会返回false。

我们同样可以用先前的代码调用它:

var le = BuildALetterEnumerator("hello world").GetEnumerator();
while (le.MoveNext())
Console.WriteLine(le.Current);

下面用一个习题结束本章,请写一个遍历2~100以内素数的函数,并且用foreach调用它。

参考程序:
static void Main(string[] args)
{
foreach (int prime in Prime100())
Console.WriteLine(prime);
}

static IEnumerable<int> Prime100()
{
for (int i = 2; i <= 100; i++)
{
bool isPrime = true;
for (int j = 2; j < i; j++)
{
if (i % j == 0)
{
isPrime = false;
break;
}
}
if (isPrime) yield return i;
}
}


下一篇
...全文
1160 24 打赏 收藏 转发到动态 举报
写回复
用AI写文章
24 条回复
切换为时间正序
请发表友善的回复…
发表回复
一只熊猫 2014-05-21
  • 打赏
  • 举报
回复
看漏了
一只熊猫 2014-05-21
  • 打赏
  • 举报
回复
yield return 没讲呢?
  • 打赏
  • 举报
回复
maggie_cg 2014-05-18
  • 打赏
  • 举报
回复
支持,写的真好!
cqut2013 2014-05-18
  • 打赏
  • 举报
回复
比较不错。。。while循环其实用的地方太多了
shluochen 2014-05-16
  • 打赏
  • 举报
回复
学习了,看着很专业的样子
TTforlove 2014-05-16
  • 打赏
  • 举报
回复
xiaoyaoju 2014-05-15
  • 打赏
  • 举报
回复
laoer_2002 2014-05-15
  • 打赏
  • 举报
回复
谢谢楼主分享,继续学习
Neusoft06 2014-05-15
  • 打赏
  • 举报
回复
一口气都看完了,等待更新。。。。。
softheaded 2014-05-15
  • 打赏
  • 举报
回复
感谢楼主这么专业的解释!辛苦了!
newnazi 2014-05-14
  • 打赏
  • 举报
回复
果断收藏 果断收藏
格拉 2014-05-14
  • 打赏
  • 举报
回复
学习了
xiaocongzhi 2014-05-14
  • 打赏
  • 举报
回复
果断收藏
laoer_2002 2014-05-14
  • 打赏
  • 举报
回复
谢谢楼主分享,支持
xuculiang 2014-05-14
  • 打赏
  • 举报
回复
guanzhu
ghngfoiju 2014-05-13
  • 打赏
  • 举报
回复
非常好很实用 谢谢

110,545

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧