与Rust等语言类似,Vala中也有所有权的概念。Vala主要采用自动的引用计数进行内存管理,对于支持引用计数的类,需要关注其中有没有循环引用,如果存在循环引用,则需要手动使用week
关键字标记,打破循环。Vala中还存在着一些没有在GObject类型系统中注册的紧凑类,这些类型往往不支持引用计数,Vala同样利用所有权与生命周期来管理它们。
本文主要介绍Vala中的所有权操作方式及其意义。
声明:Vala中的内存管理(包括所有权)需要人为关注的地方很少,很多编程场景下不需要了解,本文仅起介绍作用,不是Vala劝退文
无属引用
无属引用不会增加引用计数类型的引用次数,也不会引起不可变的紧凑类的拷贝。
需要注意的是,对于无属的函数返回值,只有在赋值到声明为无属的变量时才时真正的无属引用,而赋值到没有声明为无属的变量时,将会产生一次对可引用计数对象的强引用,或者对不可变紧凑类的拷贝。
需要注意的是,如果函数返回的无属引用的内容没有在其他地方有强引用,那么当函数返回后,对象将会是无属的,将直接被销毁,这样的函数也永远无法返回有效的引用。例如,属性的get
方法默认是无属的,因此不能返回在其中新建的对象:
class Foo {
string bar {
get {
return new Object (); // 编译错误
}
}
}
在上述例子中,get
方法默认是无属的,不会增加对象的强引用数,因此返回对象后对象的引用计数为0,将直接被删除,无法返回有效内容,导致编译错误。
因此,也不能这样:
class Foo {
public string bar {
get {
return "Hello!"; // 编译错误
}
}
}
这是因为Vala中如果没有将函数的字符串声明为无属类型,返回的内容又是一个字符串面值,就会复制字符串面值生成一个新的拷贝对象;又由于get
方法默认无属,将会出现与上一个例子相同的问题,导致编译错误。
由于字符串面值默认还具有一个所有者 —— 整个程序模块,因此将该属性改为unowned
后,可以直接返回未拷贝的字符串面值,而不会出现所有权问题,以下代码是正确的:
class Foo {
public unowned string bar {
get {
return "Hello!";
}
}
}
关键字owned
可以明确要求属性返回有属引用,因此,给get
方法加上owned
关键字后,可以返回其中新建的对象,以下代码是正确的:
class Foo {
string bar {
owned get {
return new Object ();
}
}
}
无属引用除了避免增加支持引用计数的类型的引用计数,使对象及时销毁外,还可以用于避免不支持引用计数的不可变类的拷贝,例如:
void main () {
string[] foo = {"Test1", "Test2", "Test3"};
foreach (unowned var i in foo) {
print ("%s\n", i);
}
}
将foreach
中的i
用unowned
声明可以避免字符串拷贝,此时变量i
只是对数组foo
中内容的无属引用;如果不加unowned
声明,则变量i
将会由foo
中的内容复制生成,有额外的性能开销。
另外需要注意的是,Vala的unowned
无属引用与Rust的所有权租借并不相同。Vala的无属引用直接指向原来的对象的内存地址,在原来的对象所有权转让以后,无属引用仍然有效。
所有权转让
关键字owned
可以用于所有权转让。
- 作为参数类型时,表示对象的所有权被转让到该代码语段
- 作为类型转换操作符时,用于在同一语段下移交所有权,可以避免拷贝不能引用计数的类
- 例如:
Foo foo = (owned) bar
- 与Rust有些类似,此时
foo
将继承原来bar
的引用和所有权,而bar
将会被设置为null
- 例如:
示例:字符串的所有权
Vala中的字符串实际上是gchar *
(等价于char *
)类型,并没有在GObject类型系统中注册,属于非引用计数的不可变紧凑类。以下示例演示了有关字符串的所有权操作及其结果:
#!/usr/bin/env -S vala -X -O2 -X -march=native --cc="ccache cc" -X -pipe -X -fuse-ld=mold
inline unowned string equal_sign (string? s1, string? s2) {
return ((void *) s1 == (void *) s2) ? "==" : "!=";
}
void main () {
unowned var s = "Hello";
print ("\"var s1 = s;\" --- Copy\n");
var s1 = s; // 行为:复制,指向地址不同
print ("s:%s s1:%s\n", s, s1);
print ("%p %s %p\n", s, equal_sign (s, s1), s1);
print ("\n\"unowned var s2 = s1;\" --- Unowned refer\n");
unowned var s2 = s1; // 行为:s2与s1指向同一内存地址
print ("s1:%s s2:%s\n", s1, s2);
print ("%p %s %p\n", s1, equal_sign (s1, s2), s2);
print ("\n\"var s3 = (owned) s1;\" --- Transfer\n");
var s3 = (owned) s1; // 行为:s3接替s1的引用/所有权关系,而s1变为null
print ("s1:%s s3:%s\n", s1, s3);
print ("%p %s %p\n", s1, equal_sign (s1, s3), s3);
// s2指向被接替前的s1所指向的内存地址(即现在的s3),而非s1这个指针本身,因此仍然为"Hello"
print ("\nBut what is s2 now?\n");
print ("s2:%s\n", s2);
}
运行结果示例(操作系统分配的具体内存地址可能不一样):
"var s1 = s;" --- Copy
s:Hello s1:Hello
0x55b7dd9bb708 != 0x55b7dde9f830
"unowned var s2 = s1;" --- Unowned refer
s1:Hello s2:Hello
0x55b7dde9f830 == 0x55b7dde9f830
"var s3 = (owned) s1;" --- Transfer
s1:(null) s3:Hello
(nil) != 0x55b7dde9f830
But what is s2 now?
s2:Hello
正如上文提及的那样,这个例子也可以说明Vala的无属引用与Rust的所有权租借不同。本例子中s1
的所有权转移给s3
后,s2
的无属引用仍然有效。这是因为Vala的无属引用是将变量的指针直接指向被引用的对象的内存地址,而非被引用对象的指针地址;在被引用的变量的所有权转让后,无属引用所指向的内存地址及其内容都没有变化,因此没有影响。也可以理解为,在s1
的所有权转移给s3
时,s2
变成了s3
的无属引用。
不过,对于字符串类型,除语言绑定的场景外,基本不用担心所有权问题,因为字符串在赋值的时候默认会复制(如上述例子中s1
与s
间的赋值),有时考虑所有权问题是出于性能因素;在语言绑定中,字符串的所有权问题较为重要,需要特别考虑。