【问题标题】:How should I refactor a long chain of try-and-catch-wrapped speculative casting operations我应该如何重构一长串的try-and-catch-wrapped投机操作
【发布时间】:2024-01-22 06:22:01
【问题描述】:

我有一些使用 .NET 框架中的 Xml.Schema 类遍历 XML 模式的 C# 代码。各种简单类型限制在框架中被抽象为从 Xml.Schema.XmlSchemaFacet 派生的一大堆类。除非我遗漏了某些东西,否则要知道给定 facet 是哪种派生 facet 类型的唯一方法是将其推测性地转换为其中一个,在失败的情况下捕获结果 InvalidCastOperation。这样做会给我留下一个非常丑陋的功能:

private void NavigateFacet(XmlSchemaFacet facet)
{
    try
    {
        handler.Length((XmlSchemaLengthFacet)facet);
    }
    catch(InvalidCastException)
    {
        try
        {
            handler.MinLength((XmlSchemaMinLengthFacet)facet);
        }
        catch(InvalidCastException)
        {
            try
            {
                handler.MaxLength((XmlSchemaMaxLengthFacet)facet);
            }
            catch(InvalidCastException)
            {
                ...
            }
        }
    }
}

我认为必须有更优雅的方法来做到这一点;要么使用我在 .NET 框架中遗漏的一些属性,要么使用一些巧妙的 OO 技巧。谁能赐教?

【问题讨论】:

    标签: c# .net refactoring xsd try-catch


    【解决方案1】:

    因为我更喜欢调试数据而不是调试代码,所以我会这样做,特别是如果代码必须处理所有 XmlSchemaFacet 子类:

    Dictionary<Type, Action<XmlSchemaFacet>> HandlerMap = 
       new Dictionary<Type, Action<XmlSchemaFacet>>
    {
       {typeof(XmlSchemaLengthFacet), handler.Length},
       {typeof(XmlSchemaMinLengthFacet), handler.MinLength},
       {typeof(XmlSchemaMaxLengthFacet), handler.MaxLength}
    };
    
    HandlerMap[facet.GetType()](facet);
    

    如果facet 不是已知类型,这将抛出KeyNotFoundException。请注意,所有处理程序方法都必须从XmlSchemaFacet 转换它们的参数,因此您可能不会节省总代码行数,但您肯定会节省通过代码的路径数。

    还有一点(假设映射是预先构建的)使用字典将类型映射到方法将比遍历类型的线性列表更快,这基本上是使用一堆 if 块得到的你。

    【讨论】:

    • 真的喜欢这个外观。我现在在我的代码中尝试它,它会在它进入后回来投票。
    • 哇,再次感谢这个,它运行良好,代码现在更干净了。此外,正如您所指出的,新代码运行得更快。尽管系统中的单元测试数量随着这一变化而增加,但 NUnit 报告的时间明显减少。如果我可以多次投票,我会的。 :)
    • +1 much 比这里的所有其他答案都好。这就是我在评论 Bevan 的回答时所希望的。
    • 我喜欢这个。您可能会考虑的一项改进是将其初始化为一个字段,这样多次调用就不必重新创建字典。
    • @ChaosPandion:我想这就是他所说的“假设地图是预先构建好的”的意思。
    【解决方案2】:

    您可以尝试使用as 关键字。其他一些人建议改用is 关键字。我发现this 很好地解释了为什么as 更好。

    一些示例代码:

    private void NavigateFacet(XmlSchemaFacet facet)
    {
      XmlSchemaLengthFacet lengthFacet = facet as XmlSchemaLengthFacet;
      if (lengthFacet != null)
      {
        handler.Length(lengthFacet);
      }
      else
      {
        // Re-try with XmlSchemaMinLengthFacet, etc.
      }
    }
    

    【讨论】:

    • 谢谢,我赞成这个答案,因为它绝对是对我最初的混乱的改进,但从代码清洁度的角度来看,罗伯特的答案非常好,所以他被接受了。
    • @Matt:查看我对基准测试的更新isas。我会把它归为“真的不重要”的类别。
    【解决方案3】:
    private void NavigateFacet(XmlSchemaFacet facet)
    {
        if (facet is XmlSchemaLengthFacet)
        {
            handler.Length((XmlSchemaLengthFacet)facet);
        }
        else if (facet is XmlSchemaMinLengthFacet)
        {
            handler.MinLength((XmlSchemaMinLengthFacet)facet);
        }
        else if (facet is XmlSchemaMaxLengthFacet)
        {
            handler.MinLength((XmlSchemaMaxLengthFacet)facet);
        }
    
        // etc.
    }
    

    更新:我决定对这里讨论的不同方法进行基准测试(isas)。这是我使用的代码:

    object c1 = new Class1();
    int trials = 10000000;
    Class1 tester;
    Stopwatch watch = Stopwatch.StartNew();
    for (int i = 0; i < trials; i++)
    {
        if (c1 is Class1)
        {
            tester = (Class1)c1;
        }
    }
    watch.Stop(); 
    MessageBox.Show(watch.ElapsedMilliseconds.ToString()); // ~104 ms 
    watch.Reset();
    watch.Start();
    for (int i = 0; i < trials; i++)
    {
        tester = c1 as Class1;
        if (tester != null)
        {
            // 
        }
    }
    watch.Stop(); 
    MessageBox.Show(watch.ElapsedMilliseconds.ToString()); // ~86 ms
    watch.Reset();
    watch.Start();
    for (int i = 0; i < trials; i++)
    {
        if (c1 is Class1)
        {
            // 
        }
    }
    watch.Stop();     
    MessageBox.Show(watch.ElapsedMilliseconds.ToString()); // ~74 ms
    watch.Reset();
    watch.Start();
    for (int i = 0; i < trials; i++)
    {
        //
    }
    watch.Stop();     
    MessageBox.Show(watch.ElapsedMilliseconds.ToString()); // ~50 ms
    

    正如预期的那样,使用 as 关键字然后检查 null 比使用 is 关键字和强制转换更快(36 毫秒对 54 毫秒,减去循环本身的成本) )。

    然而,使用 is 关键字然后 not 转换更快(24ms),这意味着通过一系列is 找到正确的类型检查,然后在识别出正确的类型时只进行一次转换可能实际上更快(取决于在识别出正确的类型之前必须在此方法中进行的不同类型检查的数量)。 p>

    然而,更深层次的一点是,这个测试的试验次数是 1000 万次, 这意味着你使用哪种方法并没有太大的区别。使用 is 和强制转换需要 0.0000054 毫秒,而使用 as 和检查 null 需要 0.0000036 毫秒(在我的古老笔记本上)。

    【讨论】:

    • 您好,感谢您的回答,我对此表示赞同,但罗伯特在下面的解决方案中得到了接受的答案。
    • @Phil:没问题,我也更喜欢他的回答。
    【解决方案4】:

    您可以尝试使用 as 关键字 - 如果转换失败,您会得到 null 而不是异常。

    private void NavigateFacet(XmlSchemaFacet facet)
    {
        var length = facet as XmlSchemaLengthFacet;
        if (length != null)
        {
            handler.Length(length);
            return;
        }
    
        var minlength = facet as XmlSchemaMinLengthFacet;
        if (minlength != null)
        {
            handler.MinLength(minlength);
            return;
        }
    
        var maxlength = facet as XmlSchemaMaxLengthFacet;
        if (maxlength != null)
        {
            handler.MaxLength(maxlength);
            return;
        }
        ...
    }
    

    如果您可以控制类,我建议您使用访问者模式的变体(又名Double Despatch 以更干净地恢复类型信息,但由于您没有,这是一种相对简单的方法。

    更新:使用变量来存储as 强制转换的结果,避免了两次类型检查逻辑的需要。

    更新 2:当 C# 4 可用时,您将能够使用 dynamic 为您执行调度:

    public class HandlerDemo 
    {
        public void Handle(XmlSchemaLengthFacet facet) { ... }
        public void Handle(XmlSchemaMinLengthFacet facet) { ... }
        public void Handle(XmlSchemaMaxLengthFacet facet) { ... }
        ...
    }
    
    private void NavigateFacet(XmlSchemaFacet facet)
    {
        dynamic handler = new HandlerDemo();
        handler.Handle(facet);
    }
    

    这将起作用,因为动态对象上的方法分派使用与普通方法覆盖相同的规则,但在运行时而不是编译时进行评估。

    在幕后,动态语言运行时 (DLR) 将执行与此(和其他答案)中显示的代码大致相同的技巧,但增加了缓存以提高性能。

    【讨论】:

    • 你真的更喜欢我的答案还是MusiGenesis的答案?
    • 就个人而言,我不喜欢这里的任何答案(包括我的)。要么你必须输入每个 Type 两次,要么你必须输入 Type 并检查每个 Type 是否为 null。您在此方法中真正要做的就是将每个类型与handler 中的特定方法相关联(不管是什么) - 必须有一种更简洁的 LINQ-y 方式来执行此操作。
    【解决方案5】:

    这是一种无需任何尝试捕获的正确方法。

    if (facet is XmlSchemaLengthFacet)
    {
        handler.Length((XmlSchemaLengthFacet)facet); 
    } 
    else if (facet is XmlSchemaMinLengthFacet)
    {
        handler.MinLength((XmlSchemaMinLengthFacet)facet); 
    } 
    else if (facet is XmlSchemaMaxLengthFacet)
    {
        handler.MaxLength((XmlSchemaMaxLengthFacet)facet); 
    } 
    else
    {
        //Handle Error
    }
    

    【讨论】:

      【解决方案6】:
      • 使用“is”来确定对象是否属于给定类型

      • 使用“as”进行类型转换,它比普通转换更快,不会抛出异常,而是在错误时返回 null

      你可以这样做:

      private void NavigateFacet(XmlSchemaFacet facet)
      {
        if (facet is XmlSchemaLengthFacet)
        {
              handler.Length(facet as XmlSchemaLengthFacet);
        }
        else if (facet is XmlSchemaMinLengthFacet)
        {
              handler.MinLength(facet as XmlSchemaMinLengthFacet);
        }
        else if (facet is XmlSchemaMaxLengthFacet)
        {
             handler.MaxLength(facet as XmlSchemaMaxLengthFacet);
        }
      }
      

      【讨论】:

      • 我更喜欢使用 "as" 并检查 != null - 这样,有问题的对象只需检查一次(通过反射),并且代码仍然完全安全。
      • 同时使用“is”和“as”是多余的。我会改用 Bevan 的解决方案(更高效)或 MusiGenesis 的解决方案(更简单)。
      最近更新 更多