包装IEnumerable并捕获异常

时间:2010-09-30 23:54:12

标签: c# exception ienumerable wrapper

我有一堆可以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: Cannot yield a value in the body of a try block with a catch clause,这不起作用。

所以我想写一些概念上等同但编译的东西。 :-)我有这个:

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 个答案:

答案 0 :(得分:5)

如果您想要做的是在处理枚举结果期间处理异常,那么您尝试逻辑只需要直接进入for / while循环。

但是您的示例读取就好像您正试图捕获并跳过枚举提供程序引发的异常。

据我所知,C#中没有办法迭代枚举器并跳过枚举器本身内发生的异常。 如果枚举器引发异常,那么将来对MoveNext()的所有调用都将导致错误输出。

解释原因的最简单方法是使用 very 简单枚举:

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上找到的解决这个问题的所有可行示例都没有进入枚举中的下一个项目,它们在第一个异常时都中断

因此,要在枚举中跳过例外,您需要提供商来促进它。这只有在您编写提供程序时才可能实现,或者您可以联系开发人员,以下是如何实现此目的的过度简化示例:

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和枚举,我们都会得到一个非常普遍的想法。功能强大但存在一些逻辑限制。

答案 1 :(得分:4)

可以编写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()

答案 2 :(得分:0)

如果process对象一次只处理列表中的一个项目(如果可能的话),则可能更好,这样您就可以收集每个单独的异常并返回{{1}就像任务并行库一样。

[如果进程在整个列表中运行,或者如果一个例外需要中止整个过程,那么这个建议显然不合适。]

答案 3 :(得分:-1)

您可以在工具箱中使用这些辅助函数,而不是仅解决您的特定问题:

    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>稍微调整它,并允许异常回调来决定迭代是否继续。

答案 4 :(得分:-2)

不需要循环和良​​率(至少在您的示例中)。简单地删除它们并且不再限制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;
    }
}