Как вернуть доход внутри анонимных методов? - PullRequest
29 голосов
/ 24 марта 2011

По сути, у меня есть анонимный метод, который я использую для моего BackgroundWorker:

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

Когда я делаю это, компилятор говорит мне:

"Оператор yield не можетиспользоваться внутри анонимного метода или лямбда-выражения "

Так что в этом случае, какой самый элегантный способ сделать это?Кстати, этот метод DoWork находится внутри статического метода, на случай, если это имеет значение для решения.

Ответы [ 7 ]

14 голосов
/ 24 марта 2011

К сожалению, вы не можете.

Компилятор не позволяет объединять два «волшебных» фрагмента кода. Оба включают переписывание вашего кода для поддержки того, что вы хотите сделать:

  1. Анонимный метод выполняется путем перемещения кода в правильный метод и переноса локальных переменных в поля класса с помощью этого метода
  2. Метод итератора переписывается как конечный автомат

Однако вы можете переписать код, чтобы вернуть коллекцию, поэтому в вашем конкретном случае я бы сделал следующее:

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

хотя событие (sender, e) выглядит странным, что вообще что-либо возвращает. Вы уверены, что демонстрируете нам реальный сценарий?


Редактировать Хорошо, я думаю Я вижу, что вы пытаетесь сделать здесь.

У вас есть вызов статического метода, а затем вы хотите выполнить код в фоновом режиме, а затем вернуть данные из этого статического метода после завершения фонового вызова.

Это, хотя и возможно, не очень хорошее решение, поскольку вы фактически приостанавливаете один поток, ожидая другого, который был запущен непосредственно перед тем, как вы приостановили поток. Другими словами, все, что вы делаете, это добавляете накладные расходы на переключение контекста.

Вместо этого вам нужно просто запустить фоновую работу, а затем, когда эта работа будет завершена, обработать полученные данные.

10 голосов
/ 24 марта 2011

Возможно, просто вернуть выражение linq и отложить выполнение, как yield:

return GlobalGraph.Effects.Select(x => image.Apply(x));
5 голосов
/ 24 марта 2011

Если я что-то упустил, вы не можете делать то, что просите.

(у меня есть ответ для вас, поэтому, пожалуйста, прочитайте мое объяснение, почему вы не можете сделать то, чтовы делаете сначала, а потом читаете дальше.)

Ваш полный метод будет выглядеть примерно так:

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, лямбда-выражение не выполняется до тех пор, пока не сработает событие 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, как «планировать» обработку подписок для наблюдателя.

Надеюсь, это поможет.

1 голос
/ 24 марта 2011

Хорошо, поэтому я сделал что-то вроде этого, что делает то, что я хотел (некоторые переменные опущены):

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 голос
/ 24 марта 2011

Рабочий должен установить свойство Result объекта DoWorkEventArgs.

worker.DoWork += (s, e) => e.Result = GlobalGraph.Effects.Select(x => image.Apply(x));
1 голос
/ 24 марта 2011

DoWork имеет тип DoWorkEventHandler, который ничего не возвращает (void), так что в вашем случае это вообще невозможно.

0 голосов
/ 12 сентября 2016

Для новых читателей: самый элегантный способ реализации «анонимных итераторов» (т. Е. Вложенных в другие методы) в C # 5, вероятно, похож на этот крутой прием с 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; }
        }
    }
}
...