【问题标题】:Is "pass by reference" bad design? [closed]“通过引用”是糟糕的设计吗? [关闭]
【发布时间】:2013-02-15 00:34:30
【问题描述】:

所以,我刚开始学习 Java,发现根本没有“通过引用”这种东西。我正在将一个应用程序从 C# 移植到 Java,而原始应用程序具有 int 和 double,它们是“ref”或“out”参数。

起初我以为我可以传入“整数”或“双精度”,因为那是引用类型,所以值会改变。但后来我了解到这些引用类型是不可变的。

所以,我创建了一个“MutableInteger”类和一个“MutableDouble”类,并将它们传递给我的函数。它有效,但我想我必须违背该语言的原始设计意图。

“通过引用传递”是一般糟糕的设计吗?我应该如何改变我的思维方式?

有这样的功能似乎是合理的:

bool MyClass::changeMyAandB(ref int a, ref int b)
{
    // perform some computation on a and b here

    if (success)
        return true;
    else return false;
}

这是糟糕的设计吗?

【问题讨论】:

  • 是的,这是糟糕的设计。另外,写return success就行了。
  • ... 该函数的最后 3 行也是如此。 return success;
  • 这对code review stack exchange来说会更好。
  • 这在很大程度上取决于程序员/设计师的观点。在 Java 中,设计者虽然是个坏主意,但在 C# 中,设计者虽然保留 C/C++ 语言的概念会很好。因此,您的问题没有真正的答案,因为这取决于用户。
  • @RyanGates 在那里可能是合适的,但我确实认为这是一个很好的 SO 候选人,客观上存在良好的模式和良好的设计,并且许多 SO 问题都涉及这些。

标签: parameter-passing pass-by-reference


【解决方案1】:

如果您将代码结构化为清晰、易于理解的抽象,则面向对象的编程是最好的。

数字,作为一种抽象,是不可变并且没有身份(即“五”总是一个“五”,不存在“多重五个实例")。

您要发明的是一个“可变数字”,它是可变的并且具有标识。这个概念有点笨拙,你最好用更有意义的抽象(对象)来建模你的问题。

考虑代表事物并具有特定界面的对象,而不是单个值块。

【讨论】:

    【解决方案2】:

    在适当支持它的语言中设计不错,(*) 但是当您必须定义 MutableInt 类只是为了在两个方法之间进行通信时,肯定有问题。

    您发布的示例的解决方案是返回一个包含两个整数的数组,并通过 null 返回或异常表示失败。这并不总是有效,所以有时你必须...

    • 在当前对象上设置属性(当一个类中的两个方法通信时);
    • 传入该方法可能修改的整数数组(当需要多次传递大量整数时);
    • 创建一个辅助类,比如Result,它封装了计算的结果(当您处理intfloat 而不是两个整数时),并且可能将实际计算作为方法或构造函数;
    • 使用您建议的习语,然后考虑使用Apache Commons 或其他好的库中对它的支持。

    (*) 只是糟糕的语言设计。在 Python 或 Go 中,您会返回多个值并且不用担心。

    【讨论】:

    • 将单元素数组T[] 用作穷人的Reference<T> 是否是个好主意(如果我真的想要参考)?
    • @JanDvorak:可以这样使用,但并不漂亮。当一个方法产生两个整数时,它也可以将它们放在一个数组中。
    • 我想知道在野外的某个地方是否已经有一个 Reference<T> 类(并不是说很难制作)。
    • @JanDvorak:我无法立即在 Guava 中找到一个(我最喜欢的让 Java 可以接受的方式)。
    • 谢谢 :-) 我想如果我需要的话,下一个最好的选择是编写我自己的课程。
    【解决方案3】:

    通过引用传递值对象通常是一种糟糕的设计。

    它适用于某些场景,例如用于高性能排序操作的数组位置交换。

    您需要此功能的原因很少。在 C# 中,使用 OUT 关键字通常本身就是一个缺点。 out 关键字有一些可接受的用法,例如DateTime.TryParse(datetext, out DateValue),但 out 参数的标准用法是糟糕的软件设计,它希望通常模仿使用标志来表示所有内容的不良做法。

    【讨论】:

    • “通过引用传递值对象通常是一个糟糕的设计。”那么您还会考虑通过引用传递什么?
    • @newacct 实体和聚合。 en.wikipedia.org/wiki/… 值对象作为副本传递,所有其他对象...不要!如果您确实需要交出与基础设施/服务提升相关的任何内容,请改用 IOC。通常,您只希望所有这些东西都是瞬态的或静态的,而不是四处传递。
    【解决方案4】:

    “通过引用”是糟糕的设计吗?

    一般不会。您需要了解您的具体场景并问自己一个函数的作用。而且您需要正确定义您的编码风格,尤其是在您为他人编码时(分发库)。

    当您的函数返回多个输出时,通常会使用按引用传递。返回一个包含函数返回的所有信息的ResultContainer 对象通常是个好主意。以以下 C# 示例为例:

    bool isTokenValid(string token, out string username)
    

    VS

    AuthenticationResult isTokenValid(string token)
    
    class AuthenticationResult {
        public bool AuthenticationResult;
        public string Username;
    }
    

    不同之处在于带有引用(在本例中为output)参数的方法清楚地强调了它只能用于验证令牌或可选地用于提取用户信息。因此,即使您有义务传递一个参数,如果您不需要它,您也可能会丢弃它。第二个示例代码更冗长。

    如果你有这样的方法当然最好第二种设计

    bool doSomething(object a, ref object b, ref object c, ref object d, ... ref object z);
    

    因为您会将它们全部包装到一个容器中。

    现在让我澄清一下:在 Java 和 C# 中,非原始类型总是作为克隆引用传递。这意味着objects 本身没有被克隆,只有对它们的引用被克隆到堆栈中,然后你不能指望在返回后指向一个完全不同的对象。相反,您总是希望该方法修改对象的状态。否则,您只需 clone() 对象就可以了。

    所以诀窍来了:MutableInteger,或者更好的Holder 模式,是通过引用传递原始值的解决方案。

    当您的 IDL 具有引用参数时,CORBA idl2java 编译器当前使用它。

    在您的具体情况下,我无法回答您的设计好坏,因为您展示的方法过于笼统。所以想想吧。就像输入一样,如果我对多媒体信息应用某种后处理功能,甚至像加密一样,我会使用引用传递。对我来说,下面的设计看起来不错

    encrypt(x);
    

    VS

    x = encrypt(x);
    

    【讨论】:

    • “非原始类型总是作为引用传递” 这至少是一种误导。当调用者的变量本身可以被被调用者修改时,就会发生引用传递。发生的事情更像是“引用按值传递”。 stackoverflow.com/a/40523/399317
    • 实际上我们可以区分按值传递、按引用传递和按指针传递。其中引用是不可变的(指针按值传递)而指针是可变的(返回后可以指向不同的对象)
    • 请不要过于复杂。适用于大多数语言的最简单模型只考虑“变量”及其“值”。在 Java 中,变量的值要么是原始值,要么是指向某个对象的(cough)“指针”。值也是受分配给变量的影响。按值传递:值(如前定义)被复制传递,按引用传递:变量 itself 以某种方式传递。没有第三种选择。这使 C++ 值类型和 C# 结构保持一致。
    • @Kos 您的反馈非常有价值:我已经更新了我的答案,并请求提供反馈。我试图将这个概念解释为 克隆参考 以强调它是值传递而不是克隆的事实(可以修改对象的状态)
    【解决方案5】:

    您参与的糟糕设计是使用MutableInteger 类。 2 永远是 2。明天就是 2。

    标准的 Java/OO 模式通常是让此类项目成为类的实例,并让该类操作/管理它们。

    接下来是AtomicInteger。再说一次,我从来没有遇到过需要传递它的情况,但是如果你不想重构很多代码(你的问题是一个“好习惯”,所以我不得不对你很苛刻),那就更好了选项。原因是如果你让一个整数转义到另一个函数,从封装的角度来看,你不知道另一个函数会在同一个线程上运行。因此并发性是个问题,你会可能需要原子引用类提供的并发性。 (另见AtomicReference。)

    【讨论】:

      【解决方案6】:

      在调用者堆栈中修改 var 的方法可能会非常令人困惑。

      理想情况下,语言应该支持返回多个值,这样可以解决这类问题。

      但在此之前,如果您必须使用“out”参数,则必须这样做。

      【讨论】:

        【解决方案7】:

        当然,这取决于您正在处理的特定问题,但我会说,在大多数情况下,如果您需要这样的功能,您的设计就不是非常面向对象的。
        你想达到什么目的? 如果这个数字 a 和 b 必须一起操作,可能它们属于 MyClass 类,你需要的是一个实例方法。类似的东西:

        class MyClass{
             private int a;
             private int b;
             //getters/setters/constructors
             public boolean dothings(){
             // perform some computation on a and b here
                if (success)
                     return true;
                else return false;
                //why not just return success?
             } 
        
        }
        

        【讨论】:

          【解决方案8】:

          “通过引用传递”是一般糟糕的设计吗?我应该如何改变我的思维方式? 无需制作一个 POJO bean 来保存您的值,将该 bean 发送到函数并取回新值。当然,在某些情况下,您调用的函数可以返回值(如果它总是只有一件事您想要返回,但您谈论的是 vars,所以我认为它不止一个)。

          传统上制作具有需要更改的属性的 bean 示例:

          class MyProps{
          int val1;
          int val2;//similarly can have strings etc here
          public int getVal1(){
          return val1;
          }
          public void setVal1(int p){ 
          val1 = p;
          }// and so on other getters & setters
          
          }
          

          或者可以用泛型创建一个类来保存任何对象

          class TVal<E>{
          E val;
          public E getValue(){
          return val;
          }
          public void setValue(E p){
          val = p;
          }
          }
          

          现在使用你的类通过容器的引用来传递:

          public class UseIt{
              void a (){
                TVal<Integer> v1 = new TVal<Integer>();
                v1.setValue(1);//auto boxed to Integer from int
                TVal<Integer> v2 = new TVal<Integer>();
                v2.setValue(3);
                process(v1,v2);
                System.out.println("v1 " + v1 + " v2 " + v2);
              }
          
              void process(TVal<Integer> v,TVal<Integer> cc){
                  v.setValue(v.getValue() +1);
                  cc.setValue(cc.getValue() * 2);
              }
          }
          

          【讨论】:

          • 下一步是意识到 POJO 是一个糟糕的设计,并将代码从您正在调用的方法移动到 pojo 本身的方法中。我认为这个完整的往返思想是实际获得 OO 设计的第一步,而通过引用传递、返回多个值和 setter/getter 的最大缺陷是这些中的每一个都允许您停止完整的往返您需要真正进入 OO 开发思维模式。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-09-06
          • 2011-08-01
          • 1970-01-01
          • 2010-11-19
          • 1970-01-01
          相关资源
          最近更新 更多