微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如果结构浅拷贝,如何强制编译器错误?

如何解决如果结构浅拷贝,如何强制编译器错误?

我维护一个库,其中导出的结构具有不应浅拷贝的字段。例如:

type Example struct {
    Val    int
    Nums   []int
}

由于 Nums 是切片类型的字段,因此 Foo 实例的浅拷贝复制了切片头并允许存在错误

foo := Example{Val: 1,Nums: []int{100}}
bar := Example

bar.Nums[0] = 200

fmt.Println(foo.Nums) // [200]

我想防止代码导入库以浅拷贝结构。可能的解决方案:

  • 返回指针的构造函数,但无论如何都不能阻止客户端取消引用和浅拷贝:
pfoo := lib.NewFoo() // returns type `*Foo`
foo := *pfoo // Now variable foo is type `Foo`
foo2 := foo  // I don't want this
  • 向库中添加丰富的文档以阻止浅拷贝。我还可以声明我的库中所有使用 Foo 来要求指针类型的方法,以确保在我这边的安全,但同样,导入程序代码可能会编写使用 {{1} 的函数}} 值。
  • Foo 添加 Clone() 方法,但这属于“文档”问题:人们可能不会阅读它。
  • Foo 添加自定义检查,但这不会导致编译器错误
  • 重写库以避免暴露不可复制的字段
  • 忍受可能出现错误

然而,还有其他方法可以强制编译器在结构浅拷贝上抛出错误吗?

解决方法

问题 runtime: add NoCopy documentation struct type? 解决了这个问题。
comment on the issue 推荐此解决方案:

请注意,绝对必须选择加入 vet 检查的代码已经可以这样做。一个包可以定义:

type noCopy struct{}
func (*noCopy) Lock() {}

然后将 noCopy noCopy 放入任何必须由 vet 标记的结构中。

还需要 Unlock 方法来触发警告。以下是示例的完整解决方案:

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type Example struct {
    noCopy noCopy
    Val    int
    Nums   []int
}

通过此更改,go vet 命令会为以下代码打印警告 assignment copies lock value to y:Example contains noCopy

var x Example
y := x

Run the example on the playground

记录不应复制值的要求。如果应用程序需要深层复制,请提供 Clone() 方法。

该方法不会增加 Example 的大小,因为 struct{} 的大小为零。

标准库 sync.WaitGroup 类型 uses this approach

,

强制编译器错误的可移植方法是依赖语言规范,这在这种情况下没有多大帮助。

其他解决方案可以是依赖于实现的,也可以是静态分析。这是对我研究内容的解释:

使用 go vet 字段强制发出 noCopy 警告

可以强制编译器错误的情况的实用性非常有限,一般不会阻止浅拷贝。不幸的是,规格在这里没有提供任何其他有用的帮助。

另一种选择是依靠 go vet 检查 copylocks 来报告不希望使用的结构,例如具有 sync.Locker 接口实现的赋值。

要触发此警告,您只需在结构中添加一个 sync.Mutex 字段:

type Foo struct {
    m sync.Mutex
}

func main() {
    foo := Foo{}
    foo2 := foo // assignment copies lock value to foo2: play.Foo contains sync.Mutex
}

如果您不想使用不必要的字段增加结构体内存占用,您可以定义自己的实现 sync.Locker 的类型:

// implements sync.Locker
type noCopy struct {}

func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

type Foo struct {
    _ noCopy
}

不要嵌入 noCopy,将其声明为命名字段。即使该字段未导出,嵌入它也会将导出的 Lock()Unlock() 方法提升到您的结构中。

强制恐慌(不推荐)

标准库 strings.Builder 带有一个 hack,如果在使用结构后尝试写入,它会发生恐慌:

//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
}

func (b *Builder) copyCheck() {
    if b.addr == nil {
        // This hack works around a failing of Go's escape analysis
        // that was causing b to escape and be heap allocated.
        // See issue 23382.
        // TODO: once issue 7921 is fixed,this should be reverted to
        // just "b.addr = b".
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}

func (b *Builder) Write(p []byte) (int,error) {
    b.copyCheck()
    // ...
}

copyCheck 方法在 Builder 结构上发生写访问时使用。在第一次访问时,当 Builder 不再为零值时,该方法将当前指针存储在结构本身中。在随后的每次写访问中,代码都会验证当前接收器是否仍然相同。

请注意,您可以在类型为 *T 的非指针变量上调用带有指针接收器 T 的方法:

如果 x 是可寻址的并且 &x 的方法集包含 m,则 x.m()(&x).m() 的简写

因此在完成以下任务后:

var bld strings.Builder
bld.Write(/* ... */)
copied := bld

调用 copied.Write 等于 (&copied).Write(),这将具有不同的指针接收器,从而使 b.addr != b 为真。

这看起来是一种防止浅拷贝的简单方法,即使让您的导入程序在运行时恐慌可能是一个更糟糕的用户体验。然而,issue 7921 导致的逃逸分析黑客是不安全的,可能会破坏交易。

在该问题解决并恢复为代码后:

    if b.addr == nil {
        b.addr = b
    } else if b.addr != b {
        // panic
    }

恐慌复制可能成为一个可行的选择。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?