Как вернуть возврат внутри анонимных методов?

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

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

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

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

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


person Joan Venge    schedule 23.03.2011    source источник
comment
Производит ли фоновый воркер image или заполняет GlobalGraph.Effects перечислимый?   -  person Enigmativity    schedule 24.03.2011
comment
Да, BW создает изображение, но EffectResult имеет статус эффекта, а не данные изображения или что-то в этом роде.   -  person Joan Venge    schedule 24.03.2011
comment
возможный дубликат Почему нельзя? анонимный метод содержит оператор yield?   -  person Mauricio Scheffer    schedule 15.01.2012


Ответы (7)


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

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

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

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

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

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


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

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

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

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

person Lasse V. Karlsen    schedule 23.03.2011
comment
Спасибо, Лассе. На самом деле вы правы, я не уверен, правильно ли я поступаю по этому поводу. По сути, этот воркер находится внутри статического метода Run, который должен возвращать IEnumerable ‹EffectResult›. Как я должен это делать? Вне DoWork? Я просто сделал это там, потому что все, что делает этот метод (и возвращает результаты) - это применение кучи эффектов. - person Joan Venge; 24.03.2011
comment
Кроме того, хотя я показал это вот так, у меня есть цикл for, который выполняет что-то еще перед image.Apply вызывается для каждой итерации, могу ли я поместить цикл for внутри лямбда-выражения? - person Joan Venge; 24.03.2011
comment
Вы не можете сделать return внутри лямбды, которая должна возвращаться из включающего метода. return в лямбде возвращается из самой лямбды. - person Enigmativity; 24.03.2011
comment
@Enigmativity. Нет никакого способа вернуться из включающего метода вообще в анонимном методе. Это все равно, что сказать, что метод A вызывает метод B, а метод B возвращается из метода A, это невозможно. Однако это признак того, что в этом вопросе что-то не так. - person Lasse V. Karlsen; 24.03.2011
comment
В основном я пытаюсь получить результаты один за другим, а не после того, как статический метод завершит свою работу. Вы это имели в виду? - person Joan Venge; 24.03.2011

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

return GlobalGraph.Effects.Select(x => image.Apply(x));
person Tejs    schedule 23.03.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, как «запланировать» обработку подписок для наблюдателя.

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

person Enigmativity    schedule 23.03.2011
comment
Спасибо, это кажется хорошим решением. Вы тоже знаете решение чисто с использованием BW? Я хотел бы использовать Rx, но в настоящее время я не хочу добавлять новые зависимости, если это возможно. Позже я смогу поработать над украшением кода. - person Joan Venge; 24.03.2011
comment
@Joan - Вы не можете использовать BW, чтобы делать то, что хотите. Нет смысла его использовать. Ваш вызывающий код получает IEnumerable<EffectResult>, который он должен заблокировать, пока BW делает свое дело. Из-за этого вы могли бы вообще избежать BW. Вам нужно использовать что-то вроде Rx или TPL. - person Enigmativity; 24.03.2011
comment
Хорошо, тогда как мне обновить пользовательский интерфейс при использовании BW? По крайней мере, это должно быть тривиально. Я использую WPF и хочу постепенно обновлять коллекцию, к которой привязан пользовательский интерфейс, поэтому я хотел использовать IEnumerable<EffectResult>. - person Joan Venge; 24.03.2011
comment
@Joan - Выполнение любого IEnumerable<T> является блокирующим вызовом. Использование yield return не делает его асинхронным. Ваш BW должен выполнять всю обработку данных в фоновом режиме, а затем, по завершении, передавать обработанные данные пользовательскому интерфейсу за один шаг. Вы пытаетесь передать его на многих этапах (например, используя перечислимое число), и это не сработает. Попробуйте использовать TPL. Если вы используете .NET 3.5 SP1, вы можете получить TPL, установив Rx. Он там находится в комплекте. - person Enigmativity; 24.03.2011
comment
Спасибо Enigma. Я использую .NET 4.0, но раньше не использовал TPL. Я не думал, что это будет так сложно. Если я обновлю пользовательский интерфейс за 1 шаг, это будет легко, но нежелательно, потому что пользователям нужна обратная связь в реальном времени о том, что происходит: O - person Joan Venge; 24.03.2011
comment
@Joan - Создать адаптивный интерфейс сложно. Используя TPL, вы можете асинхронно возвращать все свои результаты за один шаг. Используя Rx, вы можете асинхронно возвращать каждый результат по одному. В противном случае вам нужно разрешить коду BW напрямую обновлять пользовательский интерфейс через вызовы, вызываемые в потоке пользовательского интерфейса. Все варианты беспорядочные. Rx звучит как лучший выбор для вас. В нем есть операции по выполнению фоновой обработки и обновления переднего плана без особых усилий. - person Enigmativity; 24.03.2011
comment
Спасибо Enigma, поэтому возвращение результатов 1 к 1, как я хочу, невозможно даже в TPL, а только в Rx? Мне нужно закончить это в ближайшее время, поэтому я беспокоюсь, что изучение Rx займет гораздо больше времени, когда я в него вошел. - person Joan Venge; 24.03.2011
comment
@Joan - Если вы можете опубликовать больше своего кода, я могу помочь вам с кодом Rx, если это поможет. - person Enigmativity; 24.03.2011
comment
Спасибо Enigma, это точно поможет. Я не могу отправить сообщение в SO, но я посмотрю, смогу ли я извлечь некоторые его части, чтобы сделать эту часть доступной для публикации. Вы часто используете Rx в своих собственных материалах? - person Joan Venge; 24.03.2011
comment
@Joan - Да, я использую Rx как стандартную часть всего моего кода. Вы можете написать мне по электронной почте на мое имя пользователя в Gmail. - person Enigmativity; 24.03.2011
comment
Спасибо, чувак, ценю это. Я кое-что понимаю, поэтому, если я смогу это сделать, я дам вам знать, потому что тогда мне не нужно будет выполнять сложные потоки для пользовательского интерфейса. - person Joan Venge; 24.03.2011
comment
Хорошо, мне удалось сделать это с помощью Action ‹float, EffectResult› и сделать то, что я хочу, с переданным EffectResult на каждой итерации, это работает так, как я хотел. Рад, что мне не пришлось усложнять это дальше. - person Joan Venge; 24.03.2011

Для новых читателей: наиболее элегантный способ реализации «анонимных итераторов» (т.е. вложенных в другие методы) в 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) поддерживает локальные функции, которые позволяют 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; }
        }
    }
}
person user1414213562    schedule 12.09.2016

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

person manji    schedule 23.03.2011

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

worker.DoWork += (s, e) => e.Result = GlobalGraph.Effects.Select(x => image.Apply(x));
person Richard Schneider    schedule 23.03.2011
comment
Могу ли я использовать этот результат как IEnumerable? Потому что я хочу обновлять свой пользовательский интерфейс по мере применения эффектов в отношении их результатов. - person Joan Venge; 24.03.2011
comment
Вы должны настроить обработчик для RunWorkerCompleted. Затем foreach (эффект var в (GlobalGraph.Effects) e.Result) ... - person Richard Schneider; 24.03.2011
comment
Значит, вы имеете в виду, что я должен добавить обновление прогресса и обновление пользовательского интерфейса в RunWorkerCompleted? Потому что у меня есть некоторое разделение между этим классом и пользовательским интерфейсом, с ViewModels и т. Д. - person Joan Venge; 24.03.2011
comment
Да, DoWork не может касаться пользовательского интерфейса, потому что он не находится в потоке пользовательского интерфейса. RunWorkerCompleted вызывается после завершения DoWork и включен в поток пользовательского интерфейса; здесь вы обновляете пользовательский интерфейс. Пожалуйста, отметьте "принять", если этот ответ в порядке. - person Richard Schneider; 24.03.2011
comment
Спасибо, Ричард, но RunWorkerCompleted вызывается только один раз, верно? Я хочу обновлять пользовательский интерфейс каждый раз, когда выполняется эффект, чтобы показать его результат и обновить индикатор выполнения. - person Joan Venge; 24.03.2011
comment
Тогда зачем делать это в фоновом потоке. Просто сделайте код в потоке пользовательского интерфейса с помощью BackgroundWorker. - person Richard Schneider; 24.03.2011
comment
Но поток пользовательского интерфейса вызывает этот статический метод только внутри статического класса, который отвечает за применение эффектов. Разве это не хорошая практика? Потому что в противном случае логика будет помещена в код пользовательского интерфейса, который принадлежит типу EffectGraph. - person Joan Venge; 24.03.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 );
    } );
person Joan Venge    schedule 24.03.2011