1. 程式人生 > >GObject學習教程---第十二章:GObject 訊號機制——訊號註冊

GObject學習教程---第十二章:GObject 訊號機制——訊號註冊

本文是學習學習他人的部落格的心得(具體詳見“樓主見解”),如果源網站可訪問的話,建議直接訪問源網站:

樓主見解:

主要講解訊號的流程,不想了解流程,不看也罷。

主要講解g_signal_new函式的使用,以及具體引數的意義

原型:

guint g_signal_new (const gchar        *signal_name,
                    GType               itype,
                    GSignalFlags        signal_flags,
                    guint               class_offset,
                    GSignalAccumulator  accumulator,
                    gpointer            accu_data,
                    GSignalCMarshaller  c_marshaller,
                    GType               return_type,
                    guint               n_params,
                    ...);

’呼叫:

  g_signal_new ("hello",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (SignalDemoClass, default_handler),
                      NULL,
                      NULL,
                      g_cclosure_marshal_VOID__STRING,
                      G_TYPE_NONE,
                      1, 
                      G_TYPE_STRING);

說明:

  • 第 1 個引數是字串“hello”,它表示訊號。
  • 第 2 個引數是 SignalDemo 類的型別 ID,可以使用 G_TYPE_FROM_CLASS 巨集從 SignalDemoClass 結構體中獲取,也可直接使用 signal-demo.h 中定義的巨集 SIGNAL_TYPE_DEMO。
  • 第 3 個引數可暫時略過。
  • 第 4 個引數比較關鍵,它是一個記憶體偏移量,主要用於從 SignalDemoClass 結構體中找到 default_handler 指標的位置,可以使用 G_STRUCT_OFFSET 巨集來獲取,也可以直接根據 signal-demo.h 中的 SignalDemoClass 結構體的定義,使用 sizeof (GObjectClass) 來得到記憶體偏移量,因為 default_handler 指標之前只有一個 GObjectClass 結構體成員。
  • 第 5 個和第 6 個引數暫時略過。
  • 第 7 個引數設定閉包的  marshal。在文件“函式指標、回撥函式與 GObject 閉包” 中,描述了 GObject 的閉包的概念與結構,我們可以將它視為回撥函式 + 上下文環境而構成的一種資料結構,或者再簡單一點,將其視為回撥函式。另外,在那篇文件中,我們也對 marshal 的概念進行了一些粗淺的解釋。事實上 marshal 主要是用來“翻譯”閉包的引數和返回值型別的,它將翻譯的結果傳遞給閉包。之所以不直接呼叫閉包,而是在其外加了一層 marshal 的包裝,主要是方便 GObject 庫與其他語言的繫結。例如,我們可以寫一個 pyg_closure_marshal_VOID__STRING 函式,其中可以呼叫 python 語言編寫的“閉包”並將其計算結果傳遞給 GValue 容器,然後再從 GValue 容器中提取計算結果。
  • 第 8 個引數指定 marshal 函式的返回值型別。由於本例的第 7 個引數所指定的 marshal 是 g_cclosure_marshal_VOID__STRING 函式的返回值是 void,而 void 型別在 GObject 庫的型別管理系統是 G_TYPE_NONE 型別。
  • 第 9 個引數指定 g_signal_new 函式向 marshal 函式傳遞的引數個數,由於本例使用的 marshal 函式是 g_cclosure_marshal_VOID__STRING 函式,g_signal_new 函式只向其傳遞 1 個引數。
  • 第 10 個引數是可變引數,其數量由第 8 個引數決定,用於指定 g_signal_new 函式向 marshal 函式傳遞的引數型別。由於本例使用的 marshal 函式是 g_cclosure_marshal_VOID__STRING 函式,並且 g_signal_new 函式只向其傳遞一個引數,所以傳入的引數型別為 G_TYPE_STRING(GObject 庫型別管理系統中的字串型別)。

GObject 訊號機制——訊號註冊

上一篇文件“GObject 的訊號機制”只是挖了一個坑便結束了,本篇嘗試填坑,不過也不敢有所保證。因為我也不確定會不會因為被 GObject 的訊號內幕再次搞暈。

我們先老老實實的閱讀 GObject 參考手冊的“Concepts / Signal”部分,儘量多獲得一些面上的認識。手冊中最關鍵的一句話是:每一個訊號在註冊的時候都要與某種資料型別(包括 GObject 庫中的內建型別或 GObject 子類型別)存在關聯,這種資料型別的使用者需要實現訊號與閉包的連線,這樣在訊號被髮射時,閉包會被呼叫。這句話,意味著我們要使用 GObject 訊號機制,那麼就必須要完成兩個步驟:第一個步驟是訊號註冊,主要解決訊號與資料型別的關聯問題;第二個步驟是訊號連線,主要處理訊號與閉包的連線問題。本文主要考察訊號註冊的大致過程。

訊號可以與 GObject 庫的型別管理機制中“可例項化”的資料型別進行關聯,但是 GObject 參考手冊建議我們最好是隻在 GObject 子類型別中使用訊號,因為訊號跟類/物件在邏輯上比較相符。

有 三個函式可以實現訊號註冊,即 g_signal_newv、g_signal_new_valist 以及 g_signal_new,其中 g_signal_new_valist 與 g_signal_new 函式皆基於 g_signal_newv 函式實現,但是 g_signal_new 函式的名字看上去最平易近人。我們不理睬它們內部是如何實現的,只需要理解它們所接受的引數的含義即可。所以,我們可以從分析 g_signal_new 函式的引數來理解有關訊號註冊的一些概念。

g_signal_new 函式的宣告如下:

1

2

3

4

5

6

7

8

9

10

guint g_signal_new (const gchar        *signal_name,

GType               itype,

GSignalFlags        signal_flags,

guint               class_offset,

GSignalAccumulator  accumulator,

gpointer            accu_data,

GSignalCMarshaller  c_marshaller,

GType               return_type,

guint               n_params,

...);

g_signal_new 函式的引數較多,其中每個引數多少都有點深不可測的背景,所以直接理解是非常困難的。我們需要構建例項,從而獲得最直觀的理解。

首先,我們定義一個 GObject 的子類——SignalDemo 類,其標頭檔案 signal-demo.h 內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

#ifndef SIGNAL_DEMO_H

#define SIGNAL_DEMO_H

#include <glib-object.h>

#define SIGNAL_TYPE_DEMO (signal_demo_get_type ())

#define SIGNAL_DEMO(object) \

G_TYPE_CHECK_INSTANCE_CAST ((object), SIGNAL_TYPE_DEMO, SignalDemo)

#define SIGNAL_IS_DEMO(object) \

G_TYPE_CHECK_INSTANCE_TYPE ((object), SIGNAL_TYPE_DEMO))

#define SIGNAL_DEMO_CLASS(klass) \

(G_TYPE_CHECK_CLASS_CAST ((klass), SIGNAL_TYPE_DEMO, SignalDemoClass))

#define SIGNAL_IS_DEMO_CLASS(klass) \

(G_TYPE_CHECK_CLASS_TYPE ((klass), SIGNAL_TYPE_DEMO))

#define SIGNAL_DEMO_GET_CLASS(object) (\

G_TYPE_INSTANCE_GET_CLASS ((object), SIGNAL_TYPE_DEMO, SignalDemoClass))

typedef struct _SignalDemo SignalDemo;

struct _SignalDemo {

GObject parent;

};

typedef struct _SignalDemoClass SignalDemoClass;

struct _SignalDemoClass {

GObjectClass parent_class;

void (*default_handler) (gpointer instance, const gchar *buffer, gpointer userdata);

};

GType signal_demo_get_type (void);

#endif

SignalDemo 類的原始檔 signal-demo.c 內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#include "signal-demo.h"

G_DEFINE_TYPE (SignalDemo, signal_demo, G_TYPE_OBJECT);

static void

signal_demo_default_handler (gpointer instance, const gchar *buffer, gpointer userdata)

{

g_printf ("Default handler said: %s\n", buffer);

}

void

signal_demo_init (SignalDemo *self)

{

}

void

signal_demo_class_init (SignalDemoClass *klass)

{

klass->default_handler = signal_demo_default_handler;

}

基於此前所寫的 GObject 學習筆記系列,上述程式碼不難理解,無非就是定義了一個 SignalDemo 類,其類結構體中包含了一個函式指標 default_handler,並在類結構體初始化函式中使該指標指向函式 signal_demo_default_handler。

下面我們開始為 SignalDemo 類註冊一個“hello”訊號,只需修改一下 SignalDemo 類的類結構體初始化函式,即:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

void

signal_demo_class_init (SignalDemoClass *klass)

{

klass->default_handler = signal_demo_default_handler;

g_signal_new ("hello",

G_TYPE_FROM_CLASS (klass),

G_SIGNAL_RUN_FIRST,

G_STRUCT_OFFSET (SignalDemoClass, default_handler),

NULL,

NULL,

g_cclosure_marshal_VOID__STRING,

G_TYPE_NONE,

1,

G_TYPE_STRING);

}

此時,觀察一下 g_signal_new 函式的引數:

  • 第 1 個引數是字串“hello”,它表示訊號。
  • 第 2 個引數是 SignalDemo 類的型別 ID,可以使用 G_TYPE_FROM_CLASS 巨集從 SignalDemoClass 結構體中獲取,也可直接使用 signal-demo.h 中定義的巨集 SIGNAL_TYPE_DEMO。
  • 第 3 個引數可暫時略過。
  • 第 4 個引數比較關鍵,它是一個記憶體偏移量,主要用於從 SignalDemoClass 結構體中找到 default_handler 指標的位置,可以使用 G_STRUCT_OFFSET 巨集來獲取,也可以直接根據 signal-demo.h 中的 SignalDemoClass 結構體的定義,使用 sizeof (GObjectClass) 來得到記憶體偏移量,因為 default_handler 指標之前只有一個 GObjectClass 結構體成員。
  • 第 5 個和第 6 個引數暫時略過。
  • 第 7 個引數設定閉包的  marshal。在文件“函式指標、回撥函式與 GObject 閉包” 中,描述了 GObject 的閉包的概念與結構,我們可以將它視為回撥函式 + 上下文環境而構成的一種資料結構,或者再簡單一點,將其視為回撥函式。另外,在那篇文件中,我們也對 marshal 的概念進行了一些粗淺的解釋。事實上 marshal 主要是用來“翻譯”閉包的引數和返回值型別的,它將翻譯的結果傳遞給閉包。之所以不直接呼叫閉包,而是在其外加了一層 marshal 的包裝,主要是方便 GObject 庫與其他語言的繫結。例如,我們可以寫一個 pyg_closure_marshal_VOID__STRING 函式,其中可以呼叫 python 語言編寫的“閉包”並將其計算結果傳遞給 GValue 容器,然後再從 GValue 容器中提取計算結果。
  • 第 8 個引數指定 marshal 函式的返回值型別。由於本例的第 7 個引數所指定的 marshal 是 g_cclosure_marshal_VOID__STRING 函式的返回值是 void,而 void 型別在 GObject 庫的型別管理系統是 G_TYPE_NONE 型別。
  • 第 9 個引數指定 g_signal_new 函式向 marshal 函式傳遞的引數個數,由於本例使用的 marshal 函式是 g_cclosure_marshal_VOID__STRING 函式,g_signal_new 函式只向其傳遞 1 個引數。
  • 第 10 個引數是可變引數,其數量由第 8 個引數決定,用於指定 g_signal_new 函式向 marshal 函式傳遞的引數型別。由於本例使用的 marshal 函式是 g_cclosure_marshal_VOID__STRING 函式,並且 g_signal_new 函式只向其傳遞一個引數,所以傳入的引數型別為 G_TYPE_STRING(GObject 庫型別管理系統中的字串型別)。

注意,在上述的 g_signal_new 函式的第 7 個引數的解釋中,我提到了閉包。事實上,g_signal_new 函式並沒有閉包型別的引數,但是它在內部的確是構建了一個閉包,而且是通過它的第 4 個引數實現的。因為 g_signal_new 函式在其內部呼叫了 g_signal_type_cclosure_new 函式,後者所做的工作就是從一個給定的類結構體中通過記憶體偏移地址獲得回撥函式指標,然後構建閉包返於 g_signal_new 函式。既然 g_signal_new 函式的內部是需要閉包的,那麼它的第 7~10 個引數自然都是為那個閉包做準備的。

需要注意,g_cclosure_marshal_VOID__STRING 所約定的回撥函式型別為:

1

void (*callback) (gpointer instance, const gchar *arg1, gpointer user_data)

這表明 g_cclosure_marshal_VOID__STRING 需要使用者向其回撥函式傳入 3 個引數,其中前兩個引數是回撥函式的必要引數,而第 3 個引數,即 userdata,是為使用者留的“後門”,使用者可以通過這個引數傳入自己所需要的任意資料。由於 GObject 閉包約定了回撥函式的第 1 個引數必須是物件本身,所以 g_cclosure_marshal_VOID__STRING 函式實際上要求使用者向其傳入 2 個引數,但是在本例中 g_signal_new 只向其傳遞了 1 個型別為 G_TYPE_STRING 型別的引數,這有些蹊蹺。

這 是因為 g_signal_new 函式所構建閉包只是讓訊號所關聯的資料型別能夠有一次可以自我表現的機會,即可以在訊號被觸發的時候,能夠自動呼叫該資料型別的某個方法,例如 SignalDemo 類結構體的 default_handler 指標所指向的函式。也就是說,SignalDemo 類自身是沒有必要向閉包傳遞那個“userdata”引數的,只是訊號的使用者有這種需求。這就是 g_signal_new 的引數中只表明它向閉包傳遞了 1 個 G_TYPE_STRING 型別引數的緣故。

上面講的有些凌亂。現在總結一下:g_signal_new 函式內部所構建的閉包,它在被呼叫的時候,肯定是被傳入了 3 個引數,它們被訊號所關聯的閉包分成了以下層次:

  • 第 1 個引數是訊號的預設閉包(訊號註冊階段出現)和訊號使用者提供的閉包(訊號連線階段出現)所必需的,但是這個引數是隱式存在的,由 g_signal_new 暗自向閉包傳遞。
  • 第 2 個引數是顯式的,同時也是訊號的預設閉包和訊號使用者提供的閉包所必須的,這個引數由訊號的發射函式(例如 g_signal_emit_by_name)向閉包傳遞。
  • 第 3 個引數也是顯式的,且只被訊號使用者提供的閉包所關注,這個引數由訊號的連線函式(例如 g_signal_connect)向閉包傳遞。

若要真正明白上述內容,我們必須去構建 SignalDemo 類的使用者,即 main.c 原始檔,內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

#include "signal-demo.h"

static void

my_signal_handler (gpointer *instance, gchar *buffer, gpointer userdata)

{

g_print ("my_signal_handler said: %s\n", buffer);

g_print ("my_signal_handler said: %s\n", (gchar *)userdata);

}

int

main (void)

{

g_type_init ();

gchar *userdata = "This is userdata";

SignalDemo *sd_obj = g_object_new (SIGNAL_TYPE_DEMO, NULL);

/* 訊號連線 */

g_signal_connect (sd_obj, "hello",

G_CALLBACK (my_signal_handler),

userdata);

/* 發射訊號 */

g_signal_emit_by_name (sd_obj,

"hello",

"This is the second param",

G_TYPE_NONE);

return 0;

}

編譯 signal-demo.c 與 main.c:

1

$ gcc signal-demo.c main.c -o test $(pkg-config --cflags --libs gobject-2.0)

程式執行結果如下:

1

2

3

4

$ ./test

Default handler said: This is the second param

my_signal_handler said: This is the second param

my_signal_handler said: This is userdata

結合程式的執行結果,再回顧一下第 1 個例項中的那些亂七八糟的內容,現在應該清晰了許多。

現在,我們再來看一下在第 1 個例項中被我們忽略的 g_signal_new 函式的第  3 個引數,我們將其設為 G_SIGNAL_RUN_FIRST。實際上,這個引數是列舉型別,是訊號預設閉包的呼叫階段的標識,可以是下面 7 種形式中 1 種,也可以是多種組合。

1

2

3

4

5

6

7

8

9

10

typedef enum

{

G_SIGNAL_RUN_FIRST = 1 << 0,

G_SIGNAL_RUN_LAST = 1 << 1,

G_SIGNAL_RUN_CLEANUP = 1 << 2,

G_SIGNAL_NO_RECURSE = 1 << 3,

G_SIGNAL_DETAILED = 1 << 4,

G_SIGNAL_ACTION = 1 << 5,

G_SIGNAL_NO_HOOKS = 1 << 6

} GSignalFlags;

這個引數被設為 G_SIGNAL_RUN_FIRST,表示訊號的預設閉包要先於訊號使用者的閉包被呼叫,這個觀察一下上面的 test 程式的輸出結果便可知悉。如果我們將這個引數設為 G_SIGNAL_RUN_LAST,則表示訊號的預設閉包要遲於訊號使用者的閉包而被呼叫。對於這個引數的理解暫且到此為止,後面在講述訊號連線的時候 還會再次談到它。

小結

現在,對訊號註冊的主要過程已有所瞭解,但是依 g_signal_new 函式的第 5 個與第 6 個引數,對於它們,我現在還不知道如何為其構建例項,以後再說吧。若你讀到此處並且知道它們的用法,還望不吝賜教。

相關推薦

GObject學習教程---GObject 訊號機制——訊號註冊

本文是學習學習他人的部落格的心得(具體詳見“樓主見解”),如果源網站可訪問的話,建議直接訪問源網站: 樓主見解: 主要講解訊號的流程,不想了解流程,不看也罷。 主要講解g_signal_new函式的使用,以及具體引數的意義 原型: guint g_signal_

Flask 教程 日期和時間

這是Flask Mega-Tutorial系列的第十二部分,我將告訴你如何以適配所有使用者的方式處理日期和時間,無論他們身處地球上的何處。 顯示日期和時間是Microblog應用中長期被忽略的其中一個方面。 直到現在,我也只是讓Python渲染了User模

GObject學習教程---GObject訊號機制——概覽

本文是學習學習他人的部落格的心得(具體詳見“樓主見解”),如果源網站可訪問的話,建議直接訪問源網站: 樓主見解: 主要講解訊號機制的實現,分下邊幾步 第一:繼承GObject,在class的inite函式中,建立一個訊號file_changed.函式g_signal

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 幾何著色器(The Geometry Shader)

enable 中心 functions vector 是我 符號 ref rect 可能 原文:Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十二章:幾何著色器(The Geometry Sha

查找與處理文件

路徑 完整 管理員 arc 數據庫 保存 查詢 執行 處理 目標 使用 locate 使用 find 使用 Gnome Search tool locate 基於預先定義的數據庫定義,這個數據庫保存了所有文件的路徑和權限 非實時查詢

類的無參方法

使用方法 成員變量和局部變量 必須 技術 sun公司 語句 步驟 語法規則 前綴 第十二章: 類的無參方法 類 定義類的方法 類的方法必須包括:1.方法名稱 2.方法返回值

部署Django

iter 細節 rep 控制 warning 創建 mes 性問題 pri 本章包含創建一個django程序最必不可少的步驟 在服務器上部署它 如果你一直跟著我們的例子做,你可能正在用runserver 但是runserver 要部署你的django程序,

CLR via C#學習筆記--泛型

vat message 靜態 定義 泛型接口 void 失敗 internal 運行時 泛型是CLR和編程語言提供的一種特殊機制,他支持另一種形式的代碼重用,即算法重用。 CLR允許創建泛型引用類型和泛型值類型,但不允許創建泛型枚舉類型。 此外CLR還允許創建泛型接口和泛型

CLR via C#學習筆記--泛型基礎結構

12.2 泛型基礎結構 12.2.1 開放型別和封閉型別 具有泛型型別引數的型別仍然是型別,CLR同樣會為他建立內部的型別物件。 然而具有泛型型別引數的型別稱為開放型別,CLR禁制構造開放型別的任何例項。類似於CLR禁止構造介面型別的例項。 程式碼引用泛型類時可指定一組泛型型別實參。為所有型

CLR via C#學習筆記--泛型接口、委托

pre 類型安全 sealed 2.4 span 接口 triangle 泛型類 保持 12.3 泛型接口 泛型接口的意義 顯然,泛型的主要作用就是定義泛型的引用類型和值類型。 然而對泛型接口的支持對CLR來說也很重要。沒有泛型接口,每次用非泛型接口如IComparable

CLR via C#學習筆記--泛型方法和其他成員

12.6 泛型方法 方法和類可以各自定義型別引數 定義泛型類、結構或介面時,型別中定義的任何方法都可以引用型別指定的型別引數。 型別引數可以作為方法引數、返回值或方法內部定義的區域性變數的型別使用。 CLR還允許方法指定他自己的型別引數,這些引數也可以作為引數、返回值或區域性變數的型別使用。 在下例

CLR via C#學習筆記--可驗證性和約束

12.8 可驗證性和約束 where關鍵字 編譯器和CLR支援稱為約束的機制,可通過它使泛型變得真正有用。 約束的作用限制能指定成泛型實參的型別數量,通過限制類型的數量,可以對那些型別執行更多操作: public static T Min<T>(T o1,T o2) whe

深度學習花書學習筆記 應用

大規模深度學習 首先深度學習之所以能夠在現在獲得大的突破,主要依靠於硬體技術的進步和大資料的發展。卷積神經網路需要的高併發,依賴於GPU的發展不斷進步,甚至已經有了很多專用裝置,如谷歌TPU,阿里和華為也都有最新針對AI演算法的晶片。 高併發時可能導致梯度下降出問題,目前多采用非同步梯度下降

演算法導論 叉查詢樹 筆記(叉查詢樹、查詢叉查詢樹、插入和刪除、隨機構造的叉查詢樹)

二叉查詢樹是一種樹資料結構,它與普通的二叉樹最大的不同就是二叉查詢樹滿足一個性質:對於樹中的任意一個節點,均有其左子樹中的所有節點的關鍵字值都不大於該節點的關鍵字值,其右子樹中的任意一個節點的關鍵字值都不小於該節點的關鍵字值。 在二叉查詢樹上可以進行搜尋、取最小值、取最大值、取指定節點的前驅

SEO自學網體系教程網站改版與SEO

拿我本站(https://www.seo-wangzhan.com/) 舉例子,最近我對我這個站做了一點小小的改版 第一.確定目標關鍵詞 並在首頁佈局 一般首頁確定2~3個目標關鍵詞,太多了反而會造成每個關鍵詞都做不上去 比如,我確定我網站的目標關鍵詞就是:SEO自學網 和 SEO

201711671103《JAVA程式設計》多執行緒機制學習筆記

教材學習內容總結 1.執行緒是依附於程序的,程序是分配資源的最小單位,執行緒是比程序更小的執行單位。一個程序可以產生多個執行緒,形成多條執行線索。每條線索,即每個執行緒也有它自身的產生,存在和消亡過程,也是一個動態的概念。 2.JAVA 中的多執行緒機制:多執行緒是指一個應用程式同時存在好幾

“全棧2019”Java變量

允許 ima 交流 java學習 dea https 零基礎 ref 開發 難度 初級 學習時間 10分鐘 適合人群 零基礎 開發語言 Java 開發環境 JDK v11 IntelliJ IDEA v2018.3 文章原文鏈接 “全棧2019”Java第十二章:變量

“全棧2019”Java變數

難度 初級 學習時間 10分鐘 適合人群 零基礎 開發語言 Java 開發環境 JDK v11 IntelliJ IDEA v2018.3 文章原文連結 “全棧2019”Java第十二章:變數 下一章 “全棧2019”J

SpringBoot | 給大家推薦8個SpringBoot精選專案

專案一: 支付服務   簡介:支付服務:支付寶、微信、銀聯詳細 程式碼案例,目前已經1800+Star。十分鐘讓你快速搭建一個支付服務,內附各種教程。 專案地址: https://gitee.com/52itstyle/spring-boot-pay &nbs

Spring Boot 基礎系列教程 | 使用Spring-data-jpa簡化資料訪問層(推薦)

推薦 Spring Boot/Cloud 視訊: Spring Boot中使用Spring-data-jpa讓資料訪問更簡單、更優雅 在上一篇Spring中使用JdbcTemplate訪問資料庫 中介紹了一種基本的資料訪問方式,結合構建RESTful API和