1. 程式人生 > >程式設計正規化|程式世界裡的程式設計正規化,探索程式設計本質

程式設計正規化|程式世界裡的程式設計正規化,探索程式設計本質

最近看了一些關於程式設計正規化的文章,簡要做一些小結和記錄

什麼是程式設計正規化

在現實生活中,為了適配各種規格的螺帽,我們需要許多種類的螺絲刀。

在程式設計世界中,靜態語言有許多種類的資料型別。

不過,我們可以發現,無論是傳統世界,還是程式設計世界,我們都在幹一件事情,就是通過使用一種更為通用的方式,抽象和隔離,讓複雜的“世界”變得簡單一些。

C語言的正規化例子1:swap函式

原版,swap交換變數(只能交換int型)

void swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

改進版,使用void * 抽象化資料型別,正規化程式設計:

void swap(void* x, void* y, size_t size)
{
     char tmp[size];
     memcpy(tmp, y, size);
     memcpy(y, x, size);
     memcpy(x, tmp, size);
}

函式介面中增加了一個size引數。一旦用了 void* 後,型別就會被“抽象”掉,編譯器不能通過型別得到型別的尺寸了,所以需要我們手動加上一個型別長度的標識。

函式的實現中使用了memcpy()函式。因為型別被“抽象”掉了,所以不能用賦值表示式了,很有可能傳進來的引數型別還是一個結構體,不過,為了要交換這些複雜型別的值,我們只能使用記憶體複製的方法了。

函式的實現中使用了一個temp[size]陣列。這就是交換資料時需要用的 buffer,會用 buffer 來做臨時的空間儲存。

C語言的正規化例子2:search函式

原版C語言函式,搜尋target在整型陣列中的位置:

int search(int* a, size_t size, int target) {
    for(int i=0; i<size; i++) {
        if (a[i] == target) {
            return i;
        }
    }
    return -1;
}

把search函式變為泛型,使之不僅能查詢整型陣列,也能查詢適配其他傳入的各種型別引數。

引數a,可以遍歷的陣列、結構體陣列等
target,void *,不定型別的資料(以適配正規化)
cmpFn,函式,使用者程式設計師自定義的各種資料的比較函式

int search(void* a, size_t size, void* target, 
    size_t elem_size, int(*cmpFn)(void*, void*) )
{
    for(int i=0; i<size; i++) {
        // 這裡不用memcmp比較,是因為 針對傳入的資料型別是結構體型別時,memcmp會比較記憶體地址。
        //使用 unsigned char * 計算地址
        if ( cmpFn ((unsigned char *)a + elem_size * i, target) == 0 ) {
            return i;
        }
    }
    return -1;
}

//整數比較函式
int int_cmp(int* x, int* y)
{
    return *x - *y;
}

//字串比較函式
int string_cmp(char* x, char* y){
    return strcmp(x, y);
}

//結構體比較函式
typedef struct _account {
    char name[10];
    char id[20];
} Account;

int account_cmp(Account* x, Account* y) {
    int n = strcmp(x->name, y->name);
    if (n != 0) return n;
    return strcmp(x->id, y->id);
}

上面的泛型search函式缺陷:只支援順序型別的資料結構,遇到複雜的圖、樹等無法抽象化非順序型的資料容器

另外C語言還可以使用巨集定義來泛型化。

泛型程式設計

一個良好的泛型程式設計需要解決如下幾個泛型程式設計的問題:
1.演算法的泛型;
2.型別的泛型;
3.資料結構(資料容器)的泛型。

就像前面的search()函式,裡面的 for(int i=0; i<len; i++) 這樣的遍歷方式,只能適用於順序型的資料結構的方式迭代,如:array、set、queue、list 和 link 等。並不適用於非順序型的資料結構。
如雜湊表 hash table,二叉樹 binary tree、圖 graph 等這樣資料不是按順序存放的資料結構(資料容器)。所以,如果找不到一種泛型的資料結構的操作方式(如遍歷、查詢、增加、刪除、修改……),那麼,任何的演算法或是程式都不可能做到真正意義上的泛型。

比如,如果我要在一個 hash table 中查詢一個 key,返回什麼呢?一定不是返回“索引下標”,因為在 hash table 這樣的資料結構中,資料的存放位置不是順序的,而且還會因為容量不夠的問題被重新 hash 後改變,所以返回陣列下標是沒有意義的。

對此,我們要把這個事做得泛型和通用一些。如果找到,返回找到的這個元素的一個指標(地址)會更靠譜一些。

所以,為了解決泛型的問題,我們需要動用以下幾個 C++ 的技術。
1.使用“模板”來抽象型別,這樣可以寫出型別無關的資料結構(資料容器)。
2.使用一個“迭代器”來遍歷或是操作資料結構內的元素。

C++的正規化例子1:search函式

template<typename T, typename Iter>
Iter search(Iter pStart, Iter pEnd, T target) 
{
    for(Iter p = pStart; p != pEnd; p++) {
        if ( *p == target ) 
            return p;
    }
    return NULL;
}

在 C++ 的泛型版本中,我們可以看到:

使用typename T抽象了資料結構中儲存資料的型別。

使用typename Iter,這是不同的資料結構需要自己實現的“迭代器”,這樣也就抽象掉了不同型別的資料結構,迭代器需要資料結構自己去實現。

然後,我們對資料容器的遍歷使用了Iter中的++方法,這是資料容器需要過載的操作符,這樣通過操作符過載也就泛型掉了遍歷,
為了相容原有 C 語言的程式設計習慣我們不用標準介面Iter.Next(),不用Iter.GetValue()來取代*。

在函式的入參上使用了pStart和pEnd來表示遍歷的起止。

使用Iter來取得這個“指標”的內容。這也是通過過載 取值操作符來達到的泛型。

說明:所謂的Iter,在實際程式碼中,就是像vector::iterator或map<int, string>::iterator這樣的東西。這是由相應的資料容器來實現和提供的(迭代器)。

C++ STL原始碼中的find函式

template<class InputIterator, class T>
  InputIterator find (InputIterator first, InputIterator last, const T& val)
{
  while (first!=last) {
    if (*first==val) return first;
    ++first;
  }
  return last;
}

C++的正規化例子2: Sum 函式

C語言版求和函式

long sum(int *a, size_t size) {
    long result = 0;
    for(int i=0; i<size; i++) {
        result += a[i];
    }
    return result;
}

C++泛型版(有問題):

template<typename T, typename Iter>
T sum(Iter pStart, Iter pEnd) {
    T result = 0;
    for(Iter p=pStart; p!=pEnd; p++) {
        result += *p;
    }
    return result;  
}

這裡默認了 T result = 0;也就是T假設了 Iter 中出來的型別是T。0假設了型別是int;如果型別不一樣,就會導致轉型的問題

改進版,需要迭代器
Iter在實際呼叫者那會是一個具體的像vector::iterator的東西
在這個宣告中,int已經被傳入Iter中了;所以定義result的T應該可以從Iter中來。這樣就可以保證型別是一樣的,而且不會有被轉型的問題。

C++ 泛型程式設計:迭代器

template <class T>
class container {
public:
    class iterator {
    public:
        typedef iterator self_type;
        typedef T   value_type;
        typedef T*  pointer;
        typedef T&  reference;

        reference operator*();
        pointer operator->();
        bool operator==(const self_type& rhs);
        bool operator!=(const self_type& rhs);
        self_type operator++() { self_type i = *this; ptr_++; return i; }
        self_type operator++(int junk) { ptr_++; return *this; }
        ...
        ...
    private:
        pointer _ptr;
    };

    iterator begin();
    iterator end();
    ...
    ...
};

1.首先,一個迭代器需要和一個"資料容器"(類)在一起,因為裡面是對這個容器的具體的程式碼實現,對這個容器的迭代。
2.它需要過載一些操作符,比如:取值操作*、成員操作->、比較操作==和!=,還有遍歷操作++,等等。
3.然後,還要typedef一些型別,比如value_type,告訴我們容器內的資料的實際型別是什麼樣子。
4.還有一些,如begin()和end()的基本操作。
5.我們還可以看到其中有一個pointer _ptr的內部指標來指向當前的資料(注意,pointer就是 T*)。

有了迭代器,我們讓使用者自型傳入模板T的型別,解決T result = 0出現的問題
最終Sum的正規化寫法:

template <class Iter>
typename Iter::value_type
sum(Iter start, Iter end, T init) {
    typename Iter::value_type result = init;
    while (start != end) {
        result = result + *start;
        start++;
    }
    return result;
}


int main(){
    container<int> c;
    container<int>::iterator it = c.begin();
    sum(c.begin(), c.end(), 0);
    return 0;
}

這就是整個 STL 的泛型方法,其中包括:

1.泛型的資料容器;
2.泛型資料容器的迭代器;
3.泛型的演算法;

reduce函數語言程式設計

如果我們有一個 員工結構體,再想用sum函式來求和怎麼辦?

struct Employee {
    string name;
    string id;
    int vacation;
    double salary;
};

結構體陣列增加了很多資料型別,以前sum函式就不知道怎麼辦了吧,

vector<Employee> staff;
//total salary or total vacation days?
sum(staff.begin(), staff.end(), 0);

這個例子而言,我想計算員工薪水裡面最高的,和休假最少的,或者我想計算全部員工的總共休假多少天。那麼面對這麼多的需求,我們是否可以泛型一些呢?怎樣解決這些問題呢?
引入更抽象化的函式程式設計——reduce函式

template<class Iter, class T, class Op>
T reduce (Iter start, Iter end, T init, Op op) {
    T result = init;
    while ( start != end ) {
        result = op( result, *start ); //這裡時重點
        start++;
    }
    return result;
}

reduce函式 需要增加一個引數 op,這個引數可以是一個函式,來完成我們想要的業務操作。

比如下面的業務操作函式:我們來求員工的工資和、最大工資

double sum_salaries = 
  reduce( staff.begin(), staff.end(), 0.0,
          
            {return s + e.salary;}  );

double max_salary =
  reduce( staff.begin(), staff.end(), 0.0,
          
            {return s > e.salary? s: e.salary; } );

C++STL中的count_if():

下面這個示例中,先定義了一個函式物件counter(struct裡定義函式)。這個函式物件需要一個Cond的函式物件,它是個條件判斷函式,如果滿足條件,則加 1,否則加 0。

然後,用上面的counter函式物件和reduce函式共同來打造一個counter_if演算法

當條件滿足的時候我就記個數,也就是統計滿足某個條件的個數。

//物件counter,滿足Cond函式的條件,就將引數c 加 1。
template<class T, class Cond>
struct counter {
    size_t operator()(size_t c, T t) const {
        return c + (Cond(t) ? 1 : 0);
    }
};

//count_if函式,返回上面文章中編寫使用的reduce函式
template<class Iter, class Cond>
size_t count_if(Iter begin, Iter end, Cond c){
    return reduce(begin, end, 0, counter<Iter::value_type, Cond>(c));
}

當我需要統計薪資超過 1 萬元的員工的數量時,引數3為Cond條件函式,{ return e.salary > 10000; }一行程式碼就完成了

size_t cnt = count_if(staff.begin(), staff.end(), { return e.salary > 10000; });

函數語言程式設計

修飾器模式

面向物件程式設計

原型程式設計正規化

邏輯程式設計正規化

前兩小段,是程式設計正規化的簡單介紹,C語言到C++的演化,
未完待續。。。。