本文转载自微信公众号“crossoverJie”,作者crossoverJie。转载本文请联系跨界姐公众号。前言在过去的一年多时间里,我接触了一些我不熟悉的语言,主要是Python和Go。这期间,为了快速实现需求,只是跟着代码走;我没有深入研究一些细节和原则。以传参这件事为例,各个语言的实现细节不同,但也有相似之处;很多新手在入门的时候很容易摸不着头脑,导致一些低级的错误。Java基本类型传递下面我以我最熟悉的Java为例。相信没有人会写出这样的代码:@TestpublicvoidtestBasic(){inta=10;modifyBasic(a);System.out.println(String.format("Finalresultmaina==%s",a));}privatevoidmodifyBasic(intaa){System.out.println(String.format("aa==%s",aa));aa=20;System.out.println(String.format("修改后aa==%s",aa)));}输出结果:修改前aa==10修改后aa==20最终结果maina==10但是从这段代码的目的来看似乎是要修改a的值。直观上,修改成功是可以理解的。至于结果不符合预期的根本原因,是对参数的值传递和引用传递的错误理解。在此之前,先明确一下值传递和引用传递的区别:这里先抛出Java使用值传递的结论;这也可以解释为什么上面的例子没有成功修改原始数据。参考下图更好理解:当函数调用发生时,a会把自己传入modifyBasic方法,同时复制自己的值赋值给一个新的变量aa。从图中可以看出,这是a和aa这两个变量没有关系,所以aa的修改不会影响a。这有点像我给了我妻子一个苹果,她剥了皮;但是我手里的那个没有变,因为她只是从餐盘里拿了一个一模一样的苹果,剥了皮。如果我想要她一个,我只能让她把剥好的苹果给我;它类似于方法的返回值。a=modifyBasic(a);引用类型传递我们看一下引用类型传递:privateclassCar{privateStringname;publicCar(Stringname){this.name=name;}@OverridepublicStringtoString(){return"Car{"+"name='"+name+'\''+'}';}}@Testpublicvoidtest01(){Carcar1=newCar("benz");modifyCar1(car1);System.out.println(String.format("最终结果maincar1==%s",car1));}privatevoidmodifyCar1(Carcar){System.out.println(String.format("car==%sbeforemodification",car));car.name="bwm";System.out.println(String.format("修改后car==%s",car));}本例先创建一辆benz的car1,通过方法修改为bmw。最初的car1会不会受影响?修改前car==Car{name='benz'}修改后car==Car{name='bwm'}最终结果maincar1==Car{name='bwm'}可能会出乎某些人的意料,这样的修改是它会影响原始数据吗?这不是与价值转移不一致吗?好像这是参考转账?别着急,大家分析下图就明白了:在test01方法中,我们创建了一个car1对象。对象存放在堆内存中,假设内存地址为0x1102,那么变量car1就使用了这个内存地址。当我们调用modifyCar1方法时,会在方法栈中创建一个变量car,接下来一点:这个car变量是从原来的入参car1复制过来的,所以它对应的堆内存还是0x1102;所以我们通过变量car修改数据的时候,本质上是在修改同一个堆内存中的数据。所以,原来引用这个内存地址的car1也可以看到相应的变化。这里可能理解起来比较混乱,但我们要记住一件事:传递引用类型的数据时,传递的不是引用本身,而是值;只是该值是一个内存地址。因为传递的是同一个内存地址,所以对数据的操作还是会影响到外界。所以同理,类似这样的代码也会影响外部原始数据:@TestpublicvoidtestList(){Listlist=newArrayList<>();list.add(1);addList(list);System.out.println(list);}privatevoidaddList(Listlist){list.add(2);}[1,2]如果这是代码:@Testpublicvoidtest02(){Carcar1=newCar("benz");modifyCar(car1);System.out.println(String.format("最终结果maincar1==%s",car1));}privatevoidmodifyCar(Carcar2){System.out.println(String.format("修改前car2==%"s",car2));car2=newCar("bmw");System.out.println(String.format("Aftermodificationcar2==%s",car2));}假设Java是最终引用传递的结果应该是打印宝马。修改前,car2==Car{name='benz'}修改后,car2==Car{name='bmw'}最后的结果maincar1==Car{name='benz'}从结果来看,可以证明这仍然是价值转移。如果是引用传递,则应将原来的0x1102直接替换为新创建的0x1103;但实际情况如上图所示,car2直接重新引用了一个对象,两个对象互不干扰。与Java相比,Go的用法有所不同,但我们也可以得出结论,Go语言的参数也是传值的。Go语言中的数据类型主要有两种:值类型和引用类型;值类型是值类型的一个例子:funcmain(){a:=10modifyValue(a)fmt.Printf("finala=%v",a)}funcmodifyValue(aint){a=20}输出:最终的a=10函数调用过程和之前的Java类似,传递给函数的值本质上是a的一个拷贝,所以它的修改不会影响到原来的数据。当我们稍微修改一下代码时:funcmain(){a:=10fmt.Printf("传递%p\n之前a的内存地址",&a)modifyValue(&a)fmt.Printf("finala=%v",a)}funcmodifyValue(a*int){fmt.Printf("a传递后的内存地址%p\n",&a)*a=20}a传递前的内存地址0xc0000b4040a传递后的内存地址0xc0000ae020finala=20从结果来看,a的最终值被方法修改了。这就是Go和Java最大的区别:Go中有一个指针的概念,我们可以将变量传递到不同的方法中,在方法中通过这个指针就可以访问甚至修改原始数据。这不是意味着通过引用传递吗?事实上,并非如此。如果我们仔细看一下刚才的输出,就会发现传参前后的内存地址是不一样的。传输前a的内存地址为0xc0000b4040传输后a的内存地址为0xc0000ae020这也只是演示值传输,因为这里实际传输的是指针的副本。也就是说,modifyValue方法中的参数和入参&a都是指向同一块内存的指针,但是指针本身也需要内存来存储,所以在方法调用的时候会新建一个指针a,导致它们的不同内存地址。虽然内存地址不同,但指向的数据是同一块,所以方法中修改的原始数据也会受到影响。引用类型与地图切片通道略有不同:funcmain(){varpersonList=[]string{"张三","李四"}modifySlice(personList)fmt.Printf("slice=%v\n",personList)}funcmodifySlice(personList[]string){personList[1]="王五"}slice=[张三王五]最终我们会发现原来的数据也被修改了,只是我们没有传指针;同样的特性也适用于地图。但实际上,我们查看slice的源码会发现,存放数据的数组是一个指针类型:typeslicestruct{arrayunsafe.Pointerlenintcapint},所以我们可以直接修改数据,相当于间接取一个指针。使用建议我们什么时候应该使用指针?有如下建议:如果参数是基本值类型,比如int、float,建议直接传值。如果需要修改基本值类型,只能是指针;但考虑到代码的可读性,建议返回修改后的值重新赋值。当数据量较大时,建议使用指针,减少不必要的值拷贝。(具体大小可以自己判断)Python变量是否可变在Python中是影响参数传递的重要因素:如上图所示,不可变类型如boolintfloat在参数传递时不能修改原始数据。if__name__=='__main__':x=1modify(x)print('finalx={}'.format(x))defmodify(val):val=2finalx=1原理和JavaGo类似,基于value已传,这里不再赘述。这里重点说说参数传递中可变数据类型的过程:if__name__=='__main__':x=[1]modify(x)print('finalx={}'.format(x))defmodify(val):val.append(2)最后,x=[1,2]影响到最后的数据,所以说明这是引用传递?让我们试试另一个例子:if__name__=='__main__':x=[1]modify(x)print('finalx={}'.format(x))defmodify(val):val=[1,2,3]finalx=[1]显然这不是引用传递,如果是引用传递finalx应该等于[1,2,3]。从结果来看,这个传递过程和围棋中的指针传递很相似。val得到的也是参数x的内存地址的一份拷贝;它们都指向同一个内存地址。所以修改这条数据本质上是改变了同一块数据,但是一旦重新分配,就会创建一块新的内存,这样就不会影响到原来的数据。类似于Java中的上图。所以总结一下:对于不可变数据:传递参数时传递的是值,参数的修改不会影响原始数据。对于可变数据:传递的是内存地址的副本,对参数的操作会影响到原始数据。那么这三个都是按值传递的,那么有没有引用传递的语言呢?当然,C++支持通过引用传递:#includeusingnamespacestd;classBox{public:doublelen;};voidmodify(Box&b);intmain(){Boxb1;b1.len=100;cout<<"调用前b1的值:"<