Vala 引用计数和参数传递

3

在尝试使用Vala并检查生成的C源代码后,我得出了以下Vala代码:

class Foo : GLib.Object {
    public string baz;
}

class Main : GLib.Object {
    public static Foo foo;

    public static void bar(Foo f) {
        foo = null;
        f.baz = "Hi";
    }

    public static int main(string[] args) {
        foo = new Foo();
        bar(foo);
        return 0;
    }
}

检查生成的 C 代码后,我发现 Vala 编译器在将 foo 传递给 bar 时没有增加引用计数 (RC)。因此,就我理解的而言,在 bar 中的第一行将会将 foo 的 RC 减少至 0,这将使得 foo 被释放,从而将传递的变量 f 变为一个悬空指针。然后在 bar 的第二行中访问该悬空指针。然而,程序可以正常执行,因此我不确定是否漏掉了什么或者只是纯粹的巧合。

以下是参考所生成的 C 代码:

void main_bar (Foo* f) {
    Foo* _tmp0_;
    gchar* _tmp1_;
    g_return_if_fail (f != NULL);
    _g_object_unref0 (main_foo);
    main_foo = NULL;
    _tmp0_ = f;
    _tmp1_ = g_strdup ("Hi");
    _g_free0 (_tmp0_->baz);
    _tmp0_->baz = _tmp1_;
}

gint main_main (gchar** args, int args_length1) {
    gint result = 0;
    Foo* _tmp0_;
    Foo* _tmp1_;
    _tmp0_ = foo_new ();
    _g_object_unref0 (main_foo);
    main_foo = _tmp0_;
    _tmp1_ = main_foo;
    main_bar (_tmp1_);
    result = 0;
    return result;
}

1
你是正确的,如果在Foo上添加一个析构函数,你会看到它被销毁。同时,valgrind没有报错信息。这很奇怪。 - lethalman
1个回答

4
这是正确的行为。只有owned引用才会计数。参数是unowned的,除非明确指定。因此,在bar中,f从不被计数,因为调用者负责维护引用计数。变量存储位置(类字段,栈变量,全局变量)都是owned的。
因此,让我们分别检查mainbarmain创建了一个需要放置在某处的Foo实例。它将其放在全局变量foo中,并拥有它。现在已经有一个对象通过foo被创建了单个引用。然后我们调用bar,它接受参数foo。我们知道foo已经引用了该对象,并且传递它作为参数不需要我们增加引用,除非参数是owned的。因此,我们只需将指针foo传递给barbar接受类型为Foo的参数f,它没有拥有。它将null赋值给一个完全无关的全局变量foo,这会减少对象的引用计数,必要时进行清理。然后它对f中的字段进行赋值。
为了使这个代码“正确”工作,编译器需要:1)理解foof是相同的,即使您可以使用任何参数调用bar,2)知道在某些情况下,将foo的引用计数减少到零与减少它略有不同。这对于任何不能解决停机问题的编译器来说都太复杂了。
要使您的代码按预期工作,您有两个选择:
1.将新对象分配给全局变量foo和一个堆栈变量,并将其传递给bar。现在,您已确保在调用bar期间该变量仍然有效。
2.使bar接受owned Foo f而不是Foo f。这将导致调用者在传递和完成bar时增加和减少foo的引用。
简而言之,当调用方法的生命周期内,使变量保持活动状态是调用者的责任。当该方法是async时,情况会更加复杂。

这听起来非常复杂,因为对于每个方法调用,您必须考虑所调用的方法或另一个线程可能通过将其置空来使对象引用无效。此外,应该将owned关键字的使用明确地插入到Vala教程中。 - Askaga
原始问题不是:“为什么会崩溃?”而是“为什么不会崩溃?”你解释了行为为何出现问题,但并没有解释为什么它实际上不会崩溃。 - lethalman
@BillAskaga,如果您将事物保留在本地变量而不是全局变量中,则这个问题就不是那么严重了。 - apmasell
2
@lethalman,这是GLib板块分配器的一个函数,它会进行一些自己的内存管理。即使已经释放到GLib的空闲池中,该块也尚未被释放回libc。如果您设置环境变量G_SLICE=always-malloc,Valgrind将如预期地发出警告。 - apmasell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接