最近一年多的時間陸續接觸了一些對我來講陌生的語言,主要就是 Python
和 Go
,期間爲了快速實現需求只是依葫蘆畫瓢的擼代碼;並無深究一些細節與原理。java
就拿參數傳遞一事來講各個語言的實現細節各不相同,但又有相似之處;在許多新手入門時容易搞不清楚,致使犯一些低級錯誤。python
先拿我最熟悉的 Java
來講,我相信應該沒人會寫這樣的代碼:ios
@Test public void testBasic() { int a = 10; modifyBasic(a); System.out.println(String.format("最終結果 main a==%s", a)); } private void modifyBasic(int aa) { System.out.println(String.format("修改以前 aa==%s", aa)); aa = 20; System.out.println(String.format("修改以後 aa==%s", aa)); }
輸出結果:app
修改以前 aa==10 修改以後 aa==20 最終結果 main a==10
不過從這段代碼的目的來看應該是想要修改 a
的值,從直覺上來講若是修改爲功也是能理解的。ide
至於結果與預期不符合的根本緣由是理解錯了參數的值傳遞與引用傳遞。函數
在這以前仍是先明確下值傳遞與引用傳遞的區別:學習
這裏我們先拋出結論,Java
採用的是值傳遞;這樣也能解釋爲何上文的例子沒有成功修改原始數據。this
參考下圖更好理解:編碼
當發生函數調用的時候 a
將本身傳入到 modifyBasic
方法中,同時將本身的值複製了一份並賦值給了一個新變量 aa
從圖中能夠看出這是 a
和 aa
兩個變量沒有一毛錢關係,因此對 aa
的修改並不會影響到 a
。spa
有點相似於我把蘋果給了老婆,她把蘋果削好了;但我手裏這顆並無變化,由於她只是從餐盤裏拿了一顆如出一轍的蘋果削好了。
若是我想要她那顆,只能讓她把削好的蘋果給我;也就相似於使用方法的返回值。
a = modifyBasic(a);
下面來看看引用類型的傳遞:
private class Car{ private String name; public Car(String name) { this.name = name; } @Override public String toString() { return "Car{" + "name='" + name + '\'' + '}'; } } @Test public void test01(){ Car car1 = new Car("benz"); modifyCar1(car1); System.out.println(String.format("最終結果 main car1==%s", car1)); } private void modifyCar1(Car car){ System.out.println(String.format("修改以前 car==%s", car)); car.name = "bwm"; System.out.println(String.format("修改以後 car==%s", car)); }
在這個例子裏先建立了一個 benz
的 car1
,經過一個方法修改成 bmw
那最開始的 car1
會受到影響嘛?
修改以前 car==Car{name='benz'} 修改以後 car==Car{name='bwm'} 最終結果 main car1==Car{name='bwm'}
結果可能會與部分人預期相反,這樣的修改倒是能夠影響到原有數據的?這豈不是和值傳遞
不符,看樣子這是引用傳遞
吧?
別急,經過下圖分析後你們就能明白:
在 test01
方法中咱們建立了一個 car1
的對象,該對象存放於堆內存中,假設內存地址爲 0x1102
,因而 car1
這個變量便應用了這塊內存地址。
當咱們調用 modifyCar1
這個方法的時候會在該方法棧中建立一個變量 car
,接下來重點到了:
這個 car
變量是由本來的入參 car1
複製而來,因此它所對應的堆內存依然是 0x1102
;
因此當咱們經過 car
這個變量修改了數據後,本質上修改的是同一塊堆內存中的數據。從而本來引用了這塊內存地址的 car1
也能查看到對應的變化。
這裏理解起來可能會比較繞,但咱們記住一點就行:
傳遞引用類型的數據時,傳遞的並非引用自己,依然是值;只是這個值
是內存地址罷了。
由於把相同的內存地址傳過去了,因此對數據的操做依然會影響到外部。
因此同理,相似於這樣的代碼也會影響到外部原始數據:
@Test public void testList(){ List<Integer> list = new ArrayList<>(); list.add(1); addList(list); System.out.println(list); } private void addList(List<Integer> list) { list.add(2); } [1, 2]
那若是是這樣的代碼:
@Test public void test02(){ Car car1 = new Car("benz"); modifyCar(car1); System.out.println(String.format("最終結果 main car1==%s", car1)); } private void modifyCar(Car car2) { System.out.println(String.format("修改以前 car2==%s", car2)); car2 = new Car("bmw"); System.out.println(String.format("修改以後 car2==%s", car2)); }
假設 Java
是引用傳遞那最終的結果應該是打印 bmw
纔對。
修改以前 car2==Car{name='benz'} 修改以後 car2==Car{name='bmw'} 最終結果 main car1==Car{name='benz'}
從結果又能佐證這裏依然是值傳遞。
若是是引用傳遞,本來的 0x1102
應該是被直接替換爲新建立的 0x1103
纔對;而實際狀況如上圖所示,car2
直接從新引用了一個對象,兩個對象之間互不干擾。
相對於 Java
來講 Go
的用法又有所不一樣,不過咱們也能夠先得出結論:
Go語言的參數也是值傳遞。
在 Go
語言中數據類型主要有如下兩種:
值類型與引用類型;
先以值類型舉例:
func main() { a :=10 modifyValue(a) fmt.Printf("最終 a=%v", a) } func modifyValue(a int) { a = 20 } 輸出:最終 a=10
函數調用過程與以前的 Java
相似,本質上傳遞到函數中的值也是 a
的拷貝,因此對其的修改不會影響到原始數據。
當咱們把代碼稍加修改:
func main() { a :=10 fmt.Printf("傳遞以前a的內存地址%p \n", &a) modifyValue(&a) fmt.Printf("最終 a=%v", a) } func modifyValue(a *int) { fmt.Printf("傳遞以後a的內存地址%p \n", &a) *a = 20 } 傳遞以前a的內存地址0xc0000b4040 傳遞以後a的內存地址0xc0000ae020 最終 a=20
從結果來看最終 a
的值是被方法修改了,這點即是 Go
與 Java
很大的不一樣點:
在 Go
中存在着指針的概念,咱們能夠將變量經過指針的方式傳遞到不一樣的方法中,在方法裏即可經過這個指針訪問甚至修改原始數據。
那這麼一看不就是引用傳遞嘛?
其實否則,咱們仔細看看剛纔的輸出會發現參數傳遞先後的內存地址並不相同。
傳遞以前a的內存地址0xc0000b4040 傳遞以後a的內存地址0xc0000ae020
這也剛好論證了值傳遞,由於這裏實際傳遞的是指針的拷貝。
也就是說 modifyValue
方法中的參數與入參的&a
都是同一塊內存的指針,但指針自己也是須要內存來存放的,因此在方法調用過程當中新建了一個指針 a
,從而致使他們的內存地址不一樣。
雖然內存地址不一樣,但指向的數據都是同一塊,因此方法內修改後原始數據也受到了影響。
對於 map slice channel
這類引用類型又略有不一樣:
func main() { var personList = []string{"張三","李四"} modifySlice(personList) fmt.Printf("slice=%v \n", personList) } func modifySlice(personList []string) { personList[1] = "王五" } slice=[張三 王五]
最終咱們會發現原始數據也被修改了,但咱們並無傳遞指針;一樣的特性也適用於 map
。
但其實咱們查看 slice
的源碼會發現存放數據的 array
就是指針類型:
type slice struct { array unsafe.Pointer len int cap int }
因此咱們能夠直接對數據進行修改,至關於間接的帶了指針。
那咱們在何時使用指針呢?有如下幾點建議:
int,float
建議直接傳值。在 Python
中變量是否可變是影響參數傳遞的重要因素:
如上圖所示,bool int float
這些不可變類型在參數傳遞過程當中是不能修改原始數據的。
if __name__ == '__main__': x = 1 modify(x) print('最終 x={}'.format(x)) def modify(val): val = 2 最終 x=1
原理與 Java Go
中相似,是基於值傳遞的,這裏就再也不復述。
這裏重點看看可變數據類型在參數傳遞中的過程:
if __name__ == '__main__': x = [1] modify(x) print('最終 x={}'.format(x)) def modify(val): val.append(2) 最終 x=[1, 2]
最終數據受到了影響,那麼就代表這是引用傳遞嘛?再看個例子試試:
if __name__ == '__main__': x = [1] modify(x) print('最終 x={}'.format(x)) def modify(val): val = [1, 2, 3] 最終 x=[1]
顯而易見這並非引用傳遞,若是是引用傳遞最終 x
應當等於 [1, 2 ,3]
。
從結果來看這個傳遞過程很是相似 Go
中的指針傳遞,val
拿到的也是 x
這個參數內存地址的拷貝;他們都指向了同一塊內存地址。
因此對這塊數據的修改本質上改的是同一份數據,但一旦從新賦值就會建立一塊新的內存從而不會影響到原始數據。
與 Java
中的上圖相似。
因此總結下:
這麼說來這三種都是值傳遞了,那有沒有引用傳遞的語言呢?
固然,C++
是支持引用傳遞的:
#include <iostream> using namespace std; class Box { public: double len; }; void modify(Box& b); int main () { Box b1; b1.len=100; cout << "調用前,b1 的值:" << b1.len << endl; modify(b1); cout << "調用後,b1 的值:" << b1.len << endl; return 0; } void modify(Box& b) { b.len=10.0; Box b2; b2.len = 999; b = b2; return; } 調用前,b1 的值:100 調用後,b1 的值:999
能夠看到把新對象 b2
賦值給入參 b
後是會影響到原有數據的。
其實這幾種語言看下來會發現他們中也有許多類似之處,因此一般咱們在掌握一門語言後也能快速學習其餘語言。
但每每是這些基礎中的基礎最讓人忽略,但願你們在平常編碼時可以考慮到這些基礎知識多想一想必定會寫出更漂亮的代碼(bug)。