【问题标题】:Always avoid in-out parameters in Java?总是避免在 Java 中使用 in-out 参数?
【发布时间】:2012-03-12 19:43:24
【问题描述】:

毫无疑问,输入输出参数会导致代码混乱,因为它们可能会增加意外/不可预测的副作用。

所以,很多优秀的程序员说:

避免更改可变方法参数的输入输出参数。最好保持参数不变。

对于一个希望自己的代码最简洁易懂的完美主义程序员来说,这个“规则”是否必须适用于所有情况?

例如,假设一个简单的列表添加元素的基本方法,有两种方法:

第一种方式(带入出参数):

private void addElementsToExistingList(List<String> myList){
  myList.add("Foo");
  myList.add("Bar");
}

调用者是:

List<String> myList = new ArrayList<String>();
//.......Several Instructions (or not) .....
addElementsToExistingList(myList);

不带out参数的第二种方式:

private List<String> addElementsToExistingList(List<String> originalList){
      List<String> filledList = new ArrayList<String>(originalList); //add existing elements
      filledList.add("Foo");
      filledList.add("Bar");
      return filledList; 
    }

调用者是:

List<String> myList = new ArrayList<String>();
//.......Several Instructions (or not) .....
myList.addAll(addElementsToExistingList(myList));

第二种方式的优点:

参数不被修改 => 对于新的代码阅读器没有意外副作用的风险。

第二种方式的缺点:

非常冗长且可读性极差......

当然,你会告诉我,对于这么简单的代码,第一种方法确实更方便。

但是,如果我们不考虑任何概念/代码的难度,我认为第二种方式对任何读者(无论是否初学者)来说都更合乎逻辑和更明显。

但是,它违反了 CQS 原则,即认为“命令”方法具有潜在的 void 返回(但由于它是惯例是允许的)副作用和具有返回类型且没有副作用的“查询”方法。

那么,激励程序员应该采用什么?根据代码情况混合两个?或者保持“规律”期望总是避免输入输出参数......

(当然,添加元素的方法是为显式示例而命名的,在实际代码中将是一个不好的名称选择。

【问题讨论】:

    标签: java refactoring parameter-passing


    【解决方案1】:

    我认为法律应该是:

    使用更直接的方法,但总是总是广泛地记录您的方法的行为。

    您的第二个示例是一个非常好的案例,如果没有文档,您肯定会遇到错误:方法的名称是addElementsToExistingList,但该方法不会将元素添加到现有列表中 - 它会创建一个新的一个。一个反直觉和误导性的名字,至少可以这么说......

    【讨论】:

    • 我非常同意你的观点,但它代表了一些(非实验性总是这样做的)最流行的方法风格:) 事实上,我读过很多类似的方法,名字都很糟糕我工作的地方。
    【解决方案2】:

    还有第三种方法。将List&lt;String&gt; 包装到一个知道如何向自身添加元素的类中:

    class ElementList {
       private List<String> = new ArrayList<String>();
    
       public void addElements(Element... elements);
    }
    

    我喜欢这种方法,因为它使List 实现保持私有。如果有人将不可变列表传递给您的方法或是否修改了参数,您不必担心。代码更简单。像addElementsToExistingList 这样的长方法名称是一个对象试图做另一个对象应该做的事情的代码气味。

    【讨论】:

      【解决方案3】:

      您应该在更改作为参数的对象时始终记录,否则这会对调用者产生意想不到的副作用。在第一种情况下,我同意其他人的意见,即方法名称是足够的文档。

      在您的第二个示例中,myList 中已经存在的元素似乎被添加了两次。其实你可以完全去掉 addElementsToExistingList 方法的参数,改写为:

      private List<String> getElements() {
         List<String> filledList = new ArrayList<String>();
         filledList.add("Foo");
         filledList.add("Bar");
         return filledList; 
      }
      
      List<String> myList = new ArrayList<String>();
      //.......Several Instructions (or not) .....
      myList.addAll(getElements());
      

      请注意,此代码不等同于您的第二个示例,因为元素仅添加一次,但我认为这实际上是您的意图。这是我通常喜欢的风格。此代码比第一个示例更易于理解且更灵活,无需添加额外代码(它可能会略微降低性能,但这通常不是问题)。 getElements() 的客户端现在除了将元素列表添加到现有集合之外,还可以对元素列表执行其他操作。

      【讨论】:

        【解决方案4】:

        只要记录在案,就可以更改/改变参数。当然,对于“addElementsToExistingList”的方法名称,人们还应该期待什么?但是,正如之前有人指出的那样,您的第二个实现返回一个副本并且不修改原始版本,因此方法名称现在具有误导性。您的第一种方式是一种完全可以接受的做事方式。唯一的其他额外改进是,如果仅将所有元素都添加到列表中,则可能会在返回值中添加一个 true/false 值来指示 true。

        【讨论】:

          【解决方案5】:

          在你的例子中,这个名字很清楚——“addElementsToExistingList”对我来说似乎很清楚地暗示你要……呃……你知道的。但是用一个不太明显的名字来说明你的担忧是有道理的。

          例如,在 ruby​​ 中,这通常使用命名约定来处理

          "a".upcase => 给你变量的大写,保持原来不变 "a".upcase! => 改变原来的变量

          【讨论】:

          • 除了一点细节:第二个sn-p中的hint是完全错误的,因为现有的列表没有被修改:-)
          猜你喜欢
          • 2015-12-14
          • 2020-12-20
          • 1970-01-01
          • 2020-01-26
          • 2021-11-19
          • 2014-08-13
          • 1970-01-01
          • 2011-10-12
          • 1970-01-01
          相关资源
          最近更新 更多