自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文搞懂參數(shù)傳遞原理

開發(fā) 前端
近一年多的時(shí)間陸續(xù)接觸了一些對(duì)我來說陌生的語言,主要就是 Python 和 Go,期間為了快速實(shí)現(xiàn)需求只是依葫蘆畫瓢的擼代碼;并沒有深究一些細(xì)節(jié)與原理。

[[375467]]

本文轉(zhuǎn)載自微信公眾號(hào)「crossoverJie  」,作者crossoverJie   。轉(zhuǎn)載本文請(qǐng)聯(lián)系crossoverJie公眾號(hào)。

前言

最近一年多的時(shí)間陸續(xù)接觸了一些對(duì)我來說陌生的語言,主要就是 Python 和 Go,期間為了快速實(shí)現(xiàn)需求只是依葫蘆畫瓢的擼代碼;并沒有深究一些細(xì)節(jié)與原理。

就拿參數(shù)傳遞一事來說各個(gè)語言的實(shí)現(xiàn)細(xì)節(jié)各不相同,但又有類似之處;在許多新手入門時(shí)容易搞不清楚,導(dǎo)致犯一些低級(jí)錯(cuò)誤。

Java

基本類型傳遞

先拿我最熟悉的 Java 來說,我相信應(yīng)該沒人會(huì)寫這樣的代碼:

  1. @Test 
  2.    public void testBasic() { 
  3.        int a = 10; 
  4.        modifyBasic(a); 
  5.        System.out.println(String.format("最終結(jié)果 main a==%s", a)); 
  6.    } 
  7.  
  8.    private void modifyBasic(int aa) { 
  9.        System.out.println(String.format("修改之前 aa==%s", aa)); 
  10.        aa = 20; 
  11.        System.out.println(String.format("修改之后 aa==%s", aa)); 
  12.    } 

輸出結(jié)果:

  1. 修改之前 aa==10 
  2. 修改之后 aa==20 
  3. 最終結(jié)果 main a==10 

不過從這段代碼的目的來看應(yīng)該是想要修改 a 的值,從直覺上來說如果修改成功也是能理解的。

至于結(jié)果與預(yù)期不符合的根本原因是理解錯(cuò)了參數(shù)的值傳遞與引用傳遞。

在這之前還是先明確下值傳遞與引用傳遞的區(qū)別:

這里咱們先拋出結(jié)論,Java 采用的是值傳遞;這樣也能解釋為什么上文的例子沒有成功修改原始數(shù)據(jù)。

參考下圖更好理解:

當(dāng)發(fā)生函數(shù)調(diào)用的時(shí)候 a 將自己傳入到 modifyBasic 方法中,同時(shí)將自己的值復(fù)制了一份并賦值給了一個(gè)新變量 aa 從圖中可以看出這是 a 和 aa 兩個(gè)變量沒有一毛錢關(guān)系,所以對(duì) aa 的修改并不會(huì)影響到 a。

有點(diǎn)類似于我把蘋果給了老婆,她把蘋果削好了;但我手里這顆并沒有變化,因?yàn)樗皇菑牟捅P里拿了一顆一模一樣的蘋果削好了。

如果我想要她那顆,只能讓她把削好的蘋果給我;也就類似于使用方法的返回值。

  1. a = modifyBasic(a); 

引用類型傳遞

下面來看看引用類型的傳遞:

  1. private class Car{ 
  2.        private String name
  3.  
  4.        public Car(String name) { 
  5.            this.name = name
  6.        } 
  7.  
  8.        @Override 
  9.        public String toString() { 
  10.            return "Car{" + 
  11.                    "name='" + name + '\'' + 
  12.                    '}'
  13.        } 
  14.    } 
  15.  
  16.  @Test 
  17.    public void test01(){ 
  18.        Car car1 = new Car("benz"); 
  19.        modifyCar1(car1); 
  20.        System.out.println(String.format("最終結(jié)果 main car1==%s", car1)); 
  21.    } 
  22.  
  23.    private void modifyCar1(Car car){ 
  24.        System.out.println(String.format("修改之前 car==%s", car)); 
  25.        car.name = "bwm"
  26.        System.out.println(String.format("修改之后 car==%s", car)); 
  27.    } 

在這個(gè)例子里先創(chuàng)建了一個(gè) benz 的 car1,通過一個(gè)方法修改為 bmw 那最開始的 car1 會(huì)受到影響嘛?

  1. 修改之前 car==Car{name='benz'
  2. 修改之后 car==Car{name='bwm'
  3. 最終結(jié)果 main car1==Car{name='bwm'

結(jié)果可能會(huì)與部分人預(yù)期相反,這樣的修改卻是可以影響到原有數(shù)據(jù)的?這豈不是和值傳遞不符,看樣子這是引用傳遞吧?

別急,通過下圖分析后大家就能明白:

在 test01 方法中我們創(chuàng)建了一個(gè) car1 的對(duì)象,該對(duì)象存放于堆內(nèi)存中,假設(shè)內(nèi)存地址為 0x1102 ,于是 car1 這個(gè)變量便應(yīng)用了這塊內(nèi)存地址。

當(dāng)我們調(diào)用 modifyCar1 這個(gè)方法的時(shí)候會(huì)在該方法棧中創(chuàng)建一個(gè)變量 car ,接下來重點(diǎn)到了:

這個(gè) car 變量是由原本的入?yún)?car1 復(fù)制而來,所以它所對(duì)應(yīng)的堆內(nèi)存依然是 0x1102;

所以當(dāng)我們通過 car 這個(gè)變量修改了數(shù)據(jù)后,本質(zhì)上修改的是同一塊堆內(nèi)存中的數(shù)據(jù)。從而原本引用了這塊內(nèi)存地址的 car1 也能查看到對(duì)應(yīng)的變化。

這里理解起來可能會(huì)比較繞,但我們記住一點(diǎn)就行:

傳遞引用類型的數(shù)據(jù)時(shí),傳遞的并不是引用本身,依然是值;只是這個(gè)值 是內(nèi)存地址罷了。

因?yàn)榘严嗤膬?nèi)存地址傳過去了,所以對(duì)數(shù)據(jù)的操作依然會(huì)影響到外部。

所以同理,類似于這樣的代碼也會(huì)影響到外部原始數(shù)據(jù):

  1. @Test 
  2.    public void testList(){ 
  3.        List<Integer> list = new ArrayList<>(); 
  4.        list.add(1); 
  5.        addList(list); 
  6.        System.out.println(list); 
  7.    } 
  8.  
  9.    private void addList(List<Integer> list) { 
  10.        list.add(2); 
  11.    } 
  12.  
  13.    [1, 2] 

那如果是這樣的代碼:

  1. @Test 
  2.     public void test02(){ 
  3.         Car car1 = new Car("benz"); 
  4.         modifyCar(car1); 
  5.         System.out.println(String.format("最終結(jié)果 main car1==%s", car1)); 
  6.     } 
  7.  
  8.     private void modifyCar(Car car2) { 
  9.         System.out.println(String.format("修改之前 car2==%s", car2)); 
  10.         car2 = new Car("bmw"); 
  11.         System.out.println(String.format("修改之后 car2==%s", car2)); 
  12.     } 

假設(shè) Java 是引用傳遞那最終的結(jié)果應(yīng)該是打印 bmw 才對(duì)。

  1. 修改之前 car2==Car{name='benz'
  2. 修改之后 car2==Car{name='bmw'
  3. 最終結(jié)果 main car1==Car{name='benz'

從結(jié)果又能佐證這里依然是值傳遞。

如果是引用傳遞,原本的 0x1102 應(yīng)該是被直接替換為新創(chuàng)建的 0x1103 才對(duì);而實(shí)際情況如上圖所示,car2 直接重新引用了一個(gè)對(duì)象,兩個(gè)對(duì)象之間互不干擾。

Go

相對(duì)于 Java 來說 Go 的用法又有所不同,不過我們也可以先得出結(jié)論:

Go語言的參數(shù)也是值傳遞。

在 Go 語言中數(shù)據(jù)類型主要有以下兩種:

值類型與引用類型;

值類型

先以值類型舉例:

  1. func main() { 
  2.  a :=10 
  3.  modifyValue(a) 
  4.  fmt.Printf("最終 a=%v", a) 
  5.  
  6. func modifyValue(a int) { 
  7.  a = 20 
  8. 輸出:最終 a=10 

函數(shù)調(diào)用過程與之前的 Java 類似,本質(zhì)上傳遞到函數(shù)中的值也是 a 的拷貝,所以對(duì)其的修改不會(huì)影響到原始數(shù)據(jù)。

當(dāng)我們把代碼稍加修改:

  1. func main() { 
  2.  a :=10 
  3.  fmt.Printf("傳遞之前a的內(nèi)存地址%p \n", &a) 
  4.  modifyValue(&a) 
  5.  fmt.Printf("最終 a=%v", a) 
  6.   
  7. func modifyValue(a *int) { 
  8.  fmt.Printf("傳遞之后a的內(nèi)存地址%p \n", &a) 
  9.  *a = 20 
  10.  
  11. 傳遞之前a的內(nèi)存地址0xc0000b4040  
  12. 傳遞之后a的內(nèi)存地址0xc0000ae020 
  13. 最終 a=20 

從結(jié)果來看最終 a 的值是被方法修改了,這點(diǎn)便是 Go 與 Java 很大的不同點(diǎn):

在 Go 中存在著指針的概念,我們可以將變量通過指針的方式傳遞到不同的方法中,在方法里便可通過這個(gè)指針訪問甚至修改原始數(shù)據(jù)。

那這么一看不就是引用傳遞嘛?

其實(shí)不然,我們仔細(xì)看看剛才的輸出會(huì)發(fā)現(xiàn)參數(shù)傳遞前后的內(nèi)存地址并不相同。

  1. 傳遞之前a的內(nèi)存地址0xc0000b4040  
  2. 傳遞之后a的內(nèi)存地址0xc0000ae020 

這也恰好論證了值傳遞,因?yàn)檫@里實(shí)際傳遞的是指針的拷貝。

也就是說 modifyValue 方法中的參數(shù)與入?yún)⒌?amp;a都是同一塊內(nèi)存的指針,但指針本身也是需要內(nèi)存來存放的,所以在方法調(diào)用過程中新建了一個(gè)指針 a ,從而導(dǎo)致他們的內(nèi)存地址不同。

雖然內(nèi)存地址不同,但指向的數(shù)據(jù)都是同一塊,所以方法內(nèi)修改后原始數(shù)據(jù)也受到了影響。

引用類型

對(duì)于 map slice channel 這類引用類型又略有不同:

  1. func main() { 
  2.  var personList = []string{"張三","李四"
  3.  modifySlice(personList) 
  4.  fmt.Printf("slice=%v \n", personList) 
  5. func modifySlice(personList []string) { 
  6.  personList[1] = "王五" 
  7.  
  8. slice=[張三 王五] 

最終我們會(huì)發(fā)現(xiàn)原始數(shù)據(jù)也被修改了,但我們并沒有傳遞指針;同樣的特性也適用于 map 。

但其實(shí)我們查看 slice 的源碼會(huì)發(fā)現(xiàn)存放數(shù)據(jù)的 array 就是指針類型:

  1. type slice struct { 
  2.  array unsafe.Pointer 
  3.  len   int 
  4.  cap   int 

所以我們可以直接對(duì)數(shù)據(jù)進(jìn)行修改,相當(dāng)于間接的帶了指針。

使用建議

那我們?cè)谑裁磿r(shí)候使用指針呢?有以下幾點(diǎn)建議:

  • 如果參數(shù)是基本的值類型,比如 int,float 建議直接傳值。
  • 如果需要修改基本的值類型,那只能是指針;但考慮到代碼可讀性還是建議將修改后的值返回用于重新賦值。
  • 數(shù)據(jù)量較大時(shí)建議使用指針,減少不必要的值拷貝。(具體多大可以自行判斷)

Python

在 Python 中變量是否可變是影響參數(shù)傳遞的重要因素:

如上圖所示,bool int float 這些不可變類型在參數(shù)傳遞過程中是不能修改原始數(shù)據(jù)的。

  1. if __name__ == '__main__'
  2.   x = 1 
  3.     modify(x) 
  4.     print('最終 x={}'.format(x))  
  5.  
  6. def modify(val): 
  7.     val = 2 
  8.  
  9. 最終 x=1 

原理與 Java Go中類似,是基于值傳遞的,這里就不再?gòu)?fù)述。

這里重點(diǎn)看看可變數(shù)據(jù)類型在參數(shù)傳遞中的過程:

  1. if __name__ == '__main__'
  2.   x = [1] 
  3.     modify(x) 
  4.     print('最終 x={}'.format(x))  
  5.  
  6. def modify(val): 
  7.     val.append(2) 
  8.  
  9. 最終 x=[1, 2] 

最終數(shù)據(jù)受到了影響,那么就表明這是引用傳遞嘛?再看個(gè)例子試試:

  1. if __name__ == '__main__'
  2.   x = [1] 
  3.     modify(x) 
  4.     print('最終 x={}'.format(x))  
  5.  
  6. def modify(val): 
  7.     val = [1, 2, 3] 
  8.  
  9. 最終 x=[1] 

顯而易見這并不是引用傳遞,如果是引用傳遞最終 x 應(yīng)當(dāng)?shù)扔?[1, 2 ,3] 。

從結(jié)果來看這個(gè)傳遞過程非常類似 Go 中的指針傳遞,val 拿到的也是 x 這個(gè)參數(shù)內(nèi)存地址的拷貝;他們都指向了同一塊內(nèi)存地址。

所以對(duì)這塊數(shù)據(jù)的修改本質(zhì)上改的是同一份數(shù)據(jù),但一旦重新賦值就會(huì)創(chuàng)建一塊新的內(nèi)存從而不會(huì)影響到原始數(shù)據(jù)。

與 Java 中的上圖類似。

所以總結(jié)下:

  • 對(duì)于不可變數(shù)據(jù):在參數(shù)傳遞時(shí)傳遞的是值,對(duì)參數(shù)的修改不會(huì)影響到原有數(shù)據(jù)。
  • 對(duì)于可變數(shù)據(jù):傳遞的是內(nèi)存地址的拷貝,對(duì)參數(shù)的操作會(huì)影響到原始數(shù)據(jù)。

這么說來這三種都是值傳遞了,那有沒有引用傳遞的語言呢?

當(dāng)然,C++是支持引用傳遞的:

  1. #include <iostream> 
  2. using namespace std; 
  3.   
  4. class Box 
  5.    public
  6.       double len; 
  7. }; 
  8.  
  9. void modify(Box& b); 
  10.   
  11. int main () 
  12.  Box b1; 
  13.  b1.len=100; 
  14.  cout << "調(diào)用前,b1 的值:" << b1.len << endl; 
  15.  modify(b1); 
  16.  cout << "調(diào)用后,b1 的值:" << b1.len << endl; 
  17.  return 0; 
  18.   
  19. void modify(Box& b) 
  20.  b.len=10.0; 
  21.  Box b2; 
  22.  b2.len = 999; 
  23.  b = b2; 
  24.    
  25.  return
  26.  
  27. 調(diào)用前,b1 的值:100 
  28. 調(diào)用后,b1 的值:999 

可以看到把新對(duì)象 b2 賦值給入?yún)?b 后是會(huì)影響到原有數(shù)據(jù)的。

總結(jié)

其實(shí)這幾種語言看下來會(huì)發(fā)現(xiàn)他們中也有許多相似之處,所以通常我們?cè)谡莆找婚T語言后也能快速學(xué)習(xí)其他語言。

但往往是這些基礎(chǔ)中的基礎(chǔ)最讓人忽略,希望大家在日常編碼時(shí)能夠考慮到這些基礎(chǔ)知識(shí)多想想一定會(huì)寫出更漂亮的代碼(bug)。

 

責(zé)任編輯:武曉燕 來源: crossoverJie
相關(guān)推薦

2023-09-08 08:20:46

ThreadLoca多線程工具

2024-07-12 14:46:20

2021-07-08 10:08:03

DvaJS前端Dva

2023-09-22 10:45:47

云原生云計(jì)算

2023-12-15 15:55:24

Linux線程同步

2024-04-12 12:19:08

語言模型AI

2022-03-24 08:51:48

Redis互聯(lián)網(wǎng)NoSQL

2021-04-27 19:21:48

HBase原理開源

2021-03-22 10:05:59

netstat命令Linux

2023-09-15 12:00:01

API應(yīng)用程序接口

2020-04-15 16:30:24

掃碼登錄微信前端

2019-04-03 09:27:01

MySQLInnoDB務(wù)ACID

2021-06-30 08:45:02

內(nèi)存管理面試

2022-08-15 15:39:23

JavaScript面向?qū)ο?/a>數(shù)據(jù)

2023-04-03 15:04:00

RPCPHP語言

2023-10-16 08:16:31

Bean接口類型

2024-06-05 11:43:10

2020-03-18 14:00:47

MySQL分區(qū)數(shù)據(jù)庫

2019-11-19 08:00:00

神經(jīng)網(wǎng)絡(luò)AI人工智能

2023-08-24 16:50:45

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)