GObject 子类私有属性的外部访问[转]

原文链接

参考文档

虽然,创建一个 GObject 子类对象需要一些辅助函数和宏的支持,并且它们的内幕也令人费解,但是只要将足够的信任交托给 GObject 开发者,将那些辅助函数和宏当作“语法”糖一样享用,一切还是挺简单的。至于细节,还是等较为全面的掌握 GObject 库的用法之后再去挖掘!

现在,我们基本上知道了如何将数据封装并藏匿于 GObject 子类的实例结构体中。本文打算再向前走一步,关注如何实现在外部比较安全的访问(读写)这些数据。

简单的做法

像下面这样的双向链表数据结构:

typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
        PMDListNode *prev;
        PMDListNode *next;
};
 
typedef struct _PMDList PMDList;
struct _PMDList {
        PMDListNode *head;
        PMDListNode *tail;
};

现在,我们希望能够安全访问 PMDList 结构题的两个成员,即链表的首结点指针 head 和尾结点指针 tail,以便进行一些操作,例如将两个双向链表 list1 和 list2 链接到一起。

所谓安全访问,意味着不要像下面这样简单粗暴:

/* 将 list1 与 list2 链接在一起 */
list1->tail->next = list2->head;
list2->head->prev = list1->tail;

而应当委婉一些:

PMDListNode *list1_tail, *list2_head;
 
list1_tail = pm_dlist_get (list1, TAIL);
list2_head = pm_dlist_get (list2, HEAD);
 
pm_dlist_set (list1, TAIL, NEXT, list2_head);
pm_dlist_set (list2, HEAD, PREV, list1_tail);

这样委婉的访问,有什么好处?答案很简单,可以将数据的变化与程序的功能隔离开,数据的变化不影响程序的功能。

试想,如果有一天,上述 PMDList 结构体的设计者使用 GObject 子类化的方法将双向链表定义为建 PMDList 类的形式,并且将链表的首结点指针 head 与尾结点都隐匿起来,那么上述的那个简单粗暴的数据访问方法便失效了。更糟糕的是,PMDList 类的设计者明知道很多人会受到这种数据变化的影响,对此也毫无办法。

如果 PMDList 结构体的设计者提供了 pm_dlist_setpm_dlist_get 函数,那么即便设计者基于 GObject 子类化的方式定义了 PMDList 类,他只需要修改 pm_dlist_setpm_dlist_get 函数,便可以让上述那种委婉方式访问 PMDList 结构体成员的代码不会受到任何影响。

既然 pm_dlist_setpm_dlist_get 函数这样有用,我们可以像下面这样实现它们。

typedef enum _PM_DLIST_PROPERTY PM_DLIST_PROPERTY
enum _PM_DLIST_PROPERTY {
        PM_DLIST_HEAD,
        PM_DLIST_TAIL,
        PM_DLIST_NODE_PREV,
        PM_DLIST_NODE_NEXT
};
 
PMDListNode *
pm_dlist_get (PMDList *self, PM_DLIST_PROPERTY property)
{
        PMDListNode *node = NULL;
 
        switch (property) {
        case PM_DLIST_HEAD:
                node = self->head;
                break;
        case PM_DLIST_TAIL:
                node = self->tail;
                break;
        default:
                g_print ("对不起,你访问的成员不存在!\n");
        }
 
        return node;
}
 
void
pm_dlist_set (PMDList *self, 
              PM_DLIST_PROPERTY property,
              PM_DLIST_PROPERTY subproperty,
              PMDListNode *node)
{
        switch (property) {
        case PM_DLIST_HEAD:
                if (subproperty == PM_DLIST_NODE_PREV)
                        self->head->prev = node;
                else if (subproperty == PM_DLIST_NODE_NEXT)
                        self->head->next = node;
                break;
        case PM_DLIST_TAIL:
                if (subproperty == PM_DLIST_NODE_PREV)
                        self->tail->prev = node;
                else if (subproperty == PM_DLIST_NODE_NEXT)
                        self->tail->next = node;
                break;
        default:
                g_print ("对不起,你访问的成员不存在!\n");
        }
}

事实上,上述代码所实现的功能仅仅是实现下面这 6 种赋值运算:

PMDList *list;
 
list->head->prev = aaaa;
list->head->next = bbbb;
 
list->tail->prev = cccc;
list->tail->next = dddd;
 
node = list->head;
node = list->tail;

对于区区一个链表的最原始形态的属性访问模拟便已如此,那些内建支持面向对象的编程语言、动态编程语言以及函数编程语言,它们所提供的语法越高级,那么它们等价的 C 代码量便会越庞大。

如果你所解决的问题,需要很多层的数据抽象,如果使用 C 语言的话,就不得不写很多的模拟代码。倘若这些模拟代码在你全部代码所占的比重超过了你的容忍限度,可以考虑换一种更合适的编程语言。当然,你不可能是先用 C 写完代码后,再去评估那部分模拟代码所占的比重,但是这并不妨碍你凭借现有的经验去粗略估计。

我将 gtk+ 作为使用 C 语言应用的典范,gtk+ 3.0 的全部代码大约 515500 行,而 GObject 的代码大概 20000 行,其所占比重大约为 4%,这其中还不算 GTK+ 的那些基于 GObject 的底层库的代码量。我觉得 GTK+ 开发者使用 GObject 实现足够的面向对象支持,是比较划算的。

那个很二的参数

回顾一下文档 [2] 和 [3] 中出现过的 g_object_new 函数的参数:

PMDList *list = g_object_new (PM_TYPE_DLIST, NULL);

该函数第一个参数 PM_TYPE_DLIST 的含义在文档 [2] 中已有较为详细的解释,而第二个参数的含义一直被故意的忽略,现在才是分析它的最好时机。事实上,g_object_new 接受的是可变参数[4],第二个参数后面,还可以有第三个、第四个…理论上的无穷个。这些参数的作用可以用下面的代码来表现:

PMDList *list = g_object_new (PM_TYPE_DLIST,
                              "head", NULL,
                              "tail", NULL,
                              NULL);

如果采用这种方式调用 g_object_new 函数,意味着在文档 [3] 中的 dlist.c 文件中,不需要再在 PMDList 类的实例结构体初始化函数 pm_dlist_init 中对链表首结点和尾结点指针进行赋值了,即 pm_dlist_init 函数:

static void
pm_dlist_init (PMDList *self)
{
        PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);
          
        priv->head = NULL;
        priv->tail = NULL;
}

可以为空:

static void
pm_dlist_init (PMDList *self)
{ 
}

换句话说,就是你在使用 g_object_new 函数进行对象实例化的过程中,可直接通过 g_object_new 函数的输入参数去初始化对象的属性,这是通过“属性名-属性值”参数来实现的,即 g_object_new 的第二个参数为属性名,第三个参数为属性值,它们在 g_object_new 内部会被合成为“属性名-属性值”结构;同理,第四个参数与第五个参数也可以形成“属性名-属性值”结构,依次类推,当属性名参数为 NULL 时,g_object_new 会认为“属性名:属性值”结构序列结束。上面示例中的 g_object_new 可形成 2 个“参数名:参数值”结构:

"head" : NULL
"tail" : NULL

g_object_new 函数会根据属性名匹配对象的相应属性,并将属性值赋予该属性,但是这需要 PMDList 类的设计者去实现一部分比较丑陋的代码。

将丑陋封锁在内部

要想实现上一节所讲述的让 g_object_new 函数中通过“属性名-属性值”结构为 GObject 子类对象的属性进行初始化,我们需要完成以下工作:

  • 实现 p_t_set_propertyp_t_get_property 函数,让它们来完成 g_object_new 函数的“属性名-属性值”结构向 GObject 子类属性的映射。
  • 在GObject 子类的类结构体初始化函数中,让 GObject 类(基类)的两个函数指针 set_propertyget_property 分别指向 p_t_set_propertyp_t_get_property 函数。 在 GObject 子类的类结构体初始化函数中,为 GObject 子类安装属性。 前两个步骤,可以理解为 GObject 的两个虚函数的实现。第三个步骤,可以视为为比文档 [3] 中 GObject 子类私有属性更高级一些的模拟。

现在,开始动手吧。

首先,pm_dlist_set_propertypm_dlist_get_property 函数,可以像下面这样实现:

static void
pm_dlist_set_property (GObject *object, guint property_id,
                       const GValue *value, GParamSpec *pspec)
{
        PMDList *self = PM_DLIST (object);
        PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);
         
        switch (property_id) {
        case PROPERTY_DLIST_HEAD:
                priv->head = g_value_get_pointer (value);
                break;
        case PROPERTY_DLIST_HEAD_PREV:
                priv->head->prev = g_value_get_pointer (value);
                break;
        case PROPERTY_DLIST_HEAD_NEXT:
                priv->head->next = g_value_get_pointer (value);
                break;
        case PROPERTY_DLIST_TAIL:
                priv->tail = g_value_get_pointer (value);
                break;
        case PROPERTY_DLIST_TAIL_PREV:
                priv->tail->prev = g_value_get_pointer (value);
                break;
        case PROPERTY_DLIST_TAIL_NEXT:
                priv->tail->next = g_value_get_pointer (value);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}
 
static void
pm_dlist_get_property (GObject *object, guint property_id,
                       GValue *value, GParamSpec *pspec)
{
        PMDList *self = PM_DLIST (object);
        PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);
         
        switch (property_id) {
        case PROPERTY_DLIST_HEAD:
                g_value_set_pointer (value, priv->head);
                break;
        case PROPERTY_DLIST_HEAD_PREV:
                g_value_set_pointer (value, priv->head->prev);
                break;
        case PROPERTY_DLIST_HEAD_NEXT:
                g_value_set_pointer (value, priv->head->next);
                break;
        case PROPERTY_DLIST_TAIL:
                g_value_set_pointer (value, priv->tail);
                break;
        case PROPERTY_DLIST_TAIL_PREV:
                g_value_set_pointer (value, priv->tail->prev);
                break;
        case PROPERTY_DLIST_TAIL_NEXT:
                g_value_set_pointer (value, priv->tail->next);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

哇,代码很多!但是请不要恐惧,因为所有 GObject 子类属性的 set 与 get 函数的实现,思路上均与上述代码相似。要理解这些代码,只需注意以下几点:

  • PM_DLIST (object) 宏的作用是将一个基类指针类型转换为 PMDList 类的指针类型,它需要 GObject 子类的设计者提供,我们可以将其定义为:
#define PM_DLIST(object) \
        G_TYPE_CHECK_INSTANCE_CAST ((object), PM_TYPE_DLIST, PMDList))
  • PROPERTY_DLIST_XXXX 宏,可以采用枚举类型实现。
  • GValue 类型是一个变量容器,可用于存放各种变量的值,例如整型数、指针、GObject 子类等等,上述代码主要用 GValue 存放指针变量的值。
  • GParamSpec 类型是比 GValue 高级一点的变量容器,它不仅可以存放各种变量的值,还能为这些值命名,因此它比较适合用于表示 g_object_new 函数的“属性名-属性值”结构。不过,在上述代码中,GParamSpec 类型只是昙花一现,没关系,反正下文它还会出现。

在理解了上述代码之后,我们继续前进,迈入 PMDList 类的类结构体初始化函数,首先要覆盖 GObject 类的两个函数指针:

static void
pm_dlist_class_init (PMDListClass *klass)
{
        /* 对象私有属性的安装,详见文档 [3] */
        g_type_class_add_private (klass, sizeof (PMDListPrivate));
 
 
        GObjectClass *base_class = G_OBJECT_CLASS (klass);
        base_class->set_property = pm_dlist_set_property;
        base_class->get_property = pm_dlist_get_property;
 
/* 未完,下文待续 */

set_propertyget_property 是两个函数指针,它们位于 GObject 类的类结构体中。如果你看过文档 [2],也许你还记得 GObject 库中,类是由实例结构体与类结构体构成的。对象的属性,应当存储在实例结构体中,而所有对象共享的数据,应当存储于类结构体中。因此,set_propertyget_property 是两个函数指针可以被 GObject 类及其子类的所有对象共享,并且各个对象都可以让这两个函数指针指向它所期望的函数。

类似的机制,在 C++ 中被称为“虚函数”,主要用于实现多态。不过,即便你不知道虚函数与多态是什么东西,这也无所谓,你只需要知道 PMDList 类从它的父类——GObject 类中继承了 2 个函数指针,在 PMDList 类的类结构体初始化函数中,将这 2 个函数指针指向了前文中定义的 pm_dlist_set_propertypm_dlist_get_property 函数,这些就足够了。

接下来,就是向 PMDList 类安装属性,紧接上面的代码:

/* 接前文尚未完成的 pm_dlist_class_init 函数 */
        GParamSpec *pspec;
        pspec = g_param_spec_pointer ("head",
                                      "Head node",
                                      "The head node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
        g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD, pspec);
/* 未完,下文待续 */

pm_dlist_set_propertypm_dlist_get_property 函数中昙花一现的 GParamSpec 类型终于又出现了。我知道,它看起来似乎很恐怖,但是它所作的事情却很简单,就是对一个键值对打包成一个数据结构,然后将之安装到相应的 GObject 子类中。

g_param_spec_pointer 函数,可以将“属性名:属性值”参数打包为 GParamSpec 类型的变量,该函数的第一个参数用于设定键名,第二个参数是键名的昵称,第三个参数是对这个键值对的详细描述,第四个参数用于表示键值的访问权限,G_PARAM_READABLE | G_PARAM_WRITABLE 是指定属性即可读又可写,G_PARAM_CONSTRUCT 是设定属性可以在对象示例化之时被设置。

g_object_class_install_property 函数用于将 GParamSpec 类型变量所包含的数据插入到 GObject 子类中,其中的细节可以忽略,只需要知道该函数的第一个参数为 GObject 子类的类结构体,第二个参数是 GParamSpec 对应的属性 ID。GObject 子类的属性 ID 在前文已经提及,它是 GObject 子类设计者定义的宏或枚举类型。第三个参数是要安装值向 GObject 子类中的 GParamSpec 类型的变量指针。

但是,一定要注意,g_object_class_install_property 函数的第二个参数值不能为 0。在使用枚举类型来定义 ID 时,为了避免 0 的使用,一个比较笨的技巧就是像下面这样设计一个枚举类型:

enum PropertyDList {
        PROPERTY_DLIST_0,
        PROPERTY_DLIST_HEAD,
        PROPERTY_DLIST_HEAD_PREV,
        PROPERTY_DLIST_HEAD_NEXT,
        PROPERTY_DLIST_TAIL,
        PROPERTY_DLIST_TAIL_PREV,
        PROPERTY_DLIST_TAIL_NEXT
};

其中的 PROPERTY_DLIST_0,只是占位符,它不被使用。

按照上面的属性的安装方式,我们可以陆续写处其它属性的安装代码,即 pm_dlist_class_init 函数的剩余部分:

/* 接前文尚未完成的 pm_dlist_class_init 函数 */
        pspec = g_param_spec_pointer ("head-prev",
                                      "The previous node of the head node",
                                      "The previous node of the head node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE);
        g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD_PREV, pspec);
        pspec = g_param_spec_pointer ("head-next",
                                      "The next node of the head node",
                                      "The next node of the head node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE);
        g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD_NEXT, pspec);
        pspec = g_param_spec_pointer ("tail",
                                      "Tail node",
                                      "The tail node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
        g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL, pspec);
        pspec = g_param_spec_pointer ("tail-prev",
                                      "The previous node of the tail node",
                                      "The previous node of the tail node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE);
        g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL_PREV, pspec);
        pspec = g_param_spec_pointer ("tail-next",
                                      "The next node of the tail node",
                                      "The next node of the tail node of the double list",
                                      G_PARAM_READABLE | G_PARAM_WRITABLE);
        g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL_NEXT, pspec);
}

这些代码又冗余又无趣,但是并不难理解。

将简洁留给外部

对于上一节所实现的 PMDList 类,可以采用下面的代码在对象实例化时便进行属性的初始化,即将链表的首结点和尾节点指针设为 NULL:

PMDList *list = g_object_new (PM_TYPE_DLIST,
                              "head", NULL,
                              "tail", NULL,
                              NULL); /* 要记得键值对参数之后,要以 NULL 收尾 */

也可以调用 g_object_get_property 函数获取 PMDList 类的实例属性,例如获取链表 list 的首结点指针:

GValue val = { 0, };
 
g_value_init(val,G_TYPE_POINTER);
g_object_get_property(G_OBJECT(list),"head",val);
g_value_unset (val);

也可以调用 g_object_set_property 函数设置 PMDList 类的实例属性,例如将链表 list1 的尾结点指针所指向的结点地址赋给链表 list2 的首结点的前驱结点指针:

GValue val = {0};
 
g_value_init (val,G_TYPE_POINTER);
g_object_get_property (G_OBJECT(list1), "tail", val);
g_object_set_property (G_OBJECT(list2), "head-prev", val);
g_value_unset (val);

如果我们要解决本文开始时的那个 list1 与 list2 链接的问题,可以这样:

GValue list1_tail = {0};
GValue list2_head = {0};
 
g_value_init (&list1_tail, G_TYPE_POINTER);
g_value_init (&list2_head, G_TYPE_POINTER);
 
g_object_get_property (G_OBJECT(list1), "tail", &list1_tail);
g_object_set_property (G_OBJECT(list2), "head-prev", &list1_tail);
 
 
g_object_get_property (G_OBJECT(list2), "head", &list2_head);
g_object_set_property (G_OBJECT(list1), "tail-next", &list2_head);
 
g_value_unset (&list2_head);
g_value_unset (&list1_tail);

看上去还不错。当然,前提是你需要了解一下 GValue 容器的用法,并且上述代码已经展示了它的基本用法,但是最好还是阅读文档 [5, 6]。

上述代码中使用的 g_object_set_propertyg_object_get_property 函数,看上去很无趣,每次只能设置或获取一个属性值,并且还要借助 GValue 容器,事实上它们是为那些基于 GObject 库的语言绑定使用的。对于在 C 程序中直接使用 GObject 库的用户,可以使用 g_object_setg_object_get 函数一次进行多个属性的设置与获取,它们的用法与 g_object_new 相似,可以处理以 NULL 结尾的“属性名-属性值”参数序列。

将一切放在一起

如果能够登临山顶,领略水何澹澹,山岛耸峙的景观,那么即便是需要穿越一条遍布荆棘的山路,也是很值得去做的。现在,我们已经站在了碣石山上,看到了下面的图景

gobject-property.png