Vala中的所有权简介

Vala语言知识介绍

Posted by wszqkzqk on February 7, 2023
本文字数:2917

与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中的iunowned声明可以避免字符串拷贝,此时变量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的无属引用。

不过,对于字符串类型,除语言绑定的场景外,基本不用担心所有权问题,因为字符串在赋值的时候默认会复制(如上述例子中s1s间的赋值),有时考虑所有权问题是出于性能因素;在语言绑定中,字符串的所有权问题较为重要,需要特别考虑。