除了LINQ,yield有用吗?

28
无论何时我认为可以使用yield关键字,我都会退一步看看它将如何影响我的项目。最终我总是返回一个集合而不是使用yield,因为我觉得维护yield方法的状态的开销并没有给我带来太多好处。在几乎所有情况下,当我返回一个集合时,我感到90%的时间调用方法将遍历整个集合中的所有元素,或者将在整个集合中寻找一系列元素。
我确实理解它在Linq中的有用性,但我觉得只有Linq团队编写这种复杂的可查询对象才会有用yield。
是否有人编写过类似或不像Linq的东西,其中yield很有用?

你是指LINQ之外或IEnumerable吗?我想除了在枚举器中使用yield之外,其他用途应该相当罕见(也很有趣)。Jon Skeet在他的书中提到了其中一个例子... - Benjol
在 Jeffrey Richter 的 Power Threading Library 中,yield 的使用非常有趣。 - Yuriy Zanichkovskyy
14个回答

28
注意使用yield时,您只需迭代一次集合,但当构建列表时,您将迭代两次。
例如,考虑一个过滤器迭代器:
IEnumerator<T>  Filter(this IEnumerator<T> coll, Func<T, bool> func)
{
     foreach(T t in coll)
        if (func(t))  yield return t;
}

现在,您可以将此链接起来:
 MyColl.Filter(x=> x.id > 100).Filter(x => x.val < 200).Filter (etc)

你的方法将创建(并丢弃)三个列表,而我的方法只需要一次迭代。另外,当你返回一个集合时,你就强制要求用户使用特定的实现方式,而迭代器则更加通用。

使用linq进行过滤会更加直观简单,不是吗? - Bob
6
该筛选器基本上就是 LINQ Where 扩展方法。 - Thedric Walker
这就是我的观点,我认为使用linq会更加直观,你会写过滤代码而不使用linq吗?你能得到什么好处呢? - Bob
1
@Bob,“Linq”是“语言集成查询”的缩写,即特定的关键字“from”,“where”,“orderby”等。它们会被编译器转换为类似于答案中的链接表达式。它们是等效的。Filter方法只是作为一个示例被包含进来的。 - James Curran

19

我明白在linq中它的用处,但我感觉只有linq团队才会编写这样复杂的可查询对象才有用。

yield关键字在.NET 2.0中实现后就很有用了,那时还没有人想到LINQ。

我为什么要编写这个函数:

IList<string> LoadStuff() {
  var ret = new List<string>();
  foreach(var x in SomeExternalResource)
    ret.Add(x);
  return ret;
}

何时可以使用yield,而避免为无意义的原因创建临时列表所带来的工作和复杂性:

IEnumerable<string> LoadStuff() {
  foreach(var x in SomeExternalResource)
    yield return x;
}

使用yield关键字还可以带来巨大的性能优势。如果你的代码只需要使用集合中的前5个元素,那么使用yield通常可以避免加载超出这一点的任何内容。如果你构建了一个集合然后返回它,你将浪费大量的时间和空间去加载永远不会用到的东西。

我可以继续说下去....


我相信几年前Anders Hejlsberg正在开发Linq。 - Tom Stickel

12

我最近需要用Expression类来表示数学表达式。在计算表达式时,我必须使用后序遍历树结构。为了实现这一点,我像这样实现了IEnumerable<T>:

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
C# 真的需要一个 yieldcollection 关键字来抽象出 foreach(x in collection){ yield x } 循环,这是每个人每天都要写 100 次的 :-( - Orion Edwards
3
如果你只是做 foreach(x in collection) {yield return x;},那么你可以直接用 .Select(x=>x)。如果你想对集合中的一组项进行操作,你可以创建一个扩展方法 .Foreach<T, TResult>(IEnumerable<T> col, Action<T, TResult> action)。 - Matthew Whited

11

在之前的公司,我发现自己写了如下循环:

for (DateTime date = schedule.StartDate; date <= schedule.EndDate; 
     date = date.AddDays(1))

通过一个非常简单的迭代器块,我能够将其更改为:

foreach (DateTime date in schedule.DateRange)

在我看来,它使得代码更易读了许多。


2
哇 - Jon Skeet 写的代码我不同意!=X 从第一个例子中可以明显看出你正在迭代天数,但是在第二个例子中缺乏这种清晰度。我会使用类似 'schedule.DateRange.Days()' 的东西来避免歧义。 - Erik Forbes
2
吹毛求疵没关系,苛求细节很好,对编码风格的评论和建议总是受欢迎的 :) - Jon Skeet
我不同意for循环比foreach循环更清晰地表示“天数”。StartDate和EndDate仍然指的是“DateTime值”...这并没有暗示天数。您可以在同一天内拥有多个不同小时的DateTime值。在for循环版本中,“天”的唯一真正来源是循环内部的行为...而不是循环本身。如果在foreach循环中使用相同的代码,则同样的清晰度也存在于那里。 - jrista
是的,LocalDate枚举肯定会消除任何歧义。:) 不过我发现,在这种情况下,上下文如何影响感知很有趣。 for 的情况比 foreach 的情况具有更丰富的上下文,尽管两个循环之间实际上没有任何真正的差别,但在感知清晰度和解释方面存在明显的差异。 我认为,代码的上下文常常是一个被忽视或宽泛认识的概念,可以对广大读者理解代码非常重要。 - jrista
顺便说一句,我并不是在贬低你的回答。我只是指在编写可维护和易于理解的代码的更广泛背景下。;P - jrista
显示剩余4条评论

8

yield是在C#2(在C#3中使用Linq之前)开发的。

在处理数据访问和重复计算时,我们在一个大型企业C#2 Web应用程序中广泛使用它。

集合非常适合在多次访问少量元素的情况下使用。

但是,在许多数据访问场景中,您有大量元素,不一定需要在一个大集合中传递。

这本质上就是SqlDataReader所做的——它是一个仅向前的自定义枚举器。

yield让您快速、轻松地编写自己的自定义枚举器,代码量最小。

所有yield所做的事情都可以在C#1中完成,只是需要大量的代码来完成。

Linq真正最大化了yield行为的价值,但它肯定不是唯一的应用。


2
我不确定C#中yield()的实现情况,但在动态语言中,它比创建整个集合要高效得多。在许多情况下,它使得处理比RAM大得多的数据集变得容易。

2

我是C#中巨大的Yield粉丝。特别是在大型自定义框架中,常常有方法或属性返回List,该List是另一个IEnumerable的子集。我看到的好处有:

  • 使用yield的方法的返回值是不可变的
  • 你只遍历列表一次
  • 它是一个后期或延迟执行变量,这意味着返回值的代码直到需要时才被执行(尽管如果你不知道自己在做什么,这可能会让你感到困扰)
  • 如果源列表发生更改,您不必调用以获取另一个IEnumerable,只需再次遍历IEnumeable即可
  • 还有很多其他好处

yield的另一个巨大好处是当你的方法可能会返回数百万个值时。这么多值可能会在方法返回之前耗尽内存,甚至无法构建List。使用yield,方法可以创建并返回数百万个值,只要调用者也不存储每个值。因此,它非常适合大规模数据处理/聚合操作。


2
每当您的函数返回IEnumerable时,您应该使用“yielding”。不仅在.Net> 3.0中适用。 .Net 2.0示例:
  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);
      }
        ...

1

个人而言,在日常编程中我还没有发现使用yield的必要。然而,最近我开始尝试使用机器人工作室的示例,并发现yield在那里被广泛使用,因此我也看到它与CCR(并发和协调运行时)一起使用,解决异步和并发问题。

无论如何,我仍在努力理解它。


1
请注意,yield 关键字允许您以“惰性”的方式执行操作。所谓“惰性”,是指在实际请求元素之前,不会对 IEnumberable 中的下一个元素进行评估。这使您能够具有一些不同的功能。其中之一是,您可以生成一个无限长的列表,而无需进行无限计算。其次,您可以返回函数应用的枚举。只有在迭代列表时才会应用这些函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接