【问题标题】:What is the core difference between t=&T{} and t=new(T)t=&T{} 和 t=new(T) 的核心区别是什么
【发布时间】:2020-01-19 06:48:40
【问题描述】:

似乎两种方法都创建一个全为“0”成员值的新对象指针,都返回一个指针:

type T struct{}
...
t1:=&T{}
t2:=new(T)

那么 t1 和 t2 之间的核心区别是什么,或者有什么“新”可以做而 &T{} 不能做的事情,反之亦然?

【问题讨论】:

    标签: pointers go struct new-operator


    【解决方案1】:

    [...] 有什么“新”可以做而 &T{} 不能做的事情,反之亦然?

    我能想到三个不同点:

    • “复合文字”语法(&T{}T{} 部分)仅适用于“结构、数组、切片和映射”[link],而 new 函数适用于任何类型 [ link]。
    • 对于结构或数组类型,new 函数始终为其元素生成零值,而复合文字语法允许您根据需要将某些元素初始化为非零值。
    • 对于切片或映射类型,new 函数始终返回指向 nil 的指针,而复合文字语法始终返回已初始化的切片或映射。 (对于地图来说,这非常重要,因为您不能向nil 添加元素。)此外,复合文字语法甚至可以创建一个-空切片或地图。

    (第二个和第三个要点实际上是同一件事的两个方面——new 函数总是创建零值——但我将它们分开列出,因为不同类型的含义有点不同。)

    【讨论】:

    • 技术上,一个新的切片或映射初始化(全零值)。当 map 或 slice 的元素本身是指针时(但一般保证),这一技术点尤其重要。
    • @torek:语言规范不同意你的观点。 (golang.org/ref/spec#Composite_literals 真的很明确。)
    • new 将值初始化(为零)这一事实与复合文字有什么关系? (也许不清楚,当我说“一个新切片或地图”时,我的意思是“由new 创建的一个新切片或地图。)...啊,也许你在谈论它怎么说切片由new 创建的“未初始化”。新切片的长度为零,容量为零。我认为如果长度和容量已初始化,则意味着切片初始化。在内部, unsafe.Pointer里面是nil,在重要的内部意义上也是“初始化”了。
    • 但另一方面,newmake 非常不同,后者也将所有可访问的值设置为零。
    • 基本上,所有这些都只是您可以通过使用 unsafe 包来发现的东西,所以这不是 可观察的 区别:语言并没有真正承诺事情以某种方式或另一种方式。但是对于内部 gc 代码,它很重要(为了效率)。
    【解决方案2】:

    对于结构和其他组合,两者是相同的。

    t1:=&T{}
    t2:=new(T)
    //Both are same
    

    如果不使用new,则无法返回初始化为其他基本类型(如 int)的零值的未命名变量的地址。您需要创建一个命名变量,然后获取其地址。

    func newInt() *int {            
        return new(int)                 
    }               
    
    
    func newInt() *int {
        // return &int{} --> invalid
        var dummy int
        return &dummy
    }
    

    【讨论】:

    • 对于结构类型,情况并非如此: func newT1() *T1{return &T1{}} 编译并运行良好
    • 这就是我的回答。它适用于结构类型,但不适用于 int 等其他基本类型。
    【解决方案3】:

    ruakh's answer。不过,我想指出一些内部实现细节。您不应该在生产代码中使用它们,但它们有助于阐明在 Go 运行时中幕后真正发生的事情。

    本质上,一个切片由三个值表示。 reflect 包导出一个类型,SliceHeader

    SliceHeader 是切片的运行时表示。它不能安全或便携地使用,它的表示可能会在以后的版本中发生变化。此外,Data 字段不足以保证它引用的数据不会被垃圾收集,因此程序必须保留一个单独的、正确类型的指向底层数据的指针。

    type SliceHeader struct {
            Data uintptr
            Len  int
            Cap  int
    }
    

    如果我们使用它来检查[]T 类型的变量(对于任何T 类型),我们可以看到三个部分:指向底层数组的指针、长度和容量。在内部,切片值v 始终具有所有这三个部分。有一个我认为应该保持的一般条件,如果你不使用unsafe来打破它,通过检查似乎它保持(基于有限的无论如何测试):

    • Data 字段不为零(在这种情况下,LenCap 可以但不必非零),或者
    • Data 字段为零(在这种情况下,LenCap 都应为零)。

    如果Data 字段为零,则切片值vnil

    通过使用unsafe 包,我们可以故意破坏它(然后将其全部放回原处——希望我们破坏它时不会出现任何问题),从而检查各个部分。当this code on the Go Playground 运行时(下面也有一个副本),它会打印:

    via &literal: base of array is 0x1e52bc; len is 0; cap is 0.
    Go calls this non-nil.
    
    via new: base of array is 0x0; len is 0; cap is 0.
      Go calls this nil even though we clobbered len() and cap()
      Making it non-nil by unsafe hackery, we get [42] (with cap=1).
    
    after setting *p1=nil: base of array is 0x0; len is 0; cap is 0.
      Go calls this nil even though we clobbered len() and cap()
      Making it non-nil by unsafe hackery, we get [42] (with cap=1).
    

    代码本身有点长,所以我把它留到最后(或使用上面的 Playground 链接)。但它表明,源代码中的实际p == nil 测试编译为只是检查Data 字段。

    当你这样做时:

    p2 := new([]int)
    

    new 函数实际上只分配切片header。它将所有三个部分设置为零并返回指向结果标头的指针。所以*p2 中包含三个零字段,这使其成为正确的nil 值。

    另一方面,当你这样做时:

    p1 := &[]int{}
    

    Go 编译器构建一个空数组(大小为零,保存零个整数),然后构建一个切片头:指针部分指向空数组,长度和容量设置为零。然后p1 指向此标头,带有非零Data 字段。稍后的分配 *p1 = nil 将零写入所有三个字段。

    让我用粗体字重复一遍:这些不是语言规范所承诺的,它们只是实际的实现。

    地图的工作方式非常相似。 map 变量实际上是一个指向 map header 的指针。映射头的细节比切片头更难访问:它们没有reflect 类型。实际实现在type hmap 下可见here(注意不是导出的)。

    这意味着m2 := new(map[T1]T2)实际上只分配了一个指针,并将该指针本身设置为nil。没有实际的地图! new 函数返回 nil 指针,然后 m2nil。同样var m1 map[T1]T2 只是将m1 中的一个简单指针值设置为nil。但是var m3 map[T1]T2{} 分配了一个实际的hmap 结构,填充它,并使m3 指向它。我们可以再次peek behind the curtain on the Go Playground,使用明天不能保证工作的代码,看看这个效果。

    作为编写 Go 程序的人,您不需要了解这些。但是,如果您使用过低级语言(例如汇编和 C),这些可以解释很多。特别是,这些解释了为什么你不能插入到 nil 映射中:map 变量本身持有一个指针值,直到 map 变量本身有一个指向(可能为空的)map-header 的非 nil 指针,无法进行插入。插入可以分配一个新映射并插入数据,但 map 变量 不会指向正确的 hmap 标头对象。

    (语言作者可以通过使用第二级间接来完成这项工作:映射变量可以是指向指向映射头的变量的指针。或者他们可以使映射变量始终指向头, 并让new 实际上分配了一个标头,就像make 所做的那样;那么就永远不会有一个 nil 映射。但是他们没有做这些,我们得到了我们得到的,这很好:你只是需要知道初始化地图。)


    这是切片检查器。 (使用操场链接查看地图检查器:鉴于我必须从运行时复制hmap 的定义,我希望它特别脆弱且不值得展示。切片头的结构似乎不太可能改变随着时间的推移。)

    package main
    
    import (
        "fmt"
        "reflect"
        "unsafe"
    )
    
    func main() {
        p1 := &[]int{}
        p2 := new([]int)
        show("via &literal", *p1)
        show("\nvia new", *p2)
        *p1 = nil
        show("\nafter setting *p1=nil", *p1)
    }
    
    // This demonstrates that given a slice (p), the test
    //    if p == nil
    // is really a test on p.Data.  If it's zero (nil),
    // the slice as a whole is nil.  If it's nonzero, the
    // slice as a whole is non-nil.
    func show(what string, p []int) {
        pp := unsafe.Pointer(&p)
        sh := (*reflect.SliceHeader)(pp)
        fmt.Printf("%s: base of array is %#x; len is %d; cap is %d.\n",
            what, sh.Data, sh.Len, sh.Cap)
        olen, ocap := len(p), cap(p)
        sh.Len, sh.Cap = 1, 1 // evil
        if p == nil {
            fmt.Println("  Go calls this nil even though we clobbered len() and cap()")
            answer := 42
            sh.Data = uintptr(unsafe.Pointer(&answer))
            fmt.Printf("  Making it non-nil by unsafe hackery, we get %v (with cap=%d).\n",
                p, cap(p))
            sh.Data = 0 // restore nil-ness
        } else {
            fmt.Println("Go calls this non-nil.")
        }
        sh.Len, sh.Cap = olen, ocap // undo evil
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-03-10
      • 2021-04-16
      • 1970-01-01
      • 1970-01-01
      • 2011-09-01
      • 2019-08-08
      • 1970-01-01
      相关资源
      最近更新 更多