简介
yield return 是C#中特有的一个关键字,主要的使用是用于可遍历的数组的数据返回。具体如何理解,让我们通过一个实例来看一下。
实例
要求:写一个函数,返回指定范围的奇数。
这个需求很容易实现,常规的写法如下所示:
public IEnumerable<int> GetOddNumber(int start, int end)
{
IEnumerable<int> list = new List<int>();
for (int i = start; i <= end; i++)
if (i % 2 == 1)
list.Add(size);
return list;
}
通过提供这个函数,我可以做一些更多的操作,比如找出 [20, 30] 中的第1个素数。
foreach(int num in GetOddNumber(20, 30))
{
if(IsPrim(num)) // IsPrim(int) 函数用于判断一个int是不是素数,代码略。
{
Console.WriteLine(num);
break;
}
}
运行后,结果显示为23,工作一切正常。
这里关于结果请注意,以上的需求完全可以用一个函数来实现,但是由于在实际应用中,第1个函数可能是编写后台类库的写的,而第2个函数,可能是前台的调用的,所以在实现时被分为两个函数。这种分隔非常常见,是类中提供的代码和客户端调用代码的一种常见方式。
问题
以上的示例中工作很好,但是这样的分工,往往可能会有问题。在以上的示例中,虽然需求只是找出第1个素数,但是在调用 GetOddNumber(20, 30) 时,返回的是所有满足条件的素数,即 [21, 23, 25, 27, 29] 5个数字,其中后面4个完全是多余的。在这个示例中,4个多余的数字倒也无所谓,可以忽略,但是在实际应用中,有很多情况数据量很大,是无法忽略的。这时候,我们往往要精巧地选择范围,要么使用特殊的方法另辟蹊径。
解决方案
我们解决问题,最好的方式就是以一种通用的方式来解决。这样可以适用大部分情况。为了解决以上的问题,我们就可以引入 yield return 来进行解决。我们使用这个关键字对 GetOddNumber(start, end) 进行重写,代码如下。
public IEnumerable<int> GetOddNumber(int start, int end)
{
for (int i = start; i <= end; i++)
if (i % 2 == 1)
yield return i;
}
客户端的代码,即上面的 foreach 循环,完全不用修改,并且运行的结果也是一样的。区别在于 GetOddNumber(start,end) 函数使用了 yield return 后,每次遍历时返回一个结果后,就处于休息状态,直到下次循环时才会继续。这样的好处有以下几点:
- 减少循环次数
yield return 是惰性的,每次循环到 yield return 时,直接返回给调用方满足条件的数据,不用去处理没循环到的数据,即在上例中,第1次循环GetOddNumber(start,end)返回 21给调用方,调用方判断21不满足条件,然后再次调用GetOddNumber(start,end),此时继续执行,返回23给调用方,此时调用方发现满足条件循环就结束了,而GetOddNumber(start,end)中的循环也只调用了2次,从而减少了循环次数。 - 减少中间变量定义
即不用再去定义一个IEnumerable<int> list
来接收结果,有满足条件的数据直接返回即可,操作更加方便。
补充:如果想提前结束 GetOddNumber(start, end) 中的循环,可以在循环中使用 yield break 关键字。
扩展
有了以上的理解以后, 我们如果结合Lambda表达式,可以让代码更简单,比如同样是以上的示例,类的内部可以这么写
public IEnumerable<int> GetOddNumber(Fun<int, bool> fun, int start, int end)
{
for (int i = start; i <= end; i++)
fun(i)
yield return i;
}
在调用方可以以更简洁的代码即可实现同样的功能,代码如下所示:
foreach(int num in GetOddNumber(n => IsPrim(n), 20, 30))
Console.WriteLine(num);
结论
通过使用 yield return 我们可以更加方便高效地对数列进行操作,能够同时减少代码量和循环次数。所以在代码相应的地方推荐使用 yield return 代替使用中间数据的数列操作方法。