如何在匿名方法中使用yield return?

35

我有一个匿名方法,用于我的BackgroundWorker

worker.DoWork += ( sender, e ) =>
{
    foreach ( var effect in GlobalGraph.Effects )
    {
        // Returns EffectResult
        yield return image.Apply (effect);
    }
};

当我这样做时,编译器告诉我:

"在匿名方法或lambda表达式中不能使用yield语句"

那么,在这种情况下,最优雅的方法是什么?顺便说一句,如果解决方案需要,DoWork方法位于静态方法内。


后台工作器是生成“image”还是填充“GlobalGraph.Effects”可枚举集合? - Enigmativity
是的,BW 生成了图像,但 EffectResult 中包含有关效果状态的信息,而不是图像数据或任何类似的内容。 - Joan Venge
可能是重复的问题:在 C# 中,为什么匿名方法不能包含 yield 语句? - Mauricio Scheffer
8个回答

17

很遗憾,你不能这样做。

编译器不允许将两个“神奇”的代码片段结合在一起。这两种方法都需要重写你的代码来支持你想要做的事情:

  1. 匿名方法是通过将代码移动到适当的方法中,并将局部变量提升为该方法所在类的字段来完成的。
  2. 迭代器方法被重写为状态机。

但是,你可以将代码重写为返回集合的形式,在你的特定情况下,我会这样做:

worker.DoWork += ( sender, e ) =>
{
    return GlobalGraph.Effects
        .Select(effect => image.Apply(effect));
};

虽然在事件 (sender, e) 中返回任何内容看起来很奇怪。您确定您向我们展示的是真实场景吗?


编辑:好的,我认为我知道你在这里想要做什么了。

您调用了一个静态方法,然后想要在后台执行代码,并在后台调用完成后从该静态方法返回数据。

虽然这是可能的,但并不是一个好的解决方案,因为您实际上正在暂停一个线程等待另一个线程,而后一个线程是在您暂停线程之前直接启动的。换句话说,您正在增加上下文切换的开销。

相反,您需要启动后台工作,然后在该工作完成时处理生成的数据。


谢谢Lasse。实际上你是对的,我不确定我在这件事上是否做得对。基本上,这个worker位于一个名为Run的静态方法内,该方法应返回IEnumerable<EffectResult>。我该怎么做呢?在DoWork之外吗?我只是在那里做了这件事,因为应用一堆效果就是这个方法正在做的事情(并返回结果)。 - Joan Venge
即使我展示的是这样,但在调用image.Apply之前,我有一个for循环执行其他操作,对于每次迭代,我能在lambda表达式中放置一个for循环吗? - Joan Venge
在一个 lambda 表达式中,您无法使用 return 关键字来返回其所在方法。在 lambda 表达式中使用的 return 关键字只会导致从该 lambda 表达式本身返回。 - Enigmativity
@Enigmativity 在匿名方法中,没有任何方法可以从封闭的方法中返回。这就像说方法A调用方法B并且方法B从方法A返回一样,这是不可能的。然而,这表明这个问题的整个思路存在问题。 - Lasse V. Karlsen
基本上,我正在尝试逐个获取结果,而不是在静态方法完成工作时获取结果。这就是你的意思吗? - Joan Venge

10

或许只需返回linq表达式并像yield一样延迟执行:

return GlobalGraph.Effects.Select(x => image.Apply(x));

5

除非我漏掉了什么,否则你无法做到你所要求的。

(我有一个答案可以回答你,请先阅读我解释为什么你不能做你正在做的事情,然后继续阅读。)

你的完整方法应该像这样:

public static IEnumerable<EffectResult> GetSomeValues()
{
    // code to set up worker etc
    worker.DoWork += ( sender, e ) =>
    {
        foreach ( var effect in GlobalGraph.Effects )
        {
            // Returns EffectResult
            yield return image.Apply (effect);
        }
    };
}

如果我们假设您的代码是“合法”的,那么当调用GetSomeValues时,即使将DoWork处理程序添加到worker中,lambda表达式也不会在DoWork事件触发之前执行。因此,对GetSomeValues的调用完成后没有返回任何结果,lamdba可能会在以后的某个阶段被调用-这对于GetSomeValues方法的调用者来说已经太晚了。
最好的答案是使用Rx
Rx颠覆了IEnumerable<T>的方式。Rx不是从可枚举对象中请求值,而是从IObservable<T>中推送值给你。
由于您正在使用后台工作器并响应事件,因此实际上已经将值推送给您。使用Rx可以轻松地完成您要尝试的操作。
您有几个选项。可能最简单的方法是这样做:
public static IObservable<IEnumerable<EffectResult>> GetSomeValues()
{
    // code to set up worker etc
    return from e in Observable.FromEvent<DoWorkEventArgs>(worker, "DoWork")
           select (
               from effect in GlobalGraph.Effects
               select image.Apply(effect)
           );
}

现在,您的GetSomeValues方法的调用者将会这样做:
GetSomeValues().Subscribe(ers =>
{
    foreach (var er in ers)
    {
        // process each er
    }
});

如果你知道DoWork只会被触发一次,那么这种方式可能会更好:

public static IObservable<EffectResult> GetSomeValues()
{
    // code to set up worker etc
    return Observable
        .FromEvent<DoWorkEventArgs>(worker, "DoWork")
        .Take(1)
        .Select(effect => from effect in GlobalGraph.Effects.ToObservable()
                          select image.Apply(effect))
        .Switch();  
}

这段代码看起来有点复杂,但它只是将单个“do work”事件转换为一系列EffectResult对象。

然后调用代码如下:

GetSomeValues().Subscribe(er =>
{
    // process each er
});

Rx甚至可以用来替代后台工作线程。这可能是您的最佳选择:

public static IObservable<EffectResult> GetSomeValues()
{
    // set up code etc
    return Observable
        .Start(() => from effect in GlobalGraph.Effects.ToObservable()
                     select image.Apply(effect), Scheduler.ThreadPool)
        .Switch();  
}

调用代码与之前的示例相同。 Scheduler.ThreadPool 告诉 Rx 如何 "调度" 处理对观察者的订阅。

希望这可以帮到你。


谢谢,这看起来是个不错的解决方案。你是否还知道一种纯粹使用BW的解决方案?我想使用Rx,但目前如果可能的话,我不想增加新的依赖。稍后我可以着手优化代码的美观性。 - Joan Venge
@Joan - 你不能使用BW来实现你想要的功能。这样做没有意义。你的调用代码正在获取一个IEnumerable<EffectResult>,而它必须在BW执行其任务时进行阻塞。因此,你最好完全避免使用BW。你需要使用类似Rx或TPL的东西。 - Enigmativity
那么我在使用BW时该如何更新UI呢?这似乎至少应该是微不足道的。我正在使用WPF,并希望逐渐更新UI绑定到的集合,这就是我想使用IEnumerable<EffectResult>的原因。 - Joan Venge
@Joan - 遍历任何 IEnumerable<T> 都是阻塞调用。使用 yield return 不会使其异步化。您的 BW 应在后台执行所有数据处理,然后一次性将处理后的数据交给 UI。您正在尝试分多个步骤(即使用可枚举对象)交付它,这是行不通的。尝试使用 TPL。如果您使用的是 .NET 3.5 SP1,则可以通过安装 Rx 来获取 TPL。它已经捆绑在其中了。 - Enigmativity
谢谢Enigma。我使用.NET 4.0,但以前没有使用过TPL。我没想到会这么难。如果我在一步中更新UI,那很容易,但不理想,因为用户希望实时了解正在发生的事情。 :O - Joan Venge
显示剩余7条评论

4

对于新读者:在C#5中实现“匿名迭代器”(即嵌套在其他方法中)最优雅的方法可能是像this cool trick with async/await这样的东西(不要被这些关键字所困扰,下面的代码是完全同步计算的 - 详见链接页面的细节):

        public IEnumerable<int> Numbers()
        {
            return EnumeratorMonad.Build<int>(async Yield =>
            {
                await Yield(11);
                await Yield(22);
                await Yield(33);
            });
        }

        [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
        public void TestEnum()
        {
            var v = Numbers();
            var e = v.GetEnumerator();

            int[] expected = { 11, 22, 33 };

            Numbers().Should().ContainInOrder(expected);

        }

C#7(现在在Visual Studio 15 Preview中可用)支持本地函数,允许使用yield return

public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (filter == null) throw new ArgumentNullException(nameof(filter));

    return Iterator();

    IEnumerable<T> Iterator()
    {
        foreach (var element in source) 
        {
            if (filter(element)) { yield return element; }
        }
    }
}

1
工作者应该设置DoWorkEventArgs的Result属性。
worker.DoWork += (s, e) => e.Result = GlobalGraph.Effects.Select(x => image.Apply(x));

我能像使用IEnumerable一样“接入”这个结果吗?因为我想在应用效果时更新我的UI,以反映它们的结果。 - Joan Venge
你应该为RunWorkerCompleted设置一个处理程序。然后,使用foreach(var effect in (GlobalGraph.Effects)e.Result) ... - Richard Schneider
那么您的意思是我应该将进度更新和UI更新添加到RunWorkerCompleted中吗?因为我在这个类和UI之间有一些分离,使用了ViewModels等。 - Joan Venge
是的,DoWork不能触及UI,因为它不在UI线程上。RunWorkerCompleted在DoWork完成后被调用,并且在UI线程上;这是您更新UI的地方。如果这个答案可以,请点击接受。 - Richard Schneider
那么为什么要在后台线程上执行这个操作呢?只需要在UI线程上使用BackgroundWorker来执行代码即可。 - Richard Schneider
显示剩余2条评论

1

好的,我做了类似这样的事情,它实现了我想要的功能(省略了一些变量):

public static void Run ( Action<float, EffectResult> action )
{
    worker.DoWork += ( sender, e ) =>
    {
        foreach ( var effect in GlobalGraph.Effects )
        {
            var result = image.Apply (effect);

            action (100 * ( index / count ), result );
        }
    }
};

然后在调用站点:

GlobalGraph.Run ( ( p, r ) =>
    {
        this.Progress = p;
        this.EffectResults.Add ( r );
    } );

1

DoWork 是类型为 DoWorkEventHandler 的方法,它返回空值 (void),因此在您的情况下根本不可能。


0

我想要补充user1414213562的答案,并提供一个ForEachMonad的实现。

static class ForEachMonad
{
    public static IEnumerable<A> Lift<A>(A a) { yield return a; }

    // Unfortunately, this doesn't compile

    // public static Func<IEnumerable<A>, IEnumerable<B>> Lift<A, B>(Func<A, IEnumerable<B>> f) =>
    //     (IEnumerable<A> ea) => { foreach (var a in ea) { foreach (var b in f(a)) { yield return b; } } }

    // Fortunately, this does :)
    
    public static Func<IEnumerable<A>, IEnumerable<B>> Lift<A, B>(Func<A, IEnumerable<B>> f)
    {
        IEnumerable<B> lift(IEnumerable<A> ea)
        {
            foreach (var a in ea) { foreach (var b in f(a)) { yield return b; } }
        }
        return lift;
    }

    public static void Demo()
    {
        var f = (int x) => (IEnumerable<int>)new int[] { x + 1, x + 2, x + 3 };
        var g = (int x) => (IEnumerable<double>)new double[] { Math.Sqrt(x), x*x };
        var log = (double d) => { Console.WriteLine(d); return Lift(d); };

        var e1 = Lift(0);
        var e2 = Lift(f)(e1);
        var e3 = Lift(g)(e2);
        // we call ToArray in order to materialize the IEnumerable
        Lift(log)(e3).ToArray();
    }
}

运行 ForEachMonad.Demo() 会产生以下输出:

1
1
1,4142135623730951
4
1,7320508075688772
9

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