NO IMAGE
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]