內容目錄

Google 開源專案風格指南 (繁體中文版)

Note

聲明.

本專案並非 Google 官方專案,而是個人希望提供繁體中文的 Coding Style 文件而繁體化,感謝簡體中文的翻譯者。

在繁體化的過程中,此專案修改了兩岸差異的用語以及敘述方式,並針對目前英文版的修改再次進行翻譯,希望在持續修正的過程中,可以提供適合台灣本地參考的風格指南。

如果你關注的是 Google 官方英文版,請移步 Google Style Guide

每個較大的開源專案都有自己的風格指南: 關於如何為該項目編寫代碼的一系列約定 (有時候會比較武斷)。 當所有程式碼均保持一致的風格,在理解大型程式碼庫時更為輕鬆。

“風格” 的含義涵蓋範圍廣,從 “變量使用駝峰格式 (camelCase)” 到 “決不使用全局變量” 再到 “決不使用例外”。 英文版專案維護的是在 Google 使用的編程風格指南。如果你正在修改的專案源自 Google,你可能會被引導至 英文版專案頁面,以瞭解專案所使用的風格。

目前採用 reStructuredText 純文本標記語法, 並使用 Sphinx 生成 HTML / CHM / PDF 等文檔格式.

C++ 風格指南 - 內容目錄

0. 前言

版本:

4.45 (2016/1/28 翻譯中)

原作者:
Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray
翻譯:
專案網頁:

0.1 譯者前言

Google 經常會發佈一些開源專案,意味著會接受來自其他程式碼貢獻者的程式碼。但是如果程式碼貢獻者的程式撰寫風格與 Google 不一致,會給程式碼閱讀者和其他程式碼提交這造成不小的困擾。Google 因此發佈了這份自己的程式撰寫風格指南,使所有提交程式碼的人都能獲知 Google 的程式撰寫風格。

翻譯初衷:

規則的作用就是避免混亂。但規則本身一定要權威,有說服力,並且是理性的。我們所見過的大部分程式撰寫規範,其內容或不夠嚴謹,或闡述過於簡單,或帶有一定的武斷性。

Google 保持其一貫的嚴謹精神,5 萬漢字的指南涉及廣泛,論證嚴密。我們翻譯該系列指南的主因也正是其嚴謹性。嚴謹意味著指南的價值不僅僅局限於它羅列出的規範,更具參考意義的是它為了列出規範而做的謹慎權衡過程。

指南不僅列出你要怎麼做,還告訴你為什麼要這麼做,哪些情況下可以不這麼做,以及如何權衡其利弊。其他團隊未必要完全遵照指南亦步亦趨,如前面所說,這份指南是 Google 根據自身實際情況打造的,適用於其主導的開源專案。其他團隊可以參照該指南,或從中汲取靈感,建立適合自身實際情況的規範。

我們在翻譯的過程中,收穫頗多。希望本系列指南中文版對你同樣能有所幫助。

我們翻譯時也是盡力保持嚴謹,但水平所限,bug 在所難免。有任何意見或建議,可與我們取得聯繫。

中文版和英文版一樣,使用 Artistic License/GPL 開源許可。

繁體中文版修訂歷史:

  • 2016-02: @welkineins 為了撰寫公司內部的程式撰寫風格,將簡體中文版作為基礎將其翻譯為繁體中文,並且修正兩岸間的用語差異,同時也進行了一些細微的敘述調整。

簡體中文版修訂歷史:

  • 2015-08:熱心的清華大學同學 @lilinsanity 完善了「類」章節以及其它一些小章節。至此,對 Google CPP Style Guide 4.45 的翻譯正式竣工。

  • 2015-07 4.45:acgtyrant 為了學習 C++ 的規範,順便重新翻譯了本 C++ 風格指南,特別是 C++11 的全新內容。排版大幅度優化,翻譯措辭更地道,添加了新譯者筆記。Google 總部 C++ 工程師 innocentim,清華大學不願意透露姓名的唐馬儒先生,大阪大學大學院情報科學研究科計算機科學專攻博士 farseerfc 和其它 Arch Linux 中文社區眾幫了譯者不少忙,謝謝他們。因為 C++ Primer 尚未完全入門,暫時沒有翻譯「類」章節和其它一些小章節。

  • 2009-06 3.133:YuleFox 的 1.0 版已經相當完善,但原版在近一年的時間裡,其規範也發生了一些變化。

    Yang.Y 與 YuleFox 一拍即合,以專案的形式來延續中文版 : Google 開源專案風格指南 - 中文版專案

    主要變化是同步到 3.133 最新英文版本,做部分勘誤和改善可讀性方面的修改,並改進排版效果。Yang.Y 重新翻修,YuleFox 做後續評審。

  • 2008-07 1.0:出自 YuleFox 的 Blog,很多地方摘錄的也是該版本。

0.2 背景

C++ 是 Google 大部分開源專案的主要程式撰寫語言。正如每個 C++ 開發者都知道的,C++ 有很多強大的特性,但這種強大不可避免的導致它走向複雜,使程式碼更容易產生 bug,難以閱讀和維護。

本指南的目的是通過詳細闡述 C++ 注意事項來駕馭其複雜性。這些規則在保證程式碼易於管理的同時,也能高效使用 C++ 的語言特性。

風格,亦被稱作可讀性,也就是指導 C++ 程式撰寫的約定。使用術語 “風格” 有些用詞不當,因為這些習慣遠不止程式碼文件格式化這麼簡單。

Google 主導的開源專案均符合本指南的規定。

注意: 本指南並非 C++ 課程,我們假定讀者已經對 C++ 非常熟悉。

0.3 目標

使程式碼易於管理的方法之一是加強程式碼一致性。讓任何開發者都可以快速讀懂你的程式碼。保持統一程式撰寫風格並遵守約定意味著可以很容易根據 “模式匹配” 規則來推斷各種標識符的含義。創建通用、必需的習慣用語和模式可以使程式碼更容易被理解。在一些情況下可能有充分的理由改變某些程式撰寫風格,但我們還是應該遵循一致性原則,盡量不這麼做。

本指南的另一個觀點是 C++ 特性的臃腫。C++ 是一門包含大量高級特性的龐大語言。某些情況下,我們會限制甚至禁止使用某些特性。這麼做是為了保持程式碼清爽,避免這些特性可能導致的各種問題。指南中列舉了這類特性,並解釋為什麼這些特性被限制使用。

Tip

待翻譯

1. 標頭檔 (Header Files)

通常每一個 .cc 文件都有一個對應的 .h 文件。也有一些常見例外, 如單元測試程式碼和只引入 main() 函式的 .cc 文件。

正確使用標頭檔可令程式碼在可讀性、文件大小和編譯性能上大為改觀。

下面的規則將引導你規避使用標頭檔時的各種陷阱。

1.1. 自足的標頭檔

Tip

標頭檔應該能夠自給自足(self-contained),以 .h 結尾。至於用來插入其他檔案的文件,說到底它們並不是標頭檔,所以應以 .inc 結尾。但請不要以 -inl.h 的方式來撰寫標頭檔。

所有標頭檔要能夠自給自足。換言之,使用者和重構工具不需要為了使用一個標頭檔而引入額外更多的標頭檔。特別是,一個標頭檔應該要有 1.2. #define 保護 (#define Guard)、引入所有它所需要的其它標頭檔,並且不需要任何特別的 symbols 在 include 它前需要被定義。

只有少數的例外,一個標頭檔不是自我滿足的而是用來安插到程式碼某處裡。例如某些文件會被重複的 include 或是文件內容實際上是特定平台(platform-specific)擴展部分。這些文件就要用 .inc 文件擴展名。

如果 .h 文件宣告了一個樣板或內聯 (inline) 函式,同時也在該文件加以定義。凡是有用到這些的 .cc 文件,就得統統引入該標頭檔,否則程式可能會在構建中連結失敗。現在不要把這些定義放到分離的 -inl.h 文件裡了(譯者註:過去該規範曾提倡把定義放到 -inl.h 裡過)。

有個例外:如果某函式樣板為所有相關模板參數顯式實例化,或本身就是類別的一個私有成員,那麼它就只能定義在實例化該模板的 .cc 文件裡。

1.2. #define 保護 (#define Guard)

Tip

所有標頭檔都應該使用 #define 防止標頭檔被多次引入。建議的命名格式為 <PROJECT>_<PATH>_<FILE>_H_

為保證唯一性,標頭檔的命名應該依據所在專案的完整路徑。例如:專案 foo 中的標頭檔 foo/src/bar/baz.h 可按如下方式保護:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_

1.3. 前置宣告(Forward Declaration)

Tip

避免使用前置宣告,直接引入需要的標頭檔即可。

定義:
前置宣告是不提供與之關連的定義下,宣告一個類別、函式或是樣板。

優點:

  • 由於 #include 會強制編譯器開啟更多的檔案與處理更多的輸入,利用前置宣告減少 #include 可以減少編譯時間。
  • 越多的 #include 代表程式碼更可能因為相依的標頭檔更動而被重新編譯,使用前置宣告可以節省不必要的重新編譯。

缺點:

  • 前置宣告可能隱藏掉與標頭檔間的相依關係,導致當標頭檔改變時,相依的程式碼沒有被重新編譯。
  • 前置宣告可能在函式庫進行可向下相容的 API 改動時發生編譯錯誤。例如函式庫開發者放寬了某個參數類型、替樣板增加預設參數或更改命名空間等等。
  • 前置宣告來自 std:: 命名空間的 symbols 會導致餵定義行為 (undefined behavior)。
  • 難以抉擇是要使用前置宣告或是引入完整得標頭檔。在某些狀況下,使用前置宣告替換掉 #include 可能意外的修改了程式碼的意圖。若 #include 被替換成 B 和 D 的前置宣告 test() 會呼叫到 f(void*)
  • 使用前置宣告多個 symbols 可能暴露了比直接引入標頭檔更多的訊息。
  • 為了使用前置宣告而修改程式碼(例如:使用指標成員而不是物件成員) 可能會導致程式運作較為緩慢或是更加的複雜。

結論:

  • 在任何狀況下避免使用前置宣告。
  • 當在標頭檔內使用到函式宣告時,總是引入對應的標頭檔。
  • 當使用類別樣板時,建議引入對應得標頭檔。

至於什麼時候引入標頭檔,參見 name-and-order-of-includes

1.4. 內聯函式 (Inline Functions)

Tip

只有當函式非常的短,例如只有 10 行甚至更少的時候,才將其定義為內聯函式。

定義:

當函式被宣告為內聯函式之後,代表你允許編譯器將其展開在該函式被呼叫的位置,而不是原來的函式呼叫機制進行。

優點:

當函式主體比較小的時候,內聯該函式可以產生更有效率目標程式碼 (object code)。對於存取函式 (accessors)、賦值函式 (mutators) 以及其它函式體比較短或性能關鍵的函式,可以依據需求將其轉為內聯函式。

缺點:

濫用內聯反而會導致程式變慢。內聯可能使目標程式碼變大或變小,這取決於內聯函式主體的大小。一個非常短小的存取函式被內聯通常會減少目標程式碼的大小, 但內聯一個相當大的函式將戲劇性的增加目標程式碼大小。現代的處理器 (CPU) 具備有指令緩存 (instruction cache),執行小巧的程式碼往往執行更快。

結論:

一個較為合理的經驗準則是,不要內聯超過 10 行的函式。謹慎對待解構子, 解構子往往比其表面看起來要更長, 因為有隱含的成員和父類別解構子被呼叫!

另一個實用的經驗準則: 內聯那些包含循環或 switch 語句的函式常常是得不償失的 (除非在大多數情況下, 這些循環或 switch 語句從不被執行)。

要注意的是,既使函式即使宣告為內聯,也不一定會被編譯器內聯。例如虛函式 (virtual) 和遞迴函式 (recursive) 就不會被正常內聯。通常, 遞迴函式不應該宣告成內聯函式。(譯註: 遞迴呼叫堆棧的展開並不像循環那麼簡單,例如遞迴層數在編譯時可能是未知的,大多數編譯器都不支持內聯遞迴函式)。虛函式內聯的主要原因則是想把它的函式主體放在類別的定義內, 可能式為了方便,或是當作文件描述其行為。例如存取函式或賦值函式。

1.5. #include 的路徑及順序

Tip

使用以下標準的標頭檔引入順序可增強可讀性,同時避免隱藏相依性:相關標頭檔 > C 函式庫 > C++ 函式庫 > 其他函式庫的 .h > 專案內的 .h

專案內的標頭檔應按照專案目錄樹結構排列,避免使用 UNIX 特殊的目錄捷徑 . (當前目錄) 或 .. (上層目錄)。例如:google-awesome-project/src/base/logging.h 應該按如下方式引入:

#include "base/logging.h"

另一個例子是,若 dir/foo.ccdir/foo_test.cc 的主要作用是實作或測試 dir2/foo2.h 的功能,foo.cc 中引入標頭檔的次序應如下:

  1. dir2/foo2.h
  2. C 系統文件
  3. C++ 系統文件
  4. 其他函式庫的 .h 文件
  5. 此專案內 .h 文件

使用這種排序方式,若 dir2/foo2.h 忽略了任何需要的標頭檔,在編譯 dir/foo.ccdir/foo_test.cc 就會發生錯誤。因此這個規則可以確這些功能的保開發者可以在第一時間就發現錯誤,而不是波擊到無辜的人或是其他專案。

dir/foo.ccdir2/foo2.h 通常位於同一目錄下 (如 base/basictypes_test.ccbase/basictypes.h),但也可以放在不同目錄下。

標頭檔的順序在依照類別分類後,同類別的引入順序則應該依照按字母順序排列。若現有程式碼不是按照這個規則,應該在有空閒的時間將其修正。

你所依賴的 symbols 被哪些標頭檔所定義,你就應該引入(include)那些標頭檔,較為罕見的 forward-declaration 情況除外。例如你要用到 bar.h 中的某個 symbol,哪怕你所引入的 foo.h 已經引入了 bar.h,你也應顯示的引入 bar.h,除非 foo.h 有明確說明它會向你提供 bar.h 中的 symbol。不過,cc 文件所對應的標頭檔引入的其他標頭檔,就不需要再重複引入進其 cc 文件裡面了。例如 foo.cc 只引入 foo.h 就夠了,不用再引入 foo.h 中的其它內容。

舉例來說,google-awesome-project/src/foo/internal/fooserver.cc 的引入次序如下:

#include "foo/public/fooserver.h"

#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

例外:

有時,平台特定(system-specific)的程式碼需要依據條件被引入(conditional includes),這些程式碼可以放到其它的 includes 之後。當然,盡量讓你的平台特定程式碼小 (small) 且集中 (localized),例如:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

2. 作用域 (Scoping)

2.1. 命名空間 (Namespaces)

Tip

除了少數的例外,都建議使用把程式碼放在命名空間內。一個具名的命名空間應該擁有唯一的名字,其名稱可基於專案名稱,甚至是相對路徑。而在 .cc 文件內,使用匿名的命名空間是推薦的,但禁止使用 using 指示(using-directives)和內聯命名空間(inline namespaces)。

定義:

命名空間將全域作用域細分為獨立的,具名的作用域可有效防止全域作用域的命名衝突。

優點:

命名空間可以在大型專案內避免名稱衝突,同時又可以讓多數的程式碼有合理簡短的名稱。

舉例來說, 兩個不同專案的全域作用域都有一個類別 Foo,這樣在編譯或運行時期會造成衝突。如果每個專案將程式碼置於不同命名空間中,project1::Fooproject2::Foo 在專案中就可以被視為不同的 symbols 而不會發生衝突。兩個類別在各自的命名空間中,也可以繼續使用 Foo 而不需要前綴命名空間。

內聯命名空間會自動把內部的標識符放到外層作用域,比如:

namespace X {
inline namespace Y {
void foo();
}
}

X::Y::foo()X::foo() 彼此可以互換使用。內聯命名空間主要用來保持跨版本的 ABI 相容性。

缺點:

命名空間可能造成疑惑,因為它增加了識別一個名稱所代表的意涵的難度。例如: Foo 是命名空間或是一個類別。

內聯命名空間更是容易令人疑惑,因為它並不完全符合命名空間的定義;內聯命名空間只在大型版本控制裡會被使用到。

在標頭檔中使用匿名命名空間容易導致違背 C++ 的唯一定義原則 (One Definition Rule (ODR))。

在某些狀況中,經常會需要重複的使用完整 (fully-qualified) 的名稱來參考某些 symbols。對於多層巢狀的命名空間,這會增加許多混亂。

結論:

根據下文將要提到的策略合理使用命名空間。記得在命名空間的結尾使用註解進行標示,例如下一節的範例程式一樣。
2.1.1. 匿名命名空間 (Unnamed Namespaces)
  • .cc 文件中, 可以使用匿名命名空間,甚至鼓勵使用來避免連結時期 (link time) 的命名衝突:

    namespace {                             // .cc 文件中
    
    // 命名空間的內容無需縮排
    
    // 此函式產生出來的 symbol 保證不會和連結時期的其他 symbol 相撞。且此 symbol
    // 只能在這個 .cc 文件中被看到。
    bool UpdateInternals(Frobber* f, int newval) {
      ...
    }
    
    } // namespace
    
  • 不要在 .h 文件中使用匿名命名空間。

2.1.2. 具名的命名空間

具名的命名空間使用方式如下:

  • includegflags 的宣告/定義, 以及類別的前置宣告後,把整個原始碼文件剩下部分放置在命名空間內,以區別於其它命名空間:

    // .h 文件
    namespace mynamespace {
    
    // 所有宣告都置於命名空間中
    // 注意不要使用縮排
    class MyClass {
        public:
        …
        void Foo();
    };
    
    } // namespace mynamespace
    
    // .cc 文件
    namespace mynamespace {
    
    // 函式定義都置於命名空間中
    void MyClass::Foo() {
        …
    }
    
    } // namespace mynamespace
    

    通常的 .cc 文件包含更多,更複雜的細節,例如 flags 或 using-declarations。

    #include "a.h"
    
    DEFINE_bool(someflag, false, "dummy flag");
    
    using ::foo::bar;
    
    namespace a {
    
    …code for a…
    
    } // namespace a
    
  • 不要在命名空間 std 內宣告任何東西,包括標準函式庫的類別前置宣告。在 std 命名空間宣告實體 (entities) 會導致不確定行為 (undefined behavior)。比如不可移植。要宣告標準函式庫內的實體,直接 ``include ``對應的標頭檔。

  • 最好不要使用 using-directive 來導出一個命名空間下的所有名稱。

    // 禁止 —— 這會污染命名空間
    using namespace foo;
    
  • 不要在標頭檔中的一個命名空間的作用域內使用命名空間別名 (Namespace aliases),除非該命名空間僅限於內部使用 (internal-only)。因為此操作會導致這些外部引入的東西變成此標頭檔公開的 API 的一部份。

    // 在 .cc 文件裡可以用來縮短常使用的名稱
    namespace fbz = ::foo::bar::baz;
    
    // 在 .h 文件裡可以用來縮短常使用的名稱
    namespace librarian {
    namespace impl { // 內部使用,不公開於 API
    namespace sidetable = ::pipeline_diagnostics::sidetable;
    } // namespace impl
    
    inline void my_inline_function() {
      // 命名空間別名到一個函式或方法
      namespace fbz = ::foo::bar::baz;
      ...
    }
    }  // namespace librarian
    
  • 禁止用內聯命名空間

2.2. 非成員函式、靜態成員函式和全域函式

Tip

建議將非成員函式放置在命名空間中,盡量不要使用完全的全域函示。建議利用命名空間來放置相關的多個函式,而不是放置在把類別當作命名空間來使用。類別的靜態方法一般來說要和類別的實例或類別的靜態資料有緊密的關連。

優點:

某些情況下,非成員函式和靜態成員函式是非常有用的。將非成員函式放在命名空間內可避免對於全域作用域污染。

缺點:

為非成員函式和靜態成員函式準備一個新的類別可能更有意義,特別是它們需要存取外部資源或式有大量的相依性關係時。

結論:

有時候定義一個不綁定特定類別實例的函式是有用的,甚至是必要的。這樣的函式可以被定義成靜態成員或是非成員函式。非成員函式不應該依賴於外部變數,且應該總是放置於某個命名空間內。相比單純為了封裝不共享任何靜態數據的靜態成員函式而創建一個類別,不如之直接使用 2.1. 命名空間 (Namespaces)。例如對於 myproject/foo_bar.h 標頭擋來說,可以這樣寫。
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
}
}

而不是

namespace myproject {
class FooBar {
 public:
  static void Function1();
  static void Function2();
};
}

如果你必須定義非成員函式,又只是在 .cc 文件中使用它,則可使用匿名:ref:namespaces`或 ``static` 連結關鍵字 (例如: static int Foo() {...}) 限定其作用域。

2.3. 區域變數

Tip

盡可能將函式內的變數的作用域最小化,並在變量宣告時進行初始化。

C++ 允許在函式內的任何位置宣告變數。我們鼓勵在盡可能小的作用域中宣告變量,並且離第一次使用的地方越近越好。這會讓閱讀者更容易找到變數宣告的位置、宣告的類型和初始值。要注意,應該該宣告時直接初始化變數,而不要先代宣告再後賦值, 例如:

int i;
i = f(); // 不推薦 -- 初始化和宣告分離
int j = g(); // 推薦 -- 宣告時初始化
vector<int> v;
v.push_back(1); // 用下面的括號初始化法會更好
v.push_back(2);
vector<int> v = {1, 2}; // 推薦 v 在宣告時初始化

if``while``和``for``陳述句需要的變數一般都會宣告在這些陳述句中,也就是這些變數會存活於這些作用域內。例如:

while (const char* p = strchr(str, '/')) str = p + 1;

一個特例:如果變數是一個物件,每次進入作用域時其建構子都會被呼叫,每次離開作用域時其解構子都會被呼叫。

// 沒效率的實作
for (int i = 0; i < 1000000; ++i) {
    Foo f; // 建構子和解構子分別呼叫 1000000 次。
    f.DoSomething(i);
}

在循環作用域外面宣告這類型的變數可能更加的有效率。

Foo f; // 建構子和解構子只呼叫 1 次
for (int i = 0; i < 1000000; ++i) {
    f.DoSomething(i);
}

2.4. 靜態和全域變數

禁止使用具有 靜態生存週期 (static strage duration) 的類別函式:它們會因為不確定的建構和解構子呼叫順序而產生難以發現的臭蟲。不過 constexpr 變數除外,因為它們不牽涉到動態初始化或解構。

全域變數、靜態變數、靜態類別成員變數和函式內靜態變數等都具有靜態生存週期,都必須是原始資料類型 (POD : Plain Old Data):即 int、chars 、floats 或前三者的指標、陣列和結構體。

靜態變數的建構子、解構子和初始化的順序在 C++ 中規範並不完整,甚至可能每次建置都不同,進而導致難以發現的臭蟲。所以除了禁用類別的全域變量外,也不允許使用函式的返回值來初始化靜態變數,除非該函式(比如 getenv() 或 getpid())不涉及任何全域變數。但是函式作用域裡的 POD 變數則可以使用函式返回值來初始化,畢竟它的初始化順序是有明確定義的,而且只會在程式流程執行到它的宣告時才會發生。)

同理,全域和靜態變數在程式結束時會被解構,無論所謂結束是從 main() 返回還是呼叫了 exit()。解構的順序被定義為其建構子呼叫順序的反序。但既然建構順序未定義,那麼解構順序當然也就不確定了。例如,在程式結束時某個靜態變數已經被解構了,但程式還在運行,這時可能有另一個執行緒嘗試要存取這個變數卻失敗了;另一個例子,一個靜態的 string 變量也許會在一個參考它的的其它變數被解構之前被解構掉。

解決以上解構順序問題的方法之一是用 quick_exit() 來代替 exit() 來結束程式。它們的不同之處是前者不會執行任何解構子,也不會執行 atexit() 所註冊的任何 handlers。如果在使用 quick_exit() 來結束成式時仍然想要執行 handler(例如 flush log),你可以使用 _at_quick_exit() 來註冊 handler。(如果你想在 exit()quick_exit() 都用使用該 handler,則可以將其註冊到這兩個函式。

綜上所述,我們只允許 POD 類型的靜態變數,這代表完全禁用 vector (使用 C 陣列替代) 和 string (使用 const char [])。

如果你真的需要一個 class 類型的靜態或全域變數,可以考慮在 main() 函式或 pthread_once() 內初始化一個指標且永不回收 (free)。注意只能用 raw 指標,別用智慧指標,因為後者的解構子涉及到上文指出的不定順序問題。

3. 類別 (Classes)

類別是 C++ 中程式碼的基本單元。想當然爾, 在程式中類別將被廣泛使用。本節列舉了在撰寫一個類別時該做的和不該做的事項.

3.1. 建構子的職責

Tip

不要在建構子中進行複雜的初始化 (尤其是那些有可能失敗或者需要呼叫虛函式的初始化).

定義:

在建構子體中進行初始化操作.

優點:

排版方便, 無需擔心類是否已經初始化.

缺點:

在建構子中執行操作引起的問題有:

  • 建構子中很難上報錯誤, 不能使用例外.
  • 操作失敗會造成對象初始化失敗,進入不確定狀態.
  • 如果在建構子內呼叫了自身的虛函式, 這類呼叫是不會重定向到子類的虛函式實作. 即使當前沒有子類化實作, 將來仍是隱患.
  • 如果有人創建該類型的全域變數 (雖然違背了上節提到的規則), 建構子將先 main() 一步被呼叫, 有可能破壞建構函式中暗含的假設條件. 例如, gflags 尚未初始化.

結論:

建構子不得呼叫虛函式, 或嘗試報告一個非致命錯誤. 如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法或使用工廠模式.

3.2. 初始化

Tip

如果類中定義了成員變數, 則必須在類中為每個類提供初始化函式或定義一個建構子. 若未宣告建構函式, 則編譯器會生成一個默認的構造函式, 這有可能導致某些成員未被初始化或被初始化為不恰當的值.

定義:

new 一個不帶參數的類對象時, 會呼叫這個類的默認建構子. 用 new[] 創建陣列時, 默認建構函式則總是被呼叫. 在類成員裡面進行初始化是指宣告一個成員變數的時候使用一個結構例如 int _count = 17 或者 string _name{"abc"} 來替代 int _count 或者 string _name 這樣的形式.

優點:

使用者定義的默認建構子將在沒有提供初始化操作時將對象初始化. 這樣就保證了對象在被建構之時就處於一個有效且可用的狀態, 同時保證了對象在被創建時就處於一個顯然”不可能”的狀態, 以此幫助調試.

缺點:

對程式碼編寫者來說, 這是多餘的工作.

如果一個成員變數在宣告時初始化又在建構子中初始化, 有可能造成混亂, 因為建構函式中的值會覆蓋掉宣告中的值.

結論:

簡單的初始化用類成員初始化完成, 尤其是當一個成員變數要在多個建構子里用相同的方式初始化的時候.

如果你的類中有成員變數沒有在類裡面進行初始化, 而且沒有提供其它建構子, 你必須定義一個 (不帶參數的) 默認建構函式. 把對象的內部狀態初始化成一致 / 有效的值無疑是更合理的方式.

這麼做的原因是: 如果你沒有提供其它建構子, 又沒有定義默認建構函式, 編譯器將為你自動生成一個. 編譯器生成的構造函式並不會對對象進行合理的初始化.

如果你定義的類繼承現有類, 而你又沒有增加新的成員變數, 則不需要為新類定義默認建構子.

3.3. 顯式建構子

Tip

對單個參數的建構子使用 C++ 關鍵字 explicit.

定義:

通常, 如果建構子只有一個參數, 可看成是一種隱式轉換. 打個比方, 如果你定義了 Foo::Foo(string name), 接著把一個字符串傳給一個以 Foo 對象為參數的函式, 建構函式 Foo::Foo(string name) 將被呼叫, 並將該字符串轉換為一個 Foo 的臨時對像傳給呼叫函式. 看上去很方便, 但如果你並不希望如此通過轉換生成一個新對象的話, 麻煩也隨之而來. 為避免構造函式被呼叫造成隱式轉換, 可以將其宣告為 explicit.

除單參數建構子外, 這一規則也適用於除第一個參數以外的其他參數都具有默認參數的建構函式, 例如 Foo::Foo(string name, int id = 42).

優點:

避免不合時宜的變換.

缺點:

結論:

所有單參數建構子都必須是顯式的. 在類定義中, 將關鍵字 explicit 加到單參數建構函式前: explicit Foo(string name);

例外: 在極少數情況下, 拷貝建構子可以不宣告成 explicit. 作為其它類的透明包裝器的類也是特例之一. 類似的例外情況應在註解中明確說明.

最後, 只有 std::initializer_list 的建構子可以是非 explicit, 以允許你的類型結構可以使用列表初始化的方式進行賦值. 例如:

MyType m = {1, 2};
MyType MakeMyType() { return {1, 2}; }
TakeMyType({1, 2});

3.4. 可拷貝類型和可移動類型

Tip

如果你的類型需要, 就讓它們支持拷貝 / 移動. 否則, 就把隱式產生的拷貝和移動函式禁用.

定義:

可拷貝類型允許對象在初始化時得到來自相同類型的另一對象的值, 或在賦值時被賦予相同類型的另一對象的值, 同時不改變源對象的值. 對於使用者定義的類型, 拷貝操作一般通過拷貝建構子與拷貝賦值操作符定義. string 類型就是一個可拷貝類型的例子.

可移動類型允許對象在初始化時得到來自相同類型的臨時對象的值, 或在賦值時被賦予相同類型的臨時對象的值 (因此所有可拷貝對象也是可移動的). std::unique_ptr<int> 就是一個可移動但不可複製的對象的例子. 對於使用者定義的類型, 移動操作一般是通過移動建構子和移動賦值操作符實作的.

拷貝 / 移動建構子在某些情況下會被編譯器隱式呼叫. 例如, 通過傳值的方式傳遞對象.

優點:

可移動及可拷貝類型的對象可以通過傳值的方式進行傳遞或者返回, 這使得 API 更簡單, 更安全也更通用. 與傳指標和引用不同, 這樣的傳遞不會造成所有權, 生命週期, 可變性等方面的混亂, 也就沒必要在協議中予以明確. 這同時也防止了客戶端與實作在非作用域內的交互, 使得它們更容易被理解與維護. 這樣的對象可以和需要傳值操作的通用 API 一起使用, 例如大多數容器.

拷貝 / 移動建構子與賦值操作一般來說要比它們的各種替代方案, 比如 Clone(), CopyFrom() or Swap(), 更容易定義, 因為它們能通過編譯器產生, 無論是隱式的還是通過 = 默認. 這種方式很簡潔, 也保證所有數據成員都會被複製. 拷貝與移動建構函式一般也更高效, 因為它們不需要堆的分配或者是單獨的初始化和賦值步驟, 同時, 對於類似省略不必要的拷貝這樣的優化它們也更加合適.

移動操作允許隱式且高效地將源數據轉移出右值對象. 這有時能讓程式碼風格更加清晰.

缺點:

許多類型都不需要拷貝, 為它們提供拷貝操作會讓人迷惑, 也顯得荒謬而不合理. 為父類別提供拷貝 / 賦值操作是有害的, 因為在使用它們時會造成對象切割. 默認的或者隨意的拷貝操作實作可能是不正確的, 這往往導致令人困惑並且難以診斷出的錯誤.

拷貝建構子是隱式呼叫的, 也就是說, 這些呼叫很容易被忽略. 這會讓人迷惑, 尤其是對那些所用的語言約定或強制要求傳引用的開發者來說更是如此. 同時, 這從一定程度上說會鼓勵過度拷貝, 從而導致性能上的問題.

結論:

如果需要就讓你的類型可拷貝 / 可移動. 作為一個經驗法則, 如果對於你的使用者來說這個拷貝操作不是一眼就能看出來的, 那就不要把類型設置為可拷貝. 如果讓類型可拷貝, 一定要同時給出拷貝建構子和賦值操作的定義. 如果讓類型可拷貝, 同時移動操作的效率高於拷貝操作, 那麼就把移動的兩個操作 (移動建構函式和賦值操作) 也給出定義. 如果類型不可拷貝, 但是移動操作的正確性對用戶顯然可見, 那麼把這個類型設置為只可移動並定義移動的兩個操作.

建議通過 = default 定義拷貝和移動操作. 定義非默認的移動操作目前需要例外. 時刻記得檢測默認操作的正確性. 由於存在對象切割的風險, 不要為任何有可能有派生類的對象提供賦值操作或者拷貝 / 移動建構子 (當然也不要繼承有這樣的成員函式的類). 如果你的父類別需要可複製屬性, 請提供一個 public virtual Clone() 和一個 protected 的拷貝建構函式以供派生類實作.

如果你的類不需要拷貝 / 移動操作, 請顯式地通過 = delete 或其他手段禁用之.

3.5. 委派和繼承建構子

Tip

在能夠減少重複程式碼的情況下使用委派和繼承建構子.

定義:

委派和繼承建構子是由 C++11 引進為了減少建構函式重複程式碼而開發的兩種不同的特性. 通過特殊的初始化列表語法, 委派構造函式允許類的一個構造函式呼叫其他的構造函式. 例如:

X::X(const string& name) : name_(name) {
  ...
}

X::X() : X("") { }

繼承建構子允許派生類直接呼叫父類別的建構函式, 一如繼承父類別的其他成員函式, 而無需重新宣告. 當父類別擁有多個構造函式時這一功能尤其有用. 例如:

class Base {
 public:
  Base();
  Base(int n);
  Base(const string& s);
  ...
};

class Derived : public Base {
 public:
  using Base::Base;  // Base's constructors are redeclared here.
};

如果派生類的建構子只是呼叫父類別的建構函式而沒有其他行為時, 這一功能特別有用.

優點:

委派和繼承建構子可以減少冗余程式碼, 提高可讀性. 委派建構子對 Java 開發者來說並不陌生.

缺點:

使用輔助函式可以預估出委派建構子的行為. 如果派生類和父類別相比引入了新的成員變數, 繼承建構子就會讓人迷惑, 因為父類別並不知道這些新的成員變量的存在.

結論:

只在能夠減少冗余程式碼, 提高可讀性的前提下使用委派和繼承建構子. 如果派生類有新的成員變數, 那麼使用繼承建構函式時要小心. 如果在派生類中對成員變量使用了類內部初始化的話, 繼承構造函式還是適用的.

3.6. 結構體 VS. 類

Tip

僅當只有數據時使用 struct, 其它一概使用 class.

說明:

在 C++ 中 struct 和 class 關鍵字幾乎含義一樣. 我們為這兩個關鍵字添加我們自己的語義理解, 以便未定義的數據類型選擇合適的關鍵字.

struct 用來定義包含數據的被動式對象, 也可以包含相關的常數, 但除了存取數據成員之外, 沒有別的函式功能. 並且存取功能是通過直接訪問位域, 而非函式呼叫. 除了建構子, 解構子, Initialize(), Reset(), Validate() 等類似的函式外, 不能提供其它功能的函式.

如果需要更多的函式功能, class 更適合. 如果拿不準, 就用 class.

為了和 STL 保持一致, 對於仿函式和 trait 特性可以不用 class 而是使用 struct.

注意: 類和結構體的成員變數使用不同的命名規則.

3.7. 繼承

Tip

使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <<Design Patterns>> 裡反覆強調的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為 public 繼承.

定義:

當子類繼承父類別時, 子類包含了父類別所有數據及操作的定義. C++ 實踐中, 繼承主要用於兩種場合: 實作繼承 (implementation inheritance), 子類繼承父類別的實作程式碼; 介面繼承 (interface inheritance), 子類僅繼承父類的方法名稱.

優點:

實作繼承通過原封不動的復用父類別程式碼減少了程式碼量. 由於繼承是在編譯時宣告, 開發者和編譯器都可以理解相應操作並發現錯誤. 從程式撰寫角度而言, 介面繼承是用來強制類輸出特定的 API. 在類沒有實作 API 中某個必須的方法時, 編譯器同樣會發現並報告錯誤.

缺點:

對於實作繼承, 由於子類的實作程式碼散佈在父類別和子類間之間, 要理解其實作變得更加困難. 子類不能重寫父類的非虛函式, 當然也就不能修改其實作. 父類別也可能定義了一些數據成員, 還要區分父類別的實際佈局.

結論:

所有繼承必須是 public 的. 如果你想使用私有繼承, 你應該替換成把父類別的實例作為成員對象的方式.

不要過度使用實作繼承. 組合常常更合適一些. 盡量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 Bar 的確 “是一種” Foo, Bar 才能繼承 Foo.

必要的話, 解構子宣告為 virtual. 如果你的類有虛函式, 則解構子也應該為虛函式. 注意 數據成員在任何情況下都必須是私有的.

當重載一個虛函式, 在衍生類中把它明確的宣告為 virtual. 理論依據: 如果省略 virtual 關鍵字, 程式碼閱讀者不得不檢查所有父類別, 以判斷該函式是否是虛函式.

3.8. 多重繼承

Tip

真正需要用到多重實作繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個父類別是非抽像類; 其它父類別都是以 Interface 為後綴的 純介面類.

定義:

多重繼承允許子類擁有多個父類別. 要將作為 純介面 的父類別和具有 實作 的父類別區別開來.

優點:

相比單繼承 (見 繼承), 多重實作繼承可以復用更多的程式碼.

缺點:

真正需要用到多重 實作 繼承的情況少之又少. 多重實作繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.

結論:

只有當所有父類別除第一個外都是 純介面類 時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以 Interface 為後綴.

Note

關於該規則, Windows 下有個 特例.

3.9. 介面

Tip

介面是指滿足特定條件的類, 這些類以 Interface 為後綴 (不強制).

定義:

當一個類滿足以下要求時, 稱之為純介面:

  • 只有純虛函式 (“=0”) 和靜態函式 (除了下文提到的解構子).
  • 沒有非靜態數據成員.
  • 沒有定義任何建構子. 如果有, 也不能帶有參數, 並且必須為 protected.
  • 如果它是一個子類, 也只能從滿足上述條件並以 Interface 為後綴的類繼承.

介面類不能被直接實例化, 因為它宣告了純虛函式. 為確保接口類的所有實作可被正確銷毀, 必須為之宣告虛解構子 (作為上述第 1 條規則的特例, 解構子不能是純虛函式). 具體細節可參考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 節.

優點:

Interface 為後綴可以提醒其他人不要為該介面類增加函式實作或非靜態數據成員. 這一點對於 多重繼承 尤其重要. 另外, 對於 Java 開發者來說, 接口的概念已是深入人心.

缺點:

Interface 後綴增加了類名長度, 為閱讀和理解帶來不便. 同時,介面特性作為實作細節不應暴露給使用者.

結論:

只有在滿足上述需要時, 類才以 Interface 結尾, 但反過來, 滿足上述需要的類未必一定以 Interface 結尾.

3.10. 運算子重載

Tip

除少數特定環境外,不要重載運算子.

定義:

一個類可以定義諸如 +/ 等運算子, 使其可以像內建類型一樣直接操作.

優點:

使程式碼看上去更加直觀, 類表現的和內建類型 (如 int) 行為一致. 重載運算子使 Equals(), Add() 等函式名黯然失色. 為了使一些樣板函式正確工作, 你可能必須定義操作符.

缺點:

雖然操作符重載令程式碼更加直觀, 但也有一些不足:

  • 混淆視聽, 讓你誤以為一些耗時的操作和操作內建類型一樣輕巧.
  • 更難定位重載運算子的呼叫點, 查找 Equals() 顯然比對應的 == 呼叫點要容易的多.
  • 有的運算子可以對指標進行操作, 容易導致 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 對於二者, 編譯器都不會報錯, 使其很難調試;

重載還有令你吃驚的副作用. 比如, 重載了 operator& 的類不能被前置宣告.

結論:

一般不要重載運算子. 尤其是賦值操作 (operator=) 比較詭異, 應避免重載. 如果需要的話, 可以定義類似 Equals(), CopyFrom() 等函式.

然而, 極少數情況下可能需要重載運算子以便與樣板或 “標準” C++ 類互操作 (如 operator<<(ostream&, const T&)). 只有被證明是完全合理的才能重載, 但你還是要盡可能避免這樣做. 尤其是不要僅僅為了在 STL 容器中用作鍵值就重載 operator==operator<; 相反, 你應該在宣告容器的時候, 創建相等判斷和大小比較的仿函式類型.

有些 STL 算法確實需要重載 operator== 時, 你可以這麼做, 記得別忘了在文檔中說明原因.

參考 拷貝建構子函式重載.

3.11. 存取控制

Tip

所有 數據成員宣告為 private, 並根據需要提供相應的存取函式. 例如, 某個名為 foo_ 的變數, 其取值函式是 foo(). 還可能需要一個賦值函式 set_foo().

特例是, 靜態常數數據成員 (一般寫做 kFoo) 不需要是私有成員.

一般在標頭檔中把存取函式定義成內聯函式.

參考 繼承函式命名

3.11. 宣告順序

Tip

在類中使用特定的宣告順序: public:private: 之前, 成員函式在數據成員 (變數) 前;

類的訪問控制區段的宣告順序依次為: public:, protected:, private:. 如果某區段沒內容, 可以不宣告.

每個區段內的宣告通常按以下順序:

  • typedefs 和列舉
  • 常數
  • 建構子
  • 解構子
  • 成員函式, 含靜態成員函式
  • 數據成員, 含靜態數據成員

友元宣告應該放在 private 區段. 如果用巨集 DISALLOW_COPY_AND_ASSIGN 禁用拷貝和賦值, 應當將其置於 private 區段的末尾, 也即整個類宣告的末尾. 參見可拷貝類型和可移動類型.

.cc 文件中函式的定義應盡可能和宣告順序一致.

不要在類定義中內聯大型函式. 通常, 只有那些沒有特別意義或性能要求高, 並且是比較短小的函式才能被定義為內聯函式. 更多細節參考 內聯函式.

3.12. 編寫簡短函式

Tip

傾向編寫簡短, 凝練的函式.

我們承認長函式有時是合理的, 因此並不硬性限制函式的長度. 如果函式超過 40 行, 可以思索一下能不能在不影響程式結構的前提下對其進行分割.

即使一個長函式現在工作的非常好, 一旦有人對其修改, 有可能出現新的問題. 甚至導致難以發現的 bug. 使函式盡量簡短, 便於他人閱讀和修改程式碼.

在處理程式碼時, 你可能會發現複雜的長函式. 不要害怕修改現有程式碼: 如果證實這些程式碼使用 / 調試困難, 或者你需要使用其中的一小段程式碼, 考慮將其分割為更加簡短並易於管理的若干函式.

譯者 (YuleFox) 筆記

  1. 不在建構子中做太多邏輯相關的初始化;
  2. 編譯器提供的默認建構子不會對變數進行初始化, 如果定義了其他建構函式, 編譯器不再提供, 需要編碼者自行提供默認構造函式;
  3. 為避免隱式轉換, 需將單參數建構子宣告為 explicit;
  4. 為避免拷貝建構子, 賦值操作的濫用和編譯器自動生成, 可將其宣告為 private 且無需實作;
  5. 僅在作為數據集合時使用 struct;
  6. 組合 > 實作繼承 > 介面繼承 > 私有繼承, 子類重載的虛函式也要宣告 virtual 關鍵字, 雖然編譯器允許不這樣做;
  7. 避免使用多重繼承, 使用時, 除一個父類別含有實作外, 其他父類別均為純介面;
  8. 介面類類名以 Interface 為後綴, 除提供帶實作的虛解構子, 靜態成員函式外, 其他均為純虛函式, 不定義非靜態數據成員, 不提供建構子, 提供的話,宣告為 protected;
  9. 為降低複雜性, 盡量不重載操作符, 樣板, 標準類中使用時提供文檔說明;
  10. 存取函式一般內聯在標頭檔中;
  11. 宣告次序: public -> protected -> private;
  12. 函式體盡量短小, 緊湊, 功能單一;

4. 來自 Google 的奇技

Google 用了很多自己實作的技巧 / 工具使 C++ 程式碼更加健壯, 我們使用 C++ 的方式可能和你在其它地方見到的有所不同.

4.1. 所有權與智慧指標

Tip

動態分配出的對象最好有單一且固定的所有主(onwer), 且通過智慧指標傳遞所有權(ownership).

定義:

所有權是一種登記/管理動態內存和其它資源的技術。動態分配出的對象的所有主是一個對象或函式,後者負責確保當前者無用時就自動銷毀前者。所有權有時可以共享,那麼就由最後一個所有主來負責銷毀它。甚至也可以不用共享,在程式碼中直接把所有權傳遞給其它對象。

其實你可以把智慧指標當成一個重載了 *-> 的「對象」來看。智能指針類型被用來自動化所有權的登記工作,來確保執行銷毀義務到位。std::unique_ptr 是 C++11 新推出的一種智能指針類型,用來表示動態分配出的對象的「獨一無二」所有權;當 std::unique_ptr 離開作用域,對象就會被銷毀。不能複製 std::unique_ptr, 但可以把它移動(move)給新所有主。std::shared_ptr 同樣表示動態分配對象的所有權,但可以被共享,也可以被複製;對象的所有權由所有複製者共同擁有,最後一個複製者被銷毀時,對象也會隨著被銷毀。

優點:

  • 如果沒有清晰、邏輯條理的所有權安排,不可能管理好動態分配的內存。
  • 傳遞對象的所有權,開銷比複製來得小,如果可以複製的話。
  • 傳遞所有權也比「借用」指標或引用來得簡單,畢竟它大大省去了兩個使用者一起協調對象生命週期的工作。
  • 如果所有權邏輯條理,有文檔且不亂來的話,可讀性很棒。
  • 可以不用手動完成所有權的登記工作,大大簡化了程式碼,也免去了一大波錯誤之惱。
  • 對於 const 對象來說,智慧指標簡單易用,也比深度複製高效。

缺點:

  • 不得不用指標(不管是智慧的還是原生的)來表示和傳遞所有權。指針語義可要比值語義複雜得許多了,特別是在 API 裡:你不光要操心所有權,還要顧及別名,生命週期,可變性(mutability)以及其它大大小小問題。
  • 其實值語義的開銷經常被高估,所以就所有權的性能來說,可不能光只考慮可讀性以及複雜性。
  • 如果 API 相依性所有權的傳遞,就會害得客戶端不得不用單一的內存管理模型。
  • 銷毀資源並回收的相關程式碼不是很明朗。
  • std::unique_ptr 的所有權傳遞原理是 C++11 的 move 語法,後者畢竟是剛剛推出的,容易迷惑開發者。
  • 如果原本的所有權設計已經夠完善了,那麼若要引入所有權共享機制,可能不得不重構整個系統。
  • 所有權共享機制的登記工作在運行時進行,開銷可能相當不小。
  • 某些極端情況下,所有權被共享的對象永遠不會被銷毀,比如引用死循環(cyclic references)。
  • 智慧指標並不能夠完全代替原生指針。

4.2. cpplint

Tip

使用 cpplint.py 檢查風格錯誤.

cpplint.py 是一個用來分析源文件, 能檢查出多種風格錯誤的工具. 它不並完美, 甚至還會漏報和誤報, 但它仍然是一個非常有用的工具. 在行尾加 // NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略報錯。

某些專案會指導你如何使用他們的專案工具運行 cpplint.py. 如果你參與的專案沒有提供, 你可以單獨下載 cpplint.py.

譯者(acgtyrant)筆記

  1. 把智慧指標當成對象來看待的話,就很好領會它與所指對像之間的關係了。
  2. 原來 Rust 的 Ownership 思想是受到了 C++ 智慧指標的很大啟發啊。
  3. scoped_ptrauto_ptr 已過時。 現在是 shared_ptruniqued_ptr 的天下了。
  4. 按本文來說,似乎除了智慧指標,還有其它所有權機制,值得留意。
  5. Arch Linux 使用者注意了,AUR 有對 cpplint 打包。

5. 其他 C++ 特性

5.1. 引用參數

Tip

所有按引用傳遞的參數必須加上 const.

定義:

在 C 語言中, 如果函式需要修改變數的值, 參數必須為指標, 如 int foo(int *pval). 在 C++ 中, 函式還可以宣告引用參數: int foo(int &val).

優點:

定義引用參數防止出現 (*pval)++ 這樣醜陋的程式碼. 像拷貝建構子這樣的應用也是必需的. 而且更明確, 不接受 NULL 指標.

缺點:

容易引起誤解, 因為引用在語法上是值變數卻擁有指標的語義.

結論:

函式參數列表中, 所有引用參數都必須是 const:

void Foo(const string &in, string *out);

事實上這在 Google Code 是一個硬性約定: 輸入參數是值參或 const 引用, 輸出參數為指標. 輸入參數可以是 const 指針, 但決不能是非 const 的引用參數,除非用於交換,比如 swap().

有時候,在輸入形參中用 const T* 指標比 const T& 更明智。比如:

  • 你會傳 null 指標。
  • 函式要把指標或對地址的引用賦值給輸入形參。

總之大多時候輸入形參往往是 const T&. 若用 const T* 說明輸入另有處理。所以若你要用 const T*, 則應有理有據,否則會害得讀者誤解。

5.2. 右值參考

Tip

只在定義移動建構子與移動賦值操作時使用右值參考. 不要使用 std::forward.

定義:

右值參考是一種只能綁定到臨時對象的引用的一種, 其語法與傳統的引用語法相似. 例如, void f(string&& s); 宣告了一個其參數是一個字符串的右值參考的函式.

優點:

用於定義移動建構子 (使用類的右值參考進行建構的函式) 使得移動一個值而非拷貝之成為可能. 例如, 如果 v1 是一個 vector<string>, 則 auto v2(std::move(v1)) 將很可能不再進行大量的數據複製而只是簡單地進行指標操作, 在某些情況下這將帶來大幅度的性能提升.

右值參考使得編寫通用的函式封裝來轉發其參數到另外一個函式成為可能, 無論其參數是否是臨時對象都能正常工作.

右值參考能實作可移動但不可拷貝的類型, 這一特性對那些在拷貝方面沒有實際需求, 但有時又需要將它們作為函式參數傳遞或塞入容器的類型很有用.

要高效率地使用某些標準函式庫類型, 例如 std::unique_ptr, std::move 是必需的.

缺點:

右值參考是一個相對比較新的特性 (由 C++11 引入), 它尚未被廣泛理解. 類似引用崩潰, 移動建構子的自動推導這樣的規則都是很複雜的.

結論:

只在定義移動建構子與移動賦值操作時使用右值參考, 不要使用 std::forward 功能函式. 你可能會使用 std::move 來表示將值從一個對象移動而不是複製到另一個對像.

5.3. 函式重載

Tip

若要用好函式重載,最好能讓讀者一看呼叫點(call site)就胸有成竹,不用花心思猜測呼叫的重載函式到底是哪一種。該規則適用於建構子。

定義:

你可以編寫一個參數類型為 const string& 的函式, 然後用另一個參數類型為 const char* 的函式重載它:

class MyClass {
    public:
    void Analyze(const string &text);
    void Analyze(const char *text, size_t textlen);
};

優點:

通過重載參數不同的同名函式, 令程式碼更加直觀. 樣板化程式碼需要重載, 同時為使用者帶來便利.

缺點:

如果函式單單靠不同的參數類型而重載(acgtyrant 註:這意味著參數數量不變),讀者就得十分熟悉 C++ 五花八門的匹配規則,以瞭解匹配過程具體到底如何。另外,當派生類只重載了某個函式的部分變體,繼承語義容易令人困惑。

結論:

如果你打算重載一個函式, 可以試試改在函式名裡加上參數信息。例如,用 AppendString()AppendInt() 等, 而不是一口氣重載多個 Append().

5.4. 預設參數

Tip

我們不允許使用缺省函式參數,少數極端情況除外。盡可能改用函式重載。

優點:

當你有相依性預設參數的函式時,你也許偶爾會修改修改這些缺省參數。通過缺省參數,不用再為個別情況而特意定義一大堆函式了。與函式重載相比,缺省參數語法更為清晰,程式碼少,也很好地區分了「必選參數」和「可選參數」。

缺點:

預設參數會干擾函式指標,害得後者的函式簽名(function signature)往往對不上所實際要呼叫的函式簽名。即在一個現有函式添加缺省參數,就會改變它的類型,那麼呼叫其地址的程式碼可能會出錯,不過函式重載就沒這問題了。此外,缺省參數會造成臃腫的程式碼,畢竟它們在每一個呼叫點(call site)都有重複(acgtyrant 註:我猜可能是因為呼叫函式的程式碼表面上看來省去了不少參數,但編譯器在編譯時還是會在每一個呼叫程式碼裡統統補上所有默認實參信息,造成大量的重複)。函式重載正好相反,畢竟它們所謂的「缺省參數」只會出現在函式定義裡。

結論:

由於缺點並不是很嚴重,有些人依舊偏愛預設參數勝於函式重載。所以除了以下情況,我們要求必須顯式提供所有參數(acgtyrant 註:即不能再通過缺省參數來省略參數了)。

其一,位於 .cc 文件裡的靜態函式或匿名空間函式,畢竟都只能在局部文件裡呼叫該函式了。

其二,可以在建構子里用預設參數,畢竟不可能取得它們的地址。

其三,可以用來模擬可變長度陣列。

// 通過空 AlphaNum 以支持四個形參
string StrCat(const AlphaNum &a,
              const AlphaNum &b = gEmptyAlphaNum,
              const AlphaNum &c = gEmptyAlphaNum,
              const AlphaNum &d = gEmptyAlphaNum);

5.5. 可變長度陣列和 alloca()

Tip

我們不允許使用可變長度陣列和 alloca().

優點:

可變長度陣列具有渾然天成的語法. 變長數組和 alloca() 也都很高效.

缺點:

可變長度陣列和 alloca() 不是標準 C++ 的組成部分. 更重要的是, 它們根據數據大小動態分配堆棧內存, 會引起難以發現的內存越界 bugs: “在我的機器上運行的好好的, 發佈後卻莫名其妙的掛掉了”.

結論:

改用更安全的分配器(allocator),就像 std::vectorstd::unique_ptr<T[]>.

5.6. 友元

Tip

我們允許合理的使用友元類及友元函式.

通常友元應該定義在同一文件內, 避免程式碼讀者跑到其它文件查找使用該私有成員的類. 經常用到友元的一個地方是將 FooBuilder 宣告為 Foo 的友元, 以便 FooBuilder 正確建構 Foo 的內部狀態, 而無需將該狀態暴露出來. 某些情況下, 將一個單元測試類宣告成待測類的友元會很方便.

友元擴大了 (但沒有打破) 類的封裝邊界. 某些情況下, 相對於將類成員宣告為 public, 使用友元是更好的選擇, 尤其是如果你只允許另一個類訪問該類的私有成員時. 當然, 大多數類都只應該通過其提供的公有成員進行互操作.

5.7. 例外

Tip

我們不使用 C++ 例外.

優點:

  • 例外允許應用高層決定如何處理在底層嵌套函式中「不可能發生」的失敗(failures),不用管那些含糊且容易出錯的錯誤程式碼(acgtyrant 註:error code, 我猜是C語言函式返回的非零 int 值)。
  • 很多現代語言都用例外。引入異常使得 C++ 與 Python, Java 以及其它類 C++ 的語言更一脈相承。
  • 有些第三方 C++ 庫相依性例外,禁用異常就不好用了。
  • 例外是處理建構子失敗的唯一途徑。雖然可以用工廠函式(acgtyrant 註:factory function, 出自 C++ 的一種設計模式,即「簡單工廠模式」)或 Init() 方法代替異常, but these require heap allocation or a new “invalid” state, respectively.
  • 在測試框架裡很好用。

缺點:

  • 在現有函式中添加 throw 語句時,你必須檢查所有呼叫點。要麼讓所有呼叫點統統具備最低限度的例外安全保證,要麼眼睜睜地看異常一路歡快地往上跑,最終中斷掉整個程式。舉例,f() 呼叫 g(), g() 又呼叫 h(), 且 h 丟出的異常被 f 捕獲。當心 g, 否則會沒妥善清理好。
  • 還有更常見的,例外會徹底擾亂程式的執行串流程並難以判斷,函式也許會在你意料不到的地方返回。你或許會加一大堆何時何處處理異常的規定來降低風險,然而開發者的記憶負擔更重了。
  • 例外安全需要RAII和不同的編碼實踐. 要輕鬆編寫出正確的異常安全程式碼需要大量的支持機制. 更進一步地說, 為了避免讀者理解整個呼叫表, 異常安全必須隔絕從持續狀態寫到 “提交” 狀態的邏輯. 這一點有利有弊 (因為你也許不得不為了隔離提交而混淆程式碼). 如果允許使用異常, 我們就不得不時刻關注這樣的弊端, 即使有時它們並不值得.
  • 啟用例外會增加二進制文件數據,延長編譯時間(或許影響小),還可能加大地址空間的壓力。
  • 濫用例外會變相鼓勵開發者去捕捉不合時宜,或本來就已經沒法恢復的「偽異常」。比如,使用者的輸入不符合格式要求時,也用不著拋異常。如此之類的偽異常列都列不完。

結論:

從表面上看來,使用例外利大於弊, 尤其是在新專案中. 但是對於現有程式碼, 引入異常會牽連到所有相關程式碼. 如果新專案允許異常向外擴散, 在跟以前未使用異常的程式碼整合時也將是個麻煩. 因為 Google 現有的大多數 C++ 程式碼都沒有異常處理, 引入帶有異常處理的新程式碼相當困難.

鑒於 Google 現有程式碼不接受例外, 在現有程式碼中使用異常比在新專案中使用的代價多少要大一些. 遷移過程比較慢, 也容易出錯. 我們不相信異常的使用有效替代方案, 如錯誤程式碼, 斷言等會造成嚴重負擔.

我們並不是基於哲學或道德層面反對使用例外, 而是在實踐的基礎上. 我們希望在 Google 使用我們自己的開源專案, 但專案中使用異常會為此帶來不便, 因此我們也建議不要在 Google 的開源專案中使用異常. 如果我們需要把這些專案推倒重來顯然不太現實.

對於 Windows 程式碼來說, 有個 特例.

(YuleFox 注: 對於例外處理, 顯然不是短短幾句話能夠說清楚的, 以建構子為例, 很多 C++ 書籍上都提到當建構失敗時只有異常可以處理, Google 禁止使用異常這一點, 僅僅是為了自身的方便, 說大了, 無非是基於軟件管理成本上, 實際使用中還是自己決定)

5.8. 運行時類型識別

TODO

Tip

我們禁止使用 RTTI.

定義:

RTTI 允許開發者在運行時識別 C++ 類對象的類型. 它通過使用 typeid 或者 dynamic_cast 完成.

優點:

RTTI 的標準替代 (下面將描述) 需要對有問題的類層級進行修改或重構. 有時這樣的修改並不是我們所想要的, 甚至是不可取的, 尤其是在一個已經廣泛使用的或者成熟的程式碼中.

RTTI 在某些單元測試中非常有用. 比如進行工廠類測試時, 用來驗證一個新建對象是否為期望的動態類型. RTTI 對於管理對象和派生對象的關係也很有用.

在考慮多個抽像對象時 RTTI 也很好用. 例如:

bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
  Derived* that = dynamic_cast<Derived*>(other);
  if (that == NULL)
    return false;
  ...
}

缺點:

在運行時判斷類型通常意味著設計問題. 如果你需要在運行期間確定一個對象的類型, 這通常說明你需要考慮重新設計你的類.

隨意地使用 RTTI 會使你的程式碼難以維護. 它使得基於類型的判斷樹或者 switch 語句散佈在程式碼各處. 如果以後要進行修改, 你就必須檢查它們.

結論:

RTTI 有合理的用途但是容易被濫用, 因此在使用時請務必注意. 在單元測試中可以使用 RTTI, 但是在其他程式碼中請盡量避免. 尤其是在新程式碼中, 使用 RTTI 前務必三思. 如果你的程式碼需要根據不同的對象類型執行不同的行為的話, 請考慮用以下的兩種替代方案之一查詢類型:

虛函式可以根據子類類型的不同而執行不同程式碼. 這是把工作交給了對象本身去處理.

如果這一工作需要在對象之外完成, 可以考慮使用雙重分發的方案, 例如使用訪問者設計模式. 這就能夠在對像之外進行類型判斷.

如果程式能夠保證給定的父類別實例實際上都是某個派生類的實例, 那麼就可以自由使用 dynamic_cast. 在這種情況下, 使用 dynamic_cast 也是一種替代方案.

基於類型的判斷樹是一個很強的暗示, 它說明你的程式碼已經偏離正軌了. 不要像下面這樣:

if (typeid(*data) == typeid(D1)) {
  ...
} else if (typeid(*data) == typeid(D2)) {
  ...
} else if (typeid(*data) == typeid(D3)) {
...

一旦在類層級中加入新的子類, 像這樣的程式碼往往會崩潰. 而且, 一旦某個子類的屬性改變了, 你很難找到並修改所有受影響的程式碼塊.

不要去手工實作一個類似 RTTI 的方案. 反對 RTTI 的理由同樣適用於這些方案, 比如帶類型標籤的類繼承體系. 而且, 這些方案會掩蓋你的真實意圖.

5.9. 類型轉換

Tip

使用 C++ 的類型轉換, 如 static_cast<>(). 不要使用 int y = (int)xint y = int(x) 等轉換方式;

定義:

C++ 採用了有別於 C 的類型轉換機制, 對轉換操作進行歸類.

優點:

C 語言的類型轉換問題在於模稜兩可的操作; 有時是在做強制轉換 (如 (int)3.5), 有時是在做類型轉換 (如 (int)"hello"). 另外, C++ 的類型轉換在查找時更醒目.

缺點:

噁心的語法.

結論:

不要使用 C 風格類型轉換. 而應該使用 C++ 風格.

  • static_cast 替代 C 風格的值轉換, 或某個類指標需要明確的向上轉換為父類別指針時.
  • const_cast 去掉 const 限定符.
  • reinterpret_cast 指標類型和整數或其它指針之間進行不安全的相互轉換. 僅在你對所做一切瞭然於心時使用.

至於 dynamic_cast 參見 5.8. 運行時類型識別.

5.10. 串流

Tip

只在記錄日誌時使用串流.

定義:

串流用來替代 printf()scanf().

優點:

有了串流, 在打印時不需要關心對象的類型. 不用擔心格式化字符串與參數列表不匹配 (雖然在 gcc 中使用 printf 也不存在這個問題). 流的建構和解構子會自動打開和關閉對應的文件.

缺點:

串流使得 pread() 等功能函式很難執行. 如果不使用 printf 風格的格式化字符串, 某些格式化操作 (尤其是常用的格式字符串 %.*s) 用流處理性能是很低的. 流不支持字符串操作符重新排序 (%1s), 而這一點對於軟件國際化很有用.

結論:

不要使用串流, 除非是日誌介面需要. 使用 printf 之類的代替.

使用串流還有很多利弊, 但程式碼一致性勝過一切. 不要在程式碼中使用流.

拓展討論:

對這一條規則存在一些爭論, 這兒給出點深層次原因. 回想一下唯一性原則 (Only One Way): 我們希望在任何時候都只使用一種確定的 I/O 類型, 使程式碼在所有 I/O 處都保持一致. 因此, 我們不希望使用者來決定是使用串流還是 printf + read/write. 相反, 我們應該決定到底用哪一種方式. 把日誌作為特例是因為日誌是一個非常獨特的應用, 還有一些是歷史原因.

串流的支持者們主張流是不二之選, 但觀點並不是那麼清晰有力. 他們指出的流的每個優勢也都是其劣勢. 流最大的優勢是在輸出時不需要關心打印對象的類型. 這是一個亮點. 同時, 也是一個不足: 你很容易用錯類型, 而編譯器不會報警. 使用流時容易造成的這類錯誤:

cout << this;   // 輸出地址
cout << *this;  // 輸出值

由於 << 被重載, 編譯器不會報錯. 就因為這一點我們反對使用操作符重載.

有人說 printf 的格式化醜陋不堪, 易讀性差, 但串流也好不到哪兒去. 看看下面兩段程式碼吧, 實作相同的功能, 哪個更清晰?

cerr << "Error connecting to '" << foo->bar()->hostname.first
     << ":" << foo->bar()->hostname.second << ": " << strerror(errno);

fprintf(stderr, "Error connecting to '%s:%u: %s",
        foo->bar()->hostname.first, foo->bar()->hostname.second,
        strerror(errno));

你可能會說, “把串流封裝一下就會比較好了”, 這兒可以, 其他地方呢? 而且不要忘了, 我們的目標是使語言更緊湊, 而不是添加一些別人需要學習的新裝備.

每一種方式都是各有利弊, “沒有最好, 只有更適合”. 簡單性原則告誡我們必須從中選擇其一, 最後大多數決定採用 printf + read/write.

5.11. 前置自增和自減

Tip

對於迭代器和其他樣板對象使用前綴形式 (++i) 的自增, 自減運算子.

定義:

對於變數在自增 (++ii++) 或自減 (--ii--) 後表達式的值又沒有沒用到的情況下, 需要確定到底是使用前置還是後置的自增 (自減).

優點:

不考慮返回值的話, 前置自增 (++i) 通常要比後置自增 (i++) 效率更高. 因為後置自增 (或自減) 需要對表達式的值 i 進行一次拷貝. 如果 i 是迭代器或其他非數值類型, 拷貝的代價是比較大的. 既然兩種自增方式實作的功能一樣, 為什麼不總是使用前置自增呢?

缺點:

在 C 開發中, 當表達式的值未被使用時, 傳統的做法是使用後置自增, 特別是在 for 循環中. 有些人覺得後置自增更加易懂, 因為這很像自然語言, 主語 (i) 在謂語動詞 (++) 前.

結論:

對簡單數值 (非對象), 兩種都無所謂. 對迭代器和樣板類型, 使用前置自增 (自減).

5.12. const 用法

Tip

我們強烈建議你在任何可能的情況下都要使用 const. 此外有時改用 C++11 推出的 constexpr 更好。

定義:

在宣告的變數或參數前加上關鍵字 const 用於指明變量值不可被篡改 (如 const int foo ). 為類中的函式加上 const 限定符表明該函式不會修改類成員變量的狀態 (如 class Foo { int Bar(char c) const; };).

優點:

大家更容易理解如何使用變數. 編譯器可以更好地進行類型檢測, 相應地, 也能生成更好的程式碼. 人們對編寫正確的程式碼更加自信, 因為他們知道所呼叫的函式被限定了能或不能修改變量值. 即使是在無鎖的多線程程式撰寫中, 人們也知道什麼樣的函式是安全的.

缺點:

const 是入侵性的: 如果你向一個函式傳入 const 變數, 函式原型宣告中也必須對應 const 參數 (否則變量需要 const_cast 類型轉換), 在呼叫庫函式時顯得尤其麻煩.

結論:

const 變數, 數據成員, 函式和參數為編譯時類型檢測增加了一層保障; 便於盡早發現錯誤. 因此, 我們強烈建議在任何可能的情況下使用 const:

  • 如果函式不會修改傳你入的引用或指標類型參數, 該參數應宣告為 const.
  • 盡可能將函式宣告為 const. 訪問函式應該總是 const. 其他不會修改任何數據成員, 未呼叫非 const 函式, 不會返回數據成員非 const 指標或引用的函式也應該宣告成 const.
  • 如果數據成員在對象建構之後不再發生變化, 可將其定義為 const.

然而, 也不要發了瘋似的使用 const. 像 const int * const * const x; 就有些過了, 雖然它非常精確的描述了常數 x. 關注真正有幫助意義的信息: 前面的例子寫成 const int** x 就夠了.

關鍵字 mutable 可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全.

const 的位置:

有人喜歡 int const *foo 形式, 不喜歡 const int* foo, 他們認為前者更一致因此可讀性也更好: 遵循了 const 總位於其描述的對象之後的原則. 但是一致性原則不適用於此, “不要過度使用” 的宣告可以取消大部分你原本想保持的一致性. 將 const 放在前面才更易讀, 因為在自然語言中形容詞 (const) 是在名詞 (int) 之前.

這是說, 我們提倡但不強制 const 在前. 但要保持程式碼的一致性! (Yang.Y 注: 也就是不要在一些地方把 const 寫在類型前面, 在其他地方又寫在後面, 確定一種寫法, 然後保持一致.)

5.13. constexpr 用法

Tip

在 C++11 裡,用 constexpr 來定義真正的常數,或實作常量初始化。

定義:

變數可以被宣告成 constexpr 以表示它是真正意義上的常數,即在編譯時和運行時都不變。函式或建構子也可以被宣告成 constexpr, 以用來定義 constexpr 變量。

優點:

如今 constexpr 就可以定義浮點式的真・常數,不用再相依性字面值了;也可以定義使用者自定義類型上的常量;甚至也可以定義函式呼叫所返回的常量。

缺點:

若過早把變數優化成 constexpr 變量,將來又要把它改為常規變量時,挺麻煩的;Current restrictions on what is allowed in constexpr functions and constructors may invite obscure workarounds in these definitions.

結論:

靠 constexpr 特性,方才實作了 C++ 在介面上打造真正常數機制的可能。好好用 constexpr 來定義真・常量以及支持常量的函式。Avoid complexifying function definitions to enable their use with constexpr. 千萬別癡心妄想地想靠 constexpr 來強制程式碼「內聯」。

5.14. 整數

Tip

C++ 內建整數中, 僅使用 int. 如果程式中需要不同大小的變數, 可以使用 <stdint.h> 中長度精確的整數, 如 int16_t.如果你的變量可能不小於 2^31 (2GiB), 就用 64 位變量比如 int64_t. 此外要留意,哪怕你的值並不會超出 int 所能夠表示的範圍,在計算過程中也可能會溢出。所以拿不準時,乾脆用更大的類型。

定義:

C++ 沒有指定整數的大小. 通常人們假定 short 是 16 位, int 是 32 位, long 是 32 位, long long 是 64 位.

優點:

保持宣告統一.

缺點:

C++ 中整數大小因編譯器和體系結構的不同而不同.

結論:

<stdint.h> 定義了 int16_t, uint32_t, int64_t 等整數, 在需要確保整數大小時可以使用它們代替 short, unsigned long long 等. 在 C 整型中, 只使用 int. 在合適的情況下, 推薦使用標準類型如 size_tptrdiff_t.

如果已知整數不會太大, 我們常常會使用 int, 如循環計數. 在類似的情況下使用原生類型 int. 你可以認為 int 至少為 32 位, 但不要認為它會多於 32 位. 如果需要 64 位整數, 用 int64_tuint64_t.

對於大整數, 使用 int64_t.

不要使用 uint32_t 等無符號整數, 除非你是在表示一個位組而不是一個數值, 或是你需要定義二進制補碼溢出. 尤其是不要為了指出數值永不會為負, 而使用無符號類型. 相反, 你應該使用斷言來保護數據.

如果你的程式碼涉及容器返回的大小(size),確保其類型足以應付容器各種可能的用法。拿不準時,類型越大越好。

小心整數類型轉換和整數提升(acgtyrant 註:integer promotions, 比如 intunsigned int 運算時,前者被提升為 unsigned int 而有可能溢出),總有意想不到的後果。

關於無符號整數:

有些人, 包括一些教科書作者, 推薦使用無符號類型表示非負數. 這種做法試圖達到自我文檔化. 但是, 在 C 語言中, 這一優點被由其導致的 bug 所淹沒. 看看下面的例子:

for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

上述循環永遠不會退出! 有時 gcc 會發現該 bug 並報警, 但大部分情況下都不會. 類似的 bug 還會出現在比較有符合變數和無符號變量時. 主要是 C 的類型提升機制會致使無符號類型的行為出乎你的意料.

因此, 使用斷言來指出變數為非負數, 而不是使用無符號型!

5.15. 64 位下的可移植性

Tip

程式碼應該對 64 位和 32 位系統友好. 處理打印, 比較, 結構體對齊時應切記:

  • 對於某些類型, printf() 的指示符在 32 位和 64 位系統上可移植性不是很好. C99 標準定義了一些可移植的格式化指示符. 不幸的是, MSVC 7.1 並非全部支持, 而且標準中也有所遺漏, 所以有時我們不得不自己定義一個醜陋的版本 (標頭檔 inttypes.h 仿標準風格):

    // printf macros for size_t, in the style of inttypes.h
    #ifdef _LP64
    #define __PRIS_PREFIX "z"
    #else
    #define __PRIS_PREFIX
    #endif
    
    // Use these macros after a % in a printf format string
    // to get correct 32/64 bit behavior, like this:
    // size_t size = records.size();
    // printf("%"PRIuS"\n", size);
    #define PRIdS __PRIS_PREFIX "d"
    #define PRIxS __PRIS_PREFIX "x"
    #define PRIuS __PRIS_PREFIX "u"
    #define PRIXS __PRIS_PREFIX "X"
    #define PRIoS __PRIS_PREFIX "o"
    
    類型 不要使用 使用 備註
    void * (或其他指標類型) %lx %p  
    int64_t %qd, %lld %"PRId64"  
    uint64_t %qu, %llu, %llx %"PRIu64", %"PRIx64"  
    size_t %u %"PRIuS", %"PRIxS" C99 規定 %zu
    ptrdiff_t %d %"PRIdS" C99 規定 %zd

    注意 PRI* 巨集會被編譯器擴展為獨立字符串. 因此如果使用非常數的格式化字符串, 需要將宏的值而不是宏名插入格式中. 使用 PRI* 宏同樣可以在 % 後包含長度指示符. 例如, printf("x = %30"PRIuS"\n", x) 在 32 位 Linux 上將被展開為 printf("x = %30" "u" "\n", x), 編譯器當成 printf("x = %30u\n", x) 處理 (Yang.Y 注: 這在 MSVC 6.0 上行不通, VC 6 編譯器不會自動把引號間隔的多個字符串連接一個長字符串).

  • 記住 sizeof(void *) != sizeof(int). 如果需要一個指標大小的整數要用 intptr_t.

  • 你要非常小心的對待結構體對齊, 尤其是要持久化到磁盤上的結構體 (Yang.Y 注: 持久化 - 將數據按字節串流順序保存在磁盤文件或數據庫中). 在 64 位系統中, 任何含有 int64_t/uint64_t 成員的類/結構體, 缺省都以 8 字節在結尾對齊. 如果 32 位和 64 位程式碼要共用持久化的結構體, 需要確保兩種體系結構下的結構體對齊一致. 大多數編譯器都允許調整結構體對齊. gcc 中可使用 __attribute__((packed)). MSVC 則提供了 #pragma pack()__declspec(align()) (YuleFox 注, 解決方案的專案屬性裡也可以直接設置).

  • 創建 64 位常數時使用 LL 或 ULL 作為後綴, 如:

    int64_t my_value = 0×123456789LL;
    uint64_t my_mask = 3ULL << 48;
    
  • 如果你確實需要 32 位和 64 位系統具有不同程式碼, 可以使用 #ifdef _LP64 指令來切分 32/64 位程式碼. (盡量不要這麼做, 如果非用不可, 盡量使修改局部化)

5.16. 前處理巨集

Tip

使用巨集時要非常謹慎, 盡量以內聯函式, 列舉和常數代替之.

巨集意味著你和編譯器看到的程式碼是不同的. 這可能會導致例外行為, 尤其因為宏具有全域作用域.

值得慶幸的是, C++ 中, 巨集不像在 C 中那麼必不可少. 以往用宏展開性能關鍵的程式碼, 現在可以用內聯函式替代. 用宏表示常數可被 const 變數代替. 用宏 “縮寫” 長變量名可被引用代替. 用宏進行條件編譯... 這個, 千萬別這麼做, 會令測試更加痛苦 (#define 防止標頭檔重包含當然是個特例).

巨集可以做一些其他技術無法實作的事情, 在一些程式碼庫 (尤其是底層庫中) 可以看到宏的某些特性 (如用 # 字符串化, 用 ## 連接等等). 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的.

下面給出的用法模式可以避免使用巨集帶來的問題; 如果你要宏, 盡可能遵守:

  • 不要在 .h 文件中定義巨集.
  • 在馬上要使用時才進行 #define, 使用後要立即 #undef.
  • 不要只是對已經存在的巨集使用#undef,選擇一個不會衝突的名稱;
  • 不要試圖使用展開後會導致 C++ 建構不穩定的巨集, 不然也至少要附上文檔說明其行為.
  • 不要用 ## 處理函式,類和變數的名字。

5.17. 0, nullptrNULL

Tip

整數用 0, 實數用 0.0, 指標用 nullptrNULL, 字符 (串) 用 '\0'.

整數用 0, 實數用 0.0, 這一點是毫無爭議的.

對於指標 (地址值), 到底是用 0, NULL 還是 nullptr. C++11 專案用 nullptr; C++03 專案則用 NULL, 畢竟它看起來像指針。實際上,一些 C++ 編譯器對 NULL 的定義比較特殊,可以輸出有用的警告,特別是 sizeof(NULL) 就和 sizeof(0) 不一樣。

字符 (串) 用 '\0', 不僅類型正確而且可讀性好.

5.18. sizeof

Tip

盡可能用 sizeof(varname) 代替 sizeof(type).

使用 sizeof(varname) 是因為當程式碼中變數類型改變時會自動更新. 你或許會用 sizeof(type) 處理不涉及任何變量的程式碼,比如處理來自外部或內部的數據格式,這時用變量就不合適了。

Struct data;
Struct data; memset(&data, 0, sizeof(data));

Warning

memset(&data, 0, sizeof(Struct));
if (raw_size < sizeof(int)) {
    LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
    return false;
}

5.19. auto

Tip

auto 繞過煩瑣的類型名,只要可讀性好就繼續用,別用在區域變數之外的地方。

定義:

C++11 中,若變數被宣告成 auto, 那它的類型就會被自動匹配成初始化表達式的類型。你可以用 auto 來複製初始化或綁定引用。

vector<string> v;
...
auto s1 = v[0];  // 創建一份 v[0] 的拷貝。
const auto& s2 = v[0];  // s2 是 v[0] 的一個引用。

優點:

C++ 類型名有時又長又臭,特別是涉及樣板或命名空間的時候。就像:

sparse_hash_map<string, int>::iterator iter = m.find(val);

返回類型好難讀,程式碼目的也不夠一目瞭然。重構其:

auto iter = m.find(val);

好多了。

沒有 auto 的話,我們不得不在同一個表達式裡寫同一個類型名兩次,無謂的重複,就像:

diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");

有了 auto, 可以更方便地用中間變數,顯式編寫它們的類型輕鬆點。

缺點:

類型夠明顯時,特別是初始化變數時,程式碼才會夠一目瞭然。但以下就不一樣了:

auto i = x.Lookup(key);

看不出其類型是啥,x 的類型宣告恐怕遠在幾百行之外了。

開發者必須會區分 autoconst auto& 的不同之處,否則會複製錯東西。

auto 和 C++11 列表初始化的合體令人摸不著頭腦:

auto x(3);  // 圓括號。
auto y{3};  // 大括號。

它們不是同一回事——xint, y 則是 std::initializer_list<int>. 其它一般不可見的代理類型(acgtyrant 註:normally-invisible proxy types, 它涉及到 C++ 鮮為人知的坑:Why is vector<bool> not a STL container?)也有大同小異的陷阱。

如果在介面裡用 auto, 比如宣告標頭檔裡的一個常數,那麼只要僅僅因為開發者一時修改其值而導致類型變化的話——API 要翻天覆地了。

結論:

auto 只能用在區域變數裡用。別用在文件作用域變量,命名空間作用域變量和類數據成員裡。永遠別列表初始化 auto 變量。

auto 還可以和 C++11 特性「尾置返回類型(trailing return type)」一起用,不過後者只能用在 lambda 表達式裡。

5.20. 列表初始化

Tip

你可以用列表初始化。

早在 C++03 裡,聚合類型(aggregate types)就已經可以被列表初始化了,比如陣列和不自帶建構子的結構體:

struct Point { int x; int y; };
Point p = {1, 2};

C++11 中,該特性得到進一步的推廣,任何對象類型都可以被列表初始化。示範如下:

// Vector 接收了一個初始化列表。
vector<string> v{"foo", "bar"};

// 不考慮細節上的微妙差別,大致上相同。
// 你可以任選其一。
vector<string> v = {"foo", "bar"};

// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};

// map 接收了一些 pair, 列表初始化大顯神威。
map<int, string> m = {{1, "one"}, {2, "2"}};

// 初始化列表也可以用在返回類型上的隱式轉換。
vector<int> test_function() { return {1, 2, 3}; }

// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}

// 在函式呼叫裡用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});

使用者自定義類型也可以定義接收 std::initializer_list<T> 的建構子和賦值運算子,以自動列表初始化:

class MyType {
 public:
  // std::initializer_list 專門接收 init 列表。
  // 得以值傳遞。
  MyType(std::initializer_list<int> init_list) {
    for (int i : init_list) append(i);
  }
  MyType& operator=(std::initializer_list<int> init_list) {
    clear();
    for (int i : init_list) append(i);
  }
};
MyType m{2, 3, 5, 7};

最後,列表初始化也適用於常規數據類型的建構,哪怕沒有接收 std::initializer_list<T> 的建構子。

double d{1.23};
// MyOtherType 沒有 std::initializer_list 建構子,
 // 直接上接收常規類型的建構子。
class MyOtherType {
 public:
  explicit MyOtherType(string);
  MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不過如果建構子是顯式的(explict),你就不能用 `= {}` 了。
MyOtherType m{"b"};

千萬別直接列表初始化 auto 變數,看下一句,估計沒人看得懂:

Warning

auto d = {1.23};        // d 即是 std::initializer_list<double>
auto d = double{1.23};  // 善哉 -- d 即為 double, 並非 std::initializer_list.

至於格式化,參見 braced-initializer-list-format.

5.21. Lambda 表達式

Tip

適當使用 lambda 表達式。別用默認 lambda 捕獲,所有捕獲都要顯式寫出來。

定義:

Lambda 表達式是創建匿名函式對象的一種簡易途徑,常用於把函式當參數傳,例如:

std::sort(v.begin(), v.end(), [](int x, int y) {
    return Weight(x) < Weight(y);
});

C++11 首次提出 Lambdas, 還提供了一系列處理函式對象的工具,比如多態包裝器(polymorphic wrapper) std::function.

優點:

  • 傳函式對象給 STL 算法,Lambdas 最簡易,可讀性也好。
  • Lambdas, std::functionsstd::bind 可以搭配成通用回調機制(general purpose callback mechanism);寫接收有界函式為參數的函式也很容易了。

缺點:

  • Lambdas 的變數捕獲略旁門左道,可能會造成懸空指標。
  • Lambdas 可能會失控;層層嵌套的匿名函式難以閱讀。

結論:

  • 按 format 小用 lambda 表達式怡情。
  • 禁用默認捕獲,捕獲都要顯式寫出來。打比方,比起 [=](int x) {return x + n;}, 你該寫成 [n](int x) {return x + n;} 才對,這樣讀者也好一眼看出 n 是被捕獲的值。
  • 匿名函式始終要簡短,如果函式體超過了五行,那麼還不如起名(acgtyrant 註:即把 lambda 表達式賦值給對象),或改用函式。
  • 如果可讀性更好,就顯式寫出 lambd 的尾置返回類型,就像auto.

5.22. 樣板元程式撰寫

TODO

5.23. Boost 庫

Tip

只使用 Boost 中被認可的庫.

定義:

Boost 庫集 是一個廣受歡迎, 經過同行鑒定, 免費開源的 C++ 庫集.

優點:

Boost程式碼質量普遍較高, 可移植性好, 填補了 C++ 標準函式庫很多空白, 如型別的特性, 更完善的綁定器, 更好的智慧指標。

缺點:

某些 Boost 庫提倡的程式撰寫實踐可讀性差, 比如元程式撰寫和其他高級樣板技術, 以及過度 “函式化” 的程式撰寫風格.

結論:

為了向閱讀和維護程式碼的人員提供更好的可讀性, 我們只允許使用 Boost 一部分經認可的特性子集. 目前允許使用以下庫:

  • Call Traits : boost/call_traits.hpp
  • Compressed Pair : boost/compressed_pair.hpp
  • <The Boost Graph Library (BGL) : boost/graph, except serialization (adj_list_serialize.hpp) and parallel/distributed algorithms and data structures(boost/graph/parallel/* and boost/graph/distributed/*)
  • Property Map : boost/property_map.hpp
  • The part of Iterator that deals with defining iterators: boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, and boost/function_output_iterator.hpp
  • The part of Polygon that deals with Voronoi diagram construction and doesn’t depend on the rest of Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp
  • Bimap : boost/bimap
  • Statistical Distributions and Functions : boost/math/distributions
  • Multi-index : boost/multi_index
  • Heap : boost/heap
  • The flat containers from Container: boost/container/flat_map, and boost/container/flat_set

我們正在積極考慮增加其它 Boost 特性, 所以列表中的規則將不斷變化.

以下庫可以用,但由於如今已經被 C++ 11 標準函式庫取代,不再鼓勵:

5.24. C++11

Tip

適當用 C++11(前身是 C++0x)的庫和語言擴展,在貴專案用 C++11 特性前三思可移植性。

定義:

C++11 有眾多語言和庫上的`變革 <https://en.wikipedia.org/wiki/C%2B%2B11>`_。

優點:

在二一四年八月之前,C++11 一度是官方標準,被大多 C++ 編譯器支持。它標準化很多我們早先就在用的 C++ 擴展,簡化了不少操作,大大改善了性能和安全。

缺點:

C++11 相對於前身,複雜極了:1300 頁 vs 800 頁!很多開發者也不怎麼熟悉它。於是從長遠來看,前者特性對程式碼可讀性以及維護代價難以預估。我們說不准什麼時候採納其特性,特別是在被迫相依性老實工具的專案上。

5.23. Boost 庫 一樣,有些 C++11 擴展提倡實則對可讀性有害的程式撰寫實踐——就像去除冗余檢查(比如類型名)以幫助讀者,或是鼓勵樣板元程式撰寫等等。有些擴展在功能上與原有機制衝突,容易招致困惑以及遷移代價。

缺點:

C++11 特性除了個別情況下,可以用一用。除了本指南會有不少章節會加以討若干 C++11 特性之外,以下特性最好不要用:

  • 尾置返回類型,比如用 auto foo() -> int 代替 int foo(). 為了兼容於現有程式碼的宣告風格。
  • 編譯時合數 <ratio>, 因為它涉及一個重樣板的介面風格。
  • <cfenv>``<fenv.h>` 標頭檔,因為編譯器尚不支持。
  • 默認 lambda 捕獲。

譯者(acgtyrant)筆記

  1. 實際上,預設參數會改變函式簽名的前提是改變了它接收的參數數量,比如把 void a() 改成 void a(int b = 0), 開發者改變其程式碼的初衷也許是,在不改變「程式碼相容性」的同時,又提供了可選 int 參數的餘地,然而這終究會破壞函式指標上的相容性,畢竟函式簽名確實變了。
  2. 此外把自帶預設參數的函式地址賦值給指標時,會丟失缺省參數信息。
  3. 我還發現 濫用預設參數會害得讀者光只看呼叫程式碼的話,會誤以為其函式接受的參數數量比實際上還要少。
  4. friend 實際上只對函式/類賦予了對其所在類的訪問權限,並不是有效的宣告語句。所以除了在標頭檔類內部寫 friend 函式/類,還要在類作用域之外正式地宣告一遍,最後在對應的 .cc 文件加以定義。
  5. 本風格指南都強調了「友元應該定義在同一文件內,避免程式碼讀者跑到其它文件查找使用該私有成員的類」。那麼可以把其宣告放在類宣告所在的標頭檔,定義也放在類定義所在的文件。
  6. 由於友元函式/類並不是類的一部分,自然也不會是類可呼叫的公有介面,於是我主張全集中放在類的尾部,即 :ref:`private 的數據成員 <declaration-order>`_ 之後。
  7. 對使用 C++ 例外處理應具有怎樣的態度? 非常值得一讀。
  8. 注意初始化 const 對象時,必須在初始化的同時值初始化。
  9. 用斷言代替無符號整數類型,深有啟發。
  10. auto 在涉及迭代器的循環語句裡挺常用。
  11. Should the trailing return type syntax style become the default for new C++11 programs? 討論了 auto 與尾置返回類型一起用的全新編碼風格,值得一看。

6. 命名約定

最重要的一致性規則是命名管理. 命名風格快速獲知名字代表是什麼東東: 類型? 變數? 函式? 常數? 巨集 ... ? 甚至不需要去查找類型宣告. 我們大腦中的模式匹配引擎可以非常可靠的處理這些命名規則.

命名規則具有一定隨意性, 但相比按個人喜好命名, 一致性更重, 所以不管你怎麼想, 規則總歸是規則.

6.1. 通用命名規則

Tip

函式命名,變數命名,文件命名要有描述性;少用縮寫。

盡可能給有描述性的命名,別心疼空間,畢竟讓程式碼易於新讀者理解很重要。不要用只有專案開發者能理解的縮寫,也不要通過砍掉幾個字母來縮寫單詞。

int price_count_reader;    // 無縮寫
int num_errors;            // 「num」 本來就很常見
int num_dns_connections;   // 人人都知道 「DNS」 是啥

Warning

int n;                     // 莫名其妙。
int nerr;                  // 怪縮寫。
int n_comp_conns;          // 怪縮寫。
int wgc_connections;       // 只有貴團隊知道是啥意思。
int pc_reader;             // "pc" 有太多可能的解釋了。
int cstmr_id;              // 有刪減若干字母。

6.2. 文件命名

Tip

文件名要全部小寫, 可以包含底線 (_) 或連字符 (-). 按專案約定來. 如果並沒有專案約定,”_” 更好。

可接受的文件命名:

* my_useful_class.cc
* my-useful-class.cc
* myusefulclass.cc
* muusefulclass_test.cc // ``_unittest`` 和 ``_regtest`` 已棄用。

C++ 文件要以 .cc 結尾, 標頭檔以 .h 結尾. 專門插入文本的文件則以 .inc 結尾,參見:ref:self-contained headers

不要使用已經存在於 /usr/include 下的文件名 (Yang.Y 注: 即編譯器搜索系統標頭檔的路徑), 如 db.h.

通常應盡量讓文件名更加明確. http_server_logs.h 就比 logs.h 要好. 定義類時文件名一般成對出現, 如 foo_bar.hfoo_bar.cc, 對應於類 FooBar.

內聯函式必須放在 .h 文件中. 如果內聯函式比較短, 就直接放在 .h 中.

6.3. 類型命名

Tip

類型名稱的每個單詞首字母均大寫, 不包含底線: MyExcitingClass, MyExcitingEnum.

所有類型命名 —— 類, 結構體, 類型定義 (typedef), 列舉 —— 均使用相同約定. 例如:

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// enums
enum UrlTableErrors { ...

6.4. 變數命名

Tip

變數名一律小寫, 單詞之間用底線連接. 類的成員變量以下劃線結尾, 但結構體的就不用,如:: a_local_variable, a_struct_data_member, a_class_data_member_.

普通變數命名:

舉例:

string table_name;  // 可 - 用底線。
string tablename;   // 可 - 全小寫。

Warning

string tableName;   // 差 - 混合大小寫。

類數據成員:

不管是靜態的還是非靜態的,類數據成員都可以和普通變數一樣, 但要接底線。

class TableInfo {
  ...
 private:
  string table_name_;  // 可 - 尾後加底線。
  string tablename_;   // 可。
  static Pool<TableInfo>* pool_;  // 可。
};

結構體變數:

不管是靜態的還是非靜態的,結構體數據成員都可以和普通變數一樣, 不用像類那樣接底線:

struct UrlTableProperties {
    string name;
    int num_entries;
}

結構體與類的討論參考 結構體 vs. 類 一節.

全域變數:

對全域變數沒有特別要求, 少用就好, 但如果你要用, 可以用 g_ 或其它標誌作為前綴, 以便更好的區分局部變量.

6.5. 常數命名

Tip

在全域或類裡的常數名稱前加 k: kDaysInAWeek. 且除去開頭的 k 之外每個單詞開頭字母均大寫。

所有編譯時常數, 無論是局部的, 全域的還是類中的, 和其他變數稍微區別一下. k 後接大寫字母開頭的單詞:

const int kDaysInAWeek = 7;

這規則適用於編譯時的局部作用域常數,不過要按變數規則來命名也可以。

6.6. 函式命名

Tip

常規函式使用大小寫混合, 取值和設值函式則要求與變數名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().

常規函式:

函式名的每個單詞首字母大寫, 沒有底線。

如果你的某函式出錯時就要直接 crash, 那麼就在函式名加上 OrDie. 但這函式本身必須集成在產品程式碼裡,且平時也可能會出錯。

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

取值和設值函式:

取值(Accessors)和設值(Mutators)函式要與存取的變數名匹配. 這兒摘錄一個類, num_entries_ 是該類的實例變量:

class MyClass {
    public:
        ...
        int num_entries() const { return num_entries_; }
        void set_num_entries(int num_entries) { num_entries_ = num_entries; }

    private:
        int num_entries_;
};

其它非常短小的內聯函式名也可以用小寫字母, 例如. 如果你在循環中呼叫這樣的函式甚至都不用緩存其返回值, 小寫命名就可以接受.

6.7. 命名空間命名

Tip

命名空間用小寫字母命名, 並基於專案名稱和目錄結構: google_awesome_project.

關於命名空間的討論和如何命名, 參考 命名空間 一節.

6.8. 列舉命名

Tip

列舉的命名應當和 常數巨集 一致: kEnumName 或是 ENUM_NAME.

單獨的列舉值應該優先採用 常數 的命名方式. 但 巨集 方式的命名也可以接受. 枚舉名 UrlTableErrors (以及 AlternateUrlTableErrors) 是類型, 所以要用大小寫混合的方式.
enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
    OK = 0,
    OUT_OF_MEMORY = 1,
    MALFORMED_INPUT = 2,
};

2009 年 1 月之前, 我們一直建議採用 巨集 的方式命名列舉值. 由於枚舉值和宏之間的命名衝突, 直接導致了很多問題. 由此, 這裡改為優先選擇常數風格的命名方式. 新程式碼應該盡可能優先使用常量風格. 但是老程式碼沒必要切換到常量風格, 除非宏風格確實會產生編譯期問題.

6.9. 巨集命名

Tip

你並不打算:ref:使用巨集 <preprocessor-macros>, 對吧? 如果你一定要用, 像這樣命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.

參考:ref:前處理巨集 <preprocessor-macros>; 通常 不應該 使用宏. 如果不得不用, 其命名像列舉命名一樣全部大寫, 使用底線:

#define ROUND(x) ...
#define PI_ROUNDED 3.0

6.10. 命名規則的特例

Tip

如果你命名的實體與已有 C/C++ 實體相似, 可參考現有命名策略.

bigopen():

函式名, 參照 open() 的形式

uint:

typedef

bigpos:

structclass, 參照 pos 的形式

sparse_hash_map:

STL 相似實體; 參照 STL 命名約定

LONGLONG_MAX:

常數, 如同 INT_MAX

譯者(acgtyrant)筆記

  1. 感覺 Google 的命名約定很高明,比如寫了簡單的類 QueryResult, 接著又可以直接定義一個變數 query_result, 區分度很好;再次,類內變量以底線結尾,那麼就可以直接傳入同名的形參,比如 TextQuery::TextQuery(std::string word) : word_(word) {}, 其中 word_ 自然是類內私有成員。

7. 註解

註解雖然寫起來很痛苦, 但對保證程式碼可讀性至關重要. 下面的規則描述了如何註釋以及在哪兒註釋. 當然也要記住: 註釋固然很重要, 但最好的程式碼本身應該是自文檔化. 有意義的類型名和變數名, 要遠勝過要用註釋解釋的含糊不清的名字.

你寫的註解是給程式碼讀者看的: 下一個需要理解你的程式碼的人. 慷慨些吧, 下一個人可能就是你!

7.1. 註解風格

Tip

使用 ///* */, 統一就好.

///* */ 都可以; 但 // 常用. 要在如何註解及註釋風格上確保統一.

7.2. 文件註解

Tip

在每一個文件開頭加入版權公告, 然後是文件內容描述.

法律公告和作者信息:

每個文件都應該包含以下項, 依次是:

  • 版權宣告 (比如, Copyright 2008 Google Inc.)
  • 許可證. 為專案選擇合適的許可證版本 (比如, Apache 2.0, BSD, LGPL, GPL)
  • 作者: 標識文件的原始作者.

如果你對原始作者的文件做了重大修改, 將你的信息添加到作者信息裡. 這樣當其他人對該文件有疑問時可以知道該聯繫誰.

文件內容:

緊接著版權許可和作者信息之後, 每個文件都要用註解描述文件內容.

通常, .h 文件要對所宣告的類的功能和用法作簡單說明. .cc 文件通常包含了更多的實作細節或算法技巧討論, 如果你感覺這些實作細節或算法技巧討論對於理解 .h 文件有幫助, 可以將該註解挪到 .h, 並在 .cc 中指出文檔在 .h.

不要簡單的在 .h.cc 間複製註解. 這種偏離了註釋的實際意義.

7.3. 類註解

Tip

每個類的定義都要附帶一份註解, 描述類的功能和用法.

// Iterates over the contents of a GargantuanTable.  Sample usage:
//    GargantuanTable_Iterator* iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
//    delete iter;
class GargantuanTable_Iterator {
    ...
};

如果你覺得已經在文件頂部詳細描述了該類, 想直接簡單的來上一句 “完整描述見文件頂部” 也不打緊, 但務必確保有這類註解.

如果類有任何同步前提, 文檔說明之. 如果該類的實例可被多線程訪問, 要特別注意文檔說明多線程環境下相關的規則和常數使用.

7.4. 函式註解

Tip

函式宣告處註解描述函式功能; 定義處描述函式實作.

函式宣告:

註解位於宣告之前, 對函式功能及用法進行描述. 註釋使用敘述式 (“Opens the file”) 而非指令式 (“Open the file”); 註釋只是為了描述函式, 而不是命令函式做什麼. 通常, 註釋不會描述函式如何工作. 那是函式定義部分的事情.

函式宣告處註解的內容:

  • 函式的輸入輸出.
  • 對類成員函式而言: 函式呼叫期間對象是否需要保持引用參數, 是否會釋放這些參數.
  • 如果函式分配了空間, 需要由呼叫者釋放.
  • 參數是否可以為 NULL.
  • 是否存在函式使用上的性能隱患.
  • 如果函式是可重入的, 其同步前提是什麼?

舉例如下:

// Returns an iterator for this table.  It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
//    Iterator* iter = table->NewIterator();
//    iter->Seek("");
//    return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;

但也要避免囉囉嗦嗦, 或做些顯而易見的說明. 下面的註解就沒有必要加上 “returns false otherwise”, 因為已經暗含其中了:

// Returns true if the table cannot hold any more entries.
bool IsTableFull();

註解建構/解構子時, 切記讀程式碼的人知道構造/解構子是幹啥的, 所以 “destroys this object” 這樣的註釋是沒有意義的. 註明建構子對參數做了什麼 (例如, 是否取得指標所有權) 以及解構子清理了什麼. 如果都是些無關緊要的內容, 直接省掉註釋. 解構子前沒有註釋是很正常的.

函式定義:

每個函式定義時要用註解說明函式功能和實作要點. 比如說說你用的程式撰寫技巧, 實作的大致步驟, 或解釋如此實作的理由, 為什麼前半部分要加鎖而後半部分不需要.

不要.h 文件或其他地方的函式宣告處直接複製註解. 簡要重述函式功能是可以的, 但註釋重點要放在如何實作上.

7.5. 變數註解

Tip

通常變數名本身足以很好說明變量用途. 某些情況下, 也需要額外的註解說明.

類數據成員:

每個類數據成員 (也叫實例變數或成員變量) 都應該用註解說明用途. 如果變量可以接受 NULL-1 等警戒值, 須加以說明. 比如:

private:
    // Keeps track of the total number of entries in the table.
    // Used to ensure we do not go over the limit. -1 means
    // that we don't yet know how many entries the table has.
    int num_total_entries_;

全域變數:

和數據成員一樣, 所有全域變數也要註解說明含義及用途. 比如:

// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;

7.6. 實作註解

Tip

對於程式碼中巧妙的, 晦澀的, 有趣的, 重要的地方加以註解.

程式碼前註解:

巧妙或複雜的程式碼段前要加註解. 比如:

// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
    x = (x << 8) + (*result)[i];
    (*result)[i] = x >> 1;
    x &= 1;
}

行註解:

比較隱晦的地方要在行尾加入註解. 在行尾空兩格進行註釋. 比如:

// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
    return;  // Error already logged.

注意, 這裡用了兩段註解分別描述這段程式碼的作用, 和提示函式返回時錯誤已經被記入日誌.

如果你需要連續進行多行註解, 可以使之對齊獲得更好的可讀性:

DoSomething();                  // Comment here so the comments line up.
DoSomethingElseThatIsLonger();  // Comment here so there are two spaces between
                                // the code and the comment.
{ // One space before comment when opening a new scope is allowed,
  // thus the comment lines up with the following comments and code.
  DoSomethingElse();  // Two spaces before line comments normally.
}

NULL, true/false, 1, 2, 3...:

向函式傳入 NULL, 布爾值或整數時, 要註解說明含義, 或使用常數讓程式碼望文知意. 例如, 對比:

Warning

bool success = CalculateSomething(interesting_value,
                                  10,
                                  false,
                                  NULL);  // What are these arguments??

和:

bool success = CalculateSomething(interesting_value,
                                  10,     // Default base value.
                                  false,  // Not the first time we're calling this.
                                  NULL);  // No callback.

或使用常數或描述性變數:

const int kDefaultBaseValue = 10;
const bool kFirstTimeCalling = false;
Callback *null_callback = NULL;
bool success = CalculateSomething(interesting_value,
                                  kDefaultBaseValue,
                                  kFirstTimeCalling,
                                  null_callback);

不允許:

注意 永遠不要 用自然語言翻譯程式碼作為註解. 要假設讀程式碼的人 C++ 水平比你高, 即便他/她可能不知道你的用意:

Warning

// 現在, 檢查 b 陣列並確保 i 是否存在,
// 下一個元素是 i+1.
...        // 天哪. 令人崩潰的註解.

7.7. 標點, 拼寫和語法

Tip

注意標點, 拼寫和語法; 寫的好的註解比差的要易讀的多.

註解的通常寫法是包含正確大小寫和結尾句號的完整語句. 短一點的註釋 (如程式碼行尾註釋) 可以隨意點, 依然要注意風格的一致性. 完整的語句可讀性更好, 也可以說明該註釋是完整的, 而不是一些不成熟的想法.

雖然被別人指出該用分號時卻用了逗號多少有些尷尬, 但清晰易讀的程式碼還是很重要的. 正確的標點, 拼寫和語法對此會有所幫助.

7.8. TODO 註解

Tip

對那些臨時的, 短期的解決方案, 或已經夠好但仍不完美的程式碼使用 TODO 註解.

TODO 註解要使用全大寫的字符串 TODO, 在隨後的圓括號裡寫上你的大名, 郵件地址, 或其它身份標識. 冒號是可選的. 主要目的是讓添加註釋的人 (也是可以請求提供更多細節的人) 可根據規範的 TODO 格式進行查找. 添加 TODO 註釋並不意味著你要自己來修正.

// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.

如果加 TODO 是為了在 “將來某一天做某事”, 可以附上一個非常明確的時間 “Fix by November 2005”), 或者一個明確的事項 (“Remove this code when all clients can handle XML responses.”).

7.9. 棄用註解

Tip

通過棄用註解(DEPRECATED comments)以標記某介面點(interface points)已棄用。

你可以寫上包含全大寫的 DEPRECATED 的註解,以標記某介面為棄用狀態。註釋可以放在接口宣告前,或者同一行。

DEPRECATED 一詞後,留下你的名字,郵箱地址以及括號補充。

僅僅標記介面為 DEPRECATED 並不會讓大家不約而同地棄用,你還得親自主動修正呼叫點(callsites),或是找個幫手。

修正好的程式碼應該不會再涉及棄用介面點了,著實改用新接口點。如果你不知從何下手,可以找標記棄用註解的當事人一起商量。

譯者 (YuleFox) 筆記

  1. 關於註解風格,很多 C++ 的 coders 更喜歡行註釋, C coders 或許對塊註釋依然情有獨鍾, 或者在文件頭大段大段的註釋時使用塊註釋;
  2. 文件註解可以炫耀你的成就, 也是為了捅了簍子別人可以找你;
  3. 註解要言簡意賅, 不要拖沓冗余, 複雜的東西簡單化和簡單的東西複雜化都是要被鄙視的;
  4. 對於 Chinese coders 來說, 用英文註解還是用中文註釋, it is a problem, 但不管怎樣, 註釋是為了讓別人看懂, 難道是為了炫耀程式撰寫語言之外的你的母語或外語水平嗎;
  5. 註解不要太亂, 適當的縮排才會讓人樂意看. 但也沒有必要規定註釋從第幾列開始 (我自己寫程式碼的時候總喜歡這樣), UNIX/LINUX 下還可以約定是使用 tab 還是 space, 個人傾向於 space;
  6. TODO 很不錯, 有時候, 註解確實是為了標記一些未完成的或完成的不盡如人意的地方, 這樣一搜索, 就知道還有哪些活要干, 日誌都省了.

8. 格式

程式碼風格和格式確實比較隨意, 但一個專案中所有人遵循同一風格是非常容易的. 個體未必同意下述每一處格式規則, 但整個專案服從統一的程式撰寫風格是很重要的, 只有這樣才能讓所有人能很輕鬆的閱讀和理解程式碼.

另外, 我們寫了一個 emacs 配置文件 來幫助你正確的格式化程式碼.

8.1. 行長度

Tip

每一行程式碼字符數不超過 80.

我們也認識到這條規則是有爭議的, 但很多已有程式碼都已經遵照這一規則, 我們感覺一致性更重要.

優點:

提倡該原則的人主張強迫他們調整編輯器窗口大小很野蠻. 很多人同時並排開幾個程式碼窗口, 根本沒有多餘空間拉伸窗口. 大家都把窗口最大尺寸加以限定, 並且 80 列寬是傳統標準. 為什麼要改變呢?

缺點:

反對該原則的人則認為更寬的程式碼行更易閱讀. 80 列的限制是上個世紀 60 年代的大型機的古板缺陷; 現代設備具有更寬的顯示屏, 很輕鬆的可以顯示更多程式碼.

結論:

80 個字符是最大值.

特例:

  • 如果一行註解包含了超過 80 字符的命令或 URL, 出於複製粘貼的方便允許該行超過 80 字符.
  • 包含長路徑的 #include 語句可以超出80列. 但應該盡量避免.
  • 標頭檔保護 可以無視該原則.

8.2. 非 ASCII 字符

Tip

盡量不使用非 ASCII 字符, 使用時必須使用 UTF-8 編碼.

即使是英文, 也不應將使用者界面的文本硬編碼到源程式碼中, 因此非 ASCII 字符要少用. 特殊情況下可以適當包含此類字符. 如, 程式碼分析外部數據文件時, 可以適當硬編碼數據文件中作為分隔符的非 ASCII 字符串; 更常見的是 (不需要本地化的) 單元測試程式碼可能包含非 ASCII 字符串. 此類情況下, 應使用 UTF-8 編碼, 因為很多工具都可以理解和處理 UTF-8 編碼.

十六進制編碼也可以, 能增強可讀性的情況下尤其鼓勵 —— 比如 "\xEF\xBB\xBF" 在 Unicode 中是 零寬度 無間斷 的間隔符號, 如果不用十六進制直接放在 UTF-8 格式的源文件中, 是看不到的.

(Yang.Y 注: "\xEF\xBB\xBF" 通常用作 UTF-8 with BOM 編碼標記)

u8 前綴以把帶 uXXXX 轉義序列的字符串字面值編碼成 UTF-8. 不要用在本身就帶 UTF-8 字符的字符串字面值上,因為如果編譯器不把源程式碼識別成 UTF-8, 輸出就會出錯。

別用 C++11 的 char16_tchar32_t, 它們和 UTF-8 文本沒有關係,wchar_t 同理,除非你寫的程式碼要呼叫 Windows API, 後者有用到 wchar_t 擴展。

8.3. 空格還是製表位

Tip

只使用空格, 每次縮排 2 個空格.

我們使用空格縮排. 不要在程式碼中使用制符表. 你應該設置編輯器將制符表轉為空格.

8.4. 函式宣告與定義

Tip

返回類型和函式名在同一行, 參數也盡量放在同一行,如果放不下就對形參分行。

函式看上去像這樣:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
    DoSomething();
    ...
}

如果同一行文本太多, 放不下所有參數:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
    DoSomething();
    ...
}

甚至連第一個參數都放不下:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
        Type par_name1,  // 4 空格縮排
        Type par_name2,
        Type par_name3) {
    DoSomething();  // 2 空格縮排
    ...
}

注意以下幾點:

  • 如果返回類型和函式名在一行放不下,分行。
  • 如果返回類型那個與函式宣告或定義分行了,不要縮排。
  • 左圓括號總是和函式名在同一行;
  • 函式名和左圓括號間沒有空格;
  • 圓括號與參數間沒有空格;
  • 左大括號總在最後一個參數同一行的末尾處;
  • 如果其它風格規則允許的話,右大括號總是單獨位於函式最後一行,或者與左大括號同一行。
  • 右大括號和左大括號間總是有一個空格;
  • 函式宣告和定義中的所有形參必須有命名且一致;
  • 所有形參應盡可能對齊;
  • 缺省縮排為 2 個空格;
  • 換行後的參數保持 4 個空格的縮排;

如果有些參數沒有用到, 在函式定義處將參數名註解起來:

// 介面中形參恆有命名。
class Shape {
 public:
  virtual void Rotate(double radians) = 0;
}

// 宣告中形參恆有命名。
class Circle : public Shape {
 public:
  virtual void Rotate(double radians);
}

// 定義中註解掉無用變數。
void Circle::Rotate(double /*radians*/) {}

Warning

// 差 - 如果將來有人要實作,很難猜出變數是幹什麼用的。
void Circle::Rotate(double) {}

8.5. Lambda 表達式

Tip

其它函式怎麼格式化形參和函式體,Lambda 表達式就怎麼格式化;捕獲列表同理。

若用引用捕獲,在變數名和 & 之間不留空格。

int x = 0;
auto add_to_x = [&x](int n) { x += n; };

短 lambda 就寫得和內聯函式一樣。

std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
                return blacklist.find(i) != blacklist.end();
            }),
            digits.end());

8.6. 函式呼叫

Tip

要麼一行寫完函式呼叫,要麼在圓括號裡對參數分行,要麼參數另起一行且縮排四格。如果沒有其它顧慮的話,盡可能精簡行數,比如把多個參數適當地放在同一行裡。

函式呼叫遵循如下形式:

bool retval = DoSomething(argument1, argument2, argument3);

如果同一行放不下,可斷為多行,後面每一行都和第一個實參對齊,左圓括號後和右圓括號前不要留空格:

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

參數也可以放在次行,縮排四格:

if (...) {
  ...
  ...
  if (...) {
    DoSomething(
        argument1, argument2,  // 4 空格縮排
        argument3, argument4);
  }

把多個參數放在同一行,是為了減少函式呼叫所需的行數,除非影響到可讀性。有人認為把每個參數都獨立成行,不僅更好讀,而且方便編輯參數。不過,比起所謂的參數編輯,我們更看重可讀性,且後者比較好辦:

如果一些參數本身就是略複雜的表達式,且降低了可讀性。那麼可以直接創建臨時變數描述該表達式,並傳遞給函式:

int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);

或者放著不管,補充上註解:

bool retval = DoSomething(scores[x] * y + bases[x],  // Score heuristic.
                          x, y, z);

如果某參數獨立成行,對可讀性更有幫助的話,就這麼辦。

此外,如果一系列參數本身就有一定的結構,可以酌情地按其結構來決定參數格式:

// 通過 3x3 矩陣轉換 widget.
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3);

8.7. 列表初始化格式

Tip

你平時怎麼格式化函式呼叫,就怎麼格式化:ref:braced_initializer_list

如果列表初始化伴隨著名字,比如類型或變數名,你可以當名字是函式、{} 是函式呼叫的括號來格式化它。反之,就當它有個長度為零的名字。

// 一行列表初始化示範。
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};

// 當不得不斷行時。
SomeFunction(
    {"assume a zero-length name before {"},
    some_other_function_parameter);
SomeType variable{
    some, other, values,
    {"assume a zero-length name before {"},
    SomeOtherType{
        "Very long string requiring the surrounding breaks.",
        some, other values},
    SomeOtherType{"Slightly shorter string",
                  some, other, values}};
SomeType variable{
    "This is too long to fit all in one line"};
MyType m = {  // 注意了,你可以在 { 前斷行。
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {interiorwrappinglist,
     interiorwrappinglist2}};

8.8. 條件語句

Tip

傾向於不在圓括號內使用空格. 關鍵字 ifelse 另起一行.

對基本條件語句有兩種可以接受的格式. 一種在圓括號和條件之間有空格, 另一種沒有.

最常見的是沒有空格的格式. 哪種都可以, 但 保持一致性. 如果你是在修改一個文件, 參考當前已有格式. 如果是寫新的程式碼, 參考目錄下或專案中其它文件. 還在徘徊的話, 就不要加空格了.

if (condition) {  圓括號裡沒空格緊鄰。
  ...  // 2 空格縮排。
} else {  // else 與 if 的右括號同一行。
  ...
}

如果你更喜歡在圓括號內部加空格:

if ( condition ) {  // 圓括號與空格緊鄰 - 不常見
  ...  // 2 空格縮排。
} else {  // else 與 if 的右括號同一行。
  ...
}

注意所有情況下 if 和左圓括號間都有個空格. 右圓括號和左大括號之間也要有個空格:

Warning

if(condition)     // 差 - IF 後面沒空格。
if (condition){   // 差 - { 前面沒空格。
if(condition){    // 變本加厲地差。
if (condition) {  // 可 - IF 和 { 都與空格緊鄰。

如果能增強可讀性, 簡短的條件語句允許寫在同一行. 只有當語句簡單並且沒有使用 else 子句時使用:

if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

如果語句有 else 分支則不允許:

Warning

// 不可以這樣子 - 當有 ELSE 分支時 IF 塊卻只有一行
if (x) DoThis();
else DoThat();

通常, 單行語句不需要使用大括號, 如果你喜歡用也沒問題; 複雜的條件或循環語句用大括號可讀性會更好. 也有一些專案要求 if 必須總是使用大括號:

if (condition)
  DoSomething();  // 2 空格縮排。

if (condition) {
  DoSomething();  // 2 空格縮排。
}

但如果語句中某個 if-else 分支使用了大括號的話, 其它分支也必須使用:

Warning

// 不可以這樣子 - IF 有大括號 ELSE 卻沒有。
if (condition) {
    foo;
} else
    bar;

// 不可以這樣子 - ELSE 有大括號 IF 卻沒有。
if (condition)
    foo;
else {
    bar;
}
// 只要其中一個分支用了大括號,兩個分支都要用上大括號。
if (condition) {
  foo;
} else {
  bar;
}

8.9. 循環和開關選擇語句

Tip

switch 語句可以使用大括號分段,以表明 cases 之間不是連在一起的。在單語句循環裡,括號可用可不用。空循環體應使用 {}continue.

switch 語句中的 case 塊可以使用大括號也可以不用, 取決於你的個人喜好. 如果用的話, 要按照下文所述的方法.

如果有不滿足 case 條件的列舉值, switch 應該總是包含一個 default 匹配 (如果有輸入值沒有 case 去處理, 編譯器將報警). 如果 default 應該永遠執行不到, 簡單的加條 assert:

switch (var) {
  case 0: {  // 2 空格縮排
    ...      // 4 空格縮排
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

在單語句循環裡,括號可用可不用:

for (int i = 0; i < kSomeNumber; ++i)
    printf("I love you\n");

for (int i = 0; i < kSomeNumber; ++i) {
    printf("I take it back\n");
}

空循環體應使用 {}continue, 而不是一個簡單的分號.

while (condition) {
  // 反覆循環直到條件失效。
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循環體。
while (condition) continue;  // 可 - contunue 表明沒有邏輯。

Warning

while (condition);  // 差 - 看起來僅僅只是 while/loop 的部分之一。

8.10. 指標和引用表達式

Tip

句點或箭頭前後不要有空格. 指標/地址操作符 (*, &) 之後不能有空格.

下面是指標和引用表達式的正確使用範例:

x = *p;
p = &x;
x = r.y;
x = r->y;
注意:啊
  • 在訪問成員時, 句點或箭頭前後沒有空格.
  • 指標操作符 *& 後沒有空格.

在宣告指標變數或參數時, 星號與類型或變量名緊挨都可以:

// 好樣的,空格前置。
char *c;
const string &str;

// 好樣的,空格後置。
char* c;    // 但別忘了 "char* c, *d, *e, ...;"!
const string& str;

Warning

char * c;  // 差 - * 兩邊都有空格
const string & str;  // 差 - & 兩邊都有空格。

在單個文件內要保持風格一致, 所以, 如果是修改現有文件, 要遵照該文件的風格.

8.11. 布爾表達式

Tip

如果一個布爾表達式超過 標準行寬, 斷行方式要統一一下.

下例中, 邏輯與 (&&) 操作符總位於行尾:

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another & last_one) {
  ...
}

注意, 上例的邏輯與 (&&) 操作符均位於行尾. 這格式在 Google 裡很常見,你要把所有操作符放在開頭也可以。可以考慮額外插入圓括號, 合理使用的話對增強可讀性是很有幫助的. 此外直接用符號形式的操作符,比如 &&~, 不要用詞語形式的 andcompl.

8.12. 函式返回值

Tip

return 表達式裡時沒必要都用圓括號。

假如你寫 x = epr 時本來就會加上括號,那 return expr; 也可如法炮製。

函式返回時不要使用圓括號:

return result;                  // 返回值很簡單,沒有圓括號。
// 可以用圓括號把複雜表達式圈起來,改善可讀性。
return (some_long_condition &&
        another_condition);

Warning

return (value);                // 畢竟你從來不會寫 var = (value);
return(result);                // return 可不是函式!

8.13. 變數及陣列初始化

Tip

=, (){} 均可.

你可以用 =, (){}, 以下都對:

int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};

請務必小心列表初始化 {...} 用 std::initializer_list 建構子初始化出的類型。非空列表初始化就會優先呼叫 std::initializer_list, 不過空列表初始化除外,後者原則上會呼叫默認建構函式。為了強制禁用 std::initializer_list 構造函式,請改用括號。

vector<int> v(100, 1);  // A vector of 100 1s.
vector<int> v{100, 1};  // A vector of 100, 1.

此外,列表初始化不允許整數類型的四捨五入,這可以用來避免一些類型上的程式撰寫失誤。

int pi(3.14);  // 可 -- pi == 3.
int pi{3.14};  // Compile error: narrowing conversion.

8.14. 前處理指令

Tip

前處理指令不要縮排, 從行首開始.

即使前處理指令位於縮排程式碼塊中, 指令也應從行首開始.

// 可 - directives at beginning of line
  if (lopsided_score) {
#if DISASTER_PENDING      // 正確 -- 行開頭起。
    DropEverything();
#endif
    BackToNormal();
  }

Warning

// 差 - indented directives
  if (lopsided_score) {
    #if DISASTER_PENDING  // 錯了! "#if" 應該放在行開頭
    DropEverything();
    #endif                // 錯了! "#endif" 不要縮排
    BackToNormal();
  }

8.15. 類格式

Tip

訪問控制塊的宣告依次序是 public:, protected:, private:, 每次縮排 1 個空格.

類宣告 (對類註解不瞭解的話, 參考 類註釋) 的基本格式如下:

class MyClass : public OtherClass {
 public:      // 注意有 1 空格縮排!
  MyClass();  // 照常,2 空格縮排。
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
  DISALLOW_COPY_AND_ASSIGN(MyClass);
};

注意事項:

  • 所有父類別名應在 80 列限制下盡量與子類名放在同一行.
  • 關鍵詞 public:, protected:, private: 要縮排 1 個空格.
  • 除第一個關鍵詞 (一般是 public) 外, 其他關鍵詞前要空一行. 如果類比較小的話也可以不空.
  • 這些關鍵詞後不要保留空行.
  • public 放在最前面, 然後是 protected, 最後是 private.
  • 關於宣告順序的規則請參考 宣告順序 一節.

8.16. 建構子初始值列表

Tip

建構子初始值列表放在同一行或按四格縮排並排幾行.

下面兩種初始值列表方式都可以接受:

// 當全放在一行合適時:
MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) {

// 如果要斷成多行,縮排四格,冒號放在第一行初始化句:
MyClass::MyClass(int var)
    : some_var_(var),             // 4 空格縮排
      some_other_var_(var + 1) {  // 對準
  ...
  DoSomething();
  ...
}

8.17. 命名空間格式化

Tip

命名空間內容不縮排.

命名空間 不要增加額外的縮排層次, 例如:

namespace {

void foo() {  // 正確。命名空間內沒有額外的縮排。
  ...
}

}  // namespace

不要縮排命名空間:

Warning

namespace {

  // 錯,縮排多餘了。
  void foo() {
    ...
  }

}  // namespace

宣告嵌套命名空間時,每命名空間都獨立成行。

namespace foo {
namespace bar {

8.18. 水平留白

Tip

水平留白的使用因地制宜. 永遠不要在行尾添加沒意義的留白.

常規:

void f(bool b) {  // 左大括號前恆有空格。
  ...
int i = 0;  // 分號前不加空格。
int x[] = { 0 };  // 大括號內部可與空格緊鄰也不可,不過兩邊都要加上。
int x[] = {0};
// 繼承與初始化列表中的冒號前後恆有空格。
class Foo : public Bar {
 public:
  // 至於內聯函式實作,在大括號內部加上空格並編寫實作。
  Foo(int b) : Bar(), baz_(b) {}  // 大括號裡面是空的話,不加空格。
  void Reset() { baz_ = 0; }  // 用括號把大括號與實作分開。
  ...

添加冗余的留白會給其他人編輯時造成額外負擔. 因此, 行尾不要留空格. 如果確定一行程式碼已經修改完畢, 將多餘的空格去掉; 或者在專門清理空格時去掉(確信沒有其他人在處理). (Yang.Y 注: 現在大部分程式碼編輯器稍加設置後, 都支持自動刪除行首/行尾空格, 如果不支持, 考慮換一款編輯器或 IDE)

循環和條件語句:

if (b) {          // if 條件語句和循環語句關鍵字後均有空格。
} else {          // else 前後有空格。
}
while (test) {}   // 圓括號內部不緊鄰空格。
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) {    // 循環和條件語句的圓括號裡可以與空格緊鄰。
if ( test ) {     // 圓括號,但這很少見。總之要一致。
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) {  // 循環裡內 ; 後恆有空格,; 前可以加個空格。
switch (i) {
  case 1:         // switch case 的冒號前無空格。
    ...
  case 2: break;  // 如果冒號有程式碼,加個空格。

操作符:

// 賦值操作系統前後恆有空格。
x = 0;

// 其它二元操作符也前後恆有空格,不過對 factors 前後不加空格也可以。
// 圓括號內部不緊鄰空格。
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在參數和一元操作符之間不加空格。
x = -5;
++x;
if (x && !y)
  ...

樣板和轉換:

// 尖叫括號(< and >) 不與空格緊鄰,< 前沒有空格,>( 之間也沒有。
vector<string> x;
y = static_cast<char*>(x);

// 在類型與指標操作符之間留空格也可以,但要保持一致。
vector<char *> x;
set<list<string>> x;        // 在 C++11 程式碼裡可以這樣用了。
set<list<string> > x;       // C++03 中要在 > > 裡留個空格。

// 你或許可以在 < < 裡加上一對對稱的空格。
set< list<string> > x;

8.19. 垂直留白

Tip

垂直留白越少越好.

這不僅僅是規則而是原則問題了: 不在萬不得已, 不要使用空行. 尤其是: 兩個函式定義之間的空行不要超過 2 行, 函式體首尾不要留空行, 函式體中也不要隨意添加空行.

基本原則是: 同一屏可以顯示的程式碼越多, 越容易理解程式的控制串流. 當然, 過於密集的程式碼塊和過於疏鬆的程式碼塊同樣難看, 取決於你的判斷. 但通常是垂直留白越少越好.

空行心得如下:

  • 函式體內開頭或結尾的空行可讀性微乎其微。
  • 在多重 if-else 塊裡加空行或許有點可讀性。

譯者 (YuleFox) 筆記

  1. 對於程式碼格式, 因人, 系統而異各有優缺點, 但同一個專案中遵循同一標準還是有必要的;
  2. 行寬原則上不超過 80 列, 把 22 寸的顯示屏都占完, 怎麼也說不過去;
  3. 盡量不使用非 ASCII 字符, 如果使用的話, 參考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考慮寬字符), 盡量不將字符串常數耦合到程式碼中, 比如獨立出資源文件, 這不僅僅是風格問題了;
  4. UNIX/Linux 下無條件使用空格, MSVC 的話使用 Tab 也無可厚非;
  5. 函式參數, 邏輯條件, 初始化列表: 要麼所有參數和函式名放在同一行, 要麼所有參數並排分行;
  6. 除函式定義的左大括號可以置於行首外, 包括函式/類/結構體/列舉宣告, 各種語句的左大括號置於行尾, 所有右大括號獨立成行;
  7. ./-> 操作符前後不留空格, */& 不要前後都留, 一個就可, 靠左靠右依各人喜好;
  8. 前處理指令/命名空間不使用額外縮排, 類/結構體/列舉/函式/語句使用縮排;
  9. 初始化用 = 還是 () 依個人喜好, 統一就好;
  10. return 不要加 ();
  11. 水平/垂直留白不要濫用, 怎麼易讀怎麼來.
  12. 關於 UNIX/Linux 風格為什麼要把左大括號置於行尾 (.cc 文件的函式實作處, 左大括號位於行首), 我的理解是程式碼看上去比較簡約, 想想行首除了函式體被一對大括號封在一起之外, 只有右大括號的程式碼看上去確實也舒服; Windows 風格將左大括號置於行首的優點是匹配情況一目瞭然.

譯者(acgtyrant)筆記

  1. 80 行限制事實上有助於避免程式碼可讀性失控,比如超多重嵌套塊,超多重函式呼叫等等。
  2. Linux 上設置好了 Locale 就幾乎一勞永逸設置好所有開發環境的編碼,不像奇葩的 Windows.
  3. Google 強調有一對 if-else 時,不論有沒有嵌套,都要有大括號。Apple 正好`有栽過跟頭 <http://coolshell.cn/articles/11112.html>`_.
  4. 其實我主張指標/地址操作符與變數名緊鄰,int* a, b vs int *a, b, 新手會誤以為前者的 bint * 變量,但後者就不一樣了,高下立判。
  5. 在這風格指南裡我才剛知道 C++ 原來還有所謂的 Alternative operator representations, 大概沒人用吧。
  6. 注意建構子初始值列表(Constructer Initializer List)與列表初始化(Initializer List)是兩碼事,我就差點混淆了它們的翻譯。
  7. 事實上,如果你熟悉英語本身的書寫規則,就會發現該風格指南在格式上的規定與英語語法相當一脈相承。比如普通標點符號和單詞後面還有文本的話,總會留一個空格;特殊符號與單詞之間就不用留了,比如 if (true) 中的圓括號與 true.
  8. 本風格指南沒有明確規定 void 函式里要不要用 return 語句,不過就 Google 開源專案 leveldb 並沒有寫;此外從 Is a blank return statement at the end of a function whos return type is void necessary? 來看,return;return ; 更約定俗成(事實上 cpplint 會對後者報錯,指出分號前有多餘的空格),且可用來提前跳出函式棧。

9. 規則特例

前面說明的程式撰寫習慣基本都是強制性的. 但所有優秀的規則都允許例外, 這裡就是探討這些特例.

9.1. 現有不合規範的程式碼

Tip

對於現有不符合既定程式撰寫風格的程式碼可以網開一面.

當你修改使用其他風格的程式碼時, 為了與程式碼原有風格保持一致可以不使用本指南約定. 如果不放心可以與程式碼原作者或現在的負責人員商討, 記住, 一致性 包括原有的一致性.

9.2. Windows 程式碼

Tip

Windows 開發者有自己的程式撰寫習慣, 主要源於 Windows 標頭檔和其它 Microsoft 程式碼. 我們希望任何人都可以順利讀懂你的程式碼, 所以針對所有平台的 C++ 程式撰寫只給出一個單獨的指南.

如果你習慣使用 Windows 編碼風格, 這兒有必要重申一下某些你可能會忘記的指南:

  • 不要使用匈牙利命名法 (比如把整數變數命名成 iNum). 使用 Google 命名約定, 包括對源文件使用 .cc 擴展名.
  • Windows 定義了很多原生類型的同義詞 (YuleFox 注: 這一點, 我也很反感), 如 DWORD, HANDLE 等等. 在呼叫 Windows API 時這是完全可以接受甚至鼓勵的. 但還是盡量使用原有的 C++ 類型, 例如, 使用 const TCHAR * 而不是 LPCTSTR.
  • 使用 Microsoft Visual C++ 進行編譯時, 將警告級別設置為 3 或更高, 並將所有 warnings 當作 errors 處理.
  • 不要使用 #pragma once; 而應該使用 Google 的標頭檔保護規則. 標頭檔保護的路徑應該相對於專案根目錄 (Yang.Y 注: 如 #ifndef SRC_DIR_BAR_H_, 參考 #define 保護 一節).
  • 除非萬不得已, 不要使用任何非標準的擴展, 如 #pragma__declspec. 允許使用 __declspec(dllimport)__declspec(dllexport); 但你必須通過巨集來使用, 比如 DLLIMPORTDLLEXPORT, 這樣其他人在分享使用這些程式碼時很容易就去掉這些擴展.

在 Windows 上, 只有很少的一些情況下, 我們可以偶爾違反規則:

  • 通常我們 禁止使用多重繼承, 但在使用 COM 和 ATL/WTL 類時可以使用多重繼承. 為了實作 COM 或 ATL/WTL 類/介面, 你可能不得不使用多重實作繼承.
  • 雖然程式碼中不應該使用例外, 但是在 ATL 和部分 STL(包括 Visual C++ 的 STL) 中異常被廣泛使用. 使用 ATL 時, 應定義 _ATL_NO_EXCEPTIONS 以禁用異常. 你要研究一下是否能夠禁用 STL 的異常, 如果無法禁用, 啟用編譯器異常也可以. (注意這只是為了編譯 STL, 自己程式碼裡仍然不要含異常處理.)
  • 通常為了利用標頭檔預編譯, 每個每個源文件的開頭都會包含一個名為 StdAfx.hprecompile.h 的文件. 為了使程式碼方便與其他專案共享, 避免顯式包含此文件 (precompile.cc), 使用 /FI 編譯器選項以自動包含.
  • 資源標頭檔通常命名為 resource.h, 且只包含巨集的, 不需要遵守本風格指南.

10. 結語

Tip

運用常識和判斷力, 並 保持一致.

編輯程式碼時, 花點時間看看專案中的其它程式碼, 並熟悉其風格. 如果其它程式碼中 if 語句使用空格, 那麼你也要使用. 如果其中的註解用星號 (*) 圍成一個盒子狀, 你同樣要這麼做.

風格指南的重點在於提供一個通用的程式撰寫規範, 這樣大家可以把精力集中在實作內容而不是表現形式上. 我們展示了全域的風格規範, 但局部風格也很重要, 如果你在一個文件中新加的程式碼和原有程式碼風格相去甚遠, 這就破壞了文件本身的整體美觀, 也影響閱讀, 所以要盡量避免.

好了, 關於編碼風格寫的夠多了; 程式碼本身才更有趣. 盡情享受吧!

Objective-C 風格指南 - 內容目錄

Google Objective-C Style Guide 中文版

版本:

2.36

原作者:
Mike Pinkerton
Greg Miller
Dave MacLachlan
翻譯:
項目主頁:

譯者的話

ewanke

一直想翻譯這個 style guide ,終於在週末花了7個小時的時間用vim敲出了HTML。很多術語的翻譯很難,平時看的中文技術類書籍有限,對很多術語的中文譯法不是很清楚,難免有不恰當之處,請讀者指出並幫我改進:王軻 」ewangke at gmail.com」 2011.03.27

Yang.Y

對 Objective-C 的瞭解有限,憑著感覺和 C/C++ 方面的理解:

  • 把指南更新到 2.36 版本
  • 調整了一些術語和句子

背景介紹

Objective-C 是 C 語言的擴展,增加了動態類型和面對對象的特性。它被設計成具有易讀易用的,支持複雜的面向對像設計的編程語言。它是 Mac OS X 以及 iPhone 的主要開發語言。

Cocoa 是 Mac OS X 上主要的應用程序框架之一。它由一組 Objective-C 類組成,為快速開發出功能齊全的 Mac OS X 應用程序提供支持。

蘋果公司已經有一份非常全面的 Objective-C 編碼指南。Google 為 C++ 也寫了一份類似的編碼指南。而這份 Objective-C 指南則是蘋果和 Google 常規建議的最佳結合。因此,在閱讀本指南之前,請確定你已經閱讀過:

Note

所有在 Google 的 C++ 風格指南中所禁止的事情,如未明確說明,也同樣不能在Objective-C++ 中使用。

本文檔的目的在於為所有的 Mac OS X 的代碼提供編碼指南及實踐。許多準則是在實際的項目和小組中經過長期的演化、驗證的。Google 開發的開源項目遵從本指南的要求。

Google 已經發佈了遵守本指南開源代碼,它們屬於 Google Toolbox for Mac project 項目(本文以縮寫 GTM 指代)。GTM 代碼庫中的代碼通常為了可以在不同項目中復用。

注意,本指南不是 Objective-C 教程。我們假定讀者對 Objective-C 非常熟悉。如果你剛剛接觸 Objective-C 或者需要溫習,請閱讀 The Objective-C Programming Language

例子

都說一個例子頂上一千句話,我們就從一個例子開始,來感受一下編碼的風格、留白以及命名等等。

一個頭文件的例子,展示了在 @interface 聲明中如何進行正確的註釋以及留白。

//  Foo.h
//  AwesomeProject
//
//  Created by Greg Miller on 6/13/08.
//  Copyright 2008 Google, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>

// A sample class demonstrating good Objective-C style. All interfaces,
// categories, and protocols (read: all top-level declarations in a header)
// MUST be commented. Comments must also be adjacent to the object they're
// documenting.
//
// (no blank line between this comment and the interface)
@interface Foo : NSObject {
 @private
  NSString *bar_;
  NSString *bam_;
}

// Returns an autoreleased instance of Foo. See -initWithBar: for details
// about |bar|.
+ (id)fooWithBar:(NSString *)bar;

// Designated initializer. |bar| is a thing that represents a thing that
// does a thing.
- (id)initWithBar:(NSString *)bar;

// Gets and sets |bar_|.
- (NSString *)bar;
- (void)setBar:(NSString *)bar;

// Does some work with |blah| and returns YES if the work was completed
// successfully, and NO otherwise.
- (BOOL)doWorkWithBlah:(NSString *)blah;

@end

一個源文件的例子,展示了 @implementation 部分如何進行正確的註釋、留白。同時也包括了基於引用實現的一些重要方法,如 getterssettersinit 以及 dealloc

//
//  Foo.m
//  AwesomeProject
//
//  Created by Greg Miller on 6/13/08.
//  Copyright 2008 Google, Inc. All rights reserved.
//

#import "Foo.h"


@implementation Foo

+ (id)fooWithBar:(NSString *)bar {
  return [[[self alloc] initWithBar:bar] autorelease];
}

// Must always override super's designated initializer.
- (id)init {
  return [self initWithBar:nil];
}

- (id)initWithBar:(NSString *)bar {
  if ((self = [super init])) {
    bar_ = [bar copy];
    bam_ = [[NSString alloc] initWithFormat:@"hi %d", 3];
  }
  return self;
}

- (void)dealloc {
  [bar_ release];
  [bam_ release];
  [super dealloc];
}

- (NSString *)bar {
  return bar_;
}

- (void)setBar:(NSString *)bar {
  [bar_ autorelease];
  bar_ = [bar copy];
}

- (BOOL)doWorkWithBlah:(NSString *)blah {
  // ...
  return NO;
}

@end

不要求在 @interface@implementation@end 前後空行。如果你在 @interface 聲明了實例變量,則須在關括號 } 之後空一行。

除非接口和實現非常短,比如少量的私有方法或橋接類,空行方有助於可讀性。

留白和格式

空格 vs. 製表符

Tip

只使用空格,且一次縮進兩個空格。

我們使用空格縮進。不要在代碼中使用製表符。你應該將編輯器設置成自動將製表符替換成空格。

行寬

盡量讓你的代碼保持在 80 列之內。

我們深知 Objective-C 是一門繁冗的語言,在某些情況下略超 80 列可能有助於提高可讀性,但這也只能是特例而已,不能成為開脫。

如果閱讀代碼的人認為把把某行行寬保持在 80 列仍然有不失可讀性,你應該按他們說的去做。

我們意識到這條規則是有爭議的,但很多已經存在的代碼堅持了本規則,我們覺得保證一致性更重要。

通過設置 Xcode > Preferences > Text Editing > Show page guide,來使越界更容易被發現。

方法聲明和定義

Tip

  • / + 和返回類型之間須使用一個空格,參數列表中只有參數之間可以有空格。

方法應該像這樣:

- (void)doSomethingWithString:(NSString *)theString {
  ...
}

星號前的空格是可選的。當寫新的代碼時,要與先前代碼保持一致。

如果一行有非常多的參數,更好的方式是將每個參數單獨拆成一行。如果使用多行,將每個參數前的冒號對齊。

- (void)doSomethingWith:(GTMFoo *)theFoo
                   rect:(NSRect)theRect
               interval:(float)theInterval {
  ...
}

當第一個關鍵字比其它的短時,保證下一行至少有 4 個空格的縮進。這樣可以使關鍵字垂直對齊,而不是使用冒號對齊:

- (void)short:(GTMFoo *)theFoo
    longKeyword:(NSRect)theRect
    evenLongerKeyword:(float)theInterval {
  ...
}

方法調用

Tip

方法調用應盡量保持與方法聲明的格式一致。當格式的風格有多種選擇時,新的代碼要與已有代碼保持一致。

調用時所有參數應該在同一行:

[myObject doFooWith:arg1 name:arg2 error:arg3];

或者每行一個參數,以冒號對齊:

[myObject doFooWith:arg1
               name:arg2
              error:arg3];

不要使用下面的縮進風格:

[myObject doFooWith:arg1 name:arg2  // some lines with >1 arg
              error:arg3];

[myObject doFooWith:arg1
               name:arg2 error:arg3];

[myObject doFooWith:arg1
          name:arg2  // aligning keywords instead of colons
          error:arg3];

方法定義與方法聲明一樣,當關鍵字的長度不足以以冒號對齊時,下一行都要以四個空格進行縮進。

[myObj short:arg1
    longKeyword:arg2
    evenLongerKeyword:arg3];

@public@private

Tip

@public@private 訪問修飾符應該以一個空格縮進。

與 C++ 中的 public, private 以及 protected 非常相似。

@interface MyClass : NSObject {
 @public
  ...
 @private
  ...
}
@end

異常

Tip

每個 @ 標籤應該有獨立的一行,在 @{} 之間需要有一個空格, @catch 與被捕捉到的異常對象的聲明之間也要有一個空格。

如果你決定使用 Objective-C 的異常,那麼就按下面的格式。不過你最好先看看 避免拋出異常 瞭解下為什麼不要使用異常。

@try {
  foo();
}
@catch (NSException *ex) {
  bar(ex);
}
@finally {
  baz();
}

協議名

Tip

類型標識符和尖括號內的協議名之間,不能有任何空格。

這條規則適用於類聲明、實例變量以及方法聲明。例如:

@interface MyProtocoledClass : NSObject<NSWindowDelegate> {
 @private
  id<MyFancyDelegate> delegate_;
}
- (void)setDelegate:(id<MyFancyDelegate>)aDelegate;
@end

塊(閉包)

Tip

塊(block)適合用在 target/selector 模式下創建回調方法時,因為它使代碼更易讀。塊中的代碼應該縮進 4 個空格。

取決於塊的長度,下列都是合理的風格準則:

  • 如果一行可以寫完塊,則沒必要換行。
  • 如果不得不換行,關括號應與塊聲明的第一個字符對齊。
  • 塊內的代碼須按 4 空格縮進。
  • 如果塊太長,比如超過 20 行,建議把它定義成一個局部變量,然後再使用該變量。
  • 如果塊不帶參數,^{ 之間無須空格。如果帶有參數,^( 之間無須空格,但 ) { 之間須有一個空格。
  • 塊內允許按兩個空格縮進,但前提是和項目的其它代碼保持一致的縮進風格。
// The entire block fits on one line.
[operation setCompletionBlock:^{ [self onOperationDone]; }];

// The block can be put on a new line, indented four spaces, with the
// closing brace aligned with the first character of the line on which
// block was declared.
[operation setCompletionBlock:^{
    [self.delegate newDataAvailable];
}];

// Using a block with a C API follows the same alignment and spacing
// rules as with Objective-C.
dispatch_async(fileIOQueue_, ^{
    NSString* path = [self sessionFilePath];
    if (path) {
      // ...
    }
});

// An example where the parameter wraps and the block declaration fits
// on the same line. Note the spacing of |^(SessionWindow *window) {|
// compared to |^{| above.
[[SessionService sharedService]
    loadWindowWithCompletionBlock:^(SessionWindow *window) {
        if (window) {
          [self windowDidLoad:window];
        } else {
          [self errorLoadingWindow];
        }
    }];

// An example where the parameter wraps and the block declaration does
// not fit on the same line as the name.
[[SessionService sharedService]
    loadWindowWithCompletionBlock:
        ^(SessionWindow *window) {
            if (window) {
              [self windowDidLoad:window];
            } else {
              [self errorLoadingWindow];
            }
        }];

// Large blocks can be declared out-of-line.
void (^largeBlock)(void) = ^{
    // ...
};
[operationQueue_ addOperationWithBlock:largeBlock];

命名

對於易維護的代碼而言,命名規則非常重要。Objective-C 的方法名往往十分長,但代碼塊讀起來就像散文一樣,不需要太多的代碼註釋。

當編寫純粹的 Objective-C 代碼時,我們基本遵守標準的 Objective-C naming rules,這些命名規則可能與 C++ 風格指南中的大相逕庭。例如,Google 的 C++ 風格指南中推薦使用下劃線分隔的單詞作為變量名,而(蘋果的)風格指南則使用駝峰命名法,這在 Objective-C 社區中非常普遍。

任何的類、類別、方法以及變量的名字中都使用全大寫的 首字母縮寫。這遵守了蘋果的標準命名方式,如 URL、TIFF 以及 EXIF。

當編寫 Objective-C++ 代碼時,事情就不這麼簡單了。許多項目需要實現跨平台的 C++ API,並混合一些 Objective-C、Cocoa 代碼,或者直接以 C++ 為後端,前端用本地 Cocoa 代碼。這就導致了兩種命名方式直接不統一。

我們的解決方案是:編碼風格取決於方法/函數以哪種語言實現。如果在一個 @implementation 語句中,就使用 Objective-C 的風格。如果實現一個 C++ 的類,就使用 C++ 的風格。這樣避免了一個函數里面實例變量和局部變量命名規則混亂,嚴重影響可讀性。

文件名

Tip

文件名須反映出其實現了什麼類 – 包括大小寫。遵循你所參與項目的約定。

文件的擴展名應該如下:

.h C/C++/Objective-C 的頭文件
.m Ojbective-C 實現文件
.mm Ojbective-C++ 的實現文件
.cc 純 C++ 的實現文件
.c 純 C 的實現文件

類別的文件名應該包含被擴展的類名,如:GTMNSString+Utils.h 或``GTMNSTextView+Autocomplete.h``。

Objective-C++

Tip

源代碼文件內,Ojbective-C++ 代碼遵循你正在實現的函數/方法的風格。

為了最小化 Cocoa/Objective-C 與 C++ 之間命名風格的衝突,根據待實現的函數/方法選擇編碼風格。實現 @implementation 語句塊時,使用 Objective-C 的命名規則;如果實現一個 C++ 的類,就使用 C++ 命名規則。

// file: cross_platform_header.h

class CrossPlatformAPI {
 public:
  ...
  int DoSomethingPlatformSpecific();  // impl on each platform
 private:
  int an_instance_var_;
};

// file: mac_implementation.mm
#include "cross_platform_header.h"

// A typical Objective-C class, using Objective-C naming.
@interface MyDelegate : NSObject {
 @private
  int instanceVar_;
  CrossPlatformAPI* backEndObject_;
}
- (void)respondToSomething:(id)something;
@end
@implementation MyDelegate
- (void)respondToSomething:(id)something {
  // bridge from Cocoa through our C++ backend
  instanceVar_ = backEndObject->DoSomethingPlatformSpecific();
  NSString* tempString = [NSString stringWithInt:instanceVar_];
  NSLog(@"%@", tempString);
}
@end

// The platform-specific implementation of the C++ class, using
// C++ naming.
int CrossPlatformAPI::DoSomethingPlatformSpecific() {
  NSString* temp_string = [NSString stringWithInt:an_instance_var_];
  NSLog(@"%@", temp_string);
  return [temp_string intValue];
}

類名

Tip

類名(以及類別、協議名)應首字母大寫,並以駝峰格式分割單詞。

應用層 的代碼,應該盡量避免不必要的前綴。為每個類都添加相同的前綴無助於可讀性。當編寫的代碼期望在不同應用程序間復用時,應使用前綴(如:GTMSendMessage)。

類別名

Tip

類別名應該有兩三個字母的前綴以表示類別是項目的一部分或者該類別是通用的。類別名應該包含它所擴展的類的名字。

比如我們要基於 NSString 創建一個用於解析的類別,我們將把類別放在一個名為 GTMNSString+Parsing.h 的文件中。類別本身命名為 GTMStringParsingAdditions (是的,我們知道類別名和文件名不一樣,但是這個文件中可能存在多個不同的與解析有關類別)。類別中的方法應該以 gtm_myCategoryMethodOnAString: 為前綴以避免命名衝突,因為 Objective-C 只有一個名字空間。如果代碼不會分享出去,也不會運行在不同的地址空間中,方法名字就不那麼重要了。

類名與包含類別名的括號之間,應該以一個空格分隔。

Objective-C 方法名

Tip

方法名應該以小寫字母開頭,並混合駝峰格式。每個具名參數也應該以小寫字母開頭。

方法名應盡量讀起來就像句子,這表示你應該選擇與方法名連在一起讀起來通順的參數名。(例如,convertPoint:fromRect:replaceCharactersInRange:withString:)。詳情參見 Apple』s Guide to Naming Methods

訪問器方法應該與他們 要獲取的 成員變量的名字一樣,但不應該以get作為前綴。例如:

- (id)getDelegate;  // AVOID
- (id)delegate;     // GOOD

這僅限於 Objective-C 的方法名。C++ 的方法與函數的命名規則應該遵從 C++ 風格指南中的規則。

變量名

Tip

變量名應該以小寫字母開頭,並使用駝峰格式。類的成員變量應該以下劃線作為後綴。例如:myLocalVariablemyInstanceVariable_。如果不能使用 Objective-C 2.0 的 @property,使用 KVO/KVC 綁定的成員變量可以以一個下劃線作為前綴。

普通變量名

對於靜態的屬性(int 或指針),不要使用匈牙利命名法。盡量為變量起一個描述性的名字。不要擔心浪費列寬,因為讓新的代碼閱讀者立即理解你的代碼更重要。例如:

  • 錯誤的命名:

    int w;
    int nerr;
    int nCompConns;
    tix = [[NSMutableArray alloc] init];
    obj = [someObject object];
    p = [network port];
    
  • 正確的命名:

    int numErrors;
    int numCompletedConnections;
    tickets = [[NSMutableArray alloc] init];
    userInfo = [someObject object];
    port = [network port];
    
實例變量

實例變量應該混合大小寫,並以下劃線作為後綴,如 usernameTextField_。然而,如果不能使用 Objective-C 2.0(操作系統版本的限制),並且使用了 KVO/KVC 綁定成員變量時,我們允許例外(譯者註: KVO=Key Value Observing,KVC=Key Value Coding)。這種情況下,可以以一個下劃線作為成員變量名字的前綴,這是蘋果所接受的鍵/值命名慣例。如果可以使用 Objective-C 2.0,@property 以及 @synthesize 提供了遵從這一命名規則的解決方案。

常量

常量名(如宏定義、枚舉、靜態局部變量等)應該以小寫字母 k 開頭,使用駝峰格式分隔單詞,如:kInvalidHandle,kWritePerm

註釋

雖然寫起來很痛苦,但註釋是保證代碼可讀性的關鍵。下面的規則給出了你應該什麼時候、在哪進行註釋。記住:儘管註釋很重要,但最好的代碼應該自成文檔。與其給類型及變量起一個晦澀難懂的名字,再為它寫註釋,不如直接起一個有意義的名字。

當你寫註釋的時候,記得你是在給你的聽眾寫,即下一個需要閱讀你所寫代碼的貢獻者。大方一點,下一個讀代碼的人可能就是你!

記住所有 C++ 風格指南裡的規則在這裡也同樣適用,不同的之處後續會逐步指出。

文件註釋

Tip

每個文件的開頭以文件內容的簡要描述起始,緊接著是作者,最後是版權聲明和/或許可證樣板。

版權信息及作者

每個文件應該按順序包括如下項:

  • 文件內容的簡要描述
  • 代碼作者
  • 版權信息聲明(如:Copyright 2008 Google Inc.
  • 必要的話,加上許可證樣板。為項目選擇一個合適的授權樣板(例如,Apache 2.0, BSD, LGPL, GPL)。

如果你對其他人的原始代碼作出重大的修改,請把你自己的名字添加到作者裡面。當另外一個代碼貢獻者對文件有問題時,他需要知道怎麼聯繫你,這十分有用。

聲明部分的註釋

Tip

每個接口、類別以及協議應輔以註釋,以描述它的目的及與整個項目的關係。

// A delegate for NSApplication to handle notifications about app
// launch and shutdown. Owned by the main app controller.
@interface MyAppDelegate : NSObject {
  ...
}
@end

如果你已經在文件頭部詳細描述了接口,可以直接說明 「完整的描述請參見文件頭部」,但是一定要有這部分註釋。

另外,公共接口的每個方法,都應該有註釋來解釋它的作用、參數、返回值以及其它影響。

為類的線程安全性作註釋,如果有的話。如果類的實例可以被多個線程訪問,記得註釋多線程條件下的使用規則。

實現部分的註釋

Tip

使用 | 來引用註釋中的變量名及符號名而不是使用引號。

這會避免二義性,尤其是當符號是一個常用詞彙,這使用語句讀起來很糟糕。例如,對於符號 count

// Sometimes we need |count| to be less than zero.

或者當引用已經包含引號的符號:

// Remember to call |StringWithoutSpaces("foo bar baz")|

對像所有權

Tip

當與 Objective-C 最常規的作法不同時,盡量使指針的所有權模型盡量明確。

繼承自 NSObject 的對象的實例變量指針,通常被假定是強引用關係(retained),某些情況下也可以註釋為弱引用(weak)或使用 __weak 生命週期限定符。同樣,聲明的屬性如果沒有被類 retained,必須指定是弱引用或賦予 @property 屬性。然而,Mac 軟件中標記上 IBOutlets 的實例變量,被認為是不會被類 retained 的。

當實例變量指向 CoreFoundation、C++ 或者其它非 Objective-C 對像時,不論指針是否會被 retained,都需要使用 __strong__weak 類型修飾符明確指明。CoreFoundation 和其它非 Objective-C 對像指針需要顯式的內存管理,即便使用了自動引用計數或垃圾回收機制。當不允許使用 __weak 類型修飾符(比如,使用 clang 編譯時的 C++ 成員變量),應使用註釋替代說明。

注意:Objective-C 對像中的 C++ 對象的自動封裝,缺省是不允許的,參見 這裡 的說明。

強引用及弱引用聲明的例子:

@interface MyDelegate : NSObject {
 @private
  IBOutlet NSButton *okButton_;  // normal NSControl; implicitly weak on Mac only

  AnObjcObject* doohickey_;  // my doohickey
  __weak MyObjcParent *parent_;  // so we can send msgs back (owns me)

  // non-NSObject pointers...
  __strong CWackyCPPClass *wacky_;  // some cross-platform object
  __strong CFDictionaryRef *dict_;
}
@property(strong, nonatomic) NSString *doohickey;
@property(weak, nonatomic) NSString *parent;
@end

(譯註:強引用 - 對像被類 retained。弱引用 - 對像沒有被類 retained,如委託)

Cocoa 和 Objective-C 特性

成員變量應該是 @private

Tip

成員變量應該聲明為 @private

@interface MyClass : NSObject {
 @private
  id myInstanceVariable_;
}
// public accessors, setter takes ownership
- (id)myInstanceVariable;
- (void)setMyInstanceVariable:(id)theVar;
@end

明確指定構造函數

Tip

註釋並且明確指定你的類的構造函數。

對於需要繼承你的類的人來說,明確指定構造函數十分重要。這樣他們就可以只重寫一個構造函數(可能是幾個)來保證他們的子類的構造函數會被調用。這也有助於將來別人調試你的類時,理解初始化代碼的工作流程。

重載指定構造函數

Tip

當你寫子類的時候,如果需要 init… 方法,記得重載父類的指定構造函數。

如果你沒有重載父類的指定構造函數,你的構造函數有時可能不會被調用,這會導致非常隱秘而且難以解決的 bug。

重載 NSObject 的方法

Tip

如果重載了 NSObject 類的方法,強烈建議把它們放在 @implementation 內的起始處,這也是常見的操作方法。

通常適用(但不局限)於 init...copyWithZone:,以及 dealloc 方法。所有 init... 方法應該放在一起,copyWithZone: 緊隨其後,最後才是 dealloc 方法。

初始化

Tip

不要在 init 方法中,將成員變量初始化為 0 或者 nil;毫無必要。

剛分配的對象,默認值都是 0,除了 isa 指針(譯者註:NSObjectisa 指針,用於標識對象的類型)。所以不要在初始化器裡面寫一堆將成員初始化為 0 或者 nil 的代碼。

避免 +new

Tip

不要調用 NSObject 類方法 new,也不要在子類中重載它。使用 allocinit 方法創建並初始化對象。

現代的 Ojbective-C 代碼通過調用 allocinit 方法來創建並 retain 一個對象。由於類方法 new 很少使用,這使得有關內存分配的代碼審查更困難。

保持公共 API 簡單

Tip

保持類簡單;避免 「廚房水槽(kitchen-sink)」 式的 API。如果一個函數壓根沒必要公開,就不要這麼做。用私有類別保證公共頭文件整潔。

與 C++ 不同,Objective-C 沒有方法來區分公共的方法和私有的方法 – 所有的方法都是公共的(譯者註:這取決於 Objective-C 運行時的方法調用的消息機制)。因此,除非客戶端的代碼期望使用某個方法,不要把這個方法放進公共 API 中。盡可能的避免了你你不希望被調用的方法卻被調用到。這包括重載父類的方法。對於內部實現所需要的方法,在實現的文件中定義一個類別,而不是把它們放進公有的頭文件中。

// GTMFoo.m
#import "GTMFoo.h"

@interface GTMFoo (PrivateDelegateHandling)
- (NSString *)doSomethingWithDelegate;  // Declare private method
@end

@implementation GTMFoo(PrivateDelegateHandling)
...
- (NSString *)doSomethingWithDelegate {
  // Implement this method
}
...
@end

Objective-C 2.0 以前,如果你在私有的 @interface 中聲明了某個方法,但在 @implementation 中忘記定義這個方法,編譯器不會抱怨(這是因為你沒有在其它的類別中實現這個私有的方法)。解決文案是將方法放進指定類別的 @implemenation 中。

如果你在使用 Objective-C 2.0,相反你應該使用 類擴展 來聲明你的私有類別,例如:

@interface GMFoo () { ... }

這麼做確保如果聲明的方法沒有在 @implementation 中實現,會觸發一個編譯器告警。

再次說明,「私有的」 方法其實不是私有的。你有時可能不小心重載了父類的私有方法,因而製造出很難查找的 Bug。通常,私有的方法應該有一個相當特殊的名字以防止子類無意地重載它們。

Ojbective-C 的類別可以用來將一個大的 @implementation 拆分成更容易理解的小塊,同時,類別可以為最適合的類添加新的、特定應用程序的功能。例如,當添加一個 「middle truncation」 方法時,創建一個 NSString 的新類別並把方法放在裡面,要比創建任意的一個新類把方法放進裡面好得多。

#import and #include

Tip

#import Ojbective-C/Objective-C++ 頭文件,#include C/C++ 頭文件。

基於你所包括的頭文件的編程語言,選擇使用 #import 或是 #include

  • 當包含一個使用 Objective-C、Objective-C++ 的頭文件時,使用 #import
  • 當包含一個使用標準 C、C++ 頭文件時,使用 #include。頭文件應該使用 #define 保護

一些 Ojbective-C 的頭文件缺少 #define 保護,需要使用 #import 的方式包含。由於 Objective-C 的頭文件只會被 Objective-C 的源文件及頭文件包含,廣泛地使用 #import 是可以的。

文件中沒有 Objective-C 代碼的標準 C、C++ 頭文件,很可能會被普通的 C、C++ 包含。由於標準 C、C++ 裡面沒有 #import 的用法,這些文件將被 #include。在 Objective-C 源文件中使用 #include 包含這些頭文件,意味著這些頭文件永遠會在相同的語義下包含。

這條規則幫助跨平台的項目避免低級錯誤。某個 Mac 開發者寫了一個新的 C 或 C++ 頭文件,如果忘記使用 #define 保護,在 Mac 下使用 #import 這個頭文件不回引起問題,但是在其它平台下使用 #include 將可能編譯失敗。在所有的平台上統一使用 #include,意味著構造更可能全都成功或者失敗,防止這些文件只能在某些平台下能夠工作。

#import <Cocoa/Cocoa.h>
#include <CoreFoundation/CoreFoundation.h>
#import "GTMFoo.h"
#include "base/basictypes.h"

使用根框架

Tip

#import 根框架而不是單獨的零散文件

當你試圖從框架(如 Cocoa 或者 Foundation)中包含若干零散的系統頭文件時,實際上包含頂層根框架的話,編譯器要做的工作更少。根框架通常已經經過預編譯,加載更快。另外記得使用 #import 而不是 #include 來包含 Objective-C 的框架。

#import <Foundation/Foundation.h>     // good

#import <Foundation/NSArray.h>        // avoid
#import <Foundation/NSString.h>
...

構建時即設定 autorelease

Tip

當創建臨時對像時,在同一行使用 autolease,而不是在同一個方法的後面語句中使用一個單獨的 release

儘管運行效率會差一點,但避免了意外刪除 release 或者插入 return 語句而導致內存洩露的可能。例如:

// AVOID (unless you have a compelling performance reason)
MyController* controller = [[MyController alloc] init];
// ... code here that might return ...
[controller release];

// BETTER
MyController* controller = [[[MyController alloc] init] autorelease];

autorelease 優先 retain 其次

Tip

給對像賦值時遵守 autorelease``之後 ``retain 的模式。

當給一個變量賦值新的對象時,必須先釋放掉舊的對象以避免內存洩露。有很多 「正確的」 方法可以處理這種情況。我們則選擇 「autorelease 之後 retain」 的方法,因為事實證明它不容易出錯。注意大的循環會填滿 autorelease 池,並且可能效率上會差一點,但權衡之下我們認為是可以接受的。

- (void)setFoo:(GMFoo *)aFoo {
  [foo_ autorelease];  // Won't dealloc if |foo_| == |aFoo|
  foo_ = [aFoo retain];
}

initdealloc 內避免使用訪問器

Tip

initdealloc 方法執行的過程中,子類可能會處在一個不一致的狀態,所以這些方法中的代碼應避免調用訪問器。

子類尚未初始化,或在 initdealloc 方法執行時已經被銷毀,會使訪問器方法很可能不可靠。實際上,應在這些方法中直接對 ivals 進行賦值或釋放操作。

正確:

- (id)init {
  self = [super init];
  if (self) {
    bar_ = [[NSMutableString alloc] init];  // good
  }
  return self;
}

- (void)dealloc {
  [bar_ release];                           // good
  [super dealloc];
}

錯誤:

- (id)init {
  self = [super init];
  if (self) {
    self.bar = [NSMutableString string];  // avoid
  }
  return self;
}

- (void)dealloc {
  self.bar = nil;                         // avoid
  [super dealloc];
}

按聲明順序銷毀實例變量

Tip

dealloc 中實例變量被釋放的順序應該與它們在 @interface 中聲明的順序一致,這有助於代碼審查。

代碼審查者在評審新的或者修改過的 dealloc 實現時,需要保證每個 retained 的實例變量都得到了釋放。

為了簡化 dealloc 的審查,retained 實例變量被釋放的順序應該與他們在 @interface 中聲明的順序一致。如果 dealloc 調用了其它方法釋放成員變量,添加註釋解釋這些方法釋放了哪些實例變量。

setter 應複製 NSStrings

Tip

接受 NSString 作為參數的 setter,應該總是 copy 傳入的字符串。

永遠不要僅僅 retain 一個字符串。因為調用者很可能在你不知情的情況下修改了字符串。不要假定別人不會修改,你接受的對象是一個 NSString 對像而不是 NSMutableString 對象。

- (void)setFoo:(NSString *)aFoo {
  [foo_ autorelease];
  foo_ = [aFoo copy];
}

避免拋異常

Tip

不要 @throw Objective-C 異常,同時也要時刻準備捕獲從第三方或 OS 代碼中拋出的異常。

我們的確允許 -fobjc-exceptions 編譯開關(主要因為我們要用到 @synchronized ),但我們不使用 @throw。為了合理使用第三方的代碼,@try@catch@finally 是允許的。如果你確實使用了異常,請明確註釋你期望什麼方法拋出異常。

不要使用 NS_DURINGNS_HANDLERNS_ENDHANDLERNS_VALUERETURNNS_VOIDRETURN 宏,除非你寫的代碼需要在 Mac OS X 10.2 或之前的操作系統中運行。

注意:如果拋出 Objective-C 異常,Objective-C++ 代碼中基於棧的對象不會被銷毀。比如:

class exceptiontest {
 public:
  exceptiontest() { NSLog(@"Created"); }
  ~exceptiontest() { NSLog(@"Destroyed"); }
};

void foo() {
  exceptiontest a;
  NSException *exception = [NSException exceptionWithName:@"foo"
                                                   reason:@"bar"
                                                 userInfo:nil];
  @throw exception;
}

int main(int argc, char *argv[]) {
  GMAutoreleasePool pool;
  @try {
    foo();
  }
  @catch(NSException *ex) {
    NSLog(@"exception raised");
  }
  return 0;
}

會輸出:

注意:這裡析構函數從未被調用。這主要會影響基於棧的 smartptr,比如 shared_ptrlinked_ptr,以及所有你可能用到的 STL 對象。因此我們不得不痛苦的說,如果必須在 Objective-C++ 中使用異常,就只用 C++ 的異常機制。永遠不應該重新拋出 Objective-C 異常,也不應該在 @try@catch@finally 語句塊中使用基於棧的 C++ 對象。

nil 檢查

Tip

nil 檢查只用在邏輯流程中。

使用 nil 的檢查來檢查應用程序的邏輯流程,而不是避免崩潰。Objective-C 運行時會處理向 nil 對像發送消息的情況。如果方法沒有返回值,就沒關係。如果有返回值,可能由於運行時架構、返回值類型以及 OS X 版本的不同而不同,參見 Apple』s documentation

注意,這和 C/C++ 中檢查指針是否為 ‵‵NULL`` 很不一樣,C/C++ 運行時不做任何檢查,從而導致應用程序崩潰。因此你仍然需要保證你不會對一個 C/C++ 的空指針解引用。

BOOL 若干陷阱

Tip

將普通整形轉換成 BOOL 時要小心。不要直接將 BOOL 值與 YES 進行比較。

Ojbective-C 中把 BOOL 定義成無符號字符型,這意味著 BOOL 類型的值遠不止 YES``(1)或 ``NO``(0)。不要直接把整形轉換成 ``BOOL。常見的錯誤包括將數組的大小、指針值及位運算的結果直接轉換成 BOOL ,取決於整型結果的最後一個字節,很可能會產生一個 NO 值。當轉換整形至 BOOL 時,使用三目操作符來返回 YES 或者 NO。(譯者註:讀者可以試一下任意的 256 的整數的轉換結果,如 256、512 …)

你可以安全在 BOOL_Bool 以及 bool 之間轉換(參見 C++ Std 4.7.4, 4.12 以及 C99 Std 6.3.1.2)。你不能安全在 BOOL 以及 Boolean 之間轉換,因此請把 Boolean 當作一個普通整形,就像之前討論的那樣。但 Objective-C 的方法標識符中,只使用 BOOL

BOOL 使用邏輯運算符(&&||!)是合法的,返回值也可以安全地轉換成 BOOL,不需要使用三目操作符。

錯誤的用法:

- (BOOL)isBold {
  return [self fontTraits] & NSFontBoldTrait;
}
- (BOOL)isValid {
  return [self stringValue];
}

正確的用法:


  • (BOOL)isBold { return ([self fontTraits] & NSFontBoldTrait) ? YES : NO;

} - (BOOL)isValid {

return [self stringValue] != nil;

} - (BOOL)isEnabled {

return [self isValid] && [self isBold];

}

同樣,不要直接比較 YES/NOBOOL 變量。不僅僅因為影響可讀性,更重要的是結果可能與你想的不同。

錯誤的用法:

BOOL great = [foo isGreat];
if (great == YES)
  // ...be great!

正確的用法:

BOOL great = [foo isGreat];
if (great)
  // ...be great!

屬性(Property)

Tip

屬性(Property)通常允許使用,但需要清楚的瞭解:屬性(Property)是 Objective-C 2.0 的特性,會限制你的代碼只能跑在 iPhone 和 Mac OS X 10.5 (Leopard) 及更高版本上。點引用只允許訪問聲明過的 @property

命名

屬性所關聯的實例變量的命名必須遵守以下劃線作為後綴的規則。屬性的名字應該與成員變量去掉下劃線後綴的名字一模一樣。

使用 @synthesize 指示符來正確地重命名屬性。

@interface MyClass : NSObject {
 @private
  NSString *name_;
}
@property(copy, nonatomic) NSString *name;
@end

@implementation MyClass
@synthesize name = name_;
@end
位置

屬性的聲明必須緊靠著類接口中的實例變量語句塊。屬性的定義必須在 @implementation 的類定義的最上方。他們的縮進與包含他們的 @interface 以及 @implementation 語句一樣。

@interface MyClass : NSObject {
 @private
  NSString *name_;
}
@property(copy, nonatomic) NSString *name;
@end

@implementation MyClass
@synthesize name = name_;
- (id)init {
...
}
@end
字符串應使用 copy 屬性(Attribute)

應總是用 copy 屬性(attribute)聲明 NSString 屬性(property)。

從邏輯上,確保遵守 NSStringsetter 必須使用 copy 而不是 retain 的原則。

原子性

一定要注意屬性(property)的開銷。缺省情況下,所有 synthesizesettergetter 都是原子的。這會給每個 get 或者 set 帶來一定的同步開銷。將屬性(property)聲明為 nonatomic,除非你需要原子性。

點引用

點引用是地道的 Objective-C 2.0 風格。它被使用於簡單的屬性 setget 操作,但不應該用它來調用對象的其它操作。

正確的做法:

NSString *oldName = myObject.name;
myObject.name = @"Alice";

錯誤的做法:

NSArray *array = [[NSArray arrayWithObject:@"hello"] retain];

NSUInteger numberOfItems = array.count;  // not a property
array.release;                           // not a property

沒有實例變量的接口

Tip

沒有聲明任何實例變量的接口,應省略空花括號。

正確的做法:

@interface MyClass : NSObject // Does a lot of stuff - (void)fooBarBam; @end

錯誤的做法:

@interface MyClass : NSObject { } // Does a lot of stuff - (void)fooBarBam; @end

自動 synthesize 實例變量

Tip

只運行在 iOS 下的代碼,優先考慮使用自動 synthesize 實例變量。

synthesize 實例變量時,使用 @synthesize var = var_; 防止原本想調用 self.var = blah; 卻不慎寫成了 var = blah;

不要synthesize CFType的屬性 CFType應該永遠使用@dynamic實現指示符。 儘管CFType不能使用retain屬性特性,開發者必須自己處理retain和release。很少有情況你需要僅僅對它進行賦值,因此最好顯示地實現getter和setter,並作出註釋說明。 列出所有的實現指示符 儘管@dynamic是默認的,顯示列出它以及其它的實現指示符會提高可讀性,代碼閱讀者可以一眼就知道類的每個屬性是如何實現的。

// Header file
@interface Foo : NSObject
// A guy walks into a bar.
@property(nonatomic, copy) NSString *bar;
@end

// Implementation file
@interface Foo ()
@property(nonatomic, retain) NSArray *baz;
@end

@implementation Foo
@synthesize bar = bar_;
@synthesize baz = baz_;
@end

Cocoa 模式

委託模式

Tip

委託對像不應該被 retain

實現委託模式的類應:

  1. 擁有一個名為 delegate_ 的實例變量來引用委託。
  2. 因此,訪問器方法應該命名為 delegatesetDelegate:
  3. delegate_ 對像不應該被 retain

模型/視圖/控制器(MVC)

Tip

分離模型與視圖。分離控制器與視圖、模型。回調 API 使用 @protocol

  • 分離模型與視圖:不要假設模型或者數據源的表示方法。保持數據源與表示層之間的接口抽像。視圖不需要瞭解模型的邏輯(主要的規則是問問你自己,對於數據源的一個實例,有沒有可能有多種不同狀態的表示方法)。
  • 分離控制器與模型、視圖:不要把所有的 「業務邏輯」 放進跟視圖有關的類中。這使代碼非常難以復用。使用控制器類來處理這些代碼,但保證控制器不需要瞭解太多表示層的邏輯。
  • 使用 @protocol 來定義回調 API,如果不是所有的方法都必須實現,使用 @optional``(特例:使用 Objective-C 1.0 時,``@optional 不可用,可使用類別來定義一個 「非正規的協議」)。

Python 風格指南 - 內容目錄

Google Python 風格指南 - 中文版

版本:

2.59

原作者:
Amit Patel
Antoine Picard
Eugene Jhong
Jeremy Hylton
Matt Smart
Mike Shields
翻譯:
guoqiao v2.19
xuxinkun v2.59
項目主頁:

目錄

背景

Python 是 Google主要的腳本語言。這本風格指南主要包含的是針對python的編程準則。

為幫助讀者能夠將代碼準確格式化,我們提供了針對 Vim的配置文件 。對於Emacs用戶,保持默認設置即可。

Python語言規範
Lint

Tip

對你的代碼運行pylint

定義:
pylint是一個在Python源代碼中查找bug的工具. 對於C和C++這樣的不那麼動態的(譯者注: 原文是less dynamic)語言, 這些bug通常由編譯器來捕獲. 由於Python的動態特性, 有些警告可能不對. 不過偽告警應該很少.
優點:
可以捕獲容易忽視的錯誤, 例如輸入錯誤, 使用未賦值的變量等.
缺點:
pylint不完美. 要利用其優勢, 我們有時侯需要: a) 圍繞著它來寫代碼 b) 抑制其告警 c) 改進它, 或者d) 忽略它.
結論:

確保對你的代碼運行pylint.抑制不準確的警告,以便能夠將其他警告暴露出來。

你可以通過設置一個行註釋來抑制告警. 例如:

dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

pylint警告是以一個數字編號(如 C0112 )和一個符號名(如 empty-docstring )來標識的. 在編寫新代碼或更新已有代碼時對告警進行醫治, 推薦使用符號名來標識.

如果警告的符號名不夠見名知意,那麼請對其增加一個詳細解釋。

採用這種抑制方式的好處是我們可以輕鬆查找抑制並回顧它們.

你可以使用命令 pylint --list-msgs 來獲取pylint告警列表. 你可以使用命令 pylint --help-msg=C6409 , 以獲取關於特定消息的更多信息.

相比較於之前使用的 pylint: disable-msg , 本文推薦使用 pylint: disable .

要抑制”參數未使用”告警, 你可以用”_”作為參數標識符, 或者在參數名前加”unused_”. 遇到不能改變參數名的情況, 你可以通過在函數開頭”提到”它們來消除告警. 例如:

def foo(a, unused_b, unused_c, d=None, e=None):
    _ = d, e
    return a
導入

Tip

僅對包和模塊使用導入

定義:
模塊間共享代碼的重用機制.
優點:
命名空間管理約定十分簡單. 每個標識符的源都用一種一致的方式指示. x.Obj表示Obj對像定義在模塊x中.
缺點:
模塊名仍可能衝突. 有些模塊名太長, 不太方便.
結論:

使用 import x 來導入包和模塊.

使用 from x import y , 其中x是包前綴, y是不帶前綴的模塊名.

使用 from x import y as z, 如果兩個要導入的模塊都叫做z或者y太長了.

例如, 模塊 sound.effects.echo 可以用如下方式導入:

from sound.effects import echo
...
echo.EchoFilter(input, output, delay=0.7, atten=4)

導入時不要使用相對名稱. 即使模塊在同一個包中, 也要使用完整包名. 這能幫助你避免無意間導入一個包兩次.

Tip

使用模塊的全路徑名來導入每個模塊

優點:
避免模塊名衝突. 查找包更容易.
缺點:
部署代碼變難, 因為你必須複製包層次.
結論:

所有的新代碼都應該用完整包名來導入每個模塊.

應該像下面這樣導入:

# Reference in code with complete name.
import sound.effects.echo

# Reference in code with just module name (preferred).
from sound.effects import echo
異常

Tip

允許使用異常, 但必須小心

定義:
異常是一種跳出代碼塊的正常控制流來處理錯誤或者其它異常條件的方式.
優點:
正常操作代碼的控制流不會和錯誤處理代碼混在一起. 當某種條件發生時, 它也允許控制流跳過多個框架. 例如, 一步跳出N個嵌套的函數, 而不必繼續執行錯誤的代碼.
缺點:
可能會導致讓人困惑的控制流. 調用庫時容易錯過錯誤情況.
結論:

異常必須遵守特定條件:

  1. 像這樣觸發異常: raise MyException("Error message") 或者 raise MyException . 不要使用兩個參數的形式( raise MyException, "Error message" )或者過時的字符串異常( raise "Error message" ).

  2. 模塊或包應該定義自己的特定域的異常基類, 這個基類應該從內建的Exception類繼承. 模塊的異常基類應該叫做”Error”.

    class Error(Exception):
        pass
    
  3. 永遠不要使用 except: 語句來捕獲所有異常, 也不要捕獲 Exception 或者 StandardError , 除非你打算重新觸發該異常, 或者你已經在當前線程的最外層(記得還是要打印一條錯誤消息). 在異常這方面, Python非常寬容, except: 真的會捕獲包括Python語法錯誤在內的任何錯誤. 使用 except: 很容易隱藏真正的bug.

  4. 盡量減少try/except塊中的代碼量. try塊的體積越大, 期望之外的異常就越容易被觸發. 這種情況下, try/except塊將隱藏真正的錯誤.

  5. 使用finally子句來執行那些無論try塊中有沒有異常都應該被執行的代碼. 這對於清理資源常常很有用, 例如關閉文件.

  6. 當捕獲異常時, 使用 as 而不要用逗號. 例如

    try:
        raise Error
    except Error as error:
        pass
    
全局變量

Tip

避免全局變量

定義:
定義在模塊級的變量.
優點:
偶爾有用.
缺點:
導入時可能改變模塊行為, 因為導入模塊時會對模塊級變量賦值.
結論:

避免使用全局變量, 用類變量來代替. 但也有一些例外:

  1. 腳本的默認選項.
  2. 模塊級常量. 例如: PI = 3.14159. 常量應該全大寫, 用下劃線連接.
  3. 有時候用全局變量來緩存值或者作為函數返回值很有用.
  4. 如果需要, 全局變量應該僅在模塊內部可用, 並通過模塊級的公共函數來訪問.
嵌套/局部/內部類或函數

Tip

鼓勵使用嵌套/本地/內部類或函數

定義:
類可以定義在方法, 函數或者類中. 函數可以定義在方法或函數中. 封閉區間中定義的變量對嵌套函數是只讀的.
優點:
允許定義僅用於有效範圍的工具類和函數.
缺點:
嵌套類或局部類的實例不能序列化(pickled).
結論:
推薦使用.
列表推導(List Comprehensions)

Tip

可以在簡單情況下使用

定義:
列表推導(list comprehensions)與生成器表達式(generator expression)提供了一種簡潔高效的方式來創建列表和迭代器, 而不必借助map(), filter(), 或者lambda.
優點:
簡單的列表推導可以比其它的列表創建方法更加清晰簡單. 生成器表達式可以十分高效, 因為它們避免了創建整個列表.
缺點:
複雜的列表推導或者生成器表達式可能難以閱讀.
結論:

適用於簡單情況. 每個部分應該單獨置於一行: 映射表達式, for語句, 過濾器表達式. 禁止多重for語句或過濾器表達式. 複雜情況下還是使用循環.

Yes:
  result = []
  for x in range(10):
      for y in range(5):
          if x * y > 10:
              result.append((x, y))

  for x in xrange(5):
      for y in xrange(5):
          if x != y:
              for z in xrange(5):
                  if y != z:
                      yield (x, y, z)

  return ((x, complicated_transform(x))
          for x in long_generator_function(parameter)
          if x is not None)

  squares = [x * x for x in range(10)]

  eat(jelly_bean for jelly_bean in jelly_beans
      if jelly_bean.color == 'black')
No:
  result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

  return ((x, y, z)
          for x in xrange(5)
          for y in xrange(5)
          if x != y
          for z in xrange(5)
          if y != z)
默認迭代器和操作符

Tip

如果類型支持, 就使用默認迭代器和操作符. 比如列表, 字典及文件等.

定義:
容器類型, 像字典和列表, 定義了默認的迭代器和關係測試操作符(in和not in)
優點:
默認操作符和迭代器簡單高效, 它們直接表達了操作, 沒有額外的方法調用. 使用默認操作符的函數是通用的. 它可以用於支持該操作的任何類型.
缺點:
你沒法通過閱讀方法名來區分對象的類型(例如, has_key()意味著字典). 不過這也是優點.
結論:

如果類型支持, 就使用默認迭代器和操作符, 例如列表, 字典和文件. 內建類型也定義了迭代器方法. 優先考慮這些方法, 而不是那些返回列表的方法. 當然,這樣遍歷容器時,你將不能修改容器.

Yes:  for key in adict: ...
      if key not in adict: ...
      if obj in alist: ...
      for line in afile: ...
      for k, v in dict.iteritems(): ...
No:   for key in adict.keys(): ...
      if not adict.has_key(key): ...
      for line in afile.readlines(): ...
生成器

Tip

按需使用生成器.

定義:
所謂生成器函數, 就是每當它執行一次生成(yield)語句, 它就返回一個迭代器, 這個迭代器生成一個值. 生成值後, 生成器函數的運行狀態將被掛起, 直到下一次生成.
優點:
簡化代碼, 因為每次調用時, 局部變量和控制流的狀態都會被保存. 比起一次創建一系列值的函數, 生成器使用的內存更少.
缺點:
沒有.
結論:

鼓勵使用. 注意在生成器函數的文檔字符串中使用”Yields:”而不是”Returns:”.

(譯者注: 參看 註釋 )

Lambda函數

Tip

適用於單行函數

定義:
與語句相反, lambda在一個表達式中定義匿名函數. 常用於為 map()filter() 之類的高階函數定義回調函數或者操作符.
優點:
方便.
缺點:
比本地函數更難閱讀和調試. 沒有函數名意味著堆棧跟蹤更難理解. 由於lambda函數通常只包含一個表達式, 因此其表達能力有限.
結論:

適用於單行函數. 如果代碼超過60-80個字符, 最好還是定義成常規(嵌套)函數.

對於常見的操作符,例如乘法操作符,使用 operator 模塊中的函數以代替lambda函數. 例如, 推薦使用 operator.mul , 而不是 lambda x, y: x * y .

條件表達式

Tip

適用於單行函數

定義:
條件表達式是對於if語句的一種更為簡短的句法規則. 例如: x = 1 if cond else 2 .
優點:
比if語句更加簡短和方便.
缺點:
比if語句難於閱讀. 如果表達式很長, 難於定位條件.
結論:
適用於單行函數. 在其他情況下,推薦使用完整的if語句.
默認參數值

Tip

適用於大部分情況.

定義:
你可以在函數參數列表的最後指定變量的值, 例如, def foo(a, b = 0): . 如果調用foo時只帶一個參數, 則b被設為0. 如果帶兩個參數, 則b的值等於第二個參數.
優點:
你經常會碰到一些使用大量默認值的函數, 但偶爾(比較少見)你想要覆蓋這些默認值. 默認參數值提供了一種簡單的方法來完成這件事, 你不需要為這些罕見的例外定義大量函數. 同時, Python也不支持重載方法和函數, 默認參數是一種”仿造”重載行為的簡單方式.
缺點:
默認參數只在模塊加載時求值一次. 如果參數是列表或字典之類的可變類型, 這可能會導致問題. 如果函數修改了對象(例如向列表追加項), 默認值就被修改了.
結論:

鼓勵使用, 不過有如下注意事項:

不要在函數或方法定義中使用可變對像作為默認值.

Yes: def foo(a, b=None):
         if b is None:
             b = []
No:  def foo(a, b=[]):
         ...
No:  def foo(a, b=time.time()):  # The time the module was loaded???
         ...
No:  def foo(a, b=FLAGS.my_thing):  # sys.argv has not yet been parsed...
         ...
屬性(properties)

Tip

訪問和設置數據成員時, 你通常會使用簡單, 輕量級的訪問和設置函數. 建議用屬性(properties)來代替它們.

定義:
一種用於包裝方法調用的方式. 當運算量不大, 它是獲取和設置屬性(attribute)的標準方式.
優點:
通過消除簡單的屬性(attribute)訪問時顯式的get和set方法調用, 可讀性提高了. 允許懶惰的計算. 用Pythonic的方式來維護類的接口. 就性能而言, 當直接訪問變量是合理的, 添加訪問方法就顯得瑣碎而無意義. 使用屬性(properties)可以繞過這個問題. 將來也可以在不破壞接口的情況下將訪問方法加上.
缺點:
屬性(properties)是在get和set方法聲明後指定, 這需要使用者在接下來的代碼中注意: set和get是用於屬性(properties)的(除了用 @property 裝飾器創建的只讀屬性). 必須繼承自object類. 可能隱藏比如操作符重載之類的副作用. 繼承時可能會讓人困惑.
結論:

你通常習慣於使用訪問或設置方法來訪問或設置數據, 它們簡單而輕量. 不過我們建議你在新的代碼中使用屬性. 只讀屬性應該用 @property 裝飾器 來創建.

如果子類沒有覆蓋屬性, 那麼屬性的繼承可能看上去不明顯. 因此使用者必須確保訪問方法間接被調用, 以保證子類中的重載方法被屬性調用(使用模板方法設計模式).

Yes: import math

     class Square(object):
         """A square with two properties: a writable area and a read-only perimeter.

         To use:
         >>> sq = Square(3)
         >>> sq.area
         9
         >>> sq.perimeter
         12
         >>> sq.area = 16
         >>> sq.side
         4
         >>> sq.perimeter
         16
         """

         def __init__(self, side):
             self.side = side

         def __get_area(self):
             """Calculates the 'area' property."""
             return self.side ** 2

         def ___get_area(self):
             """Indirect accessor for 'area' property."""
             return self.__get_area()

         def __set_area(self, area):
             """Sets the 'area' property."""
             self.side = math.sqrt(area)

         def ___set_area(self, area):
             """Indirect setter for 'area' property."""
             self._SetArea(area)

         area = property(___get_area, ___set_area,
                         doc="""Gets or sets the area of the square.""")

         @property
         def perimeter(self):
             return self.side * 4

(譯者注: 老實說, 我覺得這段示例代碼很不恰當, 有必要這麼蛋疼嗎?)

True/False的求值

Tip

盡可能使用隱式false

定義:
Python在布爾上下文中會將某些值求值為false. 按簡單的直覺來講, 就是所有的”空”值都被認為是false. 因此0, None, [], {}, “” 都被認為是false.
優點:
使用Python布爾值的條件語句更易讀也更不易犯錯. 大部分情況下, 也更快.
缺點:
對C/C++開發人員來說, 可能看起來有點怪.
結論:

盡可能使用隱式的false, 例如: 使用 if foo: 而不是 if foo != []: . 不過還是有一些注意事項需要你銘記在心:

  1. 永遠不要用==或者!=來比較單件, 比如None. 使用is或者is not.

  2. 注意: 當你寫下 if x: 時, 你其實表示的是 if x is not None . 例如: 當你要測試一個默認值是None的變量或參數是否被設為其它值. 這個值在布爾語義下可能是false!

  3. 永遠不要用==將一個布爾量與false相比較. 使用 if not x: 代替. 如果你需要區分false和None, 你應該用像 if not x and x is not None: 這樣的語句.

  4. 對於序列(字符串, 列表, 元組), 要注意空序列是false. 因此 if not seq: 或者 if seq:if len(seq):if not len(seq): 要更好.

  5. 處理整數時, 使用隱式false可能會得不償失(即不小心將None當做0來處理). 你可以將一個已知是整型(且不是len()的返回結果)的值與0比較.

    Yes: if not users:
             print 'no users'
    
         if foo == 0:
             self.handle_zero()
    
         if i % 10 == 0:
             self.handle_multiple_of_ten()
    
    No:  if len(users) == 0:
             print 'no users'
    
         if foo is not None and not foo:
             self.handle_zero()
    
         if not i % 10:
             self.handle_multiple_of_ten()
    
  6. 注意‘0’(字符串)會被當做true.

過時的語言特性

Tip

盡可能使用字符串方法取代字符串模塊. 使用函數調用語法取代apply(). 使用列表推導, for循環取代filter(), map()以及reduce().

定義:
當前版本的Python提供了大家通常更喜歡的替代品.
結論:

我們不使用不支持這些特性的Python版本, 所以沒理由不用新的方式.

Yes: words = foo.split(':')

     [x[1] for x in my_list if x[2] == 5]

     map(math.sqrt, data)    # Ok. No inlined lambda expression.

     fn(*args, **kwargs)
No:  words = string.split(foo, ':')

     map(lambda x: x[1], filter(lambda x: x[2] == 5, my_list))

     apply(fn, args, kwargs)
詞法作用域(Lexical Scoping)

Tip

推薦使用

定義:

嵌套的Python函數可以引用外層函數中定義的變量, 但是不能夠對它們賦值. 變量綁定的解析是使用詞法作用域, 也就是基於靜態的程序文本. 對一個塊中的某個名稱的任何賦值都會導致Python將對該名稱的全部引用當做局部變量, 甚至是賦值前的處理. 如果碰到global聲明, 該名稱就會被視作全局變量.

一個使用這個特性的例子:

def get_adder(summand1):
    """Returns a function that adds numbers to a given number."""
    def adder(summand2):
        return summand1 + summand2

    return adder

(譯者注: 這個例子有點詭異, 你應該這樣使用這個函數: sum = get_adder(summand1)(summand2) )

優點:
通常可以帶來更加清晰, 優雅的代碼. 尤其會讓有經驗的Lisp和Scheme(還有Haskell, ML等)程序員感到欣慰.
缺點:

可能導致讓人迷惑的bug. 例如下面這個依據 PEP-0227 的例子:

i = 4
def foo(x):
    def bar():
        print i,
    # ...
    # A bunch of code here
    # ...
    for i in x:  # Ah, i *is* local to Foo, so this is what Bar sees
        print i,
    bar()

因此 foo([1, 2, 3]) 會打印 1 2 3 3 , 不是 1 2 3 4 .

(譯者注: x是一個列表, for循環其實是將x中的值依次賦給i.這樣對i的賦值就隱式的發生了, 整個foo函數體中的i都會被當做局部變量, 包括bar()中的那個. 這一點與C++之類的靜態語言還是有很大差別的.)

結論:
鼓勵使用.
函數與方法裝飾器

Tip

如果好處很顯然, 就明智而謹慎的使用裝飾器

定義:

用於函數及方法的裝飾器 (也就是@標記). 最常見的裝飾器是@classmethod 和@staticmethod, 用於將常規函數轉換成類方法或靜態方法. 不過, 裝飾器語法也允許用戶自定義裝飾器. 特別地, 對於某個函數 my_decorator , 下面的兩段代碼是等效的:

class C(object):
   @my_decorator
   def method(self):
       # method body ...
class C(object):
    def method(self):
        # method body ...
    method = my_decorator(method)
優點:
優雅的在函數上指定一些轉換. 該轉換可能減少一些重複代碼, 保持已有函數不變(enforce invariants), 等.
缺點:
裝飾器可以在函數的參數或返回值上執行任何操作, 這可能導致讓人驚異的隱藏行為. 而且, 裝飾器在導入時執行. 從裝飾器代碼的失敗中恢復更加不可能.
結論:

如果好處很顯然, 就明智而謹慎的使用裝飾器. 裝飾器應該遵守和函數一樣的導入和命名規則. 裝飾器的python文檔應該清晰的說明該函數是一個裝飾器. 請為裝飾器編寫單元測試.

避免裝飾器自身對外界的依賴(即不要依賴於文件, socket, 數據庫連接等), 因為裝飾器運行時這些資源可能不可用(由 pydoc 或其它工具導入). 應該保證一個用有效參數調用的裝飾器在所有情況下都是成功的.

裝飾器是一種特殊形式的”頂級代碼”. 參考後面關於 Main 的話題.

線程

Tip

不要依賴內建類型的原子性.

雖然Python的內建類型例如字典看上去擁有原子操作, 但是在某些情形下它們仍然不是原子的(即: 如果__hash__或__eq__被實現為Python方法)且它們的原子性是靠不住的. 你也不能指望原子變量賦值(因為這個反過來依賴字典).

優先使用Queue模塊的 Queue 數據類型作為線程間的數據通信方式. 另外, 使用threading模塊及其鎖原語(locking primitives). 瞭解條件變量的合適使用方式, 這樣你就可以使用 threading.Condition 來取代低級別的鎖了.

威力過大的特性

Tip

避免使用這些特性

定義:
Python是一種異常靈活的語言, 它為你提供了很多花哨的特性, 諸如元類(metaclasses), 字節碼訪問, 任意編譯(on-the-fly compilation), 動態繼承, 對像父類重定義(object reparenting), 導入黑客(import hacks), 反射, 系統內修改(modification of system internals), 等等.
優點:
強大的語言特性, 能讓你的代碼更緊湊.
缺點:
使用這些很”酷”的特性十分誘人, 但不是絕對必要. 使用奇技淫巧的代碼將更加難以閱讀和調試. 開始可能還好(對原作者而言), 但當你回顧代碼, 它們可能會比那些稍長一點但是很直接的代碼更加難以理解.
結論:
在你的代碼中避免這些特性.
Python風格規範
分號

Tip

不要在行尾加分號, 也不要用分號將兩條命令放在同一行.

行長度

Tip

每行不超過80個字符

例外:

  1. 長的導入模塊語句
  2. 註釋裡的URL

不要使用反斜槓連接行.

Python會將 圓括號, 中括號和花括號中的行隱式的連接起來 , 你可以利用這個特點. 如果需要, 你可以在表達式外圍增加一對額外的圓括號.

Yes: foo_bar(self, width, height, color='black', design=None, x='foo',
             emphasis=None, highlight=0)

     if (width == 0 and height == 0 and
         color == 'red' and emphasis == 'strong'):

如果一個文本字符串在一行放不下, 可以使用圓括號來實現隱式行連接:

x = ('This will build a very long long '
     'long long long long long long string')

在註釋中,如果必要,將長的URL放在一行上。

Yes:  # See details at
      # http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
No:  # See details at
     # http://www.example.com/us/developer/documentation/api/content/\
     # v2.0/csv_file_name_extension_full_specification.html

注意上面例子中的元素縮進; 你可以在本文的 縮進 部分找到解釋.

括號

Tip

寧缺毋濫的使用括號

除非是用於實現行連接, 否則不要在返回語句或條件語句中使用括號. 不過在元組兩邊使用括號是可以的.

Yes: if foo:
         bar()
     while x:
         x = bar()
     if x and y:
         bar()
     if not x:
         bar()
     return foo
     for (x, y) in dict.items(): ...
No:  if (x):
         bar()
     if not(x):
         bar()
     return (foo)
縮進

Tip

用4個空格來縮進代碼

絕對不要用tab, 也不要tab和空格混用. 對於行連接的情況, 你應該要麼垂直對齊換行的元素(見 行長度 部分的示例), 或者使用4空格的懸掛式縮進(這時第一行不應該有參數):

Yes:   # Aligned with opening delimiter
       foo = long_function_name(var_one, var_two,
                                var_three, var_four)

       # Aligned with opening delimiter in a dictionary
       foo = {
           long_dictionary_key: value1 +
                                value2,
           ...
       }

       # 4-space hanging indent; nothing on first line
       foo = long_function_name(
           var_one, var_two, var_three,
           var_four)

       # 4-space hanging indent in a dictionary
       foo = {
           long_dictionary_key:
               long_dictionary_value,
           ...
       }
No:    # Stuff on first line forbidden
      foo = long_function_name(var_one, var_two,
          var_three, var_four)

      # 2-space hanging indent forbidden
      foo = long_function_name(
        var_one, var_two, var_three,
        var_four)

      # No hanging indent in a dictionary
      foo = {
          long_dictionary_key:
              long_dictionary_value,
              ...
      }
空行

Tip

頂級定義之間空兩行, 方法定義之間空一行

頂級定義之間空兩行, 比如函數或者類定義. 方法定義, 類定義與第一個方法之間, 都應該空一行. 函數或方法中, 某些地方要是你覺得合適, 就空一行.

空格

Tip

按照標準的排版規範來使用標點兩邊的空格

括號內不要有空格.

Yes: spam(ham[1], {eggs: 2}, [])
No:  spam( ham[ 1 ], { eggs: 2 }, [ ] )

不要在逗號, 分號, 冒號前面加空格, 但應該在它們後面加(除了在行尾).

Yes: if x == 4:
         print x, y
     x, y = y, x
No:  if x == 4 :
         print x , y
     x , y = y , x

參數列表, 索引或切片的左括號前不應加空格.

Yes: spam(1)
no: spam (1)
Yes: dict['key'] = list[index]
No:  dict ['key'] = list [index]

在二元操作符兩邊都加上一個空格, 比如賦值(=), 比較(==, <, >, !=, <>, <=, >=, in, not in, is, is not), 布爾(and, or, not). 至於算術操作符兩邊的空格該如何使用, 需要你自己好好判斷. 不過兩側務必要保持一致.

Yes: x == 1
No:  x<1

當’=’用於指示關鍵字參數或默認參數值時, 不要在其兩側使用空格.

Yes: def complex(real, imag=0.0): return magic(r=real, i=imag)
No:  def complex(real, imag = 0.0): return magic(r = real, i = imag)

不要用空格來垂直對齊多行間的標記, 因為這會成為維護的負擔(適用於:, #, =等):

Yes:
     foo = 1000  # comment
     long_name = 2  # comment that should not be aligned

     dictionary = {
         "foo": 1,
         "long_name": 2,
         }
No:
     foo       = 1000  # comment
     long_name = 2     # comment that should not be aligned

     dictionary = {
         "foo"      : 1,
         "long_name": 2,
         }
Shebang

Tip

大部分.py文件不必以#!作為文件的開始. 根據 PEP-394 , 程序的main文件應該以 #!/usr/bin/python2或者 #!/usr/bin/python3開始.

(譯者注: 在計算機科學中, Shebang (也稱為Hashbang)是一個由井號和歎號構成的字符串行(#!), 其出現在文本文件的第一行的前兩個字符. 在文件中存在Shebang的情況下, 類Unix操作系統的程序載入器會分析Shebang後的內容, 將這些內容作為解釋器指令, 並調用該指令, 並將載有Shebang的文件路徑作為該解釋器的參數. 例如, 以指令#!/bin/sh開頭的文件在執行時會實際調用/bin/sh程序.)

#!先用於幫助內核找到Python解釋器, 但是在導入模塊時, 將會被忽略. 因此只有被直接執行的文件中才有必要加入#!.

註釋

Tip

確保對模塊, 函數, 方法和行內註釋使用正確的風格

文檔字符串

Python有一種獨一無二的的註釋方式: 使用文檔字符串. 文檔字符串是包, 模塊, 類或函數里的第一個語句. 這些字符串可以通過對象的__doc__成員被自動提取, 並且被pydoc所用. (你可以在你的模塊上運行pydoc試一把, 看看它長什麼樣). 我們對文檔字符串的慣例是使用三重雙引號”“”( PEP-257 ). 一個文檔字符串應該這樣組織: 首先是一行以句號, 問號或驚歎號結尾的概述(或者該文檔字符串單純只有一行). 接著是一個空行. 接著是文檔字符串剩下的部分, 它應該與文檔字符串的第一行的第一個引號對齊. 下面有更多文檔字符串的格式化規範.

模塊

每個文件應該包含一個許可樣板. 根據項目使用的許可(例如, Apache 2.0, BSD, LGPL, GPL), 選擇合適的樣板.

函數和方法

下文所指的函數,包括函數, 方法, 以及生成器.

一個函數必須要有文檔字符串, 除非它滿足以下條件:

  1. 外部不可見
  2. 非常短小
  3. 簡單明瞭

文檔字符串應該包含函數做什麼, 以及輸入和輸出的詳細描述. 通常, 不應該描述”怎麼做”, 除非是一些複雜的算法. 文檔字符串應該提供足夠的信息, 當別人編寫代碼調用該函數時, 他不需要看一行代碼, 只要看文檔字符串就可以了. 對於複雜的代碼, 在代碼旁邊加註釋會比使用文檔字符串更有意義.

關於函數的幾個方面應該在特定的小節中進行描述記錄, 這幾個方面如下文所述. 每節應該以一個標題行開始. 標題行以冒號結尾. 除標題行外, 節的其他內容應被縮進2個空格.

Args:
列出每個參數的名字, 並在名字後使用一個冒號和一個空格, 分隔對該參數的描述.如果描述太長超過了單行80字符,使用2或者4個空格的懸掛縮進(與文件其他部分保持一致). 描述應該包括所需的類型和含義. 如果一個函數接受*foo(可變長度參數列表)或者**bar (任意關鍵字參數), 應該詳細列出*foo和**bar.
Returns: (或者 Yields: 用於生成器)
描述返回值的類型和語義. 如果函數返回None, 這一部分可以省略.
Raises:
列出與接口有關的所有異常.
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
    """Fetches rows from a Bigtable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by big_table.  Silly things may happen if
    other_silly_variable is not None.

    Args:
        big_table: An open Bigtable Table instance.
        keys: A sequence of strings representing the key of each table row
            to fetch.
        other_silly_variable: Another optional variable, that has a much
            longer name than the other args, and which does nothing.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

        {'Serak': ('Rigel VII', 'Preparer'),
         'Zim': ('Irk', 'Invader'),
         'Lrrr': ('Omicron Persei 8', 'Emperor')}

        If a key from the keys argument is missing from the dictionary,
        then that row was not found in the table.

    Raises:
        IOError: An error occurred accessing the bigtable.Table object.
    """
    pass

類應該在其定義下有一個用於描述該類的文檔字符串. 如果你的類有公共屬性(Attributes), 那麼文檔中應該有一個屬性(Attributes)段. 並且應該遵守和函數參數相同的格式.

class SampleClass(object):
    """Summary of class here.

    Longer class information....
    Longer class information....

    Attributes:
        likes_spam: A boolean indicating if we like SPAM or not.
        eggs: An integer count of the eggs we have laid.
    """

    def __init__(self, likes_spam=False):
        """Inits SampleClass with blah."""
        self.likes_spam = likes_spam
        self.eggs = 0

    def public_method(self):
        """Performs operation blah."""

塊註釋和行註釋

最需要寫註釋的是代碼中那些技巧性的部分. 如果你在下次 代碼審查 的時候必須解釋一下, 那麼你應該現在就給它寫註釋. 對於複雜的操作, 應該在其操作開始前寫上若干行註釋. 對於不是一目瞭然的代碼, 應在其行尾添加註釋.

# We use a weighted dictionary search to find out where i is in
# the array.  We extrapolate position based on the largest num
# in the array and the array size and then do binary search to
# get the exact number.

if i & (i-1) == 0:        # true iff i is a power of 2

為了提高可讀性, 註釋應該至少離開代碼2個空格.

另一方面, 絕不要描述代碼. 假設閱讀代碼的人比你更懂Python, 他只是不知道你的代碼要做什麼.

# BAD COMMENT: Now go through the b array and make sure whenever i occurs
# the next element is i+1

Tip

如果一個類不繼承自其它類, 就顯式的從object繼承. 嵌套類也一樣.

Yes: class SampleClass(object):
         pass


     class OuterClass(object):

         class InnerClass(object):
             pass


     class ChildClass(ParentClass):
         """Explicitly inherits from another class already."""
No: class SampleClass:
        pass


    class OuterClass:

        class InnerClass:
            pass

繼承自 object 是為了使屬性(properties)正常工作, 並且這樣可以保護你的代碼, 使其不受Python 3000的一個特殊的潛在不兼容性影響. 這樣做也定義了一些特殊的方法, 這些方法實現了對象的默認語義, 包括 __new__, __init__, __delattr__, __getattribute__, __setattr__, __hash__, __repr__, and __str__ .

字符串

Tip

即使參數都是字符串, 使用%操作符或者格式化方法格式化字符串. 不過也不能一概而論, 你需要在+和%之間好好判定.

Yes: x = a + b
     x = '%s, %s!' % (imperative, expletive)
     x = '{}, {}!'.format(imperative, expletive)
     x = 'name: %s; score: %d' % (name, n)
     x = 'name: {}; score: {}'.format(name, n)
No: x = '%s%s' % (a, b)  # use + in this case
    x = '{}{}'.format(a, b)  # use + in this case
    x = imperative + ', ' + expletive + '!'
    x = 'name: ' + name + '; score: ' + str(n)

避免在循環中用+和+=操作符來累加字符串. 由於字符串是不可變的, 這樣做會創建不必要的臨時對像, 並且導致二次方而不是線性的運行時間. 作為替代方案, 你可以將每個子串加入列表, 然後在循環結束後用 .join 連接列表. (也可以將每個子串寫入一個 cStringIO.StringIO 緩存中.)

Yes: items = ['<table>']
     for last_name, first_name in employee_list:
         items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
     items.append('</table>')
     employee_table = ''.join(items)
No: employee_table = '<table>'
    for last_name, first_name in employee_list:
        employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
    employee_table += '</table>'

在同一個文件中, 保持使用字符串引號的一致性. 使用單引號’或者雙引號”之一用以引用字符串, 並在同一文件中沿用. 在字符串內可以使用另外一種引號, 以避免在字符串中使用. GPyLint已經加入了這一檢查.

(譯者注:GPyLint疑為筆誤, 應為PyLint.)

Yes:
     Python('Why are you hiding your eyes?')
     Gollum("I'm scared of lint errors.")
     Narrator('"Good!" thought a happy Python reviewer.')
No:
     Python("Why are you hiding your eyes?")
     Gollum('The lint. It burns. It burns us.')
     Gollum("Always the great lint. Watching. Watching.")

為多行字符串使用三重雙引號”“”而非三重單引號’‘’. 當且僅當項目中使用單引號’來引用字符串時, 才可能會使用三重’‘’為非文檔字符串的多行字符串來標識引用. 文檔字符串必須使用三重雙引號”“”. 不過要注意, 通常用隱式行連接更清晰, 因為多行字符串與程序其他部分的縮進方式不一致.

Yes:
    print ("This is much nicer.\n"
           "Do it this way.\n")
No:
      print """This is pretty ugly.
  Don't do this.
  """
文件和sockets

Tip

在文件和sockets結束時, 顯式的關閉它.

除文件外, sockets或其他類似文件的對象在沒有必要的情況下打開, 會有許多副作用, 例如:

  1. 它們可能會消耗有限的系統資源, 如文件描述符. 如果這些資源在使用後沒有及時歸還系統, 那麼用於處理這些對象的代碼會將資源消耗殆盡.
  2. 持有文件將會阻止對於文件的其他諸如移動、刪除之類的操作.
  3. 僅僅是從邏輯上關閉文件和sockets, 那麼它們仍然可能會被其共享的程序在無意中進行讀或者寫操作. 只有當它們真正被關閉後, 對於它們嘗試進行讀或者寫操作將會跑出異常, 並使得問題快速顯現出來.

而且, 幻想當文件對像析構時, 文件和sockets會自動關閉, 試圖將文件對象的生命週期和文件的狀態綁定在一起的想法, 都是不現實的. 因為有如下原因:

  1. 沒有任何方法可以確保運行環境會真正的執行文件的析構. 不同的Python實現採用不同的內存管理技術, 比如延時垃圾處理機制. 延時垃圾處理機制可能會導致對像生命週期被任意無限制的延長.
  2. 對於文件意外的引用,會導致對於文件的持有時間超出預期(比如對於異常的跟蹤, 包含有全局變量等).

推薦使用 “with”語句 以管理文件:

with open("hello.txt") as hello_file:
    for line in hello_file:
        print line

對於不支持使用”with”語句的類似文件的對象,使用 contextlib.closing():

import contextlib

with contextlib.closing(urllib.urlopen("http://www.python.org/")) as front_page:
    for line in front_page:
        print line

Legacy AppEngine 中Python 2.5的代碼如使用”with”語句, 需要添加 “from __future__ import with_statement”.

TODO註釋

Tip

為臨時代碼使用TODO註釋, 它是一種短期解決方案. 不算完美, 但夠好了.

TODO註釋應該在所有開頭處包含”TODO”字符串, 緊跟著是用括號括起來的你的名字, email地址或其它標識符. 然後是一個可選的冒號. 接著必須有一行註釋, 解釋要做什麼. 主要目的是為了有一個統一的TODO格式, 這樣添加註釋的人就可以搜索到(並可以按需提供更多細節). 寫了TODO註釋並不保證寫的人會親自解決問題. 當你寫了一個TODO, 請注上你的名字.

# TODO(kl@gmail.com): Use a "*" here for string repetition.
# TODO(Zeke) Change this to use relations.

如果你的TODO是”將來做某事”的形式, 那麼請確保你包含了一個指定的日期(“2009年11月解決”)或者一個特定的事件(“等到所有的客戶都可以處理XML請求就移除這些代碼”).

導入格式

Tip

每個導入應該獨佔一行

Yes: import os
     import sys
No:  import os, sys

導入總應該放在文件頂部, 位於模塊註釋和文檔字符串之後, 模塊全局變量和常量之前. 導入應該按照從最通用到最不通用的順序分組:

  1. 標準庫導入
  2. 第三方庫導入
  3. 應用程序指定導入

每種分組中, 應該根據每個模塊的完整包路徑按字典序排序, 忽略大小寫.

import foo
from foo import bar
from foo.bar import baz
from foo.bar import Quux
from Foob import ar
語句

Tip

通常每個語句應該獨佔一行

不過, 如果測試結果與測試語句在一行放得下, 你也可以將它們放在同一行. 如果是if語句, 只有在沒有else時才能這樣做. 特別地, 絕不要對 try/except 這樣做, 因為try和except不能放在同一行.

Yes:

  if foo: bar(foo)
No:

  if foo: bar(foo)
  else:   baz(foo)

  try:               bar(foo)
  except ValueError: baz(foo)

  try:
      bar(foo)
  except ValueError: baz(foo)
訪問控制

Tip

在Python中, 對於瑣碎又不太重要的訪問函數, 你應該直接使用公有變量來取代它們, 這樣可以避免額外的函數調用開銷. 當添加更多功能時, 你可以用屬性(property)來保持語法的一致性.

(譯者注: 重視封裝的面向對像程序員看到這個可能會很反感, 因為他們一直被教育: 所有成員變量都必須是私有的! 其實, 那真的是有點麻煩啊. 試著去接受Pythonic哲學吧)

另一方面, 如果訪問更複雜, 或者變量的訪問開銷很顯著, 那麼你應該使用像 get_foo()set_foo() 這樣的函數調用. 如果之前的代碼行為允許通過屬性(property)訪問 , 那麼就不要將新的訪問函數與屬性綁定. 這樣, 任何試圖通過老方法訪問變量的代碼就沒法運行, 使用者也就會意識到複雜性發生了變化.

命名

Tip

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_VAR_NAME, instance_var_name, function_parameter_name, local_var_name.

應該避免的名稱

  1. 單字符名稱, 除了計數器和迭代器.
  2. 包/模塊名中的連字符(-)
  3. 雙下劃線開頭並結尾的名稱(Python保留, 例如__init__)

命名約定

  1. 所謂”內部(Internal)”表示僅模塊內可用, 或者, 在類內是保護或私有的.
  2. 用單下劃線(_)開頭表示模塊變量或函數是protected的(使用import * from時不會包含).
  3. 用雙下劃線(__)開頭的實例變量或方法表示類內私有.
  4. 將相關的類和頂級函數放在同一個模塊裡. 不像Java, 沒必要限制一個類一個模塊.
  5. 對類名使用大寫字母開頭的單詞(如CapWords, 即Pascal風格), 但是模塊名應該用小寫加下劃線的方式(如lower_with_under.py). 儘管已經有很多現存的模塊使用類似於CapWords.py這樣的命名, 但現在已經不鼓勵這樣做, 因為如果模塊名碰巧和類名一致, 這會讓人困擾.

Python之父Guido推薦的規範

Type Public Internal
Modules lower_with_under _lower_with_under
Packages lower_with_under  
Classes CapWords _CapWords
Exceptions CapWords  
Functions lower_with_under() _lower_with_under()
Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER
Global/Class Variables lower_with_under _lower_with_under
Instance Variables lower_with_under _lower_with_under (protected) or __lower_with_under (private)
Method Names lower_with_under() _lower_with_under() (protected) or __lower_with_under() (private)
Function/Method Parameters lower_with_under  
Local Variables lower_with_under  
Main

Tip

即使是一個打算被用作腳本的文件, 也應該是可導入的. 並且簡單的導入不應該導致這個腳本的主功能(main functionality)被執行, 這是一種副作用. 主功能應該放在一個main()函數中.

在Python中, pydoc以及單元測試要求模塊必須是可導入的. 你的代碼應該在執行主程序前總是檢查 if __name__ == '__main__' , 這樣當模塊被導入時主程序就不會被執行.

def main():
      ...

if __name__ == '__main__':
    main()

所有的頂級代碼在模塊導入時都會被執行. 要小心不要去調用函數, 創建對像, 或者執行那些不應該在使用pydoc時執行的操作.

臨別贈言

請務必保持代碼的一致性

如果你正在編輯代碼, 花幾分鐘看一下周邊代碼, 然後決定風格. 如果它們在所有的算術操作符兩邊都使用空格, 那麼你也應該這樣做. 如果它們的註釋都用標記包圍起來, 那麼你的註釋也要這樣.

制定風格指南的目的在於讓代碼有規可循, 這樣人們就可以專注於”你在說什麼”, 而不是”你在怎麼說”. 我們在這裡給出的是全局的規範, 但是本地的規範同樣重要. 如果你加到一個文件裡的代碼和原有代碼大相逕庭, 它會讓讀者不知所措. 避免這種情況.

Revision 2.59

Amit Patel
Antoine Picard
Eugene Jhong
Gregory P. Smith
Jeremy Hylton
Matt Smart
Mike Shields
Shane Liebling