GObject教程

GObject Tutorial

GObject Tutorial
Ryan McDougall(2004)

目的

這篇文檔可用於兩個目的:一是作爲一篇學習Glib的GObject類型系統的教程,二是用作一篇按步驟的使用GObject類型系統的入門文章。文章從如何用C語言來設計一個面相對想的類型系統開始,使用GObject作爲假設的解決方案。這種介紹的方式可以更好的解釋這個開發庫爲何採用這種形式來設計,以及使用它爲什麼需要這些步驟。入門文章被安排在教程之後,使用了一種按步驟的,實際的,沒有過多解釋的組織形式,這樣對於某些實際的程序員會更有用些。

讀者

這篇教程假想的讀者是那些熟悉面向對象概念,但是剛開始接觸GObject或者GTK+的開發人員。我會認爲您已經瞭解一門面向對象的語言,和一些C語言的基本命令。

動機

使用一種根本不支持面向對象的語言來編寫一個面向對象的系統,這讓人聽上去有些瘋狂。然而我們的確有一些很好的理由來做這樣的事情。 但我不會試着去證明作者決定的正確性,並且我認爲讀者自己就有一些使用GLib的好理由,這裏我將指出這個系統的一些重要特性:

  • C是一門可移植性很強的語言
  • 一個完全動態的系統,新的類型可以在運行時被添加上

這樣系統的可擴展性要遠強於一門標準的語言,所以新的特性也可以被很快的加入進來。
對面嚮對象語言來說,面向對象的特性和能力是用語法來定義的。然而因爲C並不支持面向對象,所以GObject系統必須要手動的將面向對象的能力移植過來。一般來說要實現這個目標需要一些乏味的工作或者偶爾使用某些神奇的手段。我需要做的是枚舉出所有必要的步驟或”咒語”,來使得程序執行起來,也希望能說明這些步驟對您的程序意味着什麼。

1. 創建一個非繼承的對象
設計


在面向對象領域,對象包含兩種類型成員:數據和方法,它們處於同一個對象引用之下。有一種辦法可以使用C來實現對象,那就是C的結構體(struct),這樣普通的公用成員可以是數據,方法則可以被實現爲指向函數的指針。

然而這樣的實現卻存在着一些嚴重的缺陷:彆扭的語法,類型安全問題,缺少封裝。更實際的問題是 – 空間浪費嚴重。每個實例化後的對象需要一個4字節的指針來指向其每一個成員方法,而這些方法在類封裝的範圍內是完全相同的,所以這是完全冗餘的。例如我們有一個類需要有4個成員方法,一個程序實例化了1000個這個類的對象,這樣我們就浪費了接近16KB的空間。

很明顯我們只保留一張包含這些指針的表,以供從這個類實例化出來的對象調用要好的多,這樣會節省下不少內存資源。
這樣的表被稱作虛方法表(vtable),GObject系統爲每個類在內存中保存了一份。當你想調用一個虛方法時,你必須先向系統請求查找這個對象所對應的虛方法表,這張表如上所述只包含了一個由函數指針組成的結構體。這樣你就能復引用這個指針,通過它來調用方法了。

我們稱這兩種類型爲“對象結構體”和“類結構體”,並且將這兩種結構體的實例分別稱爲“對象實例”和“類實例“。這兩種結構體合併在一起形成的一個概念上的單元,稱作“類”,對這個“類”的實例稱作“對象”。
爲什麼將這樣的函數稱作“虛函數”的原因是調用它需要在運行時查找合適的函數指針,這樣就能允許繼承自它的類覆蓋這個方法(只需要簡單的更改虛函數表中的函數指針指向相應函數入口即可)。這樣子類在向上轉型(upcast)爲父類時就會正常工作,正如我們所知道的C++裏的虛方法一樣。

儘管這樣做可以節省內存和實現虛方法,但從語法上來看,將成員方法與對象用“點操作符”關聯起來的能力就不具備了。(譯者:因爲點操作符關聯的將是struct裏的方法,而不是vtable裏的)。因此我們將使用如下的命名約定來聲明類的成員方法:
NAMESPACE_TYPE_METHOD (OBJECT*, PARAMETERS)

非虛方法將被實現在一個普通的C函數裏,虛方法也是實現在普通的C函數中,但不同的是這個函數將調用虛函數表中某個合適的方法。私有成員將被實現成只存活在源文件中,而不被導出聲明在頭文件中。

面向對象通常使用信息隱藏來作爲封裝的一部分,但在C中卻沒有簡單的辦法能隱藏私有成員。一種辦法是將私有成員放到一個獨立的結構體中,該結構體只定義在源文件中,再向你的公有對象結構體中添加一個指向這個私有類的指針。然而,在開放源代碼的世界裏,這種保護對於用戶執意要做這件錯誤的事情是微不足道的。大部分的開發者也只是簡單的寫上幾句註釋,標明這些成員他們希望被保護爲私有的,並且希望用戶能尊重這種區別。
到現在爲止我們有了兩種不同的結構體,但我們沒有一個簡單的辦法來通過一個實例化後的對象找到合適的虛方法表。如我們在上面暗指到的,這應該是系統的職責,系統只需要要求我們向它註冊上新聲明的類型,就應該能夠處理這個問題。系統同時要求我們去向它註冊(對象的和類的)結構體構造和析構函數(以及其他的重要信息),這樣系統才能正確的實例化我們的對象。系統會通過枚舉化所有的向它註冊的類型來記錄新的對象類型,並且要求所有實例化對象的第一個成員是一個指向它自己類的虛函數表的指針,每個虛函數表的第一個成員是它在系統中保存的枚舉類型的數字表示。
類型系統要求所有類型的對象結構體和類結構體的第一個成員是一個特殊結構體。在對象結構體中,該特殊結構體是一個指向其類型的對象。因爲C語言保證在結構體中聲明的第一個成員也是在內存中保存的第一個數據,因此這個類對象可以很容易的通過將這個對象結構體轉型而獲得到。又因爲類型系統要求我們將被繼承的父結構體指針聲明爲子結構體的第一個成員,這樣我們只需要在父類中聲明一次這個特殊的結構體(譯者:即那個指向其類型的對象),我們總是能夠通過一次轉型而找到虛函數表。
最後我們需要一些函數來定義如何管理對象的生命期:創建類對象的函數,創建實例對象的函數,銷燬類對象的函數。但不需要銷燬實例對象的函數,因爲實例對象的內存管理是一個比較複雜的問題,我們將把這個工作留給更高層的代碼來處理。

代碼(頭文件)

a. 使用struct關鍵字來創建實例對象和類對象,實現“C風格”的對象
我們向結構體名字前添加了一個下劃線,然後又增加了一個前置的類型定義typedef,用來給我們的結構體一個合適的名字。這是因爲C的語法不允許你聲明SomeObject指針在SomeObject中(這對聲明鏈表之類的數據結構很有用)。向上面的約定一節所描述的一樣,我們還可以創建一個命名域,稱其爲“Some“。

[c]
/* 我們的“實例結構體”定義了所有的數據域,這使得對象將是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GTypeInstance gtype;

gint m_a;
gchar* m_b;
gfloat m_c;
};

/* 我們的“類結構體”定義了所有的方法函數,這是被實例化出來的對象所共享的 */
typedef struct _SomeObjectClass SomeObjectClass;
struct _SomeObjectClass
{
GTypeClass gtypeclass;

void (*method1) (SomeObject *self, gint);
void (*method2) (SomeObject *self, gchar*);
};
[/c]

b. 聲明一個函數,該函數可以在第一次被調用時向系統註冊上對象的類型,在此後的調用時就會返回系統記錄下的我們聲明的那個類型所對應的唯一數字了。這個函數被成爲”get_type”,返回值是”GType”類型,該類型實際上是一個系統用來區別已註冊類型的整型數字。由於這個函數是SomeObject類型在設計和定義時專有的,我們替它在函數前加上“some_object_”。

[c]
/* 這個方法將返回我們新聲明的對象類型所關聯的GType類型 */
GType some_object_get_type (void);
[/c]

c. 聲明管理我們對象生命期的函數:初始化時創建對象的函數,結束時銷燬對象的函數。

[c]
/* 類/實例的初始化/銷燬函數。它們的標記在gtype.h中定義。 */
void some_object_class_init (gpointer g_class, gpointer class_data);
void some_object_class_final (gpointer g_class, gpointer class_data);
void some_object_instance_init (GTypeInstance *instance, gpointer g_class);
[/c]

d. 用C函數的通用約定來定義我們的類方法。

[c]
/* 所有這些函數都是SomeObject的方法. */
void some_object_method1 (SomeObject *self, gint); /* virtual */
void some_object_method2 (SomeObject *self, gchar*); /* virtual */
void some_object_method3 (SomeObject *self, gfloat); /* non-virtual */
[/c]

e. 創建一些樣板式代碼(boiler-plate code),來符合規範,讓生活更簡單。

[c]
/* 好用的宏定義 */
#define SOME_OBJECT_TYPE (some_object_get_type ())
#define SOME_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOME_OBJECT_TYPE, SomeObject))
#define SOME_OBJECT_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SOME_OBJECT_TYPE, SomeObjectClass))
#define SOME_IS_OBJECT(obj) (G_TYPE_CHECK_TYPE ((obj), SOME_OBJECT_TYPE))
#define SOME_IS_OBJECT_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SOME_OBJECT_TYPE))
#define SOME_OBJECT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOME_OBJECT_TYPE, SomeObjectClass))
[/c]

Code(源程序)

現在我們可以繼續實現我們剛剛聲明過的源文件了。
由於虛函數現在只是一些函數指針,我們還要創建一些正常的、保存在內存中的、可以尋址到的C函數(聲明爲以”_impl”結尾的,並且不在頭文件中導出的),在虛函數中將指向這些函數。
以”some_object_”開頭的函數都是對應於SomeObject的定義的,這通常是因爲我們會顯式的將不同的指針轉型到SomeObject,或者會使用類的其它特性。(譯者:not very clear)
a. 實現虛方法。

[c]
/* 虛函數的實現 */
void some_object_method1_impl (SomeObject *self, gint a)
{
self->m_a = a;
g_print ("Method1: %i\n", self->m_a);
}

void some_object_method2_impl (SomeObject *self, gchar* b)
{
self->m_b = b;
g_print ("Method2: %s\n", self->m_b);
}
[/c]

b. 實現所有公有方法。實現虛方法時,我們必須使用“GET_CLASS”宏來從類型系統中獲取到類對象,用以調用虛函數表中的虛方法。非虛方法時,直接寫實現代碼即可。

[c]
/* 公有方法 */
void some_object_method1 (SomeObject *self, gint a)
{
SOME_OBJECT_GET_CLASS (self)->method1 (self, a);
}

void some_object_method2 (SomeObject *self, gchar* b)
{
SOME_OBJECT_GET_CLASS (self)->method2 (self, b);
}

void some_object_method3 (SomeObject *self, gfloat c)
{
self->m_c = c;
g_print ("Method3: %f\n", self->m_c);
}
[/c]

c. 實現構造/析構方法。系統給我們的是泛型指針(我們也相信這個指針的確指向的是一個合適的對象),所以我們在使用它之前必須將其轉型爲合適的類型。

[c]
/* 該函數將在類對象創建時被調用 */
void some_object_class_init (gpointer g_class, gpointer class_data)
{
SomeObjectClass *this_class = SOME_OBJECT_CLASS (g_class);

/* fill in the class struct members (in this case just a vtable) */
this_class->method1 = &some_object_method1_impl;
this_class->method2 = &some_object_method2_impl;
}

/* 該函數在類對象不再被使用時調用 */
void some_object_class_final (gpointer g_class, gpointer class_data)
{
/* No class finalization needed since the class object holds no
pointers or references to any dynamic resources which would need
to be released when the class object is no longer in use. */
}

/* 該函數在實例對象被創建時調用。系統通過g_class實例的類來傳遞該實例的類。 */
void some_object_instance_init (GTypeInstance *instance, gpointer g_class)
{
SomeObject *this_object = SOME_OBJECT (instance);

/* fill in the instance struct members */
this_object->m_a = 42;
this_object->m_b = 3.14;
this_object->m_c = NULL;
}
[/c]

d. 實現能夠返回給調用者SomeObject的GType的函數。該函數在第一次運行時,它通過向系統註冊SomeObject來獲取到GType。該GType將被保存在一個靜態變量中,以後該函數再被調用時就無須註冊可以直接返回該數值了。雖然使用一個獨立的函數來註冊該類型時可能的,但這樣的實現可以保證類在使用前是被註冊了的,該函數通常在第一個對象被實例化的時候調用。

[c]
/* 因爲該類沒有基類,所以基類構造/析構函數是空的 */
GType some_object_get_type (void)
{
static GType type = 0;

if (type == 0)
{
/* 這是系統用來完整描述類型時如何被創建,構造和析構的結構體。 */
static const GTypeInfo type_info =
{
sizeof (SomeObjectClass),
NULL, /* 基類構造函數 */
NULL, /* 基類析構函數 */
some_object_class_init, /* 類對象構造函數 */
some_object_class_final, /* 類對象析構函數 */
NULL, /* 類數據 */
sizeof (SomeObject),
0, /* 預分配的字節數 */
some_object_instance_init /* 實例對象構造函數 */
};

/* 因爲我們的類沒有父類,所以它將被認爲是“基礎類(fundamental)”,
所以我們必須要告訴系統,我們的類既是一個複合結構的類(與浮點型,整型,
或者指針不同),並且時可以被實例化的(系統可以創建實例對象,例如接口
或者抽象類不能被實例化 */
static const GTypeFundamentalInfo fundamental_info =
{
G_TYPE_FLAG_CLASSED | G_TYPE_FLAG_INSTANTIATABLE
};

type = g_type_register_fundamental
(
g_type_fundamental_next (), /* 下一個可用的GType數 */
"SomeObjectType", /* 類型的名稱 */
&type_info, /* 上面定義的type_info */
&fundamental_info, /* 上面定義的fundamental_info */
0 /* 類型不是抽象的 */
);
}

return type;
}

/* 讓我們來編寫一個測試用例吧! */

int main()
{
SomeObject *testobj = NULL;

/* 類型系統初始化 */
g_type_init ();

/* 讓系統創建實例對象 */
testobj = SOME_OBJECT (g_type_create_instance (some_object_get_type()));

/* 調用我們定義了的方法 */
if (testobj)
{
g_print ("%d\n", testobj->m_a);
some_object_method1 (testobj, 32);

g_print ("%s\n", testobj->m_b);
some_object_method2 (testobj, "New string.");

g_print ("%f\n", testobj->m_c);
some_object_method3 (testobj, 6.9);
}

return 0;
}
[/c]

最後需要考慮的

我們已經用C實現了第一個對象,但是做了很多工作,並且這並不是真正的面向對象,因爲我們故意沒有提及任何關於“繼承”的方法。在下一節我們將看到如何讓工作更加輕鬆,利用別人的代碼-使SomeObject繼承與內建的類GObject。
儘管在下文中我們將重用上面討論的思想和模型,但是嘗試去創建一個基礎類型,使得它能像其它的GTK+代碼一樣的工作是非常困難和深入的。因此建議您總是繼承GObject來創建新的類型,因爲它幫您做了大量背後的工作,使得您的類型能工作的與GTK+要求的保持一致。

二、使用內建的宏定義來自動生成代碼
設計


您可能已經注意到了,我們上面所做的大部分工作基本上都是機械性的、模板化的工作。大多數的函數都不是通用的,每創建一次類型我們就需要重寫一遍。很顯然這就是爲什麼我們發明了計算機的原因 - 讓這些工作自動化,讓我們的生活更簡單!
好的,其實我們很幸運,因爲C的預處理器將允許我們編寫宏定義來定義新的類型,這樣在編譯時這些宏定義會自動展開成爲合適的C代碼。而且使用宏定義還能幫助我們減少人爲錯誤。
然而自動化將使我們丟失部分靈活性。在上面描述的步驟中,我們能有許多可能的變化,但一個宏定義只能實現一種展開。如果這個宏提供了一種輕量級的展開,但我們想要的是一個完整的類型,這樣我們仍然需要手寫一大堆代碼。如果宏提供了一個完整的展開,但我們需要的是一種輕量級的類型,我們將得到許多冗餘的代碼,花許多時間來填寫這些我們用不上的樁代碼,或者只是一些普通的錯誤代碼。事實上C預處理器並沒有設計成能夠自動發現我們感興趣的代碼生成方式,它只包含有限的功能。

代碼

創建一個新類型的代碼非常簡單:G_DEFINE_TYPE_EXTENDED (TypeName, function_prefix, PARENT_TYPE, GTypeFlags, CODE)。
第一個參數是類型的名稱。第二個是函數名稱的前綴,這樣能夠與我們的命名規則保持一致。第三個是我們希望繼承自的基類的GType。第四個是添加到GTypeInfo結構體裏的GTypeFlag。第五個是在類型被註冊後應該立刻被執行的代碼。
看看下面的代碼將被展開成爲什麼樣將會對我們有更多的啓發。

[c]
G_DEFINE_TYPE_EXTENDED (SomeObject, some_object, 0, some_function())
[/c]

實際展開後的代碼將隨着系統版本不同而不同。你應該總是檢查一下展開後的結果而不是憑主觀臆斷。
展開後的代碼(清理了空格):

[c]
static void some_object_init (SomeObject *self);
static void some_object_class_init (SomeObjectClass *klass);
static gpointer some_object_parent_class = ((void *)0);

static void some_object_class_intern_init (gpointer klass)
{
some_object_parent_class = g_type_class_peek_parent (klass);
some_object_class_init ((SomeObjectClass*) klass);
}

GType some_object_get_type (void)
{
static GType g_define_type_id = 0;
if ((g_define_type_id == 0))
{
static const GTypeInfo g_define_type_info =
{
sizeof (SomeObjectClass),
(GBaseInitFunc) ((void *)0),
(GBaseFinalizeFunc) ((void *)0),
(GClassInitFunc) some_object_class_intern_init,
(GClassFinalizeFunc) ((void *)0),
((void *)0),
sizeof (SomeObject),
0,
(GInstanceInitFunc) some_object_init,
};

g_define_type_id = g_type_register_static
(
G_TYPE_OBJECT,
"SomeObject",
&g_define_type_info,
(GTypeFlags) 0
);

{ some_function(); }
}

return g_define_type_id;
}
[/c]

該宏定義了一個靜態變量“
_parent_class”,它是一個指針,指向我們打算創建對象的基類。這在你想去找到虛方法繼承自哪裏時派上用場,並且這個基類不是由GObject繼承下來的基類(譯者:not very clear),主要用於鏈式觸發析構函數,這些函數也幾乎總是虛的。我們接下來的代碼將不再使用這個結構,因爲有其它的函數能夠不使用靜態變量來做到這一點。
你應該注意到了,這個宏沒有生成基類的構造析構以及類對象析構函數,如果你需要這些函數,就要自己動手了。

3. 創建一個繼承自GObject的對象
設計

儘管我們現在能夠生成一個基本的對象,但事實上我們故意略過了類型系統的上下文:作爲一個複雜庫套件的基礎 - 那就是圖形庫GTK+。GTK+的設計要求所有的類應該繼承自一個根類。這樣就至少能允許一些公共的基礎功能能夠被共享:如支持信號(讓消息可以很容易的從一個對象傳遞到另一個),通過引用計數來管理對象生命期,支持屬性(針對對象的數據域生成簡單的setting和getting函數),支持構造和析構函數(用來設置信號、引用計數器、屬性)。當我們讓對象繼承自GObject時,我們獲得了上述的一切,並且當與其它基於GObject的庫交互時會更加容易。然而,在這章我們不討論信號、引用計數和屬性,或者任何其它專門的特性,這裏我們將集中描述繼承是在類型系統中如何工作的。
我們都知道,如果LuxuryCar繼承自Car,那麼LuxuryCar就是Car加上一些新的特性。那我們要如何讓系統去實現這樣的功能呢?我們可以使用C語言裏結構體的一個特性來實現:結構體定義裏的第一個成員一定是在內存的最前面。如果我們要求所有的對象將它們的基類聲明爲它們自己結構體的第一個成員的話,那麼我們就能迅速的將指向某個對象的指針轉型爲指向它基類的指針!儘管這個技巧很好用,並且語法上非常乾淨,但這種轉型的方式只適用於指針 - 你不能這樣轉型一個普通的結構體。
這種轉型技巧是類型不安全的。雖然把一個對象轉型爲它的基類對象是完全合法的,但實際上非常的不明智(譯者:not very clear)。這取決於程序員來保障他的轉型是安全的。

創建類型的實例

瞭解了這個技術後,究竟類型系統是如何實例化對象的呢?第一次我們使用g_type_create_instance讓系統創建一個實例對象時,它必須要先創建一個類對象供實例來使用。如果該類結構體繼承自其它類,系統則需要先創建和初始化這些基類。系統依靠我們指定的結構體(*_get_type函數中的GTypeInfo結構體),來完成這個工作,這個結構體描述了每個對象的實例大小,類大小,構造函數和析構函數。
- 要用g_type_create_instance來實例化一個對象
如果它沒有相關聯的類對象
創建它並且將其加入到類的層次中
創建實例對象並且返回指向它的指針

當系統創建一個新的類對象時,它先會分配足夠的內存來放置這個最終的類對象(譯者:“最終的”意指這個新的類對象,相對於其繼承的基類們)。然後從最頂端的基類開始到最末端的子類對象,內存級別的用基類的成員域覆寫掉這個最終類對象的成員域。這就是子類如何繼承自基類的。當把基類的數據複製完後,系統將會在當前狀態的類對象中執行基類的“基類初始化“函數。這個覆寫和執行“基類初始化”的工作將循環多次,直到這個繼承鏈上的每個基類都被處理過後才結束。接下來系統將在這個最終的類對象上執行最終子類的“基類初始化”和“類初始化”函數。函數“類初始化”有一個參數,該參數可以被認爲是類對象構造函數的參數,即上文所提到的“類數據”。
細心的讀者可能會問,爲什麼我們已經有了一個完整的基類對象的拷貝還需要它的基類初始化函數?因爲當完整拷貝無法爲每個類重新創建出某些數據時,我們就需要基類初始化函數。例如,一個類成員可能指向另外一個對象,並且我們想要每個類對象的成員都指向它自己的對象(內存的拷貝只是“淺拷貝”,我們也許需要一次“深拷貝”)。有經驗的GObject程序員告訴我基類初始化函數其實在實際中很少用到。
當系統創建一個新的實例對象時,它會先分配足夠的內存來將這個實例對象放進去。在從最頂端的基類開始調用這個基類的“實例初始化”函數在當前的狀態下,直到最終的子類。最後,系統在最終類對象上調用最終子類的“實例初始化”函數。
我來總結一下上面所描述到的算法:
- 實例化一個類對象
爲最終對象分配內存
從基類到子類開始循環
複製對象內容以覆蓋掉最終對象的內容
在最終對象上運行對象自己的基類初始化函數
在最終對象上運行最終對象的基類初始化函數
在最終對象上運行最終對象的類初始化函數(附帶上類數據)

- 實例化一個實例對象
爲最終對象分配內存
從基類刀子類開始循環
在最終對象上運行實例初始化函數
在最終對象上運行最終對象的實例初始化函數
此時初始化了的類對象和實例對象都已經被創建,系統將實例對象的類指針指向到類對象,這樣實例對象就能找到類對象所包含的虛函數表。這就是系統如何實例化已註冊類型的過程;其實GObject實現了自己的構造和析構語義正如我們上面所描述的那樣!

創建GObject實例

前面我們使用g_type_create_instance來創建一個實例對象。然而事實上GObject給我們提供了一個新的API來創建gobject,在上面我們討論的所有問題之上。GObject實現了三個新方法來被這個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類型,一系列屬性名(就是C的字符串)和GValue對作爲參數。
這一系列屬性被轉換爲鍵值對列表,以及相關的屬性規格,這些屬性規格將被在類初始化函數裏被安裝到系統中。定義在類對象中的構造函數將在被調用時傳入GType和構造屬性。從最底端的子類構造函數到最頂端的基類構造函數,這條鏈會一直觸發直到GObject的構造函數被執行 - 這實際上纔是第一個真正執行的初始化程序。GObject的構造函數現調用g_type_create_instance,並傳下我們通過g_object_new一路帶上的GType,這樣我們上面所描述的細節將會發生,最終創建出實例。然後它獲得最終對象的類,對從構造函數傳入的所有構造屬性調用set_property方法。這就是爲什麼我們加入一個新屬性時必須要覆蓋get_/set_property方法的原因。當這一串構造函數返回後,包含在其中的代碼將從基類執行到子類。
當基類構造函數返回後,就輪到子類來執行它自己的初始化代碼的。這樣執行代碼的順序就成爲:
a. 從GObject到ChildObject運行實例初始化函數
b. 從GObject到ChildObject運行構造函數
任何剩餘的沒有傳遞到構造函數的屬性將使用set_property方法在最後一次設置。
讀者也許會猜想在什麼情況下需要覆蓋默認構造函數,將自己的代碼放到他們自己的構造函數裏?因爲我們所有的屬性都可以使用虛方法set_property來設置,所以基本上沒有覆蓋GObject的默認構造函數的必要。
我仍嘗試使用僞碼總結一下GObject的構造函數過程:
- 基於提供的屬性鍵值對的列表創建合適的GObject對象:
在鍵值對列表中查找對應的規格
調用最終對象的構造函數並傳入規格列表和類型
遞歸的向下調用GObject的構造函數
調用g_type_create_instance,並傳入類型
調用虛方法set_property,傳入規格列表
調用set_property,傳入剩下的屬性
GObject將屬性區分爲兩類,構造和“常規”。

銷燬GObject實例

當該做的工作完成後,我們可以看看不需要這個對象時會發生些什麼。然而面向對象中析構的概念在GObject實現時被分解爲了兩步:處理和銷燬。
“處理”方法在對象知道自己將要被銷燬時調用。在該方法中,指向一些資源的引用應該被釋放,這樣可以避免造成循環引用或者資源稀缺。“處理”方法可以被調用任意次,因此該方法應該能夠安全的處理多次調用。一般常見的做法是使用一個靜態變量來保護”處理“方法。在“處理”方法調用後,對象本身應該依然能夠使用,除非產生了不可恢復的錯誤(如段錯誤),所以,“處理”方法不允許釋放或者改動某些對象成員。可恢復的錯誤,例如返回錯誤碼或者空指針則不應該有影響。
“銷燬”方法會在對象自己被從內存中清理掉之前釋放剩餘的資源引用,因此它只能被調用一次。這種分成兩個步驟的過程降低了引用計數策略中循環引用發生的可能。
如果我們自定義“處理”和“銷燬”方法,就必須要覆蓋掉默認的從基類繼承下來的相同方法。這兩個方法從子類開始調用,沿着繼承鏈向上知道最頂端的基類。
與構造函數不同的是,只要新的對象分配了資源,我們就需要自己實現“處理”和“銷燬”方法,來覆蓋掉繼承自基類的相同方法。
知道某些銷燬代碼放置到哪裏比較合適其實不是一件容易的事。然而,當與引用計數的庫(如GTK+)打交道時,我們應該在“處理”方法中解除對其它資源對象的引用,而在“銷燬”方法中釋放掉所有的內存或者關閉所有的文件描述字。
上面我們討論過g_object_new,但是我們什麼時候來銷燬這些對象呢?其實上面也有提示過,GObject使用了引用計數的技術,也就是說它爲有多少個其它對象或函數現在正在“使用”或者引用這個對象保存了一個整型的數據。當你在使用GObject時,如果你向保護你的對象不在使用時被銷燬掉,你必須及早調用g_object_ref,將對象作爲參數傳遞給它。這樣就爲引用計數器增加了1。如果你沒有做這件事就意味着你允許對象被自動銷燬,這也許會導致你的程序崩潰。
同樣的,當對象完成了它的任務後,你必須要調用g_object_unref。這樣會使引用計數器減少1,並且系統會檢查它是否爲0.當計數器爲0時,對象將被先調用“處理”方法,最終被“銷燬”掉。如果你沒有解除到該對象的引用,則會導致內存泄漏,因爲計數器永遠不會回到0。
現在我們已經準備好了來寫一些代碼了!但是不要讓上面冗長和複雜的描述嚇唬到您。如果你沒有完全理解上面所提到的,別緊張 - GObject的確是很複雜的!繼續讀下去,你會看到許多細節,試試一些例子程序,或者去睡覺吧,明天再來接着讀。
下面的程序與第一個例子很相似,事實上我去掉了更多的不合邏輯的、冗餘的代碼。

代碼(頭文件)

1. 我們仍然按照上面的方式繼續,但是這次將把基類對象放到結構體的第一個成員位置上。事實上就是GObject。

[c]
/* 我們的“實例結構體”定義了所有的數據域,這使得對象將是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GObject parent_obj;

/* 下面應該是一些數據 */
};

/* 我們的“類結構體”定義了所有的方法函數,這是被實例化出來的對象所共享的 */
typedef struct _SomeObjectClass SomeObjectClass;
struct _SomeObjectClass
{
GTypeClass parent_class;

/* 下面應該是一些方法 */
};
[/c]

2. 頭文件剩下的部分與第一個例子基本相同。

代碼(源文件)

我們需要增加一些對被覆蓋的GObject方法的聲明

[c]
/* 這些是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()));

some_object_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);
}
[/c]

GObjectConstructParam是一個有兩個成員的結構體,一個是GParamSpec類型,就是對參數的一組描述,另外一個是GValue類型,就是一組對應的值。

[c]
/* 這是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;
}
[/c]

要想討論關於創建和銷燬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,它保存了對象的gype,對象的屬性名稱,對象枚舉ID,系統需要這樣一個能把所有東西都粘在一起的大膠水。
當我們需要設置或者獲取一個屬性的值時,調用g_object_set/get_property,需要指定屬性的名字,並且帶上GValue用來保存我們要設置的值。g_object_set_property函數將在GParamSpec中查找我們要設置的屬性名稱,查找我們對象的類,並且調用對象的set_property方法。這意味着如果我們要增加一個新的屬性,我們必須覆蓋默認的set/get_property方法。而且基類包含的屬性將被它自己的set/get_property方法所正常處理,因爲GParamSpec就是從基類傳遞下來的。最後,我們必須通過事先通過對象的class_init方法來加入一個GParamSpec參數!
假設我們已經有了如上一節所描述的那樣一個可用的框架,那麼現在讓我們來爲SomeObject加入處理屬性的代碼!

代碼(頭文件)

1. 除了我們增加了兩個屬性外,其餘同上面的一樣

[c]
/* 我們的“實例結構體”定義了所有的數據域,這使得對象將是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GObject parent_obj;

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

/* 下面應該是一些數據 */
};
[/c]

代碼(源文件)

1. 創建一個枚舉類型用來內部記錄屬性。

[c]
enum
{
OBJECT_PROPERTY_A = 1 << 1;
OBJECT_PROPERTY_B = 1 << 2;
};
[/c]

2. 實現新增的處理屬性的函數。

[c]
void some_object_set_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_get_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]

3. 覆蓋繼承自基類的set/get_property方法,並且傳入GParamSpecs。

[c]
/* 這裏是我們覆蓋函數的地方 */
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);
}
[/c]


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章