1. 程式人生 > >獨家解讀:簡單又強大的配置檔案 Config 讀寫類

獨家解讀:簡單又強大的配置檔案 Config 讀寫類

一、引言

在專案過程中,難免會需要一個方便的配置檔案讀寫類,它可以像遊戲的存檔檔案一樣,記錄著我們當前專案的配置資訊,以至於方便我們每次初始化執行的時候可以從這個配置檔案讀取上一次的配置資訊,當然也可以在程式執行過程中記錄使用者的配置設定資訊。

我們理想中的這個配置檔案讀寫類,它要有以下這些方法:

1. 支援讀入一個指定配置檔案的能力

2. 支援隨時加入一個配置項的能力

3. 足夠強大,能夠寫入各種資料結構的配置資訊

滿足以上條件的配置檔案讀寫類才是我們想要的。這篇部落格顯然不是一步一步介紹如何寫出這樣一個類的文章(這不是一件容易的事情,即使寫出來也會千瘡百孔),而是一篇在網上已有的流傳久遠的一個短小精悍的配置檔案 Config 讀寫類的基礎上的解讀分析文章。

我找到的這個短小精悍的配置檔案 Config 讀寫類是來自於這篇部落格:
C++編寫Config類讀取配置檔案

這篇部落格的作者僅僅粘貼出了這個 Config 類的程式碼以及簡簡單單的幾行測試程式碼,對於大多數新手來說並不友好,因此我特意在閱讀了相關程式碼並且自行建立測試專案進行了測試之後,萌發了想要寫一篇部落格來好好解讀這個 Config 類的原理和用法的想法。

由於不喜歡在部落格裡面大篇幅的貼上程式碼,因此想要獲取到這個配置檔案 Config 讀寫類的同學可以在我的 GitHub 上閱讀這個類(也就簡簡單單三個檔案:一個 Config.h,另一個是 Config.cpp,testconfig.cpp 則是用來測試的檔案而已):

wangying2016/Config

那麼接下來,就讓我們一步一步分析這個配置檔案 Config 讀寫類的設計與實現吧!

二、Config 設計之:資料結構

要想了解一個類的設計與實現,最好的方法就是去了解它的設計目標,即需要滿足的需求。

這裡我們需要了解的就是,一個配置檔案的內容究竟是什麼樣子的:

config contents

由上圖可知,我們的一個配置檔案,是由兩部分組成的:

1. 註釋內容:在示例檔案中是由 # 來單行註釋表示的,用來解釋一些必要內容

2. 配置項內容:配置內容其實就是一個一個的鍵值對的記錄,左側是 key 值,比如這裡的 name 值,右側是 value 值,對應這裡的 wangying

。而在鍵值對中間,間插了一個符號 =(當然可以自定義的)來分割 key 值和 value 值。

可知,其實配置檔案的內容是非常簡單明瞭的。接下來,我們則需要將這種看似簡單的檔案結構抽象成我們熟悉的程式設計領域的資料結構。

如果你學過主流程式語言的話,這裡的鍵值對應該會讓你想起那麼幾個詞:
maphashdictionary 等等
在程式設計師的世界裡,鍵值對其實就是我們的對映。比如在 C++ 裡,我們要儲存這樣的資料就使用 std::map 即可。

也就是說,我們的 Config 類中,需要有一個最基本最基本的儲存配置檔案鍵值對資訊的 std::map 成員,這個成員用來將配置檔案中的每個 key 值和其對應的 value 值記錄下來。

那麼另外一個問題也就來了,我們的 std::map 究竟應該是什麼型別的呢?

哈哈,這個問題其實非常簡單,因為我們的鍵值對資訊都是要讀出寫入到檔案的,那麼 std::map 不論是 key 值還是 value 值都將會是字串型別,即 C++ STL 的 std::string (Config 類不支援中文編碼)類即可。

那麼有人就會問了,如果 value 值只是一個簡簡單單的 std::string 類的話,我想要儲存一個非常複雜的資料結構怎麼辦,比如一個 phone key 值,對應了一個電話號碼列表呢?

這個問題其實也非常簡單,這裡的 std::map 成員只是 Config 類中的最基本最基本儲存到檔案裡的字串鍵值對記錄,而 Config 為了支援使用者儲存多種複雜的 value 值,還提供了模板支援。因此,這裡只需要你提供的 value 值的結構可以被轉化為 std::string 型別,就可以使用 Config 類來儲存你的資料結構了。

因此,讓我們看看 Config 類的程式碼:

std::string m_Delimiter;  //!< separator between key and value  
std::string m_Comment;    //!< separator between value and comments  
std::map<std::string, std::string> m_Contents;  //!< extracted keys and values  

這三個內部的屬性, m_Delimiter 是我們之前提到的 key 值和 value 值的分隔符 = 的設定,m_Comment 是我們之前提到的註釋內容開頭 # 字元的設定,m_Contents 就是我們上面討論的 std::map 物件,並且以 key 值和 value 值均為 std::string 型別儲存。

此外,我們在 Config 類中看到的那麼多的模板函式,其歸根結底想要實現的,就是支援使用者自定義的 value 資料結構的讀取和寫入:

//!<Search for key and read value or optional default value, call as read<T>  
template<class T> T Read(const std::string& in_key) const;  
// Modify keys and values  
template<class T> void Add(const std::string& in_key, const T& in_value);

這裡截取了兩個重要的函式,一個用來讀取 key 值對應的 value 值,一個用來新增一個鍵值對。可以看到,這裡的 key 值永遠都是一個 std::string 型別的物件,而相應的 value 值則是模板定義的型別,支援使用者自定義傳入任何的可以轉成 std::string 型別的資料結構。

三、Config 設計之:暴露方法

接下來讓我們想想這樣一個問題,在我們看到了配置檔案的內容之後,並且將其抽象成了 std::map 的資料結構,之後我們需要做的,就是給類的呼叫者暴露方法的方法即可。

那麼應該有哪些方法呢:

1. 一個可以跟某個具體的配置檔案繫結起來的建構函式

2. 獲取指定 key 值的 value 值

3. 加入一對鍵值對

4. 修改指定 key 值的 value 值

5. 刪除一對鍵值對

暫時就想到了這些比較重要的,那麼 Config 類中提供了這些方法了嗎?

哈哈,提供了,讓我們一個一個來看:

1. 一個可以跟某個具體的配置檔案繫結起來的建構函式

Config::Config(string filename, string delimiter, string comment)
    : m_Delimiter(delimiter), m_Comment(comment)
{
    // Construct a Config, getting keys and values from given file  
    std::ifstream in(filename.c_str());
    if (!in) throw File_not_found(filename);
    in >> (*this);
}

作者使用 std::ifstream 打開了一個本地檔案,注意,呼叫這個方法之前必須保證該檔案存在。我們要注意到作者呼叫了 in >> (*this),呼叫了本類的 operator>> 過載函式,用來讀取檔案內容(此函式過於冗長,可以自行檢視原始碼)並將其儲存到 std::map

//!<Search for key and read value or optional default value, call as read<T>  
template<class T> T Read(const std::string& in_key) const;  
template<class T> T Read(const std::string& in_key, const T& in_value) const;
template<class T> bool ReadInto(T& out_var, const std::string& in_key) const;

這三個都是模板函式,主要是用來獲取使用者自定義資料結構的 value 值。需要注意的是,這三個函式的用法,第一個是返回 value 值;第二個是可以將 value 值在引數中返回;第三個直接將 value 值寫入到傳入的 var 物件中。

3. 加入一對鍵值對
4. 修改指定 key 值的 value 值
作者直接使用了一個函式即完成了第 3 點和第 4 點的工作:

template<class T>
void Config::Add(const std::string& in_key, const T& value)
{
    // Add a key with given value  
    std::string v = T_as_string(value);
    std::string key = in_key;
    Trim(key);
    Trim(v);
    m_Contents[key] = v;
    return;
}

這裡使用了 C++ 的 std::map 的特性,如果 key 值在 std::map 中存在,則更新 value 值,否則就新增一對鍵值對。需要注意的是,這裡呼叫了這行程式碼:

std::string v = T_as_string(value);

其中 T_as_string 函式將使用者傳入的自定義模板類轉化為 std::string 型別進行儲存,而該方法的實現如下:

/* static */
template<class T>
std::string Config::T_as_string(const T& t)
{
    // Convert from a T to a string  
    // Type T must support << operator  
    std::ostringstream ost;
    ost << t;
    return ost.str();
}

這個類直接呼叫了使用者自定義模板類的 operator<< 過載操作符函式,也就是說,只要使用者自定義資料結構自定義過載了 operator<< 操作符函式,就可以用 Config 類來進行 value 值的讀寫操作了。

5. 刪除一對鍵值對

void Config::Remove(const string& key)
{
    // Remove key and its value  
    m_Contents.erase(m_Contents.find(key));
    return;
}

幸而有 C++ STL 強大的功能,刪除一對鍵值對就是這麼簡單。

6. 另外的一些方法
作者為了方便使用者使用,還提供了諸如查詢檔案是否存在、鍵值是否存在、讀入檔案、設定獲取鍵值分隔符、設定獲取註釋識別符號等等方法。都是比較簡單並且易用的,感興趣的同學可以自行檢視原始碼。

四、Config 的使用 Demo

這裡,我自行編寫了一個 Demo 來測試 Config 類的功能:

#include <iostream>
#include <cstdlib>
#include <string>
#include <fstream>
#include "Config.h"

int main()
{
    // 開啟一個寫檔案流指向 config.ini 檔案
    std::string strConfigFileName("config.ini");
    std::ofstream out(strConfigFileName);
    // 初始化寫入註釋
    out << "# test for config read and write\n";
    // 寫入一對配置記錄: name = wangying
    out << "name = wangying\n";
    out.close();

    // 初始化 Config 類
    Config config(strConfigFileName);

    // 讀取鍵值
    std::string strKey = "name";
    std::string strValue;
    strValue = config.Read<std::string>(strKey);
    std::cout << "Read Key " << strKey << "'s Value is " 
         << strValue << std::endl;

    // 寫入新鍵值對
    std::string strNewKey = "age";
    std::string strNewValue = "23";
    config.Add<std::string>(strNewKey, strNewValue);

    // 將 Config 類的修改寫入檔案
    out.open(strConfigFileName, std::ios::app);
    if (out.is_open()) {
        // 利用 Config 類的 << 過載運算子
        out << config;
        std::cout << "Write config content success!" << std::endl;
    }
    out.close();

    system("pause");
    return 0;
}

幸而有強大的 Config 類,讓我操作配置檔案變成了一件這麼簡單的事情!

output

config generate

然而這裡需要注意的是,我們在使用 Config 類進行了 Add() 操作之後,我們僅僅只是在 Config 類中操作了 std::map 型別的 m_Contens 物件內容而已,我們還需要將其寫入到檔案中去,因此這裡我最後呼叫了寫檔案流進行寫入操作,注意這行程式碼:

// 利用 Config 類的 << 過載運算子
out << config;

這裡隱含呼叫了 Config 類的 operator<< 過載運算子:

std::ostream& operator<<(std::ostream& os, const Config& cf)
{
    // Save a Config to os  
    for (Config::mapci p = cf.m_Contents.begin();
        p != cf.m_Contents.end();
        ++p)
    {
        os << p->first << " " << cf.m_Delimiter << " ";
        os << p->second << std::endl;
    }
    return os;
}

哈哈哈,看吧,就這麼簡單!

至此,完結撒花 ^_^

五、總結

這是一個非常非常強大而又異常短小的配置檔案讀寫類,細細品之反而又覺得意味無窮。

回想我們引言裡說到的三點:

1. 支援讀入一個指定配置檔案的能力

2. 支援隨時加入一個配置項的能力

3. 足夠強大,能夠寫入各種資料結構的配置資訊

Config 類無一不一一滿足甚至提供了更加人性化的方法供使用者使用。

閱讀他人的程式碼並且瞭解其設計思路本身就是一個很快樂的事情:)

To be Stronger!