【问题标题】:Ref Abuse: Worth Cleaning Up?Ref Abuse:值得清理吗?
【发布时间】:2010-10-19 10:21:26
【问题描述】:

我继承了一些广泛且不必要地使用 ref 关键字的代码。最初的开发人员显然担心如果不使用 ref,对象会像原始类型一样被克隆,并且在编写 50k+ 行代码之前没有费心研究这个问题。

这与其他不良编码实践相结合,造成了一些表面上非常危险的情况。例如:


Customer person = NextInLine(); 
//person is Alice
person.DataBackend.ChangeAddress(ref person, newAddress);
//person could now be Bob, Eve, or null

你能想象走进一家商店改变你的地址,然后以完全不同的人走出去吗?


可怕,但在实践中在此应用程序中使用 ref 似乎是无害的多余。我很难证明清理它需要花费大量时间。为了帮助推销这个想法,我提出以下问题:

对 ref 的不必要使用还有什么破坏性?

我特别关心维护。最好通过示例给出合理的答案。

也欢迎您争辩说清理是不必要的。

【问题讨论】:

    标签: c# ref


    【解决方案1】:

    我想说最大的危险是如果由于某种原因在函数内部将参数设置为null

    public void MakeNull(ref Customer person)
    {
        // random code
        person = null;
        return;
    }
    

    现在,你不仅仅是一个不同的人,你已经完全从存在中消失了!

    只要开发此应用程序的人明白:

    默认情况下,对象引用是按值传递的。

    和:

    使用ref 关键字,对象引用通过引用传递。

    如果代码现在按预期工作并且您的开发人员了解其中的区别,那么将它们全部删除可能不值得。

    【讨论】:

      【解决方案2】:

      我将添加我见过的最糟糕的 ref 关键字用法,该方法看起来像这样:

      public bool DoAction(ref Exception exception) {...}
      

      是的,您必须声明并传递一个 Exception 引用才能调用该方法,然后检查该方法的返回值以查看是否已捕获异常并通过 ref 异常返回。

      【讨论】:

      • 老实说,这实际上是 ref 关键字的一个有趣用法。 out 关键字可能更合适,因此您实际上不必初始化异常,然后在检查 ex == null 否则抛出 ex 的方法之后。我更喜欢从具有 Success / Valid 等属性和信息/警告/错误消息集合的服务函数返回基于 IList 的消息类,但我认为他们的解决方案同样有效。
      • 我认为代码显示了对异常处理应该如何工作的基本误解,至少可以这么说。如果你想返回一个错误列表,你可以在每次捕获异常时设置内部异常,这样你就可以获得一个很好的从高到低级别的消息堆栈,或者使用你的警告列表作为属性创建一个自定义异常.我能想到的唯一一次真正像数据对象一样传递异常的情况是在某种日志记录或解析器类中。
      【解决方案3】:

      你能弄清楚为什么代码的创建者认为他们需要将参数作为 ref 吗?是因为他们确实更新了它然后删除了功能,还是仅仅因为他们当时不懂 c#?

      如果您认为清理工作是值得的,请继续进行 - 特别是如果您现在有时间。当真正出现问题时,您可能无法正确修复它,因为它很可能是紧急错误修复,您将没有时间做正确的工作。

      【讨论】:

      • 清理的一个重要部分是确定在哪里(如果有的话) ref 实际使用得当。我看到的 90% 的事件显然不包含对参考的任何更改,而且从未如此。
      【解决方案4】:

      在 C# 中修改方法中参数的值是很常见的,因为它们通常是按值而不是按引用。这适用于引用类型和值类型;例如,将引用设置为 null 会更改原始引用。当其他开发人员“照常”工作时,这可能会导致非常奇怪和痛苦的错误。使用 ref 参数创建递归方法是不行的。

      除此之外,您可以通过 ref 传递的内容受到限制。例如,您不能传递常量值、只读字段、属性等,因此在调用带有 ref 参数的方法时需要大量辅助变量。

      最后但并非最不重要的一点是性能可能不太好,因为它需要更多的间接性(引用只是一个需要在每次访问时解析的引用)并且还可以使对象保持更长时间,因为引用不是尽快超出范围。

      【讨论】:

        【解决方案5】:

        对我来说,这闻起来像是 C++ 开发人员做出无根据的假设。

        我会警惕对可行的东西进行大规模更改。 (我假设它有效,因为您没有评论它被破坏了,只是说它很危险)。

        你最不想做的就是打破一些微妙的事情,并且必须花费一周的时间来追踪问题。

        我建议您随时清理 - 一次使用一种方法。

        1. 在您确定不需要的地方找到使用 ref 的方法。
        2. 更改方法签名并修复调用。
        3. 测试。
        4. 重复。

        虽然您遇到的具体问题可能比大多数情况更严重,但您的情况很常见 - 拥有不符合我们当前对“正确方法”的理解的大型代码库做事。

        批发“升级”经常遇到困难。进行时进行重构——在你处理它们时使它们符合规范——更安全。

        在建筑行业有先例。例如,旧建筑(比如 19 世纪)中的电线不需要接触,除非出现问题。但是,当出现问题时,必须按照现代标准完成新工作。

        【讨论】:

          【解决方案6】:

          我会尝试修复它。只需使用正则表达式执行解决方案范围的字符串替换,然后检查单元测试。我知道这可能会破坏代码。但是你多久使用一次 ref?几乎从来没有,对吧?鉴于开发人员不知道它是如何工作的,我认为它在某处(应该的方式)使用的机会更小。如果代码中断 - 好吧,回滚......

          【讨论】:

            【解决方案7】:

            对 ref 的不必要使用还有什么破坏性?

            其他答案涉及语义问题,这绝对是最重要的事情。好的代码是自记录的,当我给出一个 ref 参数时,我认为它改变。如果不是,则 API 无法自我记录。

            但是为了好玩,我们看看另一个方面——性能怎么样?

            void ChangeAddress(ref Customer person, Address address)
            {
                person.Address = address;
            }
            

            这里person是对引用的引用,所以每次访问都会引入一些间接性。让我们看一些可能会转化为的程序集:

            mov eax, [person]           ; load the reference to person.
            mov [eax+Address], address  ; assign address to person.Address.
            

            现在是非参考版本:

            void ChangeAddress(Customer person, Address address)
            {
                person.Address = address;
            }
            

            这里没有间接性,所以我们可以去掉一个读取:

            mov [person+Address], address  ; assign address to person.Address.
            

            在实践中,人们希望 .NET 在ref 版本中缓存[person],将间接成本分摊到多次访问中。除了像这里这样的简单方法之外,它实际上可能不会减少 50% 的指令数。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2019-11-30
              • 2020-07-13
              • 1970-01-01
              • 2010-09-24
              • 2011-05-04
              • 1970-01-01
              • 2011-04-07
              • 2014-11-16
              相关资源
              最近更新 更多