Skip to content

C 語言用 K&R Coding Style 的最大理由不是省行數

以實例分析使用 K&R 風格的優點

發佈

多年來,就算我在寫其它語言時用的都不是 Allman 風格。但是只要我寫 C,我就會自然而然地用 Allman。而且我甚至有點反感 K&R,更不要說是花括號行為不一致的 Linux K&R 了。

我原本只是想整理一個自己偏好的 Coding style,但是在仔細思考詳細的規則和實際寫程式會遇到的情況後,我發現 K&R 似乎才是唯一的真理。也總算理解 Linux kernel coding style 手冊裡的那段話:

Heretic people all over the world have claimed that this inconsistency is … well … inconsistent, but all right-thinking people know that (a) K&R are right and (b) K&R are right. Besides, functions are special anyway (you can’t nest them in C)

在此之前,我覺得 K&R 的唯一優點只有「節省行數」(然後把全部的東西擠在一起),但是我發現 K&R 的其它優點,它們甚至讓節省行數都變得微不足道。

所以我想藉由這篇文章,來說明我是如何在寫草稿時還是忠實的 Allman 用戶,突然就變成 K&R 支持者。當然還有一點很重要的是,Coding style 還是有不少主觀因素在內,你完全可以不認同我的任何看法和感覺。這只是一個分享而已。

定義

首先在討論各個 Coding style 前,我想先定義一下我認為的好的 style 是什麼樣子的,有了統一的標準才有比較的依據。以下按權重排序:

  1. 使用現有規定的風格。這個不用多說,如果專案已經有規定,那就是遵守。不過這篇討論的是你有完全的控制權的情況。
  2. 看得順眼。就如同工具順手最重要。
  3. 使用方便。當然沒人想被風格影響實際功能和 Coding 效率。
  4. 規則簡單且明確。規則越簡單、例外越少就越容易遵守。明確的規則就可以不用再花時間思考。

實例比較

A. typedef

// A
typedef struct {
    int foo;
    int bar;
} foo_bar_t;
 
// B
typedef struct
{
    int foo;
    int bar;
} foo_bar_t;
 
// C
typedef struct
{
    int foo;
    int bar;
}
foo_bar_t;

B. 初始化 struct

// A
foo_bar_t fb = {
    .foo = 0,
    .bar = 1,
};
 
// B
foo_bar_t fb =
{
    .foo = 0,
    .bar = 1,
};
 
// C
foo_bar_t fb =
    {
        .foo = 0,
        .bar = 1,
    };
 
// D
foo_bar_t fb = {
                    .foo = 0,
                    .bar = 1,
               };

這邊的 struct 只是舉例,實務上如果成員這麼少的話從一開始就可以只寫成一行。這裡想表達的是成員很多、或成員名很長、或結構較複雜的 struct

這裡的 4 指的是我用 4 的空白做縮排為例,實際上到底是多少都不影響。

C. 統一初始化陣列

// A
int arr[1024] = {0};
 
// B
int arr[1024] =
    {
        0
    };

這裡同樣也適用於初始化簡單到可以寫成一行的 struct 的情況。

D. if-else

// A
if (foo) {
    foo = 0;
}
if (bar) {
    bar = 0;
}
if (foobar) {
    foobar = foo;
} else {
    foobar = bar;
}
if (fb) {
    fb = 0;
}
// B
if (foo)
{
    foo = 0;
}
if (bar)
{
    bar = 0;
}
if (foobar)
{
    foobar = foo;
}
else
{
    foobar = bar;
}
if (fb)
{
    fb = 0;
}

這個例子可能不是非常好,至少我在寫類似的程式時,會加一些空白行來分隔,不會讓它們就這樣黏著。但是在不加空白行的情況下,A 應該稍微更容易區分各個 if-else 的開頭和結尾。注意,A 的規則還是左右花括號都換行,但是這是指同一個語句(Statement),不同的 if-else 不會疊在一起。

E. 宣告函式原型

你現在已經先打好了一個函數,考慮現在要幫 foobar() 宣告函數原型…

// A
void foobar(int a, int b) {
    int foo = a;
    int bar = b;
}
 
// B
void foobar(int a, int b)
{
    int foo = a;
    int bar = b;
}

再考慮一下如果你一次寫了 5 個函數,現在要一次幫它們宣告函數原型。你已經把它們都整行複製到一起了,它們現在長這樣:

// A
int foobar1(void) {
uint16_t foobar2(int a) {
void foobar3(int a) {
uint16_t foobar4(int a, int b, const int *c) {
 
// B
int foobar1(void)
uint16_t foobar2(int a)
void foobar3(int a)
uint16_t foobar4(int a, int b, const int *c)

如果用 Vim 的話來說可能是…

不知道你覺得哪個操作比較快,我自己是更加熟悉 B 的那套操作,而且實際上要按的鍵數也比較少。看來這次是 B 的勝利。不過對於 Vim 的使用者來說,這個差距不是很大。

Coding Style

讓我們來看一下主流的 Coding style 長什麼樣。首先要注意的是,一般說的 K&R 有兩種:

  1. 第一種是網路上大家普遍討論時認為的 K&R,即所有左右花括號都不換行。以下姑且稱其為 llvm-K&R
  2. 第二種是 Linux kernel 所使用的風格,函數的左右花括號換行,其餘的左右花括號都不換行。以下稱其為 linux-K&R。這種才是 Kernighan & Ritchie 的書《The C Programming Language》中真正使用的風格。
// llvm-K&R
typedef struct {
    int foo;
    int bar;
} foo_bar_t;
 
int main(void) {
    foo_bar_t my_foobar = {
        .foo = 0,
        .bar = 1,
    };
 
    int arr[1024] = {0};
 
    if (b > 1) {
        b--;
    } else {
        b++;
    }
 
    while (1) {
        b++;
    }
}
// linux-K&R
typedef struct {
    int foo;
    int bar;
} foo_bar_t;
 
int main(void)
{
    foo_bar_t my_foobar = {
        .foo = 0,
        .bar = 1,
    };
 
    int arr[1024] = {0};
 
    if (b > 1) {
        b--;
    } else {
        b++;
    }
 
    while (1) {
        b++;
    }
}
// Allman
typedef struct
{
    int foo;
    int bar;
} foo_bar_t;
 
int main(void)
{
    foo_bar_t my_foobar = {
        .foo = 0,
        .bar = 1,
    };
 
    int arr[1024] = {0};
 
    if (b > 1)
    {
        b--;
    }
    else
    {
        b++;
    }
 
    while (1)
    {
        b++;
    }
}

嘗試歸納並描述它們的規則:

很明顯,llvm-K&R 的規則最簡單且毫無例外。linux-K&R 的規則比 llvm-K&R 多了一個唯一的例外。Allman 的規則就複雜多了。

總結

在實例比較時,我認為風格 A——所有的左右花括號都不換行——在多數情況下都是最好看、自然且直覺的,除了「宣告函式原型」的地方,因為它的操作要更多一點。而且風格 A 的規則也是最少的。

恩… K&R 一家親。至少我認為 K&R 風格是較具優勢的,無論是 llvm- 還是 linux-。

但是還有一點,在我的定義下,「看得順眼」的權重更高。你完全有理由只憑這點就推翻上面的所有比較,然後繼續用你最熟悉的 Coding style。當然你也有可能和我一樣,瞬間跳槽。

另外也可以看看我自己的完整規範:C 語言 Coding Style 規範

參考


C 語言 Coding Style 規範
簽署 commit 並設定 GitHub GPG Key 以驗證

留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)