TL;DR:如何在golang中创建包含指针的struct,并安全地将其按值传递给其他函数?(安全是指无需担心这些函数可以取消引用所说的指针并更改它所指向的变量)。如果要给出的答案是“复制函数”,那么如何删除原始复制构造函数/运算符?使用自定义复制函数覆盖它?或以其他方式阻止人们使用它?
在golang中,我可以拥有一个结构体,其中包含一个指向动态分配变量的指针。
我也可以将这些结构的实例传递给“复制”它们的函数。
然而,我不能覆盖或删除内置的复制运算符。这意味着,理论上,我可以有以下代码:
import (
"fmt"
)
type A struct {
a * int
}
func main() {
var instance A
value := 14
instance.a = &value
fmt.Println(*instance.a) // prints 14
mutator(instance)
fmt.Println(*instance.a) // prints 11 o.o
}
func mutator(instance A) {
*instance.a = 11
fmt.Println(*instance.a)
}
这种代码显然在这里有点无意义。然而,假设成员字段“a”是一个复杂的结构,那么访问它的函数可能会尝试修改它,这也是可以理解的。
调用函数"mutator"后,程序员可能希望继续使用他的A实例,并且(假设他并没有编写结构或了解其内部情况)甚至可能认为,由于他传递的是副本而不是指针,他的A实例将保持不变。
现在,除了golang之外,还有几种(3种)流行的语言允许程序员考虑分配和操作内存。我不了解Rust或C,因此我将按照自己的方式来解决C++中出现的问题:
a)假设我是类A的设计者,我可以建立一个拷贝构造函数,从而得到以下代码:
#include <iostream>
class A {
public:
int * a;
A(int value): a(new int{value}) {}
A(const A & copyFrom): a(new int{*copyFrom.a}) {}
};
void mutator(A instance) {
*instance.a = 11;
std::cout << *instance.a << "\n";
}
int main() {
A instance{14};
std::cout << *(instance.a) << "\n";
mutator(instance);
std::cout << *instance.a << "\n";
}
这使得我的类的实例可以被复制,同时指针也将被重新分配。
b) 假设我是类A的设计者,并且不想构建复制构造函数(假设a指向的任何内容都可能非常大,或者A经常在性能关键的条件下作为只读对象使用),但仍希望确保对复制进行的任何赋值都不能修改a指向的值(但仍允许通过将其赋值给新值来修改a),我可以这样编写我的类:
class A {
public:
const int * a;
A(int value): a(new const int{value}) {}
};
以下代码将无法通过编译:
void mutator(A instance) {
*instance.a = 11;
std::cout << *instance.a << "\n";
}
int main() {
A instance{14};
std::cout << *(instance.a) << "\n";
mutator(instance);
std::cout << *instance.a << "\n";
}
但是下面的代码可以编译成功:
void mutator(A instance) {
instance.a = new const int{11};
std::cout << *instance.a << "\n";
}
int main() {
A instance{14};
std::cout << *(instance.a) << "\n";
mutator(instance);
std::cout << *instance.a << "\n";
}
请注意,这是C++的“面向对象”(呕吐)设计的典型例子。我认为如果函数签名中有某种规则可以保证不修改传递给它的A实例或声明A实例为“const”并针对其动态分配的字段(而不仅是静态字段)进行“保护”,那将更好。
然而,虽然这个解决方案可能不完美,但它是一个解决方案。它使我对我的A实例的“所有权”有了清晰的想法。
在golang中,似乎任何包含指针的实例的“副本”基本上都是自由的,即使结构体的作者有这样的意图,也无法安全地传递它。
我唯一能想到的方法是编写一个“Copy”方法,返回该结构的全新实例(类似于上面示例中的复制构造函数)。但是,没有删除复制构造函数/运算符的能力,很难确保人们会使用和/或注意到它。
说实话,我觉得很奇怪,在golang中甚至允许重新编写指针的内存地址而不使用“unsafe”包或类似的东西。
是否禁止此类操作会更合理,就像许多其他操作一样?
考虑到“append”的工作方式,似乎作者的意图是支持将新变量重新分配给指针,而不是对之前指向的变量进行突变。但是,对于自定义结构(至少没有在包中封装该结构),要强制执行这一点似乎很难实现,对于像切片或数组这样的内置结构则很容易。
我是否忽略了在golang中进行复制构造(或禁止复制)的方法?是否确实是作者的原始意图在时间和内存允许的情况下鼓励重新赋值而不是突变?如果是这样的话,为什么动态分配的变量变异如此容易?有没有一种方法可以模拟结构体或文件的私有/公共行为而不是完整的包?有没有其他方法可以强制执行具有指针的结构的某种形式的所有权,我可能忽略了?