前言在过去的一年多时间里,接触了一些比较陌生的语言,主要是Python和Go。深入研究一些细节和原则。以传参这件事为例,各个语言的实现细节不同,但也有相似之处;很多新手在入门的时候很容易摸不着头脑,导致一些低级的错误。Java基本类型传递下面我以我最熟悉的Java为例。我相信没有人会写这样的代码:@TestpublicvoidtestBasic(){inta=10;修改基本(一);System.out.println(String.format("最终结果maina==%s",a));}privatevoidmodifyBasic(intaa){System.out.println(String.format("aa==%sbeforemodification",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");修改Car1(car1);系统。out.println(String.format("最终结果主car1==%s",car1));}privatevoidmodifyCar1(Carcar){System.out.println(String.format("car==%sbeforemodification",car));car.name="bwm";System.out.println(String.format("修改后car==%s",car));}在这个例子中,首先创建了一辆benzcar1,通过一个方法修改为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<>();列表.添加(1);添加列表(列表);System.out.println(列表);}privatevoidaddList(Listlist){list.add(2);}[1,2]如果这是代码:@Testpublicvoidtest02(){Carcar1=newCar("benz");修改汽车(汽车1);System.out.println(String.format("最终结果主car1==%s",car1));}privatevoidmodifyCar(Carcar2){System.out.println(String.format("修改前car2==%s",car2));car2=newCar("宝马");System.out.println(String.format("修改后car2==%s",car2));}假设Java是引用传递,最后的结果应该是打印bmw。修改前,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前a的内存地址\n",&a)modifyValue(&a)fmt.Printf("finala=%v",a)}funcmodifyValue(a*int){fmt.Printf("传%p后a的内存地址\n",&a)*a=20}a传前的内存地址0xc0000b4040a的内存地址aafterpassing0xc0000ae020finala=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变量是否可变是影响参数传递的重要因素:如上图所示,不可变类型如boolintfloat在参数传递时不能修改原始数据。if__name__=='__main__':x=1modify(x)print('finalx={}'.format(x))defmodify(val):val=2finalx=1原理和JavaGo类似,它基于价值传递,这里不再赘述。这里重点说说参数传递中可变数据类型的过程:if__name__=='__main__':x=[1]modify(x)print('finalx={}'.format(x))defmodify(val):val.append(2)finallyx=[1,2]影响到最后的数据,那是不是说明这是引用传递呢?试试另一个例子:if__name__=='__main__':x=[1]modify(x)print('finalx={}'.format(x))defmodify(val):val=[1,2,3]finalx=[1]显然这不是引用传递,如果是引用传递,最后的x应该等于[1,2,3]。从结果来看,这个传递过程和围棋中的指针传递很相似。val得到的也是参数x的内存地址的一份拷贝;它们都指向同一个内存地址。所以修改这条数据本质上是改变了同一块数据,但是一旦重新分配,就会创建一块新的内存,这样就不会影响到原来的数据。类似于Java中的上图。所以总结一下:对于不可变数据:传参的时候传值,修改参数不会影响原来的数据。对于可变数据:传递的是内存地址的副本,对参数的操作会影响到原始数据。那么这三个都是按值传递的,那么有没有引用传递的语言呢?当然,C++支持通过引用传递:#includeusingnamespacestd;类Box{public:doublelen;};voidmodify(Box&b);intmain(){框b1;b1.len=100;cout<<"调用前b1的值:"<