GTK+中文社区(gtk.awaysoft.com)

 找回密码
 马上加入

QQ登录

只需一步,快速开始

查看: 4721|回复: 0

GObject Tutorial Part 2

[复制链接]
  • TA的每日心情
    奋斗
    2016-10-11 09:20
  • 签到天数: 271 天

    连续签到: 1 天

    [LV.8]以坛为家I

    发表于 2011-7-17 17:31:32 | 显示全部楼层 |阅读模式
    本文转自:http://blog.mcuol.com/User/AT91RM9200/Article/9619_1.htm
    Ryan McDougall(2004)
    translated by neowillis
      
    3.创建一个继承自GObject的对象



    设计
    尽管我们现在能够生成一个基本的对象,但事实上我们故意略过了本类型系统的上下文:作为一个复杂的开发库套件的基础 -那就是图形库GTK+。GTK+的设计要求所有的类应该继承自一个根类。这样就至少能允许一些公共的基础功能能够被共享:如支持信号(让消息可以很容易的从一个对象传递到另一个),使用引用计数来管理对象生命期,支持属性(针对对象的数据成员生成简单的setting和getting函数),支持构造和析构函数(用来设置信号、引用计数器、属性)。当我们让对象继承自GObject时,我们就获得了上述的一切,并且当与其它基于GObject的库交互时会很容易。然而,本章节我们不讨论信号、引用计数和属性,或者任何其它专门的特性,这里我们将详细描述类型系统中继承是如何工作的。
    我们都知道,如果高档轿车继承自轿车,那么高档轿车就是轿车加上一些新的特性。那如何让系统去实现这样的功能呢?其实可以使用结构体的一个特性来实现:结构体里的第一个成员一定是在内存的最前面。只要我们要求所有的对象将它们的基类声明为它们自己结构体的第一个成员,那么我们就能迅速的将指向某个对象的指针转型为指向它基类的指针!尽管这个技巧很好用,并且语法上非常干净,但这种转型的方式只适用于指针 - 你不能这样来转型一个普通的结构体。
    注意:这种转型技巧是类型不安全的。把一个对象转型为它的基类对象虽然合法但不明智。这将要求程序员自己来保障此次转型是安全的。

    创建类型的实例
    了解了这个技术后,那么究竟类型系统是如何实例化出对象的呢?第一次我们使用g_type_create_instance让系统创建一个*实例对象时,它必须要先创建一个*类对象供实例来使用。如果该类结构体继承自其它类,系统则需要先创建和初始化这些父类。系统依靠我们指定的结构体(*_get_type函数中的!GTypeInfo结构体)来完成这个工作,这个结构体描述了每个对象的实例对象大小,类对象大小,初始化函数和销毁函数。- 要用g_type_create_instance来实例化一个对象
        如果它没有相关联的类对象
           创建类对象并且将其加入到类的层次中
        创建实例对象并且返回指向它的指针
    当系统创建一个新的类对象时,它先会分配足够的内存来放置这个最终的类对象(译者:“最终的”意指这个新的类对象,相对于其继承的父类们)。然后在继承链上从最顶端的父类开始到最末端的子类对象,用父类的成员域覆写掉这个最终类对象的成员域。这就是子类如何继承自父类的。当把父类的数据复制完后,系统将会在当前状态的类对象中执行父类的“base_init“函数。这个覆写和执行“base_init”的工作将循环多次,直到这个继承链上的每个父类都被处理过后才结束。接下来系统将在这个最终的类对象上执行最终子类的“base_init”和“class_init”函数。函数“class_init”有一个参数,即上文所提到的“class_data”,该参数会是构造函数的参数。
    细心的读者可能会问,为什么我们已经有了一个完整的父类对象的拷贝还需要它的base_init函数?因为当完整拷贝无法为每个类重新创建出某些数据时,我们就需要base_init函数。例如,某个类对象成员指向了另外一个对象,拷贝后我们希望每个类对象的成员都指向它自己的对象,而不是只拷贝对象的指针(内存的拷贝只是“浅拷贝”,这时我们需要一次“深拷贝”)。但事实上有经验的GObject程序员告诉我base_init函数会很少用到。
    当系统创建一个新的实例对象时,它会先分配足够的内存来将这个实例对象放进去。从继承链的最顶端的父类开始调用它的“instance_init”函数,直到最终的子类。最后,系统在最终类对象上调用最终子类的“instance_init”函数。
    我来总结一下上面所描述到的算法:- 实例化一个类对象
        为最终对象分配内存
        从父类到子类开始循环
            复制对象内容以覆盖掉最终对象的内容
            在最终对象上运行对象自己的base_init函数
        在最终对象上运行最终对象的base_init函数
        在最终对象上运行最终对象的class_init(附带上类数据)


    - 实例化一个实例对象
        为最终对象分配内存
        从父类到子类开始循环
            在最终对象上运行instance_init函数
        在最终对象上运行最终对象的instance_init函数此时创建的类对象和实例对象都已经被初始化,系统将实例对象的类指针指向到类对象,这样实例对象就能找到类对象所包含的虚函数表。这就是系统实例化已注册类型的过程,其实GObject实现的构造函数和析构函数语义与上述的方法也是相同的。

    创建GObject实例
    前面我们使用g_type_create_instance来创建一个实例对象。然而事实上GObject给我们提供了一个新的API来创建gobjects,可以完成我们上述所有的工作。这个API调用三个新的方法来创建和销毁新的GObject对象:构造函数(constructor),部署函数(dispose)以及析构函数(finalize)。
    因为C语言缺少真正面向对象的语言所具备的多态特性,特别是认出多个构造函数的能力,所以GObject的构造函数需要一些更复杂的实现:
    我们怎样才能灵活的传递不同种类的初始化信息到对象中,使得构造对象更容易呢?也许我们会想到限制只使用拷贝构造函数,然后用初始化数据填充一个静态的”初始化对象“,再将这个”初始化对象“传递到这个拷贝构造函数中。方法虽然简单,但是不太灵活。
    事实上GObject的作者们提供了一种更加通用的解决方案,同时还提供了方便的getting和setting方法来操作对象成员数据,这种机制被称作”属性“。在系统中我们的属性用字符串来命名,并对它进行边界和类型检查。属性还可以被声明为仅构造时可写,就像C++中的const变量一样。
    属性使用了一种多态的类型(GValue),这种类型允许程序员在不了解它实际类型的前提下安全的复制一个值。GValue会记录下它的值所持有的GType,使用类型系统来保证它总是具有一个虚函数,该函数可以处理将其自身复制到另一个GValue或转换为另一种GType。我们将在下一章详细讨论GValues和属性。
    要为一个GObject创建一个新的属性,我们要定义它的类型、名字,以及默认值,然后创建一个封装这些信息的“属性规格”对象。在GObject的类初始化函数中,我们可以通过g_object_class_install_property来将属性规格绑定到GObject的类对象上。
    注意:任何子对象要添加一个新的属性必须覆盖它从GObject继承下来的set_property和get_property虚方法。将在下一节中介绍这两个方法。
    使用属性我们可以向构造函数传递一组属性规格,附上我们希望的初始值,然后简单调用GObject的set_property,这样就能获得属性带给我们的神奇功效。但是事实上,构造函数是不会被我们直接调用的。
    GObject构造函数另一个不是那么明显的特性是,每个构造函数需要接受一个GType作为其参数之一,并且当它向上转型为其父类时,需要将这个GType传递给它父类的构造函数。这是因为GObject的构造函数使用子类的GType来调用g_type_create_instance,这样GObject的构造函数必须要知道它的最终子类对象的GType。
    注意:如果我们自己定义构造函数,我们则必须覆盖继承自父类的构造函数。自定义的构造函数必须得沿着“继承链”向上,在做任何其他的工作前,先调用完父类的构造函数。然而,因为我们使用了属性,所以事实上我们从来不用覆盖掉默认的构造函数。
    我要为上面的离题而道歉,但是这是为了理解系统是如何工作的所必须要克服的困难。如上所述,我们现在能理解GObject的构造函数了-g_object_new。这个函数接受一个子类的GType类型,一系列属性名(字符串)和GValue对作为参数。
    这一系列属性对被转换为键值对列表和相关的属性规格,这些属性规格将被在类初始化函数里被安装到系统中。调用类对象的构造函数时系统传入GType和构造属性。从最底端的子类构造函数到最顶端的基类构造函数,这条链会一直被触发直到GObject的构造函数被执行 - 这实际上才是第一个真正执行的初始化程序。GObject的构造函数现调用g_type_create_instance,并传下我们通过g_object_new一路带上的GType,这样我们上面所描述的细节将会发生,最终创建出实例。然后它将获得最终对象的类,并对传入所有构造属性调用set_property方法。这就是为什么我们加入一个新属性时必须要覆盖get_/set_property方法的原因。当这一串构造函数返回后,包含在其中的代码将从基类执行到子类。
    当父类构造函数返回后,就轮到子类来执行它自己的初始化代码了。这样执行代码的顺序就成为:
      [li]从GObject到ChildObject运行实例初始化函数 [/li][li]从GObject到ChildObject运行构造函数 [/li]
    最后任何剩余的没有传递到构造函数的属性将使用set_property方法一次设置完毕。
    读者也许会考虑在什么情况下需要覆盖默认构造函数,将自己的代码放到他们自己的构造函数里。因为我们所有的属性都可以使用虚方法set_property来设置,所以基本上没有覆盖GObject的默认构造函数的必要。
    我仍尝试使用伪码的方式来总结一下GObject的构造函数过程:- 使用属性键值对列表创建合适的GObject对象:
        在键值对列表中查找对应的属性规格
        调用最终对象的构造函数并传入规格列表和类型
            递归的向下调用直到GObject的构造函数
                对传入的类型调用g_type_create_instance
                对属性规格列表调用虚方法set_property
        对剩下的属性,调用set_property注意:GObject将属性区分为两类,构造属性和“常规”属性。

    销毁GObject实例
    该做的工作完成后,我们可以看看要清理这个对象时会发生些什么。GObject实现面向对象中的析构时,将其分解成了两步:处理(dispose)和销毁(finalize)。
    "处理"方法在对象知道自己将要被销毁时调用。实现该方法时,应该将指向资源的引用释放掉,这样可以避免循环引用或资源稀缺。“处理”方法应该允许被调用多次。要实现这一点,一般的做法是使用一个静态变量来保护”处理“方法。在“处理”方法调用后,对象本身应该依然能够使用,除非产生了不可恢复的错误(如段错误)。因此,“处理”方法不能释放或者改动某些对象成员。对于可恢复的错误,例如返回错误码或者空指针,则不应该受影响。
    “销毁”方法会在从内存中清理掉对象之前被调用,用于释放剩余的资源引用。因此它只能被调用一次。析构过程被分成两个步骤降低了引用计数策略中循环引用发生的可能。
    注意:如果我们自定义“处理”和“销毁”方法,就必须要覆盖掉继承自父类的相同方法。这两个方法从子类开始调用,沿着继承链向上直到最顶端的父类。
    与构造函数不同的是,只要新的对象分配了资源,我们就需要覆盖掉继承自父类的相同方法,自己实现“处理”和“销毁”方法。
    判断销毁代码放置到哪个函数不是件容易的事。一般来说,当与实现了引用计数的库(如GTK+)打交道时,我们应该在“处理”方法中解除对其它资源对象的引用,而在“销毁”方法中释放掉所有的内存或者关闭所有的文件描述字。
    上面我们讨论过g_object_new,但是我们什么时候来销毁这些对象呢?其实上面也有提示过,GObject使用了引用计数的技术,它保存了一个整型的数据,该数据描述了有多少个对象或函数现在正在使用或者引用这个对象。当你在使用GObject时,如果你希望新创建的对象不在使用时被销毁掉,你就必须及早调用g_object_ref,将对象作为参数传递给它,这样就为引用计数器增加了1。如果你没有做这件事就意味着对象允许被自动销毁,这也许会导致你的程序崩溃。
    同样的,当对象完成了它的任务后,你必须要调用g_object_unref。这样会使引用计数器减1,并且系统会检查它是否为0.当计数器为0时,对象将被先调用“处理”方法,最终被“销毁”掉。如果你没有解除到该对象的引用,则会导致内存泄漏,因为计数器永远不会回到0。
    现在我们已经准备好了来写一些代码了!不要让上面冗长复杂的描述吓到您。如果你没有完全理解上面所提到的,别紧张 - GObject的确是很复杂的!继续读下去,你会看到许多细节,试试一些例子程序,或者去睡觉吧,明天再来接着读。
    下面的程序与第一个例子很相似,事实上我去掉了更多的不合逻辑的、冗余的代码。

    代码(头文件)
    a. 我们仍然按照上面的方式继续,但是这次将把父类对象放到结构体的第一个成员位置上。事实上就是GObject。/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */
    typedef struct _SomeObject SomeObject;
    struct _SomeObject
    {
            GObject         parent_obj;

            /* 下面是一些数据 */
    };

    /* “类结构体”定义所有的方法函数,类对象将是共享的 */
    typedef struct _SomeObjectClass SomeObjectClass;
    struct _SomeObjectClass
    {
            GTypeClass      parent_class;

            /* 下面是一些方法 */
    };b. 头文件剩下的部分与第一个例子相同。

    代码(源文件)
    注意:我们需要增加一些对被覆盖的GObject方法的声明。/* 这些是GObject的构造和析构方法,它们的用法说明在gobject.h中 */
    void some_object_constructor(GType this_type,
                                 guint n_properties,
                                 GObjectConstructParam *properties)
    {
       /* 如果有子类要继承我们的对象,那么this_type将不是SOME_OBJECT_TYPE,
    g_type_peek_parent再是SOME_OBJECT_TYPE的话,将会造成无穷循环 */

    GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent (SOME_OBJECT_TYPE()));
             
    parent_class-> constructor (self_type, n_properties, properties);

    /* 很少需要再做其它工作 */
    }

    void    some_object_dispose (GObject *self)
    {
      GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent(SOME_OBJECT_TYPE()));
      static gboolean first_run = TRUE;

       if (first_run)
       {
           first_run = FALSE;
                   
          /* 对引用的所有GObject调用g_object_unref,但是不要破坏这个对象 */

           parent_class-> dispose (self);
        }
    }

    void some_object_finalize (GObject *self)
    {
      GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent(SOME_OBJECT_TYPE()));

            /* 释放内存和关闭文件 */

    parent_class-> finalize (self);
    }
    注意:GObjectConstructParam是一个有两个成员的结构体,一个是一组!GParamSpec类型,用来描述参数定义,另一个是一组GValue类型,是对应参数的值。/* 这是GObject的Get和Set方法,它们的用法说明在gobject.h中 */
    void some_object_get_property (GObject *object,
                                   guint property_id,
                                   GValue *value,
                                    GParamSpec *pspec)
    {
    }

    void some_object_set_property (GObject *object,
                                   guint property_id,
                                    const GValue *value,
                                    GParamSpec *pspec)
    {
    }

    /* 这里是我们覆盖函数的地方,因为我们没有定义属性或者任何域,下面都是不需要的 */
    void some_object_class_init  (gpointer g_class,
                                  gpointer class_data)
    {
    GObjectClass*this_class = G_OBJECT_CLASS (g_class);
            
    this_class-> constructor = &some_object_constructor;
    this_class-> dispose = &some_object_dispose;
    this_class-> finalize  = &some_object_finalize;

    this_class-> set_property = &some_object_set_property;
    this_class-> get_property  = &some_object_get_property;
    }要想讨论关于创建和销毁GObject,我们就必须要了解属性和其它特性。我将把操作属性的示例放到下一节来叙述。以避免过于复杂而使得你灰心丧气。在你对这些概念有些实作经验后,它们将开始显现出来存在的意义。如上面所言,我们现在只是将自己限制在创建一个基础的GObject类,在下一节我们将真正的编写一些函数。 重要的是我们获得了让下面的学习更轻松的工具。

    4.属性
    上面已经提到属性是个很奇妙的东西,也简单介绍了如何使用它。在进一步深入介绍属性之前,我们又得先离一会儿题。

    GValues
    C是一门强类型语言,也就是说变量声明的类型必须和它被使用的方式保持一致,否则编译器就会报错。这是一件好事,它使得程序编写起来更迅速,帮助我们发现可能会导致系统崩溃或者不安全的因素。但这又是件坏事,因为实际上程序员活在一个很难什么事都保持严格的世界上,而且我们也希望声明的类型能够具备多态的能力 -也就是说类型能够根据上下文来改变它们自己的特性。通过C语言的转型我们可以获得一些多态的能力,如上面所讨论过的继承。然而,当使用无类型指针作为参数传递给函数时,可能问题会比较多。幸运的是,类型系统给了我们另外一个C语言没有的工具:GType。
    让我们更清楚的描述一下问题吧。我需要一种数据类型,可以实现一个可以容纳多类型元素的链表,我想为这个链表编写一些接口,可以不依赖于任何特定的类型,并且不需要我为每种数据类型声明一个多余的函数。这种接口必然能涵盖多种类型,所以我们称它为GValue(Generic Value,泛型)。该如何实现这样一个类型呢?
    我们创建了封装这种类型的结构体,它具有两个成员域:所有基础类型的联合(union),和表示保存在这个union中的值的GType。这样我们就可以将值的类型隐藏在GValue中,并且通过检查对GValue的操作来保证类型是安全的。这样还减少了多余的以类型为基础的操作接口(如get_int,set_float,...),统一换成了g_value_*的形式。
    细心的读者会发现每个GValue都占据了最大的基础类型的内存大小(通常是8字节),再加上GType自己的大小。是的,GValues在空间上不是最优的,包含了不小的浪费,因此不应该被大量的使用它。它最常被用在定义一些泛型的API上。
    属性是如何工作的这一点稍稍超出了我们要讨论的范围,但是这对于理解属性本身还是很有帮助的。/* 让我们使用GValue来复制整型数据! */
    #define g_value_new(type) g_value_init (g_new (GValue, 1), type)

    GValue *a = g_value_new (G_TYPE_UCHAR);
    GValue *b = g_value_new (G_TYPE_INT);
    int c = 0;

    g_value_set_uchar (a, ''a'');
    g_value_copy (a, b);

    c = g_value_get (b);
    g_print ("w00t: %d\n", c);

    g_free (a);
    g_free (b);

    设计
    我们已经在上面接触过属性了,对它们有了初步的认识,现在我们将继续来了解一下设计它们的最初动机。要编写一个泛型的属性设置机制,我们需要一个将其参数化的方法,以及与实例结构体中的成员变量名查重的机制。从外部上看,我们希望使用C字符串来区分属性和公有API,但是内部上来说,这样做会严重的影响效率。因此我们枚举化了属性,使用索引来标识它们。
    上面提过属性规格,在Glib中被称作!GParamSpec,它保存了对象的gtype,对象的属性名称,属性枚举ID,属性默认值,边界值等,类型系统用!GParamSpec来将属性的字符串名转换为枚举的属性ID,GParamSpec也是一个能把所有东西都粘在一起的大胶水。
    当我们需要设置或者获取一个属性的值时,传入属性的名字,并且带上GValue用来保存我们要设置的值,调用g_object_set/get_property。g_object_set_property函数将在GParamSpec中查找我们要设置的属性名称,查找我们对象的类,并且调用对象的set_property方法。这意味着如果我们要增加一个新的属性,就必须要覆盖默认的set/get_property方法。而且基类包含的属性将被它自己的set/get_property方法所正常处理,因为!GParamSpec就是从基类传递下来的。最后,应该记住,我们必须事先通过对象的class_init方法来传入GParamSpec参数,用于安装上属性!
    假设我们已经有了如上一节所描述的那样一个可用的框架,那么现在让我们来为SomeObject加入处理属性的代码吧!

    代码(头文件)
    a. 除了我们增加了两个属性外,其余同上面的一样。/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */
    typedef struct _SomeObject SomeObject;
    struct _SomeObject
    {
            GObject         parent_obj;

            /* 新增加的属性 */
            int             a;
            float           b;

            /* 下面是一些数据 */
    };

    代码(源文件)
    a. 创建一个枚举类型用来内部记录属性。enum
    {
            OBJECT_PROPERTY_A = 1 << 1;
            OBJECT_PROPERTY_B = 1 << 2;
    };b. 实现新增的处理属性的函数。void    some_object_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
    {
            SomeObject *self = SOME_OBJECT (object);

            switch (property_id)
            {
                 case OBJECT_PROPERTY_A:
                       g_value_set_int (value, self-> a);
                       break;

                  case OBJECT_PROPERTY_B:
                       g_value_set_float (value, self-> b);
                       break;

                    default: /* 没有属性用到这个ID!! */
            }
    }

    void  some_object_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
    {
          SomeObject *self = SOME_OBJECT (object);

            switch (property_id)
            {
                 case OBJECT_PROPERTY_A:
                      self-> a = g_value_get_int (value);
                       break;

                 case OBJECT_PROPERTY_B:
                        self-> b = g_value_get_float (value);
                        break;

                  default: /* 没有属性用到这个ID!! */
            }
    }
    c. 覆盖继承自基类的set/get_property方法,并且传入GParamSpecs。/* 这里是我们覆盖函数的地方 */
    void some_object_class_init (gpointer g_class, gpointer class_data)
    {
       GObjectClass *this_class = G_OBJECT_CLASS (g_class);
       GParamSpec      *spec;
            
       this_class-> constructor = &some_object_constructor;
       this_class-> dispose  = &some_object_dispose;
       this_class-> finalize  = &some_object_finalize;

       this_class-> set_property= &some_object_set_property;
       this_class-> get_property  = &some_object_get_property;

            spec = g_param_spec_int
            (
                    "property-a",                           /* 属性名称 */
                    "a",                                    /* 属性昵称 */
                    "Mysterty value 1",                     /* 属性描述 */
                    5,                                      /* 属性最大值 */
                    10,                                     /* 属性最小值 */
                    5,                                      /* 属性默认值 */
                    G_PARAM_READABLE |G_PARAM_WRITABLE      /* GParamSpecFlags */
            );
    g_object_class_install_property(this_class,OBJECT_PROPERTY_A, spec);

            spec = g_param_spec_float
            (
                    "property-b",                           /* 属性名称 */
                    "b",                                    /* 属性昵称 */
                    "Mysterty value 2"                      /* 属性描述 */
                    0.0,                                    /* 属性最大值 */
                    1.0,                                    /* 属性最小值 */
                    0.5,                                    /* 属性默认值 */
                    G_PARAM_READABLE |G_PARAM_WRITABLE      /* GParamSpecFlags */
            );
    g_object_class_install_property (this_class, OBJECT_PROPERTY_B, spec);
    }
    *滑块验证:
    您需要登录后才可以回帖 登录 | 马上加入

    本版积分规则

    申请友链|Archiver|小黑屋|手机版|GTK+中文社区 ( 粤ICP备13080851号 )

    我要啦免费统计

    GMT+8, 2024-12-22 00:07 , Processed in 0.025286 second(s), 7 queries , Redis On.

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表