Is yield useful outside of LINQ?
每当我认为我可以使用yield关键字时,我会后退一步,看看它将如何影响我的项目。我总是返回一个集合而不是yeilding,因为我觉得维护yeilding方法状态的开销并不能给我带来多少好处。在我返回集合的几乎所有情况下,我都认为90%的时间内,调用方法将迭代集合中的所有元素,或者在整个集合中查找一系列元素。
我确实理解它在LINQ中的有用性,但是我觉得只有LINQ团队在编写如此复杂的可查询对象,从而产生有用的结果。
有没有人写过类似或不类似于Linq的东西,在那里收益是有用的?
注意,使用yield,您将对集合进行一次迭代,但是当您构建一个列表时,您将对它进行两次迭代。
以过滤器迭代器为例:
1 2 3 4 5 | IEnumerator<T> Filter(this IEnumerator<T> coll, Func<T, bool> func) { foreach(T t in coll) if (func(t)) yield return t; } |
现在,您可以链接这个:
1 | MyColl.Filter(x=> x.id > 100).Filter(x => x.val < 200).Filter (etc) |
您的方法将创建(并抛出)三个列表。我的方法只迭代一次。
此外,当您返回集合时,您将强制用户执行特定的实现。迭代器更通用。
I do understand its usefulness in linq, but I feel that only the linq team is writing such complex queriable objects that yield is useful.
一旦在.NET 2.0中实现了yield,它就很有用了,这比任何人想到linq都早。
我为什么要写这个函数:
1 2 3 4 5 6 | IList<string> LoadStuff() { var ret = new List<string>(); foreach(var x in SomeExternalResource) ret.Add(x); return ret; } |
当我可以使用yield,并且省去创建临时列表的工作和复杂性时,没有好的理由:
1 2 3 4 | IEnumerable<string> LoadStuff() { foreach(var x in SomeExternalResource) yield return x; } |
它还具有巨大的性能优势。如果您的代码只使用集合的前5个元素,那么使用yield通常可以避免加载超过该点的任何内容。如果你建立了一个集合,然后将其返回,你将浪费大量的时间和空间来装载你永远不需要的东西。
我可以不断地……
我最近不得不用表达式类的形式来表示数学表达式。在计算表达式时,我必须使用后序树路遍历树结构。为了实现这一点,我实现了IEnumerable,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public IEnumerator<Expression<T>> GetEnumerator() { if (IsLeaf) { yield return this; } else { foreach (Expression<T> expr in LeftExpression) { yield return expr; } foreach (Expression<T> expr in RightExpression) { yield return expr; } yield return this; } } |
然后我可以简单地使用foreach遍历表达式。还可以根据需要添加属性以更改遍历算法。
在以前的一家公司,我发现自己在写这样的循环:
1 2 | for (DateTime date = schedule.StartDate; date <= schedule.EndDate; date = date.AddDays(1)) |
使用非常简单的迭代器块,我可以将其更改为:
1 | foreach (DateTime date in schedule.DateRange) |
在我看来,这使得代码更容易阅读。
我们在大型企业C_2 Web应用程序中大量使用它来处理数据访问和大量重复计算。
任何时候只要你有一些元素要多次点击,收藏都是很棒的。
然而,在许多数据访问场景中,您有大量的元素,这些元素不一定需要在很大的集合中传递。
这基本上就是
Linq确实最大化了收益行为的价值,但它肯定不是唯一的应用。
我是C的大产量粉丝。这在大型本土框架中尤其如此,在这些框架中,方法或属性通常返回另一个IEnumerable的子集列表。我看到的好处是:
- 使用yield的方法的返回值是不可变的
- 您只在列表上迭代一次
- 它是一个延迟或延迟的执行变量,这意味着返回值的代码在需要时才会执行(尽管如果您不知道自己在做什么,这会让您很痛苦)。
- 在源列表更改中,您不必调用以获取另一个IEnumerable,只需再次迭代IEnumerable。
- 更多
收益率的另一个巨大好处是,当您的方法可能返回数百万的值时。如此之多以至于有可能耗尽内存,只需在方法返回列表之前构建列表即可。使用yield,该方法只需创建和返回数百万个值,并且只要调用方不存储每个值。因此它有利于大规模的数据处理/聚合操作
我不确定C实现yield(),但是在动态语言中,它比创建整个集合效率高得多。在许多情况下,它使得使用比RAM大得多的数据集变得容易。
每当函数返回IEnumerable时,应使用"ying"。不在.NET中,仅大于3.0。
.NET 2.0示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static class FuncUtils { public delegate T Func<T>(); public delegate T Func<A0, T>(A0 arg0); public delegate T Func<A0, A1, T>(A0 arg0, A1 arg1); ... public static IEnumerable<T> Filter<T>(IEnumerable<T> e, Func<T, bool> filterFunc) { foreach (T el in e) if (filterFunc(el)) yield return el; } public static IEnumerable<R> Map<T, R>(IEnumerable<T> e, Func<T, R> mapFunc) { foreach (T el in e) yield return mapFunc(el); } ... |
注意,屈服允许你以一种"懒惰"的方式做事。通过lazy,我的意思是在实际请求元素之前,不会对IEnumerable中的下一个元素进行计算。这让你有能力做一些不同的事情。一个是你可以生成一个无限长的列表,而不需要进行无限的计算。其次,可以返回函数应用程序的枚举。只有当您遍历列表时,才会应用这些函数。
收益是有用的,因为它可以节省你的空间。编程中的大多数优化在空间(磁盘、内存、网络)和处理之间进行了权衡。yield作为一个编程构造,允许您在一个集合上按顺序迭代多次,而不需要为每个迭代分别复制该集合。
考虑这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | static IEnumerable<Person> GetAllPeople() { return new List<Person>() { new Person() { Name ="George", Surname ="Bush", City ="Washington" }, new Person() { Name ="Abraham", Surname ="Lincoln", City ="Washington" }, new Person() { Name ="Joe", Surname ="Average", City ="New York" } }; } static IEnumerable<Person> GetPeopleFrom(this IEnumerable<Person> people, string where) { foreach (var person in people) { if (person.City == where) yield return person; } yield break; } static IEnumerable<Person> GetPeopleWithInitial(this IEnumerable<Person> people, string initial) { foreach (var person in people) { if (person.Name.StartsWith(initial)) yield return person; } yield break; } static void Main(string[] args) { var people = GetAllPeople(); foreach (var p in people.GetPeopleFrom("Washington")) { // do something with washingtonites } foreach (var p in people.GetPeopleWithInitial("G")) { // do something with people with initial G } foreach (var p in people.GetPeopleWithInitial("P").GetPeopleFrom("New York")) { // etc } } |
(显然,您不需要在扩展方法中使用yield,它只是创建了一个考虑数据的强大范例。)
正如你所看到的,如果你有很多这样的"过滤"方法(但是它可以是任何一种对一系列人起作用的方法),你可以将其中的许多方法链接在一起,而不需要为每一步增加额外的存储空间。这是提高编程语言(C)的一种方法,以便更好地表达您的解决方案。
yield的第一个副作用是它延迟过滤逻辑的执行,直到您真正需要它为止。因此,如果您创建一个IEnumerable<>类型的变量(使用yield),但从未对其进行迭代,则永远不会执行逻辑,也不会消耗空间,这是一个强大而自由的优化。
另一个副作用是,yield在最低的公共集合接口(ienumerable<>)上运行,这使得可以创建具有广泛适用性的类库代码。
个人而言,我还没有发现我在日常编程中使用了yield。但是,我最近开始使用Robotics Studio示例,发现在那里广泛使用了yield,因此我还看到它与存在异步和并发问题的CCR(并发和协调运行时)结合使用。
不管怎样,我也在努力让自己的头脑清醒过来。
System.Linq IEnumerable扩展非常好,但有时您需要更多扩展。例如,考虑以下扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static class CollectionSampling { public static IEnumerable<T> Sample<T>(this IEnumerable<T> coll, int max) { var rand = new Random(); using (var enumerator = coll.GetEnumerator()); { while (enumerator.MoveNext()) { yield return enumerator.Current; int currentSample = rand.Next(max); for (int i = 1; i <= currentSample; i++) enumerator.MoveNext(); } } } } |
让步的另一个有趣的优点是调用方无法将返回值强制转换为原始集合类型并修改内部集合。
总的来说,产量是非常有用的。它使用Ruby和其他支持函数式编程的语言,所以它就像是绑定到Linq。另一方面,Linq在风格上是实用的,所以它使用了yield。
我有一个问题,我的程序在一些后台任务中使用了大量的CPU。我真正想要的是仍然能够像普通的那样编写函数,这样我就可以轻松地读取它们(即整个线程与基于事件的参数)。如果占用了太多的CPU,仍然能够将函数分解。屈服是完美的。我写了一篇关于这个的博客文章,这个消息来源可以供所有人浏览:)
我在非linq代码中使用了yeild(假设函数不在同一类中):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public IEnumerable<string> GetData() { foreach(String name in _someInternalDataCollection) { yield return name; } } ... public void DoSomething() { foreach(String value in GetData()) { //... Do something with value that doesn't modify _someInternalDataCollection } } |
您必须小心,不要无意中修改getData()函数正在迭代的集合,否则它将引发异常。