【问题标题】:Is it possible to handle exceptions within LINQ queries?是否可以在 LINQ 查询中处理异常?
【发布时间】:2009-08-18 14:29:15
【问题描述】:

例子:

myEnumerable.Select(a => ThisMethodMayThrowExceptions(a));

即使抛出异常,如何让它工作?就像带有默认值 case 的 try catch 块一样,抛出异常...

【问题讨论】:

  • 这在很大程度上取决于方法的返回类型是否应该这样做。在大多数情况下,这不是一个健康的代码模式,因为一般来说,按照范式,集合操作应该以原子方式成功。
  • @GertArnold 不能同意。捕获已知异常总比没有好。
  • @shtse8 是的,但不是 inside LINQ 语句,大多数时候。
  • @GertArnold 这样我们为什么需要实现 Catch 运算符来让我们的生活更轻松。
  • @shtse8 查看Catch 运算符,来自System.Interactive 包。 MoveNext 调用必须放在 try 块内。

标签: c# .net linq exception exception-handling


【解决方案1】:
myEnumerable.Select(a => 
  {
    try
    {
      return ThisMethodMayThrowExceptions(a));
    }
    catch(Exception)
    {
      return defaultValue;
    }
  });

但实际上,它有一些气味。

关于 lambda 语法:

x => x.something

是一种捷径,可以写成

(x) => { return x.something; }

【讨论】:

    【解决方案2】:

    调用具有 try/catch 的投影:

    myEnumerable.Select(a => TryThisMethod(a));
    
    ...
    
    public static Bar TryThisMethod(Foo a)
    {
         try
         {
             return ThisMethodMayThrowExceptions(a);
         }
         catch(BarNotFoundException)
         {
             return Bar.Default;
         }
    }
    

    诚然,我很少想要使用这种技术。总的来说,这感觉像是在滥用异常,但有时有些 API 会让您别无选择。

    (不过,我几乎肯定会将它放在一个单独的方法中,而不是将其“内联”为 lambda 表达式。)

    【讨论】:

    • 您认为什么是滥用异常?在我的业务逻辑中,我有一种计算员工工资的方法。有各种各样的事情可能会出错,比如员工没有设定工资。我做了一个自定义异常,我可以在控制器中抛出和捕获并放入我的视图模型中。这很糟糕?
    • @Pluc: 员工是否打算设定薪水?如果是这样,它的失踪听起来很特别。如果有点预期,我可能不会使用异常来处理它。
    【解决方案3】:

    如果您需要 Expression 而不是 lambda 函数(例如,从 IQueryable 中选择时),您可以使用以下内容:

    public static class ExpressionHelper
    {
        public static Expression<Func<TSource, TResult>> TryDefaultExpression<TSource, TResult>(Expression<Func<TSource, TResult>> success, TResult defaultValue)
        {
            var body = Expression.TryCatch(success.Body, Expression.Catch(Expression.Parameter(typeof(Exception)), Expression.Constant(defaultValue, typeof (TResult))));
            var lambda = Expression.Lambda<Func<TSource, TResult>>(body, success.Parameters);
    
            return lambda;
        }
    }
    

    用法:

    [Test]
    public void Test()
    {
        var strings = new object [] {"1", "2", "woot", "3", Guid.NewGuid()}.AsQueryable();
        var ints = strings.Select(ExpressionHelper.TryDefaultExpression<object, int>(x => Convert.ToInt32(x), 0));
        Assert.IsTrue(ints.SequenceEqual(new[] {1, 2, 0, 3, 0}));
    }
    

    【讨论】:

      【解决方案4】:

      当我想快速尝试/捕获IEnumerable&lt;T&gt; 的每次迭代时,我提供了一个小扩展程序

      用法

      public void Test()
      {
          List<string> completedProcesses = initialEnumerable
              .SelectTry(x => RiskyOperation(x))
              .OnCaughtException(exception => { _logger.Error(exception); return null; })
              .Where(x => x != null) // filter the ones which failed
              .ToList();
      }
      

      扩展程序

      public static class OnCaughtExceptionExtension
      {
          public static IEnumerable<SelectTryResult<TSource, TResult>> SelectTry<TSource, TResult>(this IEnumerable<TSource> enumerable, Func<TSource, TResult> selector)
          {
              foreach (TSource element in enumerable)
              {
                  SelectTryResult<TSource, TResult> returnedValue;
                  try
                  {
                      returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null);
                  }
                  catch (Exception ex)
                  {
                      returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex);
                  }
                  yield return returnedValue;
              }
          }
      
          public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<Exception, TResult> exceptionHandler)
          {
              return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException));
          }
      
          public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<TSource, Exception, TResult> exceptionHandler)
          {
              return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException));
          }
      
          public class SelectTryResult<TSource,TResult>
          {
              internal SelectTryResult(TSource source, TResult result, Exception exception)
              {
                  Source = source;
                  Result = result;
                  CaughtException = exception;
              }
      
              public TSource Source { get; private set; }
              public TResult Result { get; private set; }
              public Exception CaughtException { get; private set; }
          }
      }
      

      我们最终可以通过SkipOnException 扩展来更进一步,例如可选地接受异常处理程序。

      【讨论】:

        【解决方案5】:

        Stefan 的理解语法解决方案的变体:

        from a in myEnumerable
        select (new Func<myType>(() => {
            try
            {
                return ThisMethodMayThrowExceptions(a));
            }
            catch(Exception)
            {
                return defaultValue;
            }
        }))();
        

        虽然它也“有味道”,但这种方法有时仍可用于运行带有表达式内部副作用的代码。

        【讨论】:

          【解决方案6】:

          在处理 LINQ 时,您通常会发现表达式可能会产生不希望的副作用的情况。正如 Jon 所说,解决此类问题的最佳方法是让您的 LINQ 表达式可以使用实用方法来优雅地处理这些问题,并且不会破坏您的代码。例如,我有一个方法我不得不不时使用它包装一个 TryParse 来告诉我某事是否是一个数字。当然还有很多其他的例子。

          表达式语法的一个限制是,如果不临时中断表达式的执行以处理给定的场景,它就无法优雅地甚至根本无法完成很多事情。解析 XML 文件中的项目子集就是一个很好的例子。尝试在单个表达式中使用 XML 文件中的子子集解析复杂的父集合,您很快就会发现自己编写了多个表达式片段,这些片段组合在一起形成了整个操作。

          【讨论】:

            【解决方案7】:
            /// <summary>
            /// Catch the exception and then omit the value if exception thrown.
            /// </summary>
            public static IEnumerable<T> Catch<T>(this IEnumerable<T> source, Action<Exception> action = null)
            {
                return Catch<T, Exception>(source, action);
            }
            
            
            /// <summary>
            /// Catch the exception and then omit the value if exception thrown.
            /// </summary>
            public static IEnumerable<T> Catch<T, TException>(this IEnumerable<T> source, Action<TException> action = null) where TException : Exception
            {
                using var enumerator = source.GetEnumerator();
                while(true)
                {
                    T item;
                    try
                    {
                        if (!enumerator.MoveNext())
                            break;
                        item = enumerator.Current;
                    }
                    catch (TException e)
                    {
                        action?.Invoke(e);
                        continue;
                    }
                    yield return item;
                }
            }
            
            /// <summary>
            /// Catch the exception and then return the default value.
            /// </summary>
            public static IEnumerable<T> Catch<T>(this IEnumerable<T> source, Func<Exception, T> defaultValue)
            {
                return Catch<T, Exception>(source, defaultValue);
            }
            
            /// <summary>
            /// Catch the exception and then return the default value.
            /// </summary>
            public static IEnumerable<T> Catch<T, TException>(this IEnumerable<T> source, Func<TException, T> defaultValue) where TException : Exception
            {
                using var enumerator = source.GetEnumerator();
                while(true)
                {
                    T item;
                    try
                    {
                        if (!enumerator.MoveNext())
                            break;
                        item = enumerator.Current;
                    }
                    catch (TException e)
                    {
                        item = defaultValue(e);
                    }
                    yield return item;
                }
            }
            

            用法:

            myEnumerable.Select(a => ThisMethodMayThrowExceptions(a)).Catch(e => Console.WriteLine(e.Message));
            
            myEnumerable.Select(a => ThisMethodMayThrowExceptions(a)).Catch(e => default);
            
            myEnumerable.Select(a => ThisMethodMayThrowExceptions(a)).Catch();
            
            myEnumerable.Select(a => ThisMethodMayThrowExceptions(a)).Catch(((InvalidOperationException) e) => Console.WriteLine(e.Message));
            
            myEnumerable.Select(a => ThisMethodMayThrowExceptions(a)).Catch(((InvalidOperationException) e) => default);
            
            

            【讨论】:

            • @GertArnold 将在ThisMethodMayThrowExceptions(a) 中捕获异常,因为这是在enumerator.Current 中运行的
            • 我没有参加过测试。添加minimal reproducible example 来证明您的观点。
            • @GertArnold 对于这个问题,我得到的是一个“喜欢”返回默认值的示例,但不是必须的要求,而有些人可能想要捕获异常。因此,对于Catch 扩展,我们可以将其修改为具有两个重载:Action 用于捕获,Func&lt;T&gt; 用于返回默认值。
            • @GertArnold 表示正确的方法,有些人喜欢Linq,有些人喜欢foreach,然后是catch,甚至有些人喜欢Reactive。没有绝对正确的方法,这取决于。对我来说,如果项目使用Linq最多,请使用所有Linq。如果项目喜欢for,那么请在for 中完成所有操作。如果大多数是Reactive,请使用Reactive
            • 我更喜欢在 try block 中访问 enumerator.Current。抛出的可能性很小,但谁知道自定义枚举器的行为?
            【解决方案8】:

            为此,我创建了一个小的library。它支持 Select、SelectMany 和 Where 运算符的异常处理。 使用示例:

            var target = source.AsCatchable() // move source to catchable context
                .Select(v => int.Parse(v)) // can throw an exception
                .Catch((Exception e) => { /* some action */ }, () => -1) 
                .Select(v => v * 2)
                .ToArray();
            

            相当于

            var target = source
                .Select(v => 
                    {
                        try
                        {
                            return int.Parse(v);
                        }
                        catch (Exception)
                        {
                            return -1; // some default behaviour 
                        }
                    })
                .Select(v => v * 2)
                .ToArray();
            

            也可以处理多种类型的异常

            var collection = Enumerable.Range(0, 5)
                            .AsCatchable()
                            .Select(v =>
                            {
                                if (v == 2) throw new ArgumentException("2");
                                if (v == 3) throw new InvalidOperationException("3");
                                return v.ToString();
                            })
                            .Catch((ArgumentException e) => { /*  */ }, v => "ArgumentException")
                            .Catch((InvalidOperationException e) => { /*  */ }, v => "InvalidOperationException")
                            .Catch((Exception e) => { /*  */ })
                            .ToList();
            
            

            【讨论】:

            • 仅供参考,库 System.Interactive 还包括一个 Catch 运算符。您可能应该检查该运算符的重载,以确保在有人想要使用您的库和该库的组合时不会有歧义。
            • 感谢您的澄清! AsCatchable 返回 ICatchableEnumerable 的对象。重写的 Select、SelectMany、Where (和 Catch)也适用于该接口。所以它的工作没有歧义。我创建了新接口,因为 IEnumerable(在 System.Interactive 中使用)的类似 yield 实现会导致在第一个异常后停止迭代(MoveNext() 在抛出后总是返回 false)。
            【解决方案9】:

            用新函数包装 ThisMethodMayThrowExceptions,

            myEnumerable.Select(a =>         ThisMethodMayThrowExceptions(a)); //old
            myEnumerable.Select(a => tryCall(ThisMethodMayThrowExceptions,a)); //new
            

            这是一个泛型函数,里面有try catch。

            T2 tryCall<T1, T2>(Func<T1, T2> fn, T1 input, T2 exceptionValue = default)
            {
                try
                {
                    return fn(input);
                }
                catch
                {
                    return exceptionValue;
                }
            }
            
            var numbers = new [] {"1", "a"};
            numbers.Select(n => tryCall(double.Parse, n));             //1, 0
            numbers.Select(n => tryCall(double.Parse, n, double.NaN)); //1, NaN
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2013-05-27
              • 2014-08-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-12-17
              相关资源
              最近更新 更多