强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Vala 语言入门教程 / 05 - GObject 类型系统

第 5 章:GObject 类型系统

GObject 是整个 GNOME 技术栈的基石。理解 GObject 类型系统,是精通 Vala 的必经之路。


5.1 GObject 是什么

GObject 是 GLib 库提供的面向对象类型系统,为纯 C 语言带来了:

  • 类与继承
  • 接口
  • 属性(Properties)
  • 信号(Signals)
  • 引用计数(Reference Counting)
  • 类型内省(Introspection)
┌────────────────────────────────────────────┐
│              GObject 类型层次               │
├────────────────────────────────────────────┤
│           GObject (基类)                    │
│              ↑                              │
│    ┌────────┼────────┐                     │
│    ↑        ↑        ↑                     │
│  GFile   GSocket  GtkWidget               │
│           ↑         ↑                      │
│         GIO      GtkButton                 │
│                  ↑                          │
│               GtkWindow                    │
└────────────────────────────────────────────┘

Vala 中的每个类最终都继承自 GLib.Object(即 GObject)。编译器会自动生成注册类、定义属性、连接信号等样板代码。


5.2 类型系统基础

5.2.1 GObject 类型层次

void main () {
    // 创建对象
    var obj = new Object ();

    // 查询类型信息
    Type type = obj.get_type ();
    print ("类型名: %s\n", type.name ());
    print ("类型是否抽象: %s\n", type.is_abstract ().to_string ());
    print ("类型是否接口: %s\n", type.is_interface ().to_string ());

    // 类型检查
    var list = new GLib.List<string> ();
    Type list_type = list.get_type ();
    print ("List 类型名: %s\n", list_type.name ());

    // 类型是否是另一个类型的子类
    print ("List 继承自 Object: %s\n",
           list_type.is_a (typeof (Object)).to_string ());
}

5.2.2 typeof 和类型检查

public class Animal : Object {
    public string name { get; set; }
}

public class Dog : Animal {
    public void bark () {
        print ("汪汪!\n");
    }
}

public class Cat : Animal {
    public void meow () {
        print ("喵喵!\n");
    }
}

void main () {
    Animal dog = new Dog () { name = "旺财" };
    Animal cat = new Cat () { name = "咪咪" };

    // 类型检查
    print ("dog 是 Dog: %s\n", (dog is Dog).to_string ());
    print ("dog 是 Cat: %s\n", (dog is Cat).to_string ());
    print ("dog 是 Animal: %s\n", (dog is Animal).to_string ());

    // 安全类型转换
    if (dog is Dog) {
        Dog d = (Dog) dog;
        d.bark ();
    }

    // typeof 运算符
    Type t = typeof (Dog);
    print ("Dog 类型: %s\n", t.name ());

    // 动态创建对象
    Type type = Type.from_name ("Dog");
    if (type != 0) {
        var obj = Object.new (type);
        print ("动态创建: %s\n", obj.get_type ().name ());
    }
}

5.2.3 类型注册

Vala 编译器自动完成类型注册,但了解底层机制很重要:

// Vala 编译器生成的 C 代码(概念示例)
G_DEFINE_TYPE(MyApp, my_app, G_TYPE_OBJECT)

static void my_app_class_init(MyAppClass *klass) {
    // 注册属性
    g_object_class_install_property(object_class, PROP_NAME,
        g_param_spec_string("name", "Name", "名字", "",
            G_PARAM_READWRITE));

    // 注册信号
    signals[SIG_STARTED] = g_signal_new("started",
        G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST,
        0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

static void my_app_init(MyApp *self) {
    // 初始化实例
}
// 等价的 Vala 代码
public class MyApp : Object {
    public string name { get; set; }
    public signal void started ();
}

💡 一个 Vala 属性声明,等价于约 20 行 C 代码。


5.3 属性系统详解

5.3.1 属性规范

public class Config : Object {
    // 可读写属性
    public string host { get; set; default = "localhost"; }

    // 只读属性
    public int64 start_time { get; default = 0; }

    // 构造时设置,之后只读
    public string app_name { get; construct; }

    // 自定义实现
    private string _password = "";
    public string password {
        get { return _password; }
        set {
            _password = value;
            // 触发变化通知
            notify_property ("password");
        }
    }

    // 只在包内可写
    public string debug_info { get; internal set; }

    // owned getter(返回所有权,引用计数 +1)
    public string data {
        owned get { return "some data"; }
    }

    construct {
        start_time = GLib.get_real_time ();
    }

    public Config (string app_name) {
        Object (app_name: app_name);
    }
}

5.3.2 属性变化通知

public class Settings : Object {
    private int _volume = 50;

    public int volume {
        get { return _volume; }
        set {
            if (value < 0) value = 0;
            if (value > 100) value = 100;
            if (_volume != value) {
                _volume = value;
                // 通知属性已变化
                notify_property ("volume");
            }
        }
    }
}

void main () {
    var s = new Settings ();

    // 监听特定属性
    s.notify["volume"].connect ((obj, pspec) => {
        print ("音量已变化: %d\n", s.volume);
    });

    // 监听所有属性
    s.notify.connect ((obj, pspec) => {
        print ("属性 '%s' 已变化\n", pspec.name);
    });

    s.volume = 80;
    s.volume = 30;
    s.volume = 150;  // 会被限制为 100
}

5.3.3 使用 GObject 构造器设置属性

public class Person : Object {
    public string name { get; set; }
    public int age { get; set; }

    // 使用 Object() 构造器设置属性
    public Person (string name, int age) {
        Object (name: name, age: age);
    }

    construct {
        // construct 块在属性设置后执行
        print ("Person 已创建: %s, %d 岁\n", name, age);
    }
}

void main () {
    // 方式 1:通过构造器参数
    var p1 = new Person ("张三", 30);

    // 方式 2:使用 GObject 属性名
    var p2 = Object.new (typeof (Person),
        "name", "李四",
        "age", 25
    ) as Person;
    print ("p2: %s, %d\n", p2.name, p2.age);
}

5.4 信号系统详解

5.4.1 信号定义

public class Button : Object {
    public string label { get; set; }

    // 信号定义
    public signal void clicked ();
    public signal void pressed (int x, int y);
    public signal bool validate (string input);  // 带返回值

    public Button (string label) {
        Object (label: label);
    }

    public void simulate () {
        clicked ();
        pressed (10, 20);
        bool valid = validate ("hello");
        print ("验证结果: %s\n", valid.to_string ());
    }
}

5.4.2 信号连接方式

void main () {
    var btn = new Button ("确定");

    // 1. Lambda 表达式
    btn.clicked.connect (() => {
        print ("按钮被点击\n");
    });

    // 2. 带参数的信号
    btn.pressed.connect ((x, y) => {
        print ("按下位置: (%d, %d)\n", x, y);
    });

    // 3. 带返回值的信号(最后一个处理器的返回值生效)
    btn.validate.connect ((input) => {
        return input.length > 0;
    });

    // 4. 方法引用
    btn.clicked.connect (on_clicked);

    // 5. 连接后自动断开(第一次触发后)
    btn.clicked.connect_after (() => {
        print ("后置处理器\n");
    });

    btn.simulate ();
}

void on_clicked () {
    print ("独立函数处理器\n");
}

5.4.3 信号断开和检查

void main () {
    var obj = new Object ();

    // 连接并保存 handler ID
    ulong handler = obj.notify.connect ((obj, pspec) => {
        print ("属性变化: %s\n", pspec.name);
    });

    // 检查是否有处理器连接
    print ("信号已连接: %s\n",
           obj.handler_is_connected (handler).to_string ());

    // 断开特定处理器
    obj.disconnect (handler);

    // 或使用信号名称断开所有
    obj.disconnect_by_func ((Object obj, ParamSpec pspec) => {});
}

5.4.4 信号发射顺序

public class Demo : Object {
    public signal int compute (int value);
}

void main () {
    var demo = new Demo ();

    // 多个处理器:按连接顺序执行
    demo.compute.connect ((v) => {
        print ("处理器1: %d -> %d\n", v, v + 1);
        return v + 1;
    });

    demo.compute.connect ((v) => {
        print ("处理器2: %d -> %d\n", v, v * 2);
        return v * 2;
    });

    // 信号返回值:最后一个处理器的返回值
    int result = demo.compute (5);
    print ("最终结果: %d\n", result);  // 最后一个处理器的返回值
}

5.5 引用计数与内存管理

5.5.1 引用计数基础

GObject 使用引用计数(Reference Counting)进行内存管理:

创建对象    → ref_count = 1
增加引用    → ref_count += 1
减少引用    → ref_count -= 1
引用为零    → 对象被销毁
void main () {
    // 创建对象(ref_count = 1)
    var obj = new Object ();

    // Vala 自动管理引用计数
    // obj 超出作用域时自动 unref

    {
        var obj2 = obj;  // 引用传递,ref_count 不变(Vala 移动语义)
    }
    // obj2 超出作用域,但 obj 仍然有效

    print ("程序结束\n");
}

5.5.2 所有权语义

public class DataHolder : Object {
    private string _data;

    // owned 参数:接收所有权
    public void set_data (owned string data) {
        _data = (owned) data;  // 转移所有权
    }

    // owned 返回值:返回所有权
    public owned string get_data () {
        return _data;  // 返回时 ref_count += 1
    }

    // 非 owned 返回:返回引用(不增加 ref_count)
    public string peek_data () {
        return _data;
    }
}

5.5.3 避免循环引用

// ⚠️ 循环引用示例(会导致内存泄漏)
public class BadNode : Object {
    public string name { get; set; }
    public BadNode? parent { get; set; }    // 引用父节点
    public GLib.List<BadNode> children;      // 引用子节点
    // 这会造成循环引用!parent <-> children
}

// ✅ 正确做法:使用弱引用
public class GoodNode : Object {
    public string name { get; set; }

    // 使用 WeakRef 避免循环引用
    private WeakRef _parent;
    public GoodNode? parent {
        owned get { return (GoodNode?) _parent.get (); }
        set { _parent = WeakRef (value); }
    }

    public GLib.List<GoodNode> children = new GLib.List<GoodNode> ();

    public void add_child (GoodNode child) {
        child.parent = this;
        children.append (child);
    }
}

void main () {
    var root = new GoodNode () { name = "root" };
    var child1 = new GoodNode () { name = "child1" };
    var child2 = new GoodNode () { name = "child2" };

    root.add_child (child1);
    root.add_child (child2);

    print ("根节点: %s\n", root.name);
    print ("子节点数: %u\n", children_length (root));
}

uint children_length (GoodNode node) {
    uint len = 0;
    foreach (var child in node.children) {
        len++;
    }
    return len;
}

5.5.4 内存管理最佳实践

void main () {
    // 1. 让 Vala 自动管理(推荐)
    {
        var obj = new Object ();
        // 使用 obj
    }  // obj 在此处自动释放

    // 2. 使用 GLib.Bytes 管理二进制数据
    uint8[] data = { 0x01, 0x02, 0x03 };
    var bytes = new GLib.Bytes (data);
    // bytes 自动管理内存

    // 3. 使用 GLib.Array 管理动态数组
    var array = new GLib.Array<uint8> ();
    array.append_val (0x01);
    array.append_val (0x02);
    // array 自动管理内存

    // 4. 使用 GLib.HashTable 管理键值对
    var map = new GLib.HashTable<string, int> (str_hash, str_equal);
    map["key"] = 42;
    // map 自动管理内存
}

5.6 对象生命周期

public class Lifecycle : Object {
    public string name { get; set; }

    public Lifecycle (string name) {
        Object (name: name);
        print ("[%s] 构造函数\n", name);
    }

    construct {
        print ("[%s] construct 块\n", name);
    }

    ~Lifecycle () {
        print ("[%s] 析构函数\n", name);
    }

    public void do_something () {
        print ("[%s] 执行操作\n", name);
    }
}

void main () {
    print ("=== 创建对象 ===\n");
    var obj = new Lifecycle ("测试对象");

    print ("\n=== 使用对象 ===\n");
    obj.do_something ();

    print ("\n=== 对象即将销毁 ===\n");
    obj = null;  // 引用计数归零,触发析构

    print ("\n=== 程序结束 ===\n");
}

输出

=== 创建对象 ===
[测试对象] construct 块
[测试对象] 构造函数

=== 使用对象 ===
[测试对象] 执行操作

=== 对象即将销毁 ===
[测试对象] 析构函数

=== 程序结束 ===

5.7 泛型对象创建

// 使用 Object.new() 动态创建对象
T create_instance<T> () requires T: Object {
    Type type = typeof (T);
    return (T) Object.new (type);
}

// 带属性的动态创建
Object create_with_props (Type type, string[] prop_names, Value[] prop_values) {
    return Object.new_with_properties (type, prop_names, prop_values);
}

void main () {
    // 动态创建
    var obj = create_instance<Object> ();
    print ("类型: %s\n", obj.get_type ().name ());

    // 带属性创建
    var btn = Object.new (typeof (GLib.Object)) as Object;
    print ("按钮类型: %s\n", btn.get_type ().name ());
}

5.8 业务场景:数据模型绑定

// 数据模型
public class User : Object {
    public string id { get; set; }
    public string name { get; set; }
    public string email { get; set; }
    public bool active { get; set; default = true; }

    // 当任何属性变化时发出信号
    public signal void changed ();

    construct {
        // 监听所有属性变化
        this.notify.connect ((obj, pspec) => {
            changed ();
        });
    }

    public User (string id, string name, string email) {
        Object (id: id, name: name, email: email);
    }
}

// 视图层(简单打印)
public class UserView : Object {
    private User user;

    public UserView (User user) {
        this.user = user;
        user.changed.connect (refresh);
    }

    private void refresh () {
        print ("┌─────────────────────────────┐\n");
        print ("│ 用户: %-20s │\n", user.name);
        print ("│ 邮箱: %-20s │\n", user.email);
        print ("│ 状态: %-20s │\n", user.active ? "活跃" : "禁用");
        print ("└─────────────────────────────┘\n");
    }
}

void main () {
    var user = new User ("1", "张三", "[email protected]");
    var view = new UserView (user);

    print ("--- 初始状态 ---\n");
    user.name = "张三丰";
    user.email = "[email protected]";
    user.active = false;
}

5.9 注意事项

⚠️ GObject 常见陷阱

  1. 不要使用裸指针:Vala 的对象引用会自动管理 ref_count
  2. 循环引用必须处理:使用 WeakRef 或手动断开
  3. 信号处理器内存:连接的 lambda 会持有外部变量的引用
  4. construct 块顺序:在 Object() 调用之后执行,此时属性已设置
  5. 线程安全:GObject 不是线程安全的,跨线程使用需要额外处理
  6. unowned 引用:使用 unowned 获取不增加引用计数的引用,但要确保对象在引用期间有效

5.10 扩展阅读

资源链接
GObject 手册https://docs.gtk.org/gobject/
GObject 类型系统https://docs.gtk.org/gobject/type-system.html
GObject 属性https://docs.gtk.org/gobject/properties.html
GObject 信号https://docs.gtk.org/gobject/signals.html
引用计数https://docs.gtk.org/gobject/memory.html
GObject 内省https://gi.readthedocs.io/
Vala 和 GObjecthttps://wiki.gnome.org/Projects/Vala/GObjectInterop

5.11 总结

要点说明
GObjectGNOME 的 C 面向对象类型系统
类型注册Vala 自动完成 G_DEFINE_TYPE
属性{ get; set; }g_object_class_install_property
信号signalg_signal_new
内存管理引用计数,非 GC
循环引用使用 WeakRef 避免
类型检查使用 is 运算符,typeof() 获取类型

下一章我们将学习 Vala 的泛型编程。→ 第 6 章:泛型