【问题标题】:Try-Catch with fluent expressionsTry-Catch 流利的表达
【发布时间】:2015-02-21 04:47:13
【问题描述】:

此 LINQ 查询表达式失败并出现 Win32Exception "访问被拒绝":

Process.GetProcesses().Select(p => p.MainModule.FileName)

这失败了 IOException "设备没有准备好":

DriveInfo.GetDrives().Select(d => d.VolumeLabel)

过滤不可访问对象并避免异常的最佳方法是什么?

【问题讨论】:

    标签: c# linq select exception-handling functional-programming


    【解决方案1】:

    插入一个 WHERE 过滤器(尝试访问任何对象并吸收可能的访问错误):

       { try { var x = obj.MyProp; return true; } catch { return false; } }:
    

    第一个表达式:

    Process
       .GetProcesses()
       .Where(p => { try { var x = p.MainModule; return true; } catch { return false; } })
       .Select(p => p.MainModule.FileName)
    

    第二个表达方式:

    DriveInfo
       .GetDrives()
       .Where(d => { try { var x = d.VolumeLabel; return true; } catch { return false; } })
       .Select(d => d.VolumeLabel)
    

    【讨论】:

    • 这看起来更像是您在进行测试...您在 4 分钟内回答了自己的问题。应该是一个维基
    • 如果这确实是正确的答案。我到处寻找并想出了自己的替代方案,但不确定它是否是最好的。
    • @nixxbb 回答您自己的问题是完全合法的。 SO甚至建议扩展知识库。
    • 为了简单起见,显然没有好的通用解决方案,这似乎是正确的答案:)
    【解决方案2】:

    我会尝试第一种情况:

    //Declare logger type
    private readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    
    Process.GetProcesses()
    .Where(p => { 
        try {
            var x = p.MainModule;
            return true;
        }
        catch(Win32Exception e2)
        { IgnoreError(); } 
        })
    .Select(p => p.MainModule.FileName)
    
    public static void IgnoreError(Exception e) 
    {
        #if DEBUG
        throw e2;
        //Save the error track, I prefer log4net
        _log.Info("Something bad happened!");
        #end if
    }
    

    对于第二种情况,我更喜欢使用 IF 并保存日志:

    //Somewhere in the begging of your class, in a place whose name I do not care to remember ...
    //Declare logger type
    private readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    
    
    public List<string> VolumenLabels()
    {
        //Return the List<T>
        List<string> myVolumeLabels = new List<string>();
        //Getting the info
        DriveInfo[] allDrives = DriveInfo.GetDrives();
    
        foreach(DriveInfo drive in allDrives)
        {
            if (drive.IsReady == true)
            {
                myVolumeLabels.Add(drive.VolumeLabel.ToString());
            }
            else
            {
                _log.Info("Check the Drive: " + drive.Name + " the device is not ready.");
            }
        }    
        return myVolumeLabels;
    }
    

    希望我能帮上点忙……祝你有美好的一天!

    【讨论】:

    • 对不起,这根本没有帮助。为什么这比答案更好?
    • 我不认为它更好,它只是另一种选择,有一个优点,它使用Log4Net 保存错误,有错误的跟踪记录真是太棒了。
    【解决方案3】:

    写一个扩展方法!

    void Main()
    {
        var volumeLabels = 
            DriveInfo
            .GetDrives()
            .SelectSafe(dr => dr.VolumeLabel);
    }
    
    // Define other methods and classes here
    
    public static class LinqExtensions
    {
        public static IEnumerable<T2> SelectSafe<T,T2>(this IEnumerable<T> source, Func<T,T2> selector)
        {
            foreach (var item in source)
            {
                T2 value = default(T2);
                try
                {           
                    value = selector(item);
                }
                catch
                {
                    continue;
                }
                yield return value;
            }
        }
    }
    

    通过这种方式,您可以自定义任何您想要的行为,并且您不必创建笨重的 where 子句,这样您甚至可以让它在出现异常时返回一个替代值。

    【讨论】:

    • +1 我喜欢!它看起来也很普通,做得很好。 (我会等待一段时间来检查其他答案)
    • 我会建议像try { yield return selector(item); } catch { } 这样的东西,不需要valuecontinue
    • @NetMage 你不能在yield 块中使用try 的值。
    • @CristiS。赏金即将结束,这是一个很好的答案!只是说'哈哈
    【解决方案4】:

    你的答案是正确的。您当然可以尝试将检查逻辑隐藏在扩展方法中。

    public static IEnumerable<TElement> WhereSafe<TElement, TInner>(this IEnumerable<TElement> sequence, Func<TElement, TInner> selector)
    {
        foreach (var element in sequence)
        {
            try { selector(element); }
            catch { continue; }
            yield return element;
        }
    }
    
    
    Process
        .GetProcesses()
        .WhereSafe(p => p.MainModule)
        .Select(p => p.MainModule.FileName)
    

    或者更好:

    public static IEnumerable<TInner> TrySelect<TElement, TInner>(this IEnumerable<TElement> sequence, Func<TElement, TInner> selector)
    {
        TInner current = default(TInner);
        foreach (var element in sequence)
        {
            try { current = selector(element); }
            catch { continue; }
            yield return current;
        }
    }
    
    
    Process
       .GetProcesses()
       .TrySelect(p => p.MainModule.FileName)
    

    【讨论】:

    • 我很困惑:我在你之前看到了克林特的答案。这是真的吗?
    • 我们一定同时写过。当我刷新页面时,看到他的回答与我的如此相似,我也很惊讶。此外,我们都坚持 .NET 的命名约定和 LINQ 良好实践,因此有相似之处。 “伟大的思想都一样”,荣誉克林特;)
    • 另外,你可以看到我使用布尔值来标记正确的元素,所以这是我独自工作的一些证据。我编辑为使用 continue 关键字,而不是像 Clint 所做的那样,因为它更好。优先级是他的。
    • 我毫不怀疑你一个人工作:),但我们不能分红:)
    • 这很简单。因为TrySelect 已经完成了所有WhereSafe PLUS,所以它已经返回了预期的字段。在WhereSafe 之后,你最终不得不再次Select 做同样的事情(想象一下如果selector 很耗时)。
    【解决方案5】:

    基于 cmets 的更新:此解决方案不适用于常见的枚举器。它确实基于问题示例中使用的枚举器工作。因此,它不是通用解决方案。因为它是作为通用解决方案编写的,所以我建议不要使用它(为了简单起见)。我会保留这个答案以丰富知识库。

    另一种扩展方法解决方案。为什么我更喜欢它而不是现有的解决方案?

    • 我们只想跳过导致异常的元素。这是我们的 LINQ 扩展的唯一关注点。
    • 此实现不会混合Selecttry/catch 的关注点。
    • 我们仍然可以在需要时使用现有的 LINQ 方法,例如 Select
    • 它是可重复使用的:它允许在一个 LINQ 查询中进行多种使用。
    • 它遵循 linq 命名约定:我们实际上跳过类似于 SkipSkipWhile 的方法。

    用法:

    var result = DriveInfo
        .GetDrives()
        .Select(d => d.VolumeLabel)
        .SkipExceptions() // Our extension method
        .ToList();
    

    代码:

    public static class EnumerableExt
    {
        // We use the `Skip` name because its implied behaviour equals the `Skip` and `SkipWhile` implementations
        public static IEnumerable<TSource> SkipExceptions<TSource>(this IEnumerable<TSource> source)
        {
            // We use the enumerator to be able to catch exceptions when enumerating the source
            using (var enumerator = source.GetEnumerator())
            {
                // We use a true loop with a break because enumerator.MoveNext can throw the Exception we need to handle
                while (true)
                {
                    var exceptionCaught = false;
                    var currentElement = default(TSource);
                    try
                    {
                        if (!enumerator.MoveNext())
                        {
                            // We've finished enumerating. Break to exit the while loop                            
                            break;
                        }
    
                        currentElement = enumerator.Current;
                    }
                    catch
                    {
                        // Ignore this exception and skip this item.
                        exceptionCaught = true;
                    }
    
                    // Skip this item if we caught an exception. Otherwise return the current element.
                    if (exceptionCaught) continue;
    
                    yield return currentElement;
                }
            }
        }
    }
    

    【讨论】:

    • 我最喜欢这个解决方案,因为它与实现无关,并且巧妙地利用了 LINQ 的流模式。
    • +1 确实很棒。这应该是 .NET Framework 的一部分!您的扩展方法可以优雅地过滤掉枚举器中任何不可访问的对象。它非常通用,可以应用于任何其他枚举器。 LINQ 只是一种可能的应用程序。
    • 我也有同样的想法,但放弃了。主要是因为与来自另一个答案的TrySelect 中的具体捕获相比,如果MoveNext 的未知实现引发异常,则未定义下一次调用MoveNext 时会发生什么 - 它可能会继续引发相同的异常并且造成无限循环。或者只是返回不正确的结果。例如,Enumerable.Range(1, 10).Select((x, i) =&gt; x / i).SkipExceptions().ToList(); 将返回 0 个元素,尽管只有第一个 select 会抛出异常,因此按照想法,它应该返回其余 9 个元素。
    • @IvanStoev 这确实是我错过的一个问题。与MoveNext 实现不同的行为。任何想法我们如何处理这个问题?或者,只是解决特定问题的另一种通用解决方案并不是最好的方法:)
    • @IvanStoev 谢谢。我用关于为什么不使用它的信息更新了答案,尽管幸运的是它是示例案例的有效解决方案。
    猜你喜欢
    • 2019-11-15
    • 2019-10-07
    • 2020-12-19
    • 2018-04-03
    • 2021-09-28
    • 1970-01-01
    • 1970-01-01
    • 2022-07-22
    • 2011-08-27
    相关资源
    最近更新 更多