缩进风格

在程序设计中，缩进风格（indent style）是管理代码块缩进以表达程序结构的一种约定。本条目主要讨论自由形式语言，例如C及其后裔，但这也可以（并经常）适用于大多数其他编程语言（尤其是花括号编程语言），其中的空白字符则并不重要。缩进风格是代码风格的一个方面。

缩进在大多数编程语言中不是必要条件，而只是作为辅助符号。不过，缩进有助于更好地向人类阅读者表达程序的结构。尤其是用于澄清控制流程结构（例如条件或循环）与其内部、外部代码之间的关系。不过，部分语言（例如Python和occam）使用缩进而非花括号或关键词来确定结构，这被称为越位规则。在这种语言中，缩进对编译器或解释器有意义，而不仅仅是清晰度或风格问题。

## 研究

尽管缩进风格被广泛使用，但关于其价值的研究却很少。Weissman在1974年进行的初步实验没有显示出任何效果。

2023年，Morzeck等人的实验表明，对于嵌套的`if`语句，缩进有显著的积极影响：阅读非缩进代码所需的时间平均比缩进代码多179%。Hanenberg等人的后续实验证实了这一巨大影响（尽管在该实验中，非缩进代码的阅读时间仅增加了113%），并揭示了阅读时间的差异可以由（缩进代码中）可跳过的代码来解释。在另一个关于JSON对象的实验中，阅读非缩进代码的时间甚至增加了544%。

## 花括号位置

缩进风格的主要区别在于复合语句的花括号（`{...}`）的位置，这通常是为涵盖一个控制声明（`if`、`while`、`for`...）。下表展示了本条目中讨论的所有风格的所在位置。为了一致性，缩进深度（字符数）统一使用4个空格表示，这未考虑各风格中首选的缩进深度。

花括号位置｜风格

K&R及变种： 1TBS、Stroustrup、Linux内核、BSD KNF

```c
while (x == y) {
   something();
   somethingelse();
}
```

Allman

```c
while (x == y)
{
   something();
   somethingelse();
}
```

GNU

```c
while (x == y)
 {
   something();
   somethingelse();
 }
```

Whitesmiths

```c
while (x == y)
   {
   something();
   somethingelse();
   }
```

Horstmann

```c
while (x == y)
{   something();
   somethingelse();
}
```

Pico

```c
while (x == y)
{   something();
   somethingelse(); }
```

Ratliff

```c
while (x == y) {
   something();
   somethingelse();
   }
```

Lisp

```c
while (x == y) {
   something();
   somethingelse(); }
```

APL

```c
#define W(c,b) {while(c){b}}
W(x==y,f();b();)
```

## C/C++ 风格

C、C++和其他花括号编程语言的编码风格属性包括但不限于：

- 花括号相对于其他代码元素的位置
- 使用制表符还是空格
- 是否将单语句块包裹在花括号中。支持者认为这更安全，因为插入新语句不会导致控制流与缩进不一致。反对者则认为这会增加代码长度，因为除了 `else if` 结构和 `do{}while` 块之外，块的闭合花括号需要占用一行。

### K&R

Kernighan & Ritchie (K&R) 风格常在C、C++代码中使用，并且是许多衍生风格的基础。它用于原始的Unix内核、Kernighan和Ritchie的《C程序设计语言》一书，以及Kernighan和Plauger的《编程风格的元素》中。

尽管《C程序设计语言》没有明确定义这种风格，但它始终如一地遵循着它。书中提到：

花括号的位置不那么重要，尽管人们对此持有执着的信念。我们选择了这种广受欢迎的风格。选定一种适合你的风格，然后坚持使用它。

在这种风格中，函数的左花括号和右花括号各自独占一行，并与声明保持相同的缩进；而函数体内的语句则多缩进一级。然而，函数内部的多语句块的左花括号与其控制子句在同一行，而右花括号则独占一行，除非它后面紧跟 `else` 或 `while` 等关键字。

示例代码：

```c
int main(int argc, char *argv[])
{
   while (x == y) {
       do_something();
       do_something_else();
       if (some_error)
           fix_issue(); // 无花括号的单语句块
       else
           continue_as_usual();
   }
   final_thing();
}
```

#### 埃及花括号

多行代码块的非对齐花括号被昵称为“埃及花括号”（或“埃及括号”），因为它们类似于古埃及人画像中手臂一上一下的奇特姿势。

#### 单语句

单语句块不使用花括号，这容易导致诸如goto fail漏洞这样易被忽视的错误。

#### 变种：1TBS (OTBS)

“唯一的真括号风格”（The One True Brace Style）（缩写为1TBS或OTBS）是K&R风格的一个变种，其中单语句块的括号不被省略。

```c
bool is_negative(int x)
{
   if (x < 0) {
       return true;
   } else {
       return false;
   }
}
```

尽管C/C++等语言不要求这样做，但为单语句块使用花括号可以确保在插入新语句时，控制流不会与缩进不一致，从而避免诸如苹果公司臭名昭著的goto fail漏洞等问题。

一些来源对“唯一的真括号风格”的定义存在分歧——它可能根据特定语言中最流行的风格而略有不同，有些作者可能会根据主观偏好将截然不同的风格宣称为OTBS，而其他人则认为这是K&R风格的“黑客行话”称呼。

#### 变种：Linux内核

Linux内核源码树使用K&R风格的一种变种。林纳斯·托瓦兹建议贡献者遵循该风格。其属性包括：

- 使用制表符缩进（而非空格），并设定制表位为8个字符。
- 花括号布局与K&R一致：函数定义的花括号独占一行，复合语句的左花括号与控制子句在同一行，并用空格隔开。
- `switch` 语句中的标签（label）与外围块对齐（只有一级缩进）。
- 最大行长为100个字符，尽管首选2020年之前的80个字符限制。
- 复合语句（如if、while和do-while）的单语句体不需要用花括号包围。但是，如果 `if-else` 语句中的任何一个子语句需要花括号，则两个子语句都应包裹在花括号中：

```c
int power(int x, int y)
{
       int result;

       if (y < 0) {
               result = 0;
       } else {
               result = 1;
               while (y-- > 0)
                       result *= x;
       }
       return result;
}
```

#### 变种：Java

大量的Java代码使用K&R风格的一种变种，其中左花括号不仅对于函数内的块在同一行，对于类或方法声明也是如此。这种风格之所以普遍，主要是因为Sun Microsystems最初的风格指南使用了这种K&R变种。因此，Java API的大部分标准源代码都是用这种风格编写的。这在ActionScript和JavaScript中也是一种流行的缩进风格，与Allman风格并列。

#### 变种：Stroustrup

比雅尼·斯特劳斯特鲁普（Bjarne Stroustrup）在他的书中（如《程序设计：原理与实践（使用C++）》和《C++程序设计语言》）针对C++调整了K&R风格。

与上述变种不同，Stroustrup不使用“cuddled else”（即 `} else {` 写在同一行）。因此，Stroustrup会这样写：

```c
   if (x < 0) {
       puts("Negative");
       negative(x);
   }
   else {
       puts("Non-negative");
       nonnegative(x);
   }
```

Stroustrup对类扩展了K&R风格，写法如下：

```cpp
   class Vector {
   public:
       // construct a Vector
       Vector(int s) :elem(new double[s]), sz(s) { }
       // element access: subscripting
       double& operator[](int i) { return elem[i]; }
       int size() { return sz; }
   private:
       // pointer to the elements
       double * elem;
       // number of elements
       int sz;
   };
```

Stroustrup不对标签 public: 和 private: 进行缩进。此外，在这种风格中，函数的左花括号另起一行，而类的左花括号则与类名在同一行。

Stroustrup允许将短函数写在一行中。在Emacs编辑器中，Stroustrup风格是一种可用的命名缩进风格。Stroustrup在他的现代《C++核心指南》中鼓励使用基于K&R的C++布局。

#### 变种：BSD KNF

伯克利软件套件（BSD）操作系统使用一种有时被称为内核标准格式（Kernel Normal Form, KNF）的风格。虽然主要用于内核代码，但也广泛用于用户空间代码。它本质上是贝尔实验室第6版和第7版Unix源代码中使用的K&R风格的一个详尽文档化的变体。

SunOS内核和用户空间使用类似的缩进风格。与KNF一样，这也基于AT&T的风格文档，有时被称为Bill Joy标准格式（Bill Joy Normal Form）。SunOS指南发布于1996年。Bill Shannon编写的 cstyle 程序可以验证源代码文件的缩进正确性。

在这种风格中，硬制表符（vi中的ts）保持为8列，而软制表符通常定义为辅助工具（vi中的sw），并设置为4。硬制表符用于缩进代码块，而额外的软制表符（4个空格）用于所有必须分成多行的连续行。

此外，函数调用在括号前不使用空格，但C语言的原生语句如 `if`, `while`, `do`, `switch` 和 `return` 会使用空格（如果 `return` 使用括号的话）。在其顶层块中未声明局部变量的函数，也应在其左花括号后留一空行。

### Allman风格

Allman风格以埃里克·奥尔曼（Eric Allman）命名。有时也被称为“BSD风格”，因为Allman为BSD Unix编写了许多实用程序（但这不应与上述的“BSD KNF风格”混淆）。

这种风格将与控制语句关联的花括号放在下一行，缩进到与控制语句相同的级别。花括号内的语句则缩进到下一级。

```c
while (x == y)
{
   something();
   something_else();
}

final_thing();
```

这种风格类似于Pascal和Transact-SQL中使用的标准缩进，其中的花括号等同于关键字 `begin` 和 `end`。

这种风格的后果是，缩进的代码通过几乎全是空白的行与包含它的语句清晰地分隔开来，且右花括号与左花括号在同一列对齐。有些人认为这使得查找匹配的括号变得容易。这种块风格还将代码块与关联的控制语句划清界限。注释掉或删除控制语句或代码块，或进行代码重构时，不太可能因悬空或缺失的花括号而引入语法错误。此外，它与外部函数块的花括号位置一致。

#### 变种：Allman-8

Allman-8使用Linux内核变种K&R的8空格缩进和80列限制。据称这种风格有助于提高在投影仪上的可读性。此外，缩进大小和列限制有助于创建一个视觉提示，以识别代码块的过度嵌套。这些优势结合起来，有助于为新的开发者和学习者提供隐含的指导，以管理代码复杂性。

### Whitesmiths风格

Whitesmiths风格，有时也被称为Wishart风格，最初用于第一个商业C编译器——Whitesmiths编译器的文档中。它在Windows早期也很流行，因为它用于三本有影响力的Windows编程书籍：《Programmer's Guide to Windows》、《Programming Windows》和《Windows 3.0 Power Programming Techniques》。

根据Jargon File，Whitesmiths风格与Allman风格在1991年曾是最常见的括号风格，当时两者的流行程度大致相当。

这种风格将与控制语句关联的花括号放在下一行，并进行缩进。花括号内的语句缩进到与花括号相同的级别。

与Ratliff风格一样，右花括号与花括号内的语句缩进相同。

```c
while (x == y)
   {
   something();
   something_else();
   }

final_thing();
```

这种风格的优点与Allman风格相似。代码块与控制语句清晰地分开。花括号与代码块的对齐强调了整个代码块在概念上和程序上都是一个复合语句。缩进花括号强调了它们从属于控制语句。结束的花括号不再与语句对齐，而是与开始的花括号对齐。

`else if` 被视为一种语句，就像预处理器语句 `#elif` 一样。

### GNU风格

像Allman和Whitesmiths风格一样，GNU风格将花括号单独放在一行，缩进两个空格，而在打开函数定义时除外，此时它们不缩进。在这两种情况下，包含的代码都相对于花括号再缩进两个空格。

这种风格由理查德·斯托曼普及，其布局可能受到他编写Lisp代码背景的影响。在Lisp中，相当于块（progn）的是一等数据实体，给予它自己的缩进级别有助于强调这一点，而在C中，块仅仅是语法。这种风格也可以在20世纪60年代和70年代的一些ALGOL和XPL编程语言教科书中找到。

尽管不完全属于缩进，GNU编码风格还包括在函数名之后——参数列表的左括号之前——加一个空格。

```c
static char *
concat (char *s1, char *s2)
{
 while (x == y)
   {
     something ();
     something_else ();
   }
 final_thing ();
}
```

这种风格结合了Allman和Whitesmiths的优点，从而消除了Whitesmiths可能存在的花括号不突出的缺点。一个缺点是结束的花括号不再与它概念上所属的语句对齐。另一个可能的缺点是它可能通过使用两个视觉缩进级别来表示一个概念级别而浪费空间，但在现实中这不太可能，因为在单级缩进的系统中，每级通常至少是4个空格，与GNU风格中的2 * 2个空格相同。

GNU编码标准推荐这种风格，几乎所有GNU计划软件的维护者都使用它。

Steve McConnell在他的书《代码大全》中建议不要使用这种风格：他在一个使用该风格的代码示例上标记了一个“Coding Horror”图标，象征着特别危险的代码，并指出它阻碍了可读性。Linux内核编码风格文档也建议不要使用这种风格，敦促读者烧掉GNU编码标准的副本作为“伟大的象征性姿态”。

### Horstmann风格

Cay S. Horstmann在1997年版的《Computing Concepts with C++ Essentials》中通过将块的第一个语句放在与左花括号同一行来调整Allman风格。这种风格也用于Jensen和Wirth的《Pascal User Manual and Report》中的示例。

```c
while (x == y)
{   something();
   something_else();
   //...
   if (x < 0)
   {   printf("Negative");
       negative(x);
   }
   else
   {   printf("Non-negative");
       nonnegative(x);
   }
}
final_thing();
```

这种风格结合了Allman的优点，即保持花括号的垂直对齐以利于阅读，并易于识别块，同时也节省了K&R风格的一行。然而，2003年的版本现在完全使用Allman风格。

### Pico风格

这是Pico语言设计者最常用的风格。Pico缺乏return语句，并使用分号作为语句分隔符而不是终止符。它产生了这种语法：

```
stuff(n):
{ x: 3 * n;
 y: do_stuff(x);
 y + x }
```

其优点和缺点类似于使用K&R风格节省屏幕空间。一个额外的优点是，起始和结束花括号在应用上是一致的（都与一行代码共享空间），而K&R风格中，一个花括号与一行代码共享空间，另一个花括号则独占一行。

### Ratliff风格

在《Programmers at Work》一书中，流行的dBase-II和-III第四代编程语言背后的原始程序员C. Wayne Ratliff讨论了一种类似于1TBS的风格，但结束的花括号与嵌套块的缩进对齐。他表示这种风格最初记录在Digital Research公司的材料中。这种风格有时被称为“横幅（banner）”风格，可能因其类似于挂在杆子上的横幅。在这种风格中（它对Whitesmiths的关系就像K&R对Allman的关系），结束控制与列表中的最后一项缩进相同（因此适当地失去了显著性）。这种风格可能使某些人的视觉扫描更容易，因为任何块的标题是该级别唯一突出的东西。

```c
// In C
for (i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        do_something(i);
        }
    else {
        do_something_else(i);
        }
    }
```

## 衍生C语言风格

以下风格可以被认为是“衍生”的C风格，因为它们很大程度上受到其他非C语言缩进风格的影响。它们可能适用于大部分用这些其他语言编写的项目中的C代码部分，为了保持项目核心代码的一致“外观和感觉”，这凌驾于使用更传统的C风格的考量之上。

### Lisp风格

虽然GNU风格有时被描述为Lisp程序员缩进的C代码，但人们甚至可以更进一步，将结束花括号一起插入块的最后一行。这种风格使得缩进成为区分代码块的唯一方式，但其优点是不包含无信息的行。这很容易被称为Lisp风格，因为这种风格在Lisp代码中非常常见。在Lisp中，表达式树末尾的相同括号的分组旨在表明，视觉上跟踪嵌套级别不是用户的工作，用户只需理解树的结构。

这种风格的传统Lisp变体偏好非常窄的缩进级别（通常是两个空格），因为Lisp代码通常嵌套很深。Lisp仅具有表达式，没有独特的语句类；函数参数大多缩进到同一水平，以说明它们在封闭表达式中的共享状态。这也是因为，除了括号之外，Lisp传统上是一种非常简洁的语言，甚至省略了作为无信息样的常见样板代码，例如 `if : then | else` 块中的 `else` 关键字，而是将其统一渲染为 `(if expr1 expr2 expr3)`。

```c
// C
for (i = 0; i < 10; i++)
   {if (i % 2 == 0)
       {do_something(i);}
    else
       {do_something_else(i);
        do_third_thing(i);}}
```

```lisp
;; Lisp
(dotimes (i 10)
 (if (= (rem i 2) 0)
     (do-something i)
   (progn
     (do-something-else i)
     (do-third-thing i))))
```

### Haskell风格

Haskell是一种花括号可选的语言，也就是说，下面的两组代码在语义上是相等的：

```haskell
braceless = do
 text <- getContents
 let
   firstWord = head $ words text
   bigWord = map toUpper firstWord
 putStrLn bigWord
```

```haskell
braceful = do
 { text <- getContents
 ; let
     { firstWord = head $ words text
     ; bigWord = map toUpper firstWord
     }
 ; putStrLn bigWord
 }
```

在Haskell中，布局（layout）可以替代花括号。通常，过程式 `do`的段落和一般程序文本会省略花括号和分号，但这种风格通常用于由一对括号或花括号组成的列表、记录或其他句法元素，并用逗号或分号分隔。如果跟在关键字 `where`、`let` 或 `of` 后的代码省略了花括号和分号，那么缩进就是有意义的。

### APL风格

作为APL通常是多么简洁的例子，这是康威生命游戏的step函数实现：

```apl
life←{⊃1⍵∨.∧3 4=+/+⌿¯1 0 1∘.⊖¯1 0 1⌽¨⊂⍵}
```

APL风格的C代码类似于APL代码的简洁风格，常用于其实现中。这种风格由Arthur Whitney开创，并大量用于他自己的项目K的实现中。J编程语言也是用这种风格实现的。值得注意的是，并非所有APL实现都使用这种C风格，例如GNU APL和Dyalog APL。

除了APL风格的C缩进外，通常变量名也会缩短为单字符或双字符，以减少缩进量和跨越多行的表达式。

## 制表符、空格及缩进尺寸

缩进的尺寸通常与风格无关。通常，程序员使用相同宽度的空白字符来缩进每个代码块，常用的宽度从1到4个空格不等。1983年对PASCAL代码进行的一项实验发现，缩进大小显著影响可理解性。2到4个字符之间的缩进大小被证明是最佳的。

许多早期程序使用制表符来缩进，从而简化输入和节约源代码文件的大小。Unix编辑器通常将制表符视为等同八个字符，而Macintosh和Windows环境将它视作四个字符，这使代码在各环境间交换时产生一种混乱。现代的编程编辑器通常可以设置任意的缩进尺寸，并会插入适当的制表符与空格。对Ruby、许多shell脚本语言和某些形式的HTML格式，通常为每个缩进级别使用两个空格。

使用制表符还是空格作为缩进字符是编程界的一项持续争论。杰米·加文斯基等一些程序员认为空格而非制表符有助增加跨平台可移植性。而如WordPress编码规范的作者则认为制表符增加了可移植性。GitHub上前40万个存储库的调查显示，空格更为常见。

## 工具

目前已有许多计算机程序可以自动校正缩进风格（依照程序作者或用户的偏好）以及制表符表示的缩进长度。其中很著名的一个是`indent`，这个程序包含在许多类Unix操作系统中。

在Emacs中，有多种命令可用于自动解决缩进问题（如 `M-x indent-region`）。

Elastic tabstops是一种需要文本编辑器支持的制表风格，当块中的一行的长度改变时，整个文本块将自动对齐。

## 其他考虑

### 丢失块踪迹

在某些情况下存在着丢失块边界的轨迹的风险。这通常在包含许多复杂语句的大量代码中看到，这些复合语句嵌套了许多层的缩进。当程序员滚动到一大堆嵌套语句的底部时，他可能已经忘记了哪些控制语句转到哪里。长复合语句可能是代码异味或过于复杂的标志，面对这个问题的程序员可能会考虑代码重构以期待它在未来有更好的体验。

依赖计算左花括号的程序员可能会在K&R等风格中遇到困难，因为起始花括号在视觉上没有与控制语句分开。更多依赖缩进的程序员会从K&R等垂直紧凑的风格中受益更多，因为块更短。

为了避免丢失对 `for` 等控制语句的跟踪，可以使用大缩进（如8单位宽的硬制表符），并将大函数分解为更小、更易读的函数（Linux内核采用了这种方式）。

一些文本编辑器允许程序员在块的两个对应花括号之间跳转。例如，在vi中按 `%` 键。另一种保持块意识的方法是在右花括号后使用注释：

```c
for (int i = 0; i < total; i++) {
   foo(bar);
} //for (i)
```

```c
if (x < 0) {
  bar(foo);
} //if (x < 0)
```

### 声明的插入

在使用标准的Unix行编辑器ed时，K&R风格能防止一个常见的错误。在控制语句与循环块的开启花括号之间错误地插入的语句将使循环体变为单次执行。

```c
for (int i = 0; i < 10; i++)
   whoops(bar);   /* repeated 10 times, with i from 0 to 9 */
{
   only_once();   /* Programmer intended this to be done 10 times */
} //for (i) <-- This comment is no longer valid, and is very misleading!
```

K&R风格通过将控制语句和开启括号保持在同一行来避免此问题。

## 外部链接

- C Style: Standards and Guidelines: Defining Programming Standards for Professional C Programmers , Prentice Hall, ISBN 0-13-116898-3 / ISBN 978-0-13-116898-5 (full text is also online). Straker, David (1992).
- Contextual Indent
- GNU Coding Standards
- Computers/Programming/Development_Tools/Source_Code_Formatters/

### 制表符与空格

- Tabs versus Spaces: An Eternal Holy War by Jamie Zawinski
- Why I prefer no tabs in source code by Adam Spiers
- Why I love having tabs in source code (archived)
- Elastic tabstops – the solution to the tabs-versus-spaces issue