zoukankan      html  css  js  c++  java
  • 【架构思考】由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
    "; 
        cout << "a = " << a << " b = " << b << "
    "; 
      
        swap1(a, b); 
      
        cout << "After Swap with pass by pointer
    "; 
        cout << "a = " << a << " b = " << b << "
    "; 
    } 
    

    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
    "; 
        cout << "a = " << a << " b = " << b << "
    "; 
      
        swap2(&a, &b); 
      
        cout << "After Swap with pass by pointer
    "; 
        cout << "a = " << a << " b = " << b << "
    "; 
    } 
    

    为什么要搞这么复杂呢?

    简单来说,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);这一句,看代码一眼就能发现问题。

    参考

  • 相关阅读:
    a++与++a区别
    powerdesigner 15.1 逆向工程 sqlserver2008 、sqlserver2005 带注释
    Docker部署vue+nginx项目
    Spring Cloud项目部署(四)上传docker镜像
    Spring Cloud项目部署(三)使用maven插件上传docker镜像的相关配置
    centos7 IP设置
    Spring Cloud项目部署(二)docker开启2375端口
    Spring Cloud项目部署(一)docker安装
    高并发网站架构设计(转)
    Element UI 那些坑
  • 原文地址:https://www.cnblogs.com/maxstack/p/12883817.html
Copyright © 2011-2022 走看看