zoukankan      html  css  js  c++  java
  • 传值还是传引用

     

    调用函数时, 传入的参数的 传值 还是 传引用, 几乎是每种编程语言都会关注的问题. 最近在使用 golang 的时候, 由于 传值 和 传引用 的方式没有弄清楚, 导致了 BUG.

    经过深入的尝试, 终于弄明白了 golang 的 传值 的 传引用, 尝试过程记录如下, 供大家参考!

    golang 本质上都是传值方式调用

    严格来说, golang 中都是传值调用, 下面通过例子一一说明

    普通类型的参数

    这里的普通类型, 指的是 int, string 等原始的数据类型, 这些类型作为函数参数时, 都是 传值 调用. 这个基本没什么疑问.

    func param_ref_test01() {
     var t1 = 0
     var t2 = "000"
    
     var f1 = func(p int) {
       p += 1
     }
    
     var f2 = func(p string) {
       p += "-changed"
     }
    
     fmt.Printf(">>>调用前: t1 = %d  t2 = %s
    ", t1, t2)
     f1(t1)
     f2(t2)
     fmt.Printf("<<<调用后: t1 = %d  t2 = %s
    ", t1, t2)
    }
    

    运行的结果:

    >>>调用前: t1 = 0  t2 = 000
    <<<调用后: t1 = 0  t2 = 000
    

    struct 指针, map, slice 类型的参数

    对于这种类型的参数, 表面上是 传引用 调用, 我也被这个表面现象迷惑过…

    func param_ref_test02() {
     type Person struct {
       Name string
       Age  int
     }
    
     var t3 = &Person{
       Name: "test",
       Age:  10,
     }
     var t4 = []string{"a", "b", "c"}
     var t5 = make(map[string]int)
     t5["hello"] = 1
     t5["world"] = 2
    
     var f3 = func(p *Person) {
       p.Name = "test-change"
       p.Age = 20
     }
    
     var f4 = func(p []string) {
       p[0] = "aa"
       p = append(p, "d")
     }
    
     var f5 = func(p map[string]int) {
       p["hello"] = 11
       p["hello2"] = 22
     }
    
     fmt.Printf(">>>调用前: t3 = %v  t4 = %v  t5 = %v
    ", t3, t4, t5)
     f3(t3)
     f4(t4)
     f5(t5)
     fmt.Printf("<<<调用后: t3 = %v  t4 = %v  t5 = %v
    ", t3, t4, t5)
    }
    

    运行的结果:

    >>>调用前: t3 = &{test 10}  t4 = [a b c]  t5 = map[hello:1 world:2]
    <<<调用后: t3 = &{test-change 20}  t4 = [aa b c]  t5 = map[hello:11 hello2:22 world:2]
    

    从运行结果中, 可以看出基本符合 传引用 调用的特征, 除了 t4 的 append 没有生效之外

    既然都是传值调用, 为什么 f3 内修改了 *Person, 会导致外面的 t3 改变

    改造下 f3, 将变量的地址打印出来

    func param_ref_test03() {
      type Person struct {
        Name string
        Age  int
      }
    
      var t3 = &Person{
        Name: "test",
        Age:  10,
      }
    
      var f3 = func(p *Person) {
        p.Name = "test-change"
        p.Age = 20
        fmt.Printf("参数p 指向的内存地址 = %p
    ", p)
        fmt.Printf("参数p 内存地址 = %p
    ", &p)
      }
      fmt.Printf("t3 指向的内存地址 = %p
    ", t3)
      fmt.Printf("t3 的内存地址 = %p
    ", &t3)
      f3(t3)
    }
    

    运行的结果:

    t3 指向的内存地址 = 0xc00000fe20
    t3 的内存地址 = 0xc000010570
    参数p 指向的内存地址 = 0xc00000fe20
    参数p 内存地址 = 0xc000010578
    

    从结果可以看出, t3 和 p 都是指针类型, 但是它们的内存地址是不一样的, 所以这是一个 传值 调用. 但是, 它们指向的地址(0xc00000fe20)是一样的, 所以通过 p 修改了指向的数据(*Person), t3 指向的数据也发生了变化.

    只要 p 的指向地址变化, 就不会影响 t3 的变化了

    var f3 = func(p *Person) {
     p = &Person{}   // 这行会改变p指向的地址
     p.Name = "test-change"
     p.Age = 20
    }
    f3(t3)
    

    可以试试看, 只要加上上面代码中有注释的那行, 调用 f3 就不会改变 t3 了.

    既然都是传值调用, 为什么 f4 内修改了 []string, 会导致外面的 t4 改变

    golang 中的 slice 也是指针类型, 所以和上面 *Person 的原因一样

    为什么 f4 内对 []string append 之后, 没有导致外面的 t4 改变

    代码是最好的解释, 先观察 append 之后内存地址的变化, 我们再分析

    func param_ref_test04() {
      var s = []string{"a", "b", "c"}
      fmt.Printf("s 的内存地址 = %p
    ", &s)
      fmt.Printf("s 指向的内存地址 = %p
    ", s)
      s[0] = "aa"
      fmt.Printf("修改s[0] 之后, s 的内存地址 = %p
    ", &s)
      fmt.Printf("修改s[0] 之后, s 指向的内存地址 = %p
    ", s)
      s = append(s, "d")
      fmt.Printf("append之后, s 的内存地址 = %p
    ", &s)
      fmt.Printf("append之后, s 指向的内存地址 = %p
    ", s)
    }
    

    运行的结果:

    s 的内存地址 = 0xc00008fec0
    s 指向的内存地址 = 0xc00016d530
    修改s[0] 之后, s 的内存地址 = 0xc00008fec0
    修改s[0] 之后, s 指向的内存地址 = 0xc00016d530
    append之后, s 的内存地址 = 0xc00008fec0
    append之后, s 指向的内存地址 = 0xc000096f00
    

    首先, 无论是修改 slice 中的元素, 还是添加 slice 的元素, 都不会改变 s 本身的地址(0xc00008fec0) 其次, 修改 slice 中的元素, 不会改变 s 指向的地址(0xc00016d530), 所有在 f4 中修改 slice 的元素, 也会改变函数 f4 外面的变量 最后, append 操作会修改 s 指向的地址, append 之后, s 和 函数 f4 外的变量已经不是指向同一地址了, 所以 append 的元素不会影响函数 f4 外的变量

    既然都是传值调用, 为什么 f5 内修改了 map, 会导致外面的 t5 改变

    map 类型也是指针类型, 所以原因和上面的 *Person 一样

    为什么 f5 内增加了 map 中元素, 会导致外面的 t5 改变, 没有像 t4 那样, 只变修改的部分, 不变新增的部分

    同样, 看代码

    func param_ref_test05() {
      var m = make(map[string]int)
      m["hello"] = 1
      m["world"] = 2
      fmt.Printf("m 的内存地址 = %p
    ", &m)
      fmt.Printf("m 指向的内存地址 = %p
    ", m)
      m["hello"] = 11
      fmt.Printf("修改m 之后, m 的内存地址 = %p
    ", &m)
      fmt.Printf("修改m 之后, m 指向的内存地址 = %p
    ", m)
      m["hello2"] = 22
      fmt.Printf("追加元素之后, m 的内存地址 = %p
    ", &m)
      fmt.Printf("追加元素之后, m 指向的内存地址 = %p
    ", m)
    }
    

    运行的结果:

    m 的内存地址 = 0xc000010598
    m 指向的内存地址 = 0xc000151590
    修改m 之后, m 的内存地址 = 0xc000010598
    修改m 之后, m 指向的内存地址 = 0xc000151590
    追加元素之后, m 的内存地址 = 0xc000010598
    追加元素之后, m 指向的内存地址 = 0xc000151590
    

    根据上面的分析经验, 一目了然, 因为无论是修改还是添加 map 中的元素, m 指向的地址(0xc000151590)都没变, 所以函数 f5 中 map 参数修改元素, 添加元素之后, 都会影响函数 f5 之外的变量.

    注意 这里并不是说 map 类型的参数就是 传引用 调用, 它仍然是 传值 调用, 参数 map 的地址和函数 f5 外的变量 t5 的地址是不一样的 如果在函数 f5 中修改的 map 类型参数的指向地址, 就会像传值调用那样, 不影响函数 f5 外 t5 的值

    func param_ref_test06() {
     var t5 = make(map[string]int)
     t5["hello"] = 1
     t5["world"] = 2
    
     var f5 = func(p map[string]int) {
       fmt.Printf("修改前 参数p 指向的内存地址 = %p
    ", p)
       fmt.Printf("修改前 参数p 内存地址 = %p
    ", &p)
       p = make(map[string]int)  // 这行改变了 p 的指向, 使得 p 和 t5 不再指向同一个地方
       p["hello"] = 11
       p["hello2"] = 22
       fmt.Printf("修改后 参数p 指向的内存地址 = %p
    ", p)
       fmt.Printf("修改后 参数p 内存地址 = %p
    ", &p)
     }
    
     fmt.Printf("t5 指向的内存地址 = %p
    ", t5)
     fmt.Printf("t5内存地址 = %p
    ", &t5)
     fmt.Printf(">>>调用前: t5 = %v
    ", t5)
     f5(t5)
     fmt.Printf("<<<调用后: t5 = %v
    ", t5)
    }
    

    运行的结果:

    t5 指向的内存地址 = 0xc000151590
    t5内存地址 = 0xc000010598
    >>>调用前: t5 = map[hello:1 world:2]
    修改前 参数p 指向的内存地址 = 0xc000151590
    修改前 参数p 内存地址 = 0xc0000105a0
    修改后 参数p 指向的内存地址 = 0xc000151650
    修改后 参数p 内存地址 = 0xc0000105a0
    <<<调用后: t5 = map[hello:1 world:2]
    

    虽然是 map 类型参数, 但是调用前后, t5 的值没有改变.

    总结

    上面的尝试不敢说有多全, 但基本可以弄清 golang 函数传参的本质.

    1. 对于普通类型(int, string 等等), 就是 传值 调用, 函数内对参数的修改, 不影响外面的变量
    2. 对于 struct 指针, slice 和 map 类型, 函数内对参数的修改之所以能影响外面, 是因为参数和外面的变量指向了同一块数据的地址
    3. 对于 struct 指针, slice 和 map 类型, 函数的参数和外面的变量的地址是不一样的, 所以本质上还是 传值 调用
    4. slice 的 append 操作会改变 slice 指针的地址, 这个非常重要!!! 我曾经写了一个基于 slice 的排序算法在这个上面吃了大亏, 调研很久才发现原因

    转载: https://www.cnblogs.com/wang_yb/p/12126884.html

  • 相关阅读:
    IIS7.5 webapi 不支持 Delete、Put 解决方法
    pip 安装 MySQL-python 失败
    Windows 下针对python脚本做一个简单的进程保护
    Python 多线程 Condition 的使用
    Python 无限循环
    Window nginx+tomcat+https部署方案 支持ios9
    Window Redis分布式部署方案 java
    Struts2注解 特别注意
    PermGen space Eclipse 终极解决方案
    特别备注一下一个缓存加载的问题,百度上还搜不出来,在一个老外的网站上看到的
  • 原文地址:https://www.cnblogs.com/lovezbs/p/14062763.html
Copyright © 2011-2022 走看看