Оберните IEnumerable и поймайте исключения - PullRequest
10 голосов
/ 01 октября 2010

У меня есть куча классов, которые могут Process() объекты и возвращать свои собственные объекты:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { ... }

Я хочу написать класс процессоров, который может обернуть один из этих процессоров, и записать в журнал все необработанные исключения, которые может выдать упакованный метод Process(). Моей первой идеей было что-то вроде этого:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    try {
        foreach (var x in this.processor.Process(incoming)) {
            yield return x;
        }
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }
}

, но это не работает из-за CS1626: Невозможно получить значение в теле блока try с предложением catch .

Итак, я хочу написать что-то, что концептуально эквивалентно, но компилируется. :-) У меня есть это:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    IEnumerator<T> walker;
    try {
        walker = this.processor.Process(incoming).GetEnumerator();
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }

    while (true) {
        T value;
        try {
            if (!walker.MoveNext()) {
                break;
            }
            value = walker.Current;
        } catch (Exception e) {
            WriteToLog(e);
            throw;
        }
        yield return value;
    }
}

но это сложнее, чем я надеялся, и я не совсем уверен ни в его правильности, ни в том, что не существует более простого способа.

Я на правильном пути? Есть ли более простой способ?

Ответы [ 5 ]

5 голосов
/ 12 января 2016

Если вы хотите обработать исключение во время обработки результата перечисления, тогда вы пытаетесь, чтобы логика просто шла прямо в ваш цикл for / while.

Но ваш пример читается какесли вы пытаетесь перехватить и пропустить исключения, вызванные поставщиком перечисления.

Насколько я могу убедиться, в C # нет способа перебирать перечислитель и пропускать и исключение, которое происходит внутри самого перечислителя.,Если перечислитель вызывает исключение, то все последующие вызовы MoveNext () приведут к ложному выводу.

Самый простой способ объяснить, почему это происходит, с помощью этого простого перечислимого очень :

IEnumerable<int> TestCases()
{
    yield return 1;
    yield return 2;
    throw new ApplicationException("fail eunmeration");
    yield return 3;
    yield return 4;
}

Понятно, что когда мы смотрим на этот пример, очевидно, что выброшенное исключение вызовет выход всего этого блока, а 3-й и 4-й оператор yield никогда не будут обработаны.Фактически, вы получите обычное предупреждение компилятора «Обнаружен недоступный код» в 3-м операторе yield.

Так что, когда перечисление является более сложным, применяются те же правила:

IEnumerable<int> TestCases2()
{
    foreach (var item in Enumerable.Range(0,10))
    {
        switch(item)
        {
            case 2:
            case 5:
                throw new ApplicationException("This bit failed");
            default:
                yield return item;
                break;
        }
    }
}

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

ВСЕ работающие примеры, чтобы обойти эту проблему, которую я обнаружил на SO, не переходят кследующий элемент в перечислении, все они break при первом исключении.

Поэтому, чтобы пропустить en исключение в перечислении, вам понадобится поставщик, чтобы облегчить его.На самом деле это возможно только в том случае, если ваш код провайдера или вы можете связаться с разработчиком, который это сделал, приведенный ниже упрощенный пример того, как вы могли бы достичь этого:

IEnumerable<int> TestCases3(Action<int, Exception> exceptionHandler)
{
    foreach (var item in Enumerable.Range(0, 10))
    {
        int value = default(int);
        try
        {
            switch (item)
            {
                case 2:
                case 5:
                    throw new ApplicationException("This bit failed");
                default:
                    value = item;
                    break;
            }
        }
        catch(Exception e)
        {
            if (exceptionHandler != null)
            {
                exceptionHandler(item, e);
                continue;
            }
            else
                throw;
        }
        yield return value;
    }
}

...

foreach (var item in TestCases3(
    (int item, Exception ex) 
    => 
    Console.Out.WriteLine("Error on item: {0}, Exception: {1}", item, ex.Message)))
{
    Console.Out.WriteLine(item);
}

Это даст следующий вывод:

0
1
Error on item: 2, Exception: This bit failed
3
4
Error on item: 5, Exception: This bit failed
6
7
8
9

Я надеюсь, что это прояснит проблему для других разработчиков в будущем, так как это довольно распространенная идея, которую мы все получим, как только начнем получатьглубоко в Linq и перечисления.Мощные вещи, но есть некоторые логические ограничения.

4 голосов
/ 17 апреля 2014

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

 public static IEnumerable<T> CatchExceptions<T> (this IEnumerable<T> src, Action<Exception> action = null) {
        using (var enumerator = src.GetEnumerator()) {
            bool next = true;

            while (next) {
                try {
                    next = enumerator.MoveNext();
                } catch (Exception ex) {
                    if (action != null) {
                        action(ex);
                    }
                    continue;
                }

                if (next) {
                    yield return enumerator.Current;
                }
            }
        }
    }

Пример:

ienumerable.Select(e => e.something).CatchExceptions().ToArray()

ienumerable.Select(e => e.something).CatchExceptions((ex) => Logger.Log(ex, "something failed")).ToArray()
0 голосов
/ 01 октября 2010

Было бы лучше, если бы объект process был чем-то, что обрабатывало только один элемент в списке за раз (если это даже возможно), таким образом, вы могли бы собрать каждое отдельное исключение и вернуть AggregateException, какБиблиотека Task Parallel делает.

[Если процесс работает со списком в целом, или если единственное исключение требует прерывания всего процесса, очевидно, это предложение не подходит.]

0 голосов
/ 10 февраля 2014

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

    static IEnumerable<T> RunEnumerator<T>(Func<IEnumerator<T>> generator, 
        Action<Exception> onException)
    {
        using (var enumerator = generator())
        {
            if (enumerator == null) 
                yield break;
            for (; ; )
            {
                //Avoid creating a default value with
                //unknown type T, as we don't know is it possible to do so
                T[] value = null;
                try
                {
                    if (enumerator.MoveNext())
                        value = new T[] { enumerator.Current };
                }
                catch (Exception e)
                {
                    onException(e);
                }
                if (value != null)
                    yield return value[0];
                else
                    yield break;
            }
        }
    }

    public static IEnumerable<T> WithExceptionHandler<T>(this IEnumerable<T> orig, 
        Action<Exception> onException)
    {
        return RunEnumerator(() =>
        {
            try
            {
                return orig.GetEnumerator();
            }
            catch (Exception e)
            {
                onException(e);
                return null;
            }
        }, onException);
    }
}

WithExceptionHandler<T> преобразует IEnumerable<T> в другое, а когда возникает исключение, onExceptionобратный вызов будет вызван.Таким образом, вы можете реализовать свою функцию process следующим образом:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    return incoming.WithExceptionHandler(e => {
        WriteToLog(e);
        throw;
    }
}

Таким образом, вы также можете сделать что-то еще, например, полностью игнорировать исключение и позволить итерации просто остановиться.Вы также можете немного изменить его, используя Func<Exception, bool> вместо Action<Exception> и позволяя обратному вызову исключительной ситуации решить, будет ли итерация продолжена или нет.

0 голосов
/ 01 октября 2010

Цикл и выход не нужны (по крайней мере, в вашем примере).Просто удалите их и больше нет ограничений на блоки catch.

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    try {
        return this.processor.Process(incoming);
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }
}

Я предполагаю, что this.processor.Process(incoming) возвращает коллекцию, реализующую IEnumerable<T>, поэтому вам не нужно создавать новый итератор.Если this.processor.Process(incoming) лениво оценивает, то

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    try {
        return this.processor.Process(incoming).ToList();
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }
}
...