【架构思考】由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: []

观察打印结果,我们发现,getFromDBquery2都正确拿到了值,问题出在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代码使其达到期望的效果,有两种方法:

  1. 修改query2,从DB取到值之后,再用list.addAll()方法往原list中添加元素。
  2. 修改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);这一句,看代码一眼就能发现问题。

参考

posted @ 2020-05-18 16:19  MaxStack  阅读(234)  评论(0编辑  收藏  举报