【架构思考】由Pass by Reference or Value联想到的代码规范
Pass by Reference和Pass by Value是编程中很基础的两个概念,相信很多同学也都并不陌生。但是实际运用起来却很容易混淆,本文从实际应用出发,将这个知识点再梳理一遍。
赋值与传参
赋值与传参是Pass by Reference和Pass by Value的两个常见应用场景,下面逐一介绍。
-> 赋值
首先,我们初始化一个对象a。
然后,把a赋值给b。
接着,改变a的值。
这时,b的值也随之变化吗?
例子1
Object a = init();
Object b = a;
// change a
// value of b = ?
-> 传参
首先,我们初始化一个对象a。
然后,将a作为参数传入一个方法process,这个方法会对a进行修改。
当方法执行完毕返回之后,a的值会发生变化吗?
例子2
Object a = init();
process(a);
// value of a = ?
显然,对于Pass by Reference和Pass by Value,答案是不同的。
区分
笔者总结的一个记忆方法为:
Pass by Reference,传递的是引用,是指针,不论新的对象怎么变,追本溯源,源头都会跟着变。(删除操作除外)
Pass by Value,传递的是值,不管新的对象怎么变,源头都不变。
但是,这里有一个问题,对于同一种语言,真的可以如此“善变”吗?一会儿Pass by Reference,一会儿又Pass by Value?
还真的有。
C++
pass by value - 输出结果是a = 45, b = 35,值没变化。
// C++ program to swap two numbers using pass by value.
#include <iostream>
using namespace std;
void swap1(int x, int y)
{
int z = x;
x = y;
y = z;
}
int main()
{
int a = 45, b = 35;
cout << "Before Swap\n";
cout << "a = " << a << " b = " << b << "\n";
swap1(a, b);
cout << "After Swap with pass by pointer\n";
cout << "a = " << a << " b = " << b << "\n";
}
pass by pointer - 输出结果是a = 35, b = 45,值发生了对换。
// C++ program to swap two numbers using pass by pointer.
#include <iostream>
using namespace std;
void swap2(int* x, int* y)
{
int z = *x;
*x = *y;
*y = z;
}
int main()
{
int a = 45, b = 35;
cout << "Before Swap\n";
cout << "a = " << a << " b = " << b << "\n";
swap2(&a, &b);
cout << "After Swap with pass by pointer\n";
cout << "a = " << a << " b = " << b << "\n";
}
为什么要搞这么复杂呢?
简单来说,pass by value
- 实际上将传参copy了一份再进行调用,相对更加耗时耗资源。
- 另外,如果要copy的对象很复杂,是一个子类,那么在初始化的时候我们是该调用子类的构造函数,还是父类的构造函数呢?这里(在某些情况下)有可能会出现“对象切割”的问题,即调用了父类的构造函数。
为了避免以上两种情况,所以有了pass by reference。
Java
对于Java而言,既不是单纯的Pass by Reference也不是单纯的Pass by Value。有人将其总结为Pass by Copy。
- For primitives, you pass a copy of the actual value.
- For references to objects, you pass a copy of the reference (the remote control).
Senario 1: primitives
int x = 100; // x's value
addOne(x); // copy of x's value 100 is passed to the method
print(x); // result: 100
Senario 2: references to objects
public static void main(String args[]) {
List<String> a = new ArrayList<>();
a.add("1001");
List<String> b = a;
System.out.println(b);
// 1. b points to ArrayList 1001: b=[1001]
a.add("1002");
System.out.println(b);
// 2. a changes ArrayList to add 1002, b changes as well: b=[1001, 1002]
a = new ArrayList<>();
a.add("1003");
System.out.println(b);
// 3. a points to a new ArrayList 1003, b does not change: b=[1001, 1002]
}
Python
对Python而言,和Java类似,有人将其总结为Pass by Object Reference。我试了一下,和Java是一个意思。
a = ["1001"]
b = a
print(b)
# ['1001']
a.append("1002")
print(b)
# ['1001', '1002']
a = ["1003"]
print(b)
# ['1001', '1002']
事故多发地带
以上,我们对Pass by Reference和Pass by Value有了一定的了解。在实际应用中,当一个list被当作参数多层调用,往往容易发生错误。见下例Java:
本意是传入一个参数,然后通过层层处理,最后到DB中拿一个返回值,再传回去。
query1
-> query2
-> getFromDB
但是实际发现,最后拿到的结果是空,为什么呢?
public static List<String> query1(String param) {
List<String> list = new ArrayList<>();
// some process
query2(list, param);
System.out.println("query1:" + list);
return list;
}
public static List<String> query2(List<String> list, String param) {
list = getFromDB(param);
System.out.println("query2:" + list);
return list;
}
public static List<String> getFromDB(String param) {
List<String> result = new ArrayList<>();
result.add(param);
System.out.println("getFromDB:" + result);
return result;
}
public static void main(String args[]) {
List<String> l = query1("2001");
System.out.println("main:" + l);
}
打印结果
getFromDB :[2001]
query2 :[2001]
query1:[]
main: []
观察打印结果,我们发现,getFromDB
和query2
都正确拿到了值,问题出在query1
中。
在query1
中list被当作参数传入了query2
,即相当于创建了list的一个复制list2,并将list2传入query2
函数中。
list2 -> ArrayList_BLANK
list -> ArrayList_BLANK
如果在query2
中,是调用add
方法往其中添加元素,那没问题,list的值也会一起发生变化。
list2 -> ArrayList_BLANK.add("2001") - ArrayList_2001
list -> ArrayList_2001
但是在实际代码中,query2
是直接重新给list2赋值了。那么,list指向的对象并未发生变化,还是空。
list2 -> ArrayList_2001
list -> ArrayList_BLANK
同样地,在Python中,也可能会发生这个问题
def query1(param):
query2(list, param)
return list
def query2(list, param):
list = getFromDB(param)
return list
def getFromDB(param):
result = [param]
return result
if __name__ == '__main__':
print(query1("2001"))
# <class 'list'>
想要修改上述Java代码使其达到期望的效果,有两种方法:
- 修改
query2
,从DB取到值之后,再用list.addAll()
方法往原list中添加元素。 - 修改
query1
,直接returnquery2
函数的返回值,而不是返回list。
代码规范
从这个例子引申开来,可以总结一些代码规范,避免犯类似的错误。
首先,创建一个函数时,我们要想好,input是谁,output是谁,要不要返回值。
如果有返回值,那么调用的地方就一般应该接受该返回值,如下
a = query(a)
return a
而不是
query(a)
return
其次,当想要传入某些参数,通过query得到某些结果时,尽量使用函数返回值的形式。而不是传入一个list去装这个结果。
最后,如果实在是需要传入一个list,且要对其进行变化操作,那么在函数的开头,先给list做一个copy,然后对copy进行操作。如下例:
public void query2(List<String> list, String param) {
List copyList = list;
copyList = getFromDB(param);
// list.addAll(copyList);
}
这样的好处在于,如果我们忘记写了list.addAll(copyList);
这一句,看代码一眼就能发现问题。