C++程序員Protocol Buffers基礎指南
這篇教程提供了一個面向 C++ 程序員關于 protocol buffers 的基礎介紹。通過創(chuàng)建一個簡單的示例應用程序,它將向我們展示:
- 在 .proto 文件中定義消息格式
 - 使用 protocol buffer 編譯器
 - 使用 C++ protocol buffer API 讀寫消息
 
這不是一個關于在 C++ 中使用 protocol buffers 的全面指南。要獲取更詳細的信息,請參考 Protocol Buffer Language Guide 和 Encoding Reference。
為什么使用 Protocol Buffers
我們接下來要使用的例子是一個非常簡單的"地址簿"應用程序,它能從文件中讀取聯(lián)系人詳細信息。地址簿中的每一個人都有一個名字、ID、郵件地址和聯(lián)系電話。
如何序列化和獲取結構化的數(shù)據(jù)?這里有幾種解決方案:
- 以二進制形式發(fā)送/接收原生的內存數(shù)據(jù)結構。通常,這是一種脆弱的方法,因為接收/讀取代碼必須基于完全相同的內存布局、大小端等環(huán)境進行編譯。同時,當文件增加時,原始格式數(shù)據(jù)會隨著與該格式相關的軟件而迅速擴散,這將導致很難擴展文件格式。
 - 你可以創(chuàng)造一種 ad-hoc 方法,將數(shù)據(jù)項編碼為一個字符串——比如將 4 個整數(shù)編碼為 12:3:-23:67。雖然它需要編寫一次性的編碼和解碼代碼且解碼需要耗費一點運行時成本,但這是一種簡單靈活的方法。這最適合編碼非常簡單的數(shù)據(jù)。
 - 序列化數(shù)據(jù)為 XML。這種方法是非常吸引人的,因為 XML 是一種適合人閱讀的格式,并且有為許多語言開發(fā)的庫。如果你想與其他程序和項目共享數(shù)據(jù),這可能是一種不錯的選擇。然而,眾所周知,XML 是空間密集型的,且在編碼和解碼時,它對程序會造成巨大的性能損失。同時,使用 XML DOM 樹被認為比操作一個類的簡單字段更加復雜。
 
Protocol buffers 是針對這個問題的一種靈活、高效、自動化的解決方案。使用 Protocol buffers,你需要寫一個 .proto 說明,用于描述你所希望存儲的數(shù)據(jù)結構。利用 .proto 文件,protocol buffer 編譯器可以創(chuàng)建一個類,用于實現(xiàn)對高效的二進制格式的 protocol buffer 數(shù)據(jù)的自動化編碼和解碼。產(chǎn)生的類提供了構造 protocol buffer 的字段的 getters 和 setters,并且作為一個單元來處理讀寫 protocol buffer 的細節(jié)。重要的是,protocol buffer 格式支持格式的擴展,代碼仍然可以讀取以舊格式編碼的數(shù)據(jù)。
在哪可以找到示例代碼
示例代碼被包含于源代碼包,位于“examples”文件夾??稍?a >這里下載代碼。
定義你的協(xié)議格式
為了創(chuàng)建自己的地址簿應用程序,你需要從 .proto 開始。.proto 文件中的定義很簡單:為你所需要序列化的每個數(shù)據(jù)結構添加一個消息(message),然后為消息中的每一個字段指定一個名字和類型。這里是定義你消息的 .proto 文件 addressbook.proto。
- package tutorial;
 - message Person {
 - required string name = 1;
 - required int32 id = 2;
 - optional string email = 3;
 - enum PhoneType {
 - MOBILE = 0;
 - HOME = 1;
 - WORK = 2;
 - }
 - message PhoneNumber {
 - required string number = 1;
 - optional PhoneType type = 2 [default = HOME];
 - }
 - repeated PhoneNumber phone = 4;
 - }
 - message AddressBook {
 - repeated Person person = 1;
 - }
 
如你所見,其語法類似于 C++ 或 Java。我們開始看看文件的每一部分內容做了什么。
.proto 文件以一個 package 聲明開始,這可以避免不同項目的命名沖突。在 C++,你生成的類會被置于與 package 名字一樣的命名空間。
下一步,你需要定義消息(message)。消息只是一個包含一系列類型字段的集合。大多標準的簡單數(shù)據(jù)類型是可以作為字段類型的,包括 bool、int32、float、double 和 string。你也可以通過使用其他消息類型作為字段類型,將更多的數(shù)據(jù)結構添加到你的消息中——在以上的示例,Person 消息包含了 PhoneNumber 消息,同時 AddressBook 消息包含 Person 消息。你甚至可以定義嵌套在其他消息內的消息類型——如你所見,PhoneNumber 類型定義于 Person 內部。如果你想要其中某一個字段的值是預定義值列表中的某個值,你也可以定義 enum 類型——這兒你可以指定一個電話號碼是 MOBILE、HOME 或 WORK 中的某一個。
每一個元素上的 = 1、= 2 標記確定了用于二進制編碼的唯一“標簽”(tag)。標簽數(shù)字 1-15 的編碼比更大的數(shù)字少需要一個字節(jié),因此作為一種優(yōu)化,你可以將這些標簽用于經(jīng)常使用的元素或 repeated 元素,剩下 16 以及更高的標簽用于非經(jīng)常使用的元素或 optional 元素。每一個 repeated 字段的元素需要重新編碼標簽數(shù)字,因此 repeated 字段適合于使用這種優(yōu)化手段。
每一個字段必須使用下面的修飾符加以標注:
- required:必須提供該字段的值,否則消息會被認為是 “未初始化的”(uninitialized)。如果 libprotobuf 以調試模式編譯,序列化未初始化的消息將引起一個斷言失敗。以優(yōu)化形式構建,將會跳過檢查,并且無論如何都會寫入該消息。然而,解析未初始化的消息總是會失敗(通過 parse 方法返回 false)。除此之外,一個 required 字段的表現(xiàn)與 optional 字段完全一樣。
 - optional:字段可能會被設置,也可能不會。如果一個 optional 字段沒被設置,它將使用默認值。對于簡單類型,你可以指定你自己的默認值,正如例子中我們對電話號碼的 type 一樣,否則使用系統(tǒng)默認值:數(shù)字類型為 0、字符串為空字符串、布爾值為 false。對于嵌套消息,默認值總為消息的“默認實例”或“原型”,它的所有字段都沒被設置。調用 accessor 來獲取一個沒有顯式設置的 optional(或 required) 字段的值總是返回字段的默認值。
 - repeated:字段可以重復任意次數(shù)(包括 0 次)。repeated 值的順序會被保存于 protocol buffer??梢詫?repeated 字段想象為動態(tài)大小的數(shù)組。
 
你可以查找關于編寫 .proto 文件的完整指導——包括所有可能的字段類型——在 Protocol Buffer Language Guide 里面。不要在這里面查找與類繼承相似的特性,因為 protocol buffers 不會做這些。
required 是***性的
在把一個字段標識為 required 的時候,你應該特別小心。如果在某些情況下你不想寫入或者發(fā)送一個 required 的字段,那么將該字段更改為 optional 可能會遇到問題——舊版本的讀者(LCTT 譯注:即讀取、解析舊版本 Protocol Buffer 消息的一方)會認為不含該字段的消息是不完整的,從而有可能會拒絕解析。在這種情況下,你應該考慮編寫特別針對于應用程序的、自定義的消息校驗函數(shù)。Google 的一些工程師得出了一個結論:使用 required 弊多于利;他們更愿意使用 optional 和 repeated 而不是 required。當然,這個觀點并不具有普遍性。
編譯你的 Protocol Buffers
既然你有了一個 .proto,那你需要做的下一件事就是生成一個將用于讀寫 AddressBook 消息的類(從而包括 Person 和 PhoneNumber)。為了做到這樣,你需要在你的 .proto 上運行 protocol buffer 編譯器 protoc:
- 如果你沒有安裝編譯器,請下載這個包,并按照 README 中的指令進行安裝。
 - 現(xiàn)在運行編譯器,指定源目錄(你的應用程序源代碼位于哪里——如果你沒有提供任何值,將使用當前目錄)、目標目錄(你想要生成的代碼放在哪里;常與 $SRC_DIR 相同),以及你的 .proto 路徑。在此示例中:
 
- protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
 
因為你想要 C++ 的類,所以你使用了 --cpp_out 選項——也為其他支持的語言提供了類似選項。
在你指定的目標文件夾,將生成以下的文件:
- addressbook.pb.h,聲明你生成類的頭文件。
 - addressbook.pb.cc,包含你的類的實現(xiàn)。
 
Protocol Buffer API
讓我們看看生成的一些代碼,了解一下編譯器為你創(chuàng)建了什么類和函數(shù)。如果你查看 addressbook.pb.h,你可以看到有一個在 addressbook.proto 中指定所有消息的類。關注 Person 類,可以看到編譯器為每個字段生成了讀寫函數(shù)(accessors)。例如,對于 name、id、email 和 phone 字段,有下面這些方法:(LCTT 譯注:此處原文所指文件名有誤,徑該之。)
- // name
 - inline bool has_name() const;
 - inline void clear_name();
 - inline const ::std::string& name() const;
 - inline void set_name(const ::std::string& value);
 - inline void set_name(const char* value);
 - inline ::std::string* mutable_name();
 - // id
 - inline bool has_id() const;
 - inline void clear_id();
 - inline int32_t id() const;
 - inline void set_id(int32_t value);
 - inline bool has_email() const;
 - inline void clear_email();
 - inline const ::std::string& email() const;
 - inline void set_email(const ::std::string& value);
 - inline void set_email(const char* value);
 - inline ::std::string* mutable_email();
 - // phone
 - inline int phone_size() const;
 - inline void clear_phone();
 - inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
 - inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
 - inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
 - inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
 - inline ::tutorial::Person_PhoneNumber* add_phone();
 
正如你所見到,getters 的名字與字段的小寫名字完全一樣,并且 setter 方法以 set_ 開頭。同時每個單一(singular)(required 或 optional)字段都有 has_ 方法,該方法在字段被設置了值的情況下返回 true。***,所有字段都有一個 clear_ 方法,用以清除字段到空(empty)狀態(tài)。
數(shù)字型的 id 字段僅有上述的基本讀寫函數(shù)(accessors)集合,而 name 和 email 字段有兩個額外的方法,因為它們是字符串——一個是可以獲得字符串直接指針的mutable_ 的 getter ,另一個為額外的 setter。注意,盡管 email 還沒被設置(set),你也可以調用 mutable_email;因為 email 會被自動地初始化為空字符串。在本例中,如果你有一個單一的(required 或 optional)消息字段,它會有一個 mutable_ 方法,而沒有 set_ 方法。
repeated 字段也有一些特殊的方法——如果你看看 repeated 的 phone 字段的方法,你可以看到:
- 檢查 repeated 字段的 _size(也就是說,與 Person 相關的電話號碼的個數(shù))
 - 使用下標取得特定的電話號碼
 - 更新特定下標的電話號碼
 - 添加新的電話號碼到消息中,之后你便可以編輯。(repeated 標量類型有一個 add_ 方法,用于傳入新的值)
 
為了獲取 protocol 編譯器為所有字段定義生成的方法的信息,可以查看 C++ generated code reference。
枚舉和嵌套類
與 .proto 的枚舉相對應,生成的代碼包含了一個 PhoneType 枚舉。你可以通過 Person::PhoneType 引用這個類型,通過 Person::MOBILE、Person::HOME 和 Person::WORK 引用它的值。(實現(xiàn)細節(jié)有點復雜,但是你無須了解它們而可以直接使用)
編譯器也生成了一個 Person::PhoneNumber 的嵌套類。如果你查看代碼,你可以發(fā)現(xiàn)真正的類型為 Person_PhoneNumber,但它通過在 Person 內部使用 typedef 定義,使你可以把 Person_PhoneNumber 當成嵌套類。唯一產(chǎn)生影響的一個例子是,如果你想要在其他文件前置聲明該類——在 C++ 中你不能前置聲明嵌套類,但是你可以前置聲明 Person_PhoneNumber。
標準的消息方法
所有的消息方法都包含了許多別的方法,用于檢查和操作整個消息,包括:
- bool IsInitialized() const; :檢查是否所有 required 字段已經(jīng)被設置。
 - string DebugString() const; :返回人類可讀的消息表示,對調試特別有用。
 - void CopyFrom(const Person& from);:使用給定的值重寫消息。
 - void Clear();:清除所有元素為空的狀態(tài)。
 
上面這些方法以及下一節(jié)要講的 I/O 方法實現(xiàn)了被所有 C++ protocol buffer 類共享的消息(Message)接口。為了獲取更多信息,請查看 complete API documentation for Message。
解析和序列化
***,所有 protocol buffer 類都有讀寫你選定類型消息的方法,這些方法使用了特定的 protocol buffer 二進制格式。這些方法包括:
- bool SerializeToString(string* output) const;:序列化消息并將消息字節(jié)數(shù)據(jù)存儲在給定的字符串中。注意,字節(jié)數(shù)據(jù)是二進制格式的,而不是文本格式;我們只使用 string 類作為合適的容器。
 - bool ParseFromString(const string& data);:從給定的字符創(chuàng)解析消息。
 - bool SerializeToOstream(ostream* output) const;:將消息寫到給定的 C++ ostream。
 - bool ParseFromIstream(istream* input);:從給定的 C++ istream 解析消息。
 
這些只是兩個用于解析和序列化的選擇。再次說明,可以查看 Message API reference 完整的列表。
Protocol Buffers 和面向對象設計
Protocol buffer 類通常只是純粹的數(shù)據(jù)存儲器(像 C++ 中的結構體);它們在對象模型中并不是一等公民。如果你想向生成的 protocol buffer 類中添加更豐富的行為,***的方法就是在應用程序中對它進行封裝。如果你無權控制 .proto 文件的設計的話,封裝 protocol buffers 也是一個好主意(例如,你從另一個項目中重用一個 .proto 文件)。在那種情況下,你可以用封裝類來設計接口,以更好地適應你的應用程序的特定環(huán)境:隱藏一些數(shù)據(jù)和方法,暴露一些便于使用的函數(shù),等等。但是你絕對不要通過繼承生成的類來添加行為。這樣做的話,會破壞其內部機制,并且不是一個好的面向對象的實踐。
寫消息
現(xiàn)在我們嘗試使用 protocol buffer 類。你的地址簿程序想要做的***件事是將個人詳細信息寫入到地址簿文件。為了做到這一點,你需要創(chuàng)建、填充 protocol buffer 類實例,并且將它們寫入到一個輸出流(output stream)。
這里的程序可以從文件讀取 AddressBook,根據(jù)用戶輸入,將新 Person 添加到 AddressBook,并且再次將新的 AddressBook 寫回文件。這部分直接調用或引用 protocol buffer 類的代碼會以“// pb”標出。
- #include <iostream>
 - #include <fstream>
 - #include <string>
 - #include "addressbook.pb.h" // pb
 - using namespace std;
 - // This function fills in a Person message based on user input.
 - void PromptForAddress(tutorial::Person* person) {
 - cout << "Enter person ID number: ";
 - int id;
 - cin >> id;
 - person->set_id(id); // pb
 - cin.ignore(256, '\n');
 - cout << "Enter name: ";
 - getline(cin, *person->mutable_name()); // pb
 - cout << "Enter email address (blank for none): ";
 - string email;
 - getline(cin, email);
 - if (!email.empty()) { // pb
 - person->set_email(email); // pb
 - }
 - while (true) {
 - cout << "Enter a phone number (or leave blank to finish): ";
 - string number;
 - getline(cin, number);
 - if (number.empty()) {
 - break;
 - }
 - tutorial::Person::PhoneNumber* phone_number = person->add_phone(); //pb
 - phone_number->set_number(number); // pb
 - cout << "Is this a mobile, home, or work phone? ";
 - string type;
 - getline(cin, type);
 - if (type == "mobile") {
 - phone_number->set_type(tutorial::Person::MOBILE); // pb
 - } else if (type == "home") {
 - phone_number->set_type(tutorial::Person::HOME); // pb
 - } else if (type == "work") {
 - phone_number->set_type(tutorial::Person::WORK); // pb
 - } else {
 - cout << "Unknown phone type. Using default." << endl;
 - }
 - }
 - }
 - // Main function: Reads the entire address book from a file,
 - // adds one person based on user input, then writes it back out to the same
 - // file.
 - int main(int argc, char* argv[]) {
 - // Verify that the version of the library that we linked against is
 - // compatible with the version of the headers we compiled against.
 - GOOGLE_PROTOBUF_VERIFY_VERSION; // pb
 - if (argc != 2) {
 - cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
 - return -1;
 - }
 - tutorial::AddressBook address_book; // pb
 - {
 - // Read the existing address book.
 - fstream input(argv[1], ios::in | ios::binary);
 - if (!input) {
 - cout << argv[1] << ": File not found. Creating a new file." << endl;
 - } else if (!address_book.ParseFromIstream(&input)) { // pb
 - cerr << "Failed to parse address book." << endl;
 - return -1;
 - }
 - }
 - // Add an address.
 - PromptForAddress(address_book.add_person()); // pb
 - {
 - // Write the new address book back to disk.
 - fstream output(argv[1], ios::out | ios::trunc | ios::binary);
 - if (!address_book.SerializeToOstream(&output)) { // pb
 - cerr << "Failed to write address book." << endl;
 - return -1;
 - }
 - }
 - // Optional: Delete all global objects allocated by libprotobuf.
 - google::protobuf::ShutdownProtobufLibrary(); // pb
 - return 0;
 - }
 
注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。它是一種好的實踐——雖然不是嚴格必須的——在使用 C++ Protocol Buffer 庫之前執(zhí)行該宏。它可以保證避免不小心鏈接到一個與編譯的頭文件版本不兼容的庫版本。如果被檢查出來版本不匹配,程序將會終止。注意,每個 .pb.cc 文件在初始化時會自動調用這個宏。
同時注意在程序***調用 ShutdownProtobufLibrary()。它用于釋放 Protocol Buffer 庫申請的所有全局對象。對大部分程序,這不是必須的,因為雖然程序只是簡單退出,但是 OS 會處理釋放程序的所有內存。然而,如果你使用了內存泄漏檢測工具,工具要求全部對象都要釋放,或者你正在寫一個 Protocol Buffer 庫,該庫可能會被一個進程多次加載和卸載,那么你可能需要強制 Protocol Buffer 清除所有東西。
讀取消息
當然,如果你無法從它獲取任何信息,那么這個地址簿沒多大用處!這個示例讀取上面例子創(chuàng)建的文件,并打印文件里的所有內容。
- #include <iostream>
 - #include <fstream>
 - #include <string>
 - #include "addressbook.pb.h" // pb
 - using namespace std;
 - // Iterates though all people in the AddressBook and prints info about them.
 - void ListPeople(const tutorial::AddressBook& address_book) { // pb
 - for (int i = 0; i < address_book.person_size(); i++) { // pb
 - const tutorial::Person& person = address_book.person(i); // pb
 - cout << "Person ID: " << person.id() << endl; // pb
 - cout << " Name: " << person.name() << endl; // pb
 - if (person.has_email()) { // pb
 - cout << " E-mail address: " << person.email() << endl; // pb
 - }
 - for (int j = 0; j < person.phone_size(); j++) { // pb
 - const tutorial::Person::PhoneNumber& phone_number = person.phone(j); // pb
 - switch (phone_number.type()) { // pb
 - case tutorial::Person::MOBILE: // pb
 - cout << " Mobile phone #: ";
 - break;
 - case tutorial::Person::HOME: // pb
 - cout << " Home phone #: ";
 - break;
 - case tutorial::Person::WORK: // pb
 - cout << " Work phone #: ";
 - break;
 - }
 - cout << phone_number.number() << endl; // ob
 - }
 - }
 - }
 - // Main function: Reads the entire address book from a file and prints all
 - // the information inside.
 - int main(int argc, char* argv[]) {
 - // Verify that the version of the library that we linked against is
 - // compatible with the version of the headers we compiled against.
 - GOOGLE_PROTOBUF_VERIFY_VERSION; // pb
 - if (argc != 2) {
 - cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
 - return -1;
 - }
 - tutorial::AddressBook address_book; // pb
 - {
 - // Read the existing address book.
 - fstream input(argv[1], ios::in | ios::binary);
 - if (!address_book.ParseFromIstream(&input)) { // pb
 - cerr << "Failed to parse address book." << endl;
 - return -1;
 - }
 - }
 - ListPeople(address_book);
 - // Optional: Delete all global objects allocated by libprotobuf.
 - google::protobuf::ShutdownProtobufLibrary(); // pb
 - return 0;
 - }
 
擴展 Protocol Buffer
或早或晚在你發(fā)布了使用 protocol buffer 的代碼之后,毫無疑問,你會想要 "改善" protocol buffer 的定義。如果你想要新的 buffers 向后兼容,并且老的 buffers 向前兼容——幾乎可以肯定你很渴望這個——這里有一些規(guī)則,你需要遵守。在新的 protocol buffer 版本:
- 你絕不可以修改任何已存在字段的標簽數(shù)字
 - 你絕不可以添加或刪除任何 required 字段
 - 你可以刪除 optional 或 repeated 字段
 - 你可以添加新的 optional 或 repeated 字段,但是你必須使用新的標簽數(shù)字(也就是說,標簽數(shù)字在 protocol buffer 中從未使用過,甚至不能是已刪除字段的標簽數(shù)字)。
 
(對于上面規(guī)則有一些例外情況,但它們很少用到。)
如果你能遵守這些規(guī)則,舊代碼則可以歡快地讀取新的消息,并且簡單地忽略所有新的字段。對于舊代碼來說,被刪除的 optional 字段將會簡單地賦予默認值,被刪除的 repeated 字段會為空。新代碼顯然可以讀取舊消息。然而,請記住新的 optional 字段不會呈現(xiàn)在舊消息中,因此你需要顯式地使用 has_ 檢查它們是否被設置或者在 .proto 文件在標簽數(shù)字后使用 [default = value] 提供一個合理的默認值。如果一個 optional 元素沒有指定默認值,它將會使用類型特定的默認值:對于字符串,默認值為空字符串;對于布爾值,默認值為 false;對于數(shù)字類型,默認類型為 0。注意,如果你添加一個新的 repeated 字段,新代碼將無法辨別它被留空(被新代碼)或者從沒被設置(被舊代碼),因為 repeated 字段沒有 has_ 標志。
優(yōu)化技巧
C++ Protocol Buffer 庫已極度優(yōu)化過了。但是,恰當?shù)挠梅軌蚋嗟靥岣咝阅?。這里是一些技巧,可以幫你從庫中擠壓出***一點速度:
- 盡可能復用消息對象。即使它們被清除掉,消息也會盡量保存所有被分配來重用的內存。因此,如果我們正在處理許多相同類型或一系列相似結構的消息,一個好的辦法是重用相同的消息對象,從而減少內存分配的負擔。但是,隨著時間的流逝,對象可能會膨脹變大,尤其是當你的消息尺寸(LCTT 譯注:各消息內容不同,有些消息內容多一些,有些消息內容少一些)不同的時候,或者你偶爾創(chuàng)建了一個比平常大很多的消息的時候。你應該自己通過調用 SpaceUsed 方法監(jiān)測消息對象的大小,并在它太大的時候刪除它。
 - 對于在多線程中分配大量小對象的情況,你的操作系統(tǒng)內存分配器可能優(yōu)化得不夠好。你可以嘗試使用 google 的 tcmalloc。
 
高級用法
Protocol Buffers 絕不僅用于簡單的數(shù)據(jù)存取以及序列化。請閱讀 C++ API reference 來看看你還能用它來做什么。
protocol 消息類所提供的一個關鍵特性就是反射(reflection)。你不需要針對一個特殊的消息類型編寫代碼,就可以遍歷一個消息的字段并操作它們的值。一個使用反射的有用方法是 protocol 消息與其他編碼互相轉換,比如 XML 或 JSON。反射的一個更高級的用法可能就是可以找出兩個相同類型的消息之間的區(qū)別,或者開發(fā)某種“協(xié)議消息的正則表達式”,利用正則表達式,你可以對某種消息內容進行匹配。只要你發(fā)揮你的想像力,就有可能將 Protocol Buffers 應用到一個更廣泛的、你可能一開始就期望解決的問題范圍上。
















 
 
 



 
 
 
 