“按引用传递”是一种不好的设计吗?

31
所以,我刚开始学习Java,发现没有所谓的“按引用传递”。我正在将一个应用程序从C#移植到Java,原始应用程序具有作为“ref”或“out”参数的int和double。
起初,我认为可以传入“Integer”或“Double”,因为那是引用类型,值会改变。但后来我了解到这些引用类型是不可变的。
于是,我创建了一个“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;
}

那是糟糕的设计吗?


12
是的,这是糟糕的设计。另外,只需写“返回成功”。 - Ingo
16
那个函数的最后3行也是如此。 return success; - Austin Salonen
3
这对于[代码审查堆栈交换](http://codereview.stackexchange.com/)会更好。 - Ryan Gates
2
这在很大程度上取决于程序员/设计师的观点。在Java中,设计师认为这是一个不好的想法,在C#中,设计师认为保留从C/C++语言中继承的概念是有益的。因此,对于你的问题没有真正的答案,因为它将取决于用户。 - Luiggi Mendoza
3
@RyanGates,虽然这个话题在那里讨论也许更合适,但我认为它是一个很好的适合在 Stack Overflow 上进行讨论的候选项。因为有客观上好的模式和设计,并且很多 Stack Overflow 的问题都会涉及到这些内容。 - djechlin
显示剩余4条评论
8个回答

17
Object-oriented programming最佳实践是将代码结构化为清晰易懂的抽象。作为一种抽象,数字是不可变的,并且没有身份(即,“五”永远是“五”,不存在“多个五”的情况)。您试图发明的是“可变数字”,它是可变的并且具有身份。这个概念有点笨重,您最好使用更有意义的抽象(对象)来建模问题。考虑代表某些内容并具有特定接口的对象,而不是单独的值块。

16

如果语言有适当的支持,那么这不算是糟糕的设计(*); 但是当你需要定义一个MutableInt类来在两个方法之间通信时,肯定有些问题。

对于您发布的示例,解决方案是返回两个整数的数组,并通过null返回或异常来表示失败。这种方法并不总是有效,因此有时您需要...

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

(*) 只是糟糕的语言设计。在Python或Go中,你可以返回多个值并停止担心。


使用一个只包含一个元素的数组 T[] 作为“穷人版”的 Reference<T>(如果我确实需要引用),这是一个好主意吗? - John Dvorak
@JanDvorak:它可以这样使用,但不太美观。当一个方法产生两个整数时,它也可以将它们放在一个单一的数组中。 - Fred Foo
我在想是否已经有一个“Reference<T>”类存在于某个地方(即使它并不难制作)。 - John Dvorak
@JanDvorak:我在Guava中没有立即找到一个(这是我让Java变得可 tolerable 的最喜欢的方法)。 - Fred Foo
谢谢 :-) 我想下一步最好的选择,如果需要的话,是编写自己的类。 - John Dvorak
显示剩余2条评论

6
传递值对象时使用引用传递通常是一种不良设计。在高性能排序操作中,例如数组位置交换,有某些情况下这是有效的。但你应该极少需要这种功能。在C#中,使用OUT关键字本身就是一个缺陷。虽然有一些可接受的使用方法,比如DateTime.TryParse(datetext, out DateValue),但标准的out参数使用通常是一种不好的软件设计,它试图模拟使用标志来表示所有状态的坏习惯。

2
将值对象通过引用传递通常是不良的设计。那么你还会考虑传递什么? - newacct
@newacct 实体和聚合。http://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks_of_DDD 值对象作为传递副本,所有其他对象...不要!如果您实际上需要传递任何基础设施/服务托管相关的内容,请改用IOC。通常,您只希望所有这些内容都是短暂或静态的,而不是传递。 - Chris Marisic

3

“按引用传递”是不好的设计吗?

一般而言,并不是不好的。你需要理解具体场景并问问自己函数的作用。尤其是当你为他人编写代码(分发库)时,需要正确定义编程风格。

当你的函数返回多个输出时,通常采用按引用传递的方式。这经常是一个好主意,可以返回一个包含所有函数返回信息的 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#中,非原始类型始终作为克隆引用传递。这意味着对象本身并不被克隆,只有对它们的引用被克隆到堆栈中,然后你不能期望返回后指向完全不同的对象。相反,您始终期望该方法修改对象的状态。否则,只需使用clone()函数克隆该对象即可。
所以这里有一个技巧:MutableInteger或更好的Holder模式是通过引用传递原始值的解决方案。
目前当您的IDL具有参考参数时,它被CORBA idl2java编译器使用。
在您的特定情况下,我无法回答关于好坏设计的问题,因为您展示的方法过于通用。所以请自行思考。仅供参考,如果我有某种应用于多媒体信息的后处理功能,甚至包括加密,我将使用引用传递。对我来说,以下内容看起来是一个很好的设计。
encrypt(x);

VS

x = encrypt(x);

2
“非原始类型总是作为引用传递”这种说法至少是具有误导性的。当调用者的变量本身可以被被调用者修改时,才会发生按引用传递。实际上发生的更像是“按值传递引用”。https://dev59.com/EXVD5IYBdhLWcg3wQJOT#40523 - Kos
实际上,我们可以区分按值传递、按引用传递和按指针传递。其中,引用是不可变的(通过值传递指针),而指针是可变的(在返回后可以指向不同的对象)。 - usr-local-ΕΨΗΕΛΩΝ
2
请不要过于复杂化。在大多数语言中,最简单的模型只考虑“变量”及其“值”。在Java中,变量的值可以是原始值或指向某个对象的“指针”。赋值操作会影响变量的值。按值传递:值(如前所述)被复制并传递,按引用传递:变量本身以某种方式被传递。没有第三种选择。这与C++值类型和C#结构保持一致。 - Kos
@Kos,您的反馈非常有价值:我已经更新了我的答案,并请求对其进行反馈。我试图将概念解释为“克隆引用”,以突出它是值传递而不是克隆(对象状态可以被修改)。 - usr-local-ΕΨΗΕΛΩΝ

1
您正在使用的不好的设计是使用MutableInteger类。 2永远都是2。明天它仍将是2。
标准的Java / OO模式通常是让这些项目成为类的实例,并让该类操作/管理它们。
接下来是AtomicInteger。同样,我从未遇到过需要传递它的情况,但如果您不想重构大量代码(您的问题是“良好实践”问题,所以我必须对您严格要求),那么这是一个更好的选择。原因是,如果您让整数逃逸到另一个函数中,从封装的角度来看,您不知道其他函数是否在同一线程上运行。因此,并发是一个问题,您可能需要原子引用类提供的并发性。(也请参见AtomicReference。)

1

修改调用栈中的变量的方法可能会令人困惑。

理想情况下,语言应该支持返回多个值,这将解决这种问题。

但在此之前,如果你必须使用“out”参数,那么你就必须使用。


1
当然,这取决于你处理的特定问题,但我认为在大多数情况下,如果你需要这样的函数,那么你的设计不是非常面向对象。
你想要实现什么?如果这些数字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?
     } 

}

1
一般来说,“按引用传递”是不好的设计吗?我应该如何改变我的思考方式? 只需创建一个POJO bean来保存您的值,将该bean发送到函数并获取新值即可。当然,对于某些情况,您可以让所调用的函数返回值(如果始终只需要一个返回值,但您正在谈论out变量,因此我认为它不止一个)。
传统上,创建一个具有需要更改的属性的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);
    }
}

3
下一步是意识到POJO(简单Java对象)是一个糟糕的设计,将代码从你正在调用的方法中移动到POJO内部的一个方法中。我认为这种完整的思维循环是实际上获得面向对象设计的第一步,而通过引用传递、返回多个值和使用setter/getter的最大缺陷在于它们中的每一个都可以阻止你停止完整的思维循环,从而真正进入面向对象开发的思维模式。 - Bill K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接