基本定義
假設我們寫的是文章而不是程式,那么你一定覺得諸如文章應該分為若干個自然段、每段開頭空兩格之類的規則是理所當然的。如果段落的開頭不空兩格,或者乾脆把整個文章寫成單獨的一段,仔細想來似乎也不會影響文章實質內容的表達。既然如此,我們為什麼還要在形式上下功夫呢?構想一下,如果你手中的這本書既無章節也無目錄,正文中的不同內容都使用同樣的字型字號印刷,幾百頁紙從頭至尾洋洋灑灑如念經般地“一氣呵成”,你還有耐心看下去嗎?
這是一個人人都能理解的道理,可是當文章變成程式的時候,就不是每個人都能想得通的了。不僅僅是初學者,甚至一些熟練的開發人員,也會寫出凌亂不堪的代碼。許多人一定有過這樣的經歷:一年半載之後,自己原來寫的程式就完全看不懂了。如果這段程式只是為了交作業,或者臨時一用,那還可以不去追究,但如果這是一個商業軟體,現在需要根據客戶的要求進行修改的話,工作量可就大了——你不得不先花時間把你原來的思路看懂。
肯定會有人反駁:代碼是給機器運行的,又不是給人看的,寫那么好看有什麼用?
他的話只對了前半句:代碼確實是給機器運行的,可是機器總共才需要看它幾分鐘?你花一個月編寫的程式,機器頂多兩三分鐘就編譯好了——在這兩三分鐘之前,這代碼不都是你在看嗎?開發軟體編寫代碼不是一朝一夕的事情,更多的情況下,一個軟體的開發要經歷很長的時間,並且常常由多人合作完成。一個龐大的軟體項目,可能會動用上千名程式設計師工作數年!如果把代碼寫得連自己都看不明白,怎么與別人交流?同一個開發團隊內,一定要保持良好且一致的代碼風格,才能最大化地提高開發效率。
必要性
有的初學者會問:我現在只是一個人寫程式,並不需要和其他人合作,這些條條框框還有什麼必要嗎?
要知道,團隊協作只是一個方面。我經常遇到這類情況,一些初學者拿著他的程式來說:“這個怎么不能編譯?”我幫他把代碼整理了半天,發現有一個地方丟了半個大括弧。如果他寫程式的時候能夠稍加注意一些的話,相信此類錯誤完全可以避免。保持良好的編程習慣,能夠避免的錯誤還遠不止這些。
如果說程式代碼中對算法的清晰表述是通過長期訓練而獲得的,那么本章要介紹的這些方法則無需傷神,你不必對代碼做任何實質性的改動,只需要添加一些空行與空格,就可以使其可讀性大大提高——這些規則就像寫文章應該分段一樣簡單,只要願意遵守,那么別人在第一眼看你的代碼時,必能感覺到你那良好的編程修養,即所謂“見字如見人”。
換行的講究
雖然你完全可以在C# 里將所有的代碼都連在一行里書寫,但想必沒有人願意這么做,誰也不會自己折磨自己的眼睛,何況大多數滑鼠對於上下翻頁的支持都比左右翻滾好得多。我相信,這也是大多數人接受將每條語句分行書寫的原因,很少有人會懷疑這一點的合理性。例如下面這行代碼,雖然結構很簡單,但是它實在太長了,所以被分成了兩行:
示例1-1
由於代碼過長而進行斷行
bitmap = new Bitmap(size.Width, size.Height,
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
這一點我相信大家都能理解並願意遵循,然而問題的焦點並不在於要不要換行,而在於在什麼位置換行。
最佳斷行位置
寫程式不能像寫文章那樣,什麼時候頂到了邊界就換,而必須按照其語法規則,在可以換行的位置斷開。例如,對於包含一個超長表達式的語句來說,我們可以在某兩個表達式項之間將其斷開,如下 所示:
示例1-2
通過斷行使代碼更加清晰
if (f == ImageFormat.Jpeg.Guid ||
f == ImageFormat.Tiff.Guid ||
f == ImageFormat.Png.Guid ||
f == ImageFormat.Exif.Guid)
{
supportsPropertyItems = true;
}
else
{
supportsPropertyItems = false;
}
原本一個很長的條件表達式,通過在“||”運算符處換行,顯得更加的清晰。有一點需要我們注意的是,當我們進行折行時,要將折行位置處的分隔設定(如前一例中的逗號,這一例中的“||”運算符等)留在上一行的行末,給人以“此行並未結束”的直觀印象。這就好像在英文書寫中,如果你需要將一個單詞拆開,就需要在前一行末尾加上連字元,以表示那個單詞並沒有結束。
可以看出,換行在防止代碼超出螢幕邊界的同時,還影響著代碼的表達。因此如何選擇合適的換行位置也是很有講究的。有的時候,我們並不一定非要在臨近右邊界的時候才去換行,如果存在更為合理的分法,就應當採用,例如下面的情況:
double containerAspectRatio = (double)container.ClientWidth /
container.ClientHeight;
按理說這樣的斷行並沒有什麼問題,它在表達式的兩項之間斷開,並將運算符留在了上一行的行末。但是,我相信如果換一種斷行方式的話,能夠更加清楚地表達出原來的邏輯:
示例1-3
尋找最佳的斷行位置
double containerAspectRatio =
(double)container.ClientWidth / container.ClientHeight;
如此一來,這個除法算術表達式就顯得較為完整,相比前一種寫法而言更能體現其內在的邏輯關係。通常我們會選擇整個表達式中最高的關係層次進行斷行,例如上述代碼中的“賦值號”和“除號”都是可以考慮的斷行點,但相比較而言,除號連線的這個算術表達式只是整個賦值表達式的右半部分,如果在除號處斷行,那么不但整個表達式會被截斷,連局部的這個除法表達式也會被截斷;反之,我們選擇在賦值號處換行,可以保持除法表達式的完整,最大限度地減少換行對語句整體結構的破壞。
同樣的道理,為了將邏輯體現得更為清晰,我們甚至可以將函式調用中的每一個參數都分行書寫,如同下面這樣:
示例1-4
將函式調用中的每一個參數都分行書寫
Rectangle imageBounds = new Rectangle(
itemBounds.X + padding,
itemBounds.Y + padding,
itemBounds.Width - padding * 2,
itemBounds.Height - padding * 2
);
當參數數量較多,參數較長或者包含表達式的時候,這種排版比起單獨寫成一行更為直觀醒目。
對於LINQ查詢表達式來說,將每個子句單獨寫成一行也是好的習慣。因為這同時符合了T-SQL語言的文化傳統。例如:
示例1-5
將LINQ查詢表達式中的每個子句單獨寫成一行
IEnumerable<int> highScoresQuery =
from score in scores
where score > 80
orderby score descending
select score;
每行只寫一條
如果說換行是為了防止螢幕左右滾動的話,那么當這個情況不存在的時候,一些人就開始打算充分利用螢幕空間了:
private static void Swap(object a, object b)
{
object temp;
temp = a; a = b; b = temp;
}
看起來好像確實沒有占據多少螢幕空間,這只是把三條很短的語句湊在一行了而已——關鍵的理由是:它不會引起螢幕的左右滾動。但是當人們已經習慣於一行一條語句的時候,很可能就會忽視這裡有三條語句這個事實(不要指望每次遇到的都像這個例子一樣地簡單)。更為重要的一點是,編譯器總是按行來進行設計的,將多條語句寫在一行會引起不必要的麻煩,例如:你將永遠無法把斷點設定在後面的語句上(如圖1-1):
圖1-1:一行代碼包含多條語句時的斷點設定
有的讀者會覺得,如果代碼複雜,當然應該分開書寫,沒有必要去節省那點螢幕,但是如果像這個例子中這么簡單,寫在一起也不會帶來什麼麻煩。單純地看來,他的話不無道理,可是,對於一個開發團隊,或者將要進入開發團隊的人來說,最重要的是“統一”。如果我們允許將多條語句合併到同一行代碼內,那么怎樣的代碼才算“簡單”到可以合併的程度?是寬度小於50個字元的可以合併,還是寬度小於51個字元的可以合併?當一條規定無法被準確地定義的時候,它也就無法執行,從而必將在整個開發團隊中產生不一致性,最終導致更多的混亂。
分行定義變數
我們再來看一種情況,這類代碼出現的幾率更為頻繁,它是將相同數據類型的幾個變數聲明放在了同一條語句中:
int num, factor, index, length;
如果我說我反對這種寫法,一定會有讀者大叫起來:這明明是單獨的一條語句,何況C# 允許我們在一條語句內聲明多個變數,如此一來還可以少寫幾個“int”,為什麼不行?
這種寫法,顯而易見會給注釋帶來很大的麻煩。把它們都寫在一起以後,我怎么給每個變數添加注釋呢?如果是分開書寫的,那么我可以很容易地為每一個變數添加單獨的注釋,就像這樣:
代碼示例1-6:將每個變數分行定義將有助於單獨注釋
// 要計算的數值
int num;
// 表示影響因子
int factor;
// 元素所在的索引號
int index;
// 數據列表的總長
int length;
如果覺得這種寫法較為繁瑣,一定要節約那幾個“int”,以強調它們的數據類型相同的話,也可以採取下面的寫法:
代碼示例1-7:變數分行定義的折衷方案
int num, // 要計算的數值
factor, // 表示影響因子
index, // 元素所在的索引號
length; // 數據列表的總長
這種方式只使用了一條聲明語句,但是每個變數都書寫在單獨的行上,便於有針對性的注釋。
避免過於擁擠
想想人們為什麼喜歡為文章添加各級標題以及其他複雜的格式,是因為美觀嗎?也許是的,但我相信這些格式可以更容易地讓人們理清思路。可是在程式中,我們無法使用這些手段,所有的代碼都是純文本的,即使Visual Studio的代碼高亮功能可以為代碼的不同部分標上不同的顏色,但這並不能真正影響到代碼本身。因此,光是換行還是不夠的,我們還需要更多的手段。
空行分隔
適當地添加空行則是一個非常有效的代碼整理方式——有點像文章中的分段,一段意思也許需要若干個句子才能表達清楚,在表達下一段意思之前,應當另起一段。
首先,每個C# 代碼檔案是從命名空間引用開始的,一組引用結束之後,則是命名空間的聲明及類型的聲明。很顯然地,在命名空間引用與命名空間聲明之間,應該留有一個空行以示區隔:
代碼示例1-8:在命名空間引用之後添加空行
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using Avilla.Metadata;
using Avilla.Searching;
// 這裡用空行隔開
namespace Avilla
{
// 下面的內容省略
一個空行,意味著不同的功能塊的分隔,如果讀者稍加留心,就會發現Visual Studio自動生成的代碼,總是在類型的各個成員之間留有一個空行。我們在書寫代碼的時候,也可以模仿這一格式:
代碼示例1-9:在類型的各個成員之間添加空行
/// <summary>
/// 表示一條搜尋條件的抽象基類
/// </summary>
public abstract class SearchCondition
{
/// <summary>
/// 初始化一個 <see cref="SearchCondition"/> 類型的實例,並指明是否區分大小寫
/// </summary>
/// <param name="caseSensitive">是否區分大小寫</param>
protected SearchCondition(bool caseSensitive)
{
this.caseSensitive = caseSensitive;
}
// 這裡用空行隔開
protected bool caseSensitive = false;
/// <summary>
/// 獲取或設定一個 <see cref="Boolean"/> 類型的值,以指示是否區分大小寫
/// </summary>
public bool CaseSensitive
{
get { return caseSensitive; }
set { caseSensitive = value; }
}
// 這裡用空行隔開
/// <summary>
/// 獲取表示此搜尋條件的 SQL 篩選條件表達式
/// </summary>
/// <returns>
/// 返回一個字元串形式的條件表達式,可直接用於 SQL 語言中的 WHERE 子句
/// </returns>
public abstract string GetFilterExpression();
}
}
這樣排版無疑會使得每個成員的代碼段更富獨立性,絕大多數的編譯器,在自動生成代碼時都會遵照此方式排版。您可能會發現,上例中的caseSensitive欄位與CaseSensitive屬性之間並未留有空行,這是為了強調欄位與其對套用於公開訪問的屬性之間的聯繫,關於類似情況,我們將在後面的章節詳細討論。
然而,一個空行意味著的不僅僅是功能模組的界限,它更是對代碼邏輯塊的劃分。我們無法期望每個操作都只通過一行代碼一條語句來完成,大多數情況下,它們都需要許多行代碼來執行一個完整的操作。例如,你想查詢資料庫,那么你需要先生成SQL代碼,建立命令,然後執行這個命令並填充至數據集。這中間大約需要使用四五行代碼,而這四五行代碼便組成了一個相對緊密的邏輯塊,它與其後面的其他邏輯塊即可以通過一個空行來進行分隔。請看下面的一個例子:
代碼示例1-10:用空行分隔邏輯塊
public static string[] GetPhotoIds(string filterExpression, string sort, bool caseSensitive)
{
// 第一個邏輯段代碼根據處理後的參數取得數據行
xml.Photos.CaseSensitive = caseSensitive;
DataRow[] rows =
xml.Photos.Select(filterExpression, sort ?? string.Empty);
// 遍歷數據行,取出需要的元素
string[] ids = new string[rows.Length];
for (int i = 0; i < rows.Length; i++)
{
ids = (string)rows["Id"];
}
// 返回結果
return ids;
}
這個函式的目的是根據指定的篩選條件和排序規則返回照片的標識號(Photo IDs),函式內部自然形成了三個邏輯段:先是根據要求取得原始數據,然後從原始數據中提取我們需要的部分,最後將結果返回。用空行將這三個邏輯區分隔開來將會更加有利於我們理解其思路。關於注釋的合理使用,我們會在後面的章節中再專門介紹。
既然空行可以起到分隔代碼,提高清晰度的作用,那么有的朋友也許會為了強調這種分隔效果,多加幾個空行。可事實的效果是,連續的多個空行,在並未提高多少清晰度的同時,浪費了螢幕的空間,而且會讓人覺得前後兩個代碼段並不相關——事實上它們應該是相繼執行的。空行的意義和文章的段落一樣,僅在於表示一個停頓,而並非結束。
空格降低密度
Basic、Pascal與C這三種早期高級程式設計語言的語法,至今仍在發揮著其重要的作用。Visual Basic仍然保留著Basic的句法簡單、多用完整英文單詞、貼
近自然語序的習慣(如And、Not、Inherits、Implements、Handles等等關鍵字);而Delphi更是延續著Pascal語言那標誌性的BEGIN-END作風。C語言由於在
作業系統開發上取得了成功,使得它在軟體開發歷史上占據了絕對的優勢,相比而言,它的語法更加具有影響力,廣泛被C++、Java、C#,乃至用於編寫網頁
的ECMAScript/JavaScript和Flash的腳本語言ActionScript所吸納,因此也變化豐富。但是它那種善用符號的古老特色一直被保留了下來,有理由相信,C語
言是使用符號最多的語言。當其他語法體系都採用AND、OR等關鍵字作為運算符時,C語言卻使用了“&&”、“||”這樣的符號,雖然在語法上並沒有增加任
何複雜性,但各種奇形怪狀難以記憶的符號還是會令初學者望而卻步。讓我們來比較一下下面的幾行代碼:
BASIC: If a>b And c<>d Or Not e>f Then ...
PASCAL: If (a>b) And (c<>d) Or (Not (e>f)) Then ...
C: if(a>b&&c!=d||!(e>f)) ...
這三行的意義是完全相同的,但明顯可以讓人感覺到清晰程度的差異,Basic和Pascal的代碼看上去很容易明白,而C語言的代碼卻像螞蟻一般縮成一團。
重要的原因在於:C語言的運算符幾乎都只由“符號”構成,與變數名之間不需要用空格充當分隔設定。這樣一來,由於缺少空格的稀釋,C語言的代碼就像被濃縮過似
的——現如今它除了影響我們閱讀以外,沒有什麼好處。因此我們有必要人為地添加一點空格,幫它降低代碼的“密度”。這裡,我總結了一些關於如何在運算
添加空格規則
單目運算符
1. 單目運算符(Unary Operators)與它的運算元之間應緊密相接,不需要空格。例如:
代碼示例1-11:單目運算符的空格規則示例
y = ++x; // ++ 在這裡是前綴單目運算,它與x之間無空格
二三目運算符
2. 在雙目、三目運算符(Binary/Ternary Operators)的左右兩側分別添加空格。例如:
代碼示例1-12:雙目、三目運算符的空格規則示例
int a = 3 + 5; // 在雙目運算符左右添加空格
int b = a * 6 + 7;
int c = a & b;
int d = b++ * c--; // 雖然有單目運算符,但雙目運算符兩側仍應添加空格
int e = a > 0 ? 1 : 0; // 在三目運算符左右添加空格
括弧
3. 括弧(包括小括弧、中括弧與大括弧)的內側應該緊靠運算元或其他運算符,不需要添加額外的空格。例如:
代碼示例1-13:括弧的空格規則示例
int f = (a + b) * c; // 括弧內側緊靠運算元,因其他運算符添加的空格留在外側
int g[MAX] = {1, 2, 3}; // 中括弧與表達式中的大括弧也同樣處理
不用連續空格
4. 不要使用連續的兩個或多個空格。
其實,如果理解了這些規則,在實際書寫的時候很容易遵循。對於任何一個表達式,我們先把單目運算符和括弧去掉,然後在雙目、三目運算符的左右兩側分別
添加一個空格,再將單目運算符和括弧填回去,放在靠近自己運算元的一邊即可。
關於函式調用時,要不要在函式名和其後的括弧之間添加空格的問題已經討論了很久。其實這個是一個無傷大雅的事情,無論使用何種方式,都不會對代碼
的可讀性產生多少實質性的影響,純粹是各人喜好罷了。不過在這裡,我建議採用Visual Studio中的默認規則:在函式調用時不添加空格,而在一些類似的帶括弧的語法結構中添加空格。請看下面這段代碼:
代碼示例1-14:函式調用時的空格規則示例
string cmd = string.Empty;
// 函式形式的調用,括弧前沒有空格
cmd = Console.ReadLine();
// 語句結構,括弧前有空格
if (cmd.Length > 0)
{
Console.WriteLine(cmd.ToUpper());
}
else
{
Console.WriteLine("(Empty)");
}
這段代碼中的ReadLine、WriteLine都是函式調用,因此與其後面的括弧緊密相連,不需要添加空格。而if結構雖然同樣帶有類似的括弧結構,但是它屬於C# 的內部語法,為了以示區別,在if與括弧之間添加了一個空格。除if外,switch、for、while等都應做同樣的處理。
縮進方法
在有關代碼風格的問題中,最為顯眼的可以說就是代碼的縮進(Indent)了。所謂縮進,是通過在每一行的代碼左端空出一部分長度,更加清晰地從外觀上體現出程式的層次結構。為了更好地描述這一點,先請讀者欣賞下列這段代碼:
int kmp_match(char[] t, char[] p, int[] flink, int n, int m)
{
int i = 0, j = 0;
while (i < n)
{
while (j != -1 && p[j] != t)
{
j = flink[j];
}
if (j == m - 1)
{
return i - m + 1;
}
i++;
j++;
}
return -1;
}
我想,就算讓你檢查一下它裡面有沒有大括弧配對錯誤恐怕都很困難,更不用說這段代碼有什麼功能 了——你能一眼看清楚每個while循環的內容是什麼嗎?讓我們換個方式,看看另一段程式:
代碼示例1-15:正確縮進的例子
兩段程式,除了縮進的區別以外,一字不差。孰是孰非,相信大家都能看得出來,縮進的必要性不難理解。接下來的問題就是:應該如何縮進。
嵌套包含引起
當遇到有關命名空間、類、結構、函式、以及枚舉等等複雜程式結構的定義的時候,我們通常需要將它的內容縮進一層。在C# 語言中,大括弧是一個非常明顯的標誌,凡是遇到大括弧,都應該直接聯想到縮進。請看下面的示例:
代碼示例1-16:包含關係下的縮進
namespace MyNamespace
{
// 命名空間內的內容應縮進
public class MyClass
{
// 類的成員應縮進
public string MyMethod()
{
// 方法函式的函式體應縮進
return "Hello!";
}
private MyEnum myProperty = MyEnum.Alpha;
public MyEnum MyProperty
{
// 屬性的內容應縮進
get
{
// 屬性的get部分函式體應縮進
return myProperty;
}
set
{
// 屬性的set部分函式體應縮進
myProperty = value;
}
}
}
public enum MyEnum
{
// 枚舉類型內容應縮進
Alpha,
Beta,
Gamma
}
}
分支結構(包括if…else結構、switch結構等)和循環結構(包括for結構、while/do…while結構等)都是存在嵌套關係的,因此從條理清晰的角度來說,它同樣應該進行縮進書寫:
代碼示例1-17:嵌套關係下的縮進
// if...else結構
if (a > b)
{
// if 子句的結構體內容應縮進
max = a;
min = b;
}
else
{
// else 子句的結構體內容應縮進
max = b;
min = a;
}
// switch結構
switch (n)
{
// switch結構的內容應縮進
case 0:
// case 子句內容也應縮進
// ...
break;
case 1:
// ...
break;
default:
// ...
break;
}
// for結構
for (int i = 0; i < 100; i++)
{
// for 的循環體應縮進
s += data;
t *= data;
}
// while結構
i = 0;
while (data != 0)
{
// while 的循環體應縮進
s += data;
t *= data;
i++;
}
縮進時,應將內部結構中的所有語句都統一向右退一格,大括弧則與原來的語句保持在同一個垂直位置上。每層縮進的長度應該一致,通常為一個制表符寬或四個空格。
還有一些細節的地方也與換行相關,例如if、switch、for這類具有嵌套結構的語句,在書寫的時候都應避免將結構體與語句本身寫在同一行上,關於嵌套結構的書寫方法,我們會在後面的章節中詳細討論。
換行產生
我們在前面提到過,當一條語句太長而超出一定的寬度時,應該折行書寫。此時,從第二行起到該語句結束之間的各行應該縮進一層,至下一條語句時再恢復原來的縮進位置。
代碼示例1-18:因換行而產生的縮進
int myVar = 0;
// 這是一條很長的語句,因而出現了換行,從第二行起都縮進了一格:
myVar = myVar1 + myVar2 + myVar3 - myVar4 - myVar5 * myVar6 * myVar7 /
myVar8 / myVar9 + myVar10 + myVar11 - myVar12 - myVar13 * myVar14 *
myVar15 / myVar16;
// 後面的語句恢復正常的縮進位置
Console.Write(myVar);
如果該語句是進行函式調用,由於參數太多而造成的換行,那么在縮進規則上有一些微小的差別:
代碼示例1-19:函式調用時分行書寫參數而引起的縮進
Rectangle imageBounds = new Rectangle(
itemBounds.X + padding,
itemBounds.Y + padding,
itemBounds.Width - padding * 2,
itemBounds.Width - padding * 2
);
注意最後一行的括弧與分號並沒有縮進,因為這種結構其實是對類似if的大括弧嵌套結構的模擬。
空格或Tab鍵
如何縮進一向是一個有爭議的問題。使用Tab及Shift + Tab鍵縮進在操作上非常方便,而使用空格可以保證代碼在任何編輯器下都能正確顯示縮進格式。現在,我們依靠Visual Studio開發環境則可以輕鬆解決這個矛盾:在“選項”對話框中對C# 編輯器的代碼縮進方式進行設定(如圖1-2),選擇“插入空格”模式。
圖1-2:Visual Studio中關於代碼縮進的設定
這樣一來,我們就可以在書寫代碼時使用Tab鍵和Shift + Tab鍵來調整縮進,而Visual Studio會將其轉換為空格保存至代碼檔案中。
大括弧
從外觀上來看,類C語言的最大標誌就是它無處不在的大括弧了。在C# 中,大括弧仍然扮演著幾種不同的角色:表示層次關係(如定義命名空間、類時使用的大括弧)、表示複合語句(如if、for中的大括弧)、表示數組元素。本節將討論有關大括弧書寫的幾個基本問題。
位置
代碼風格中,如何擺放大括弧一直是人們熱衷的話題。其實,我更願意用開放的態度去看待它,具體使用何種方式並不重要,重要的是,要保持方式風格的統一,不能在同一個項目中出現不同的風格。
代碼示例1-20:K&R大括弧位置風格
public int Max(int x, int y)
{
if (x > y) {
return x;
} else {
return y;
}
}
據說,微軟公司內部使用的就是這種K&R風格,然而它在對外公開的文檔中,卻使用更為大家所熟知的一種風格,它將大括弧單獨寫成一行。本書所有示例代碼採用的都是這樣一種格式:
代碼示例1-21:C# 默認使用的大括弧位置風格
public int Max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
雖然生硬地規定應該使用哪一種是不提倡的,而且也沒有必要,但是我們仍然建議讀者儘量選擇上述兩種風格中的一種,並在自己的程式中保持風格的統一。
空的結構
所謂複合體,即指用於充當某個語法結構成份的,被大括弧括起來的多條語句。C# 的很多語法結構中都可以見到複合體的使用,如命名空間、類、結構、接口、枚舉、方法、屬性等的定義,以及if、switch、for、while、do…while、try…catch…finally結構等等。對於應該如何處理大括弧的擺放以及內容的縮進排版等問題,我們都已經詳細討論過了,現在要考慮的是複合體自身的一種特殊情況:空的複合體。
接下來的問題是:為什麼會出現空的複合體?有的時候來自於廢棄的空函式。開發過程中,很可能只寫了某個類或者函式的空聲明,打算待日後再細化。然而由於設計上的變動,這個函式不再使用,而躲在代碼某個角落的這個空函式又未能被及時刪除,結果一直保留到產品發布。在這種情況下,建議在空複合體中添加“TODO”的注釋:
代碼示例1-22:為未實現的空函式體中添加TODO注釋
public void UnusedMethod()
{
// TODO: 未實現的方法
}
因開發時的疏忽造成的空複合體還不僅僅是上面這一種情況,我曾見到過有人將if結構中的if段留空,卻在else里寫上一堆代碼,類似下面這樣:
if (table.Rows.Count == 0)
{
}
else
{
foreach (DataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}
}
也許他本來在if中是有代碼的,後來程式不斷地修改,結果在if段中無事可做了。這種情況也很容易避免,只需很簡單地將if中的條件表達式反轉過來即可:
代碼示例1-23:反轉if條件表達式以避免空的if子句
if (table.Rows.Count > 0)
{
foreach (DataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}
}
如果不是開發時的疏忽,那么空複合體的出現就是有意而為之了。在循環結構中,它出現的頻率相對較高。早期,人們會採用空循環的方法來達到“延時”的效果,那個時候常常會看到類似這樣的代碼:
printf("Waiting...\n");
for (i = 0; i < 10000; i++)
{
}
printf("Done.");
或者乾脆連大括弧結構都沒有,直接就是
printf("Waiting...\n");
for (i = 0; i < 10000; i++);
printf("Done.");
且不說現如今這種“延時”方式完全不可取,就從代碼的外觀來看,它也極易讓人將for下面的那一行printf輸出代碼當成是循環的內容。
由於C# 語言繼承了C語言語法靈活的特點,因此循環語句本身就能承擔很多複雜的工作,以至於根本就不需要循環體。下面的代碼反映了一種典型的狀況:
int sum = 0;
for (int i = 0; i < table.Rows.Count; sum += (int)table[i++]["Amout"])
{
}
這個for語句本身就把累加工作給做完了,使得循環體內已經無事可做。雖說從語法角度來說沒有多少問題,但是它的可讀性並不好,我們仍然希望for僅僅是做一些“循環”本身的事情,將具體的數據和邏輯操作放在循環體內,就像下面這樣:
代碼示例1-24:讓for循環語句本身僅控制循環,不要涉及具體事務
int sum = 0;
for (int i = 0; i < table.Rows.Count; i++)
{
sum += (int)table["Amout"];
}
這看起來更像一個for循環所體現出來的含義。
這樣總結下來,似乎空複合體完全不應該出現在代碼中,然而有一種情況下它的確可以而且應當存在,這就是構造函式。構造函式與其他的函式不同,即使沒有任何一行代碼內容,它本身的存在與否也有著極其重要的意義——直接決定這個類型是否能夠被實例化。因此,當我們不希望某個類被外部實例化的時候,哪怕我們並不需要自定義構造函式,我們仍需要為該類型添加一個非公開的空構造函式。為了避免日後將這個空函式當成是沒有用的廢棄函式,我們有必要特別加以說明:
代碼示例1-25:通過注釋強調空構造函式
public class PhotoCollection : IEnumerable, IEnumerable<Photo>
{
internal PhotoCollection()
{
// 空構造函式
}
// 其他類成員已省略
}
其他情況下,如果確實有必要使用空的複合體,也應當仿照上例書寫,添加相應的注釋說明,以防止產生誤解。絕不能省略大括弧結構,僅以一個分號代替。
僅單句結構體
我們再來討論另一種特殊的複合體。按照C# 的語法,如果if、while、for等等的結構體僅為一條語句,那么大括弧是可以不寫的——其實複合體就是將多條語句合併起來充當單條語句來用的。甚至我們可以將結構體與if、while、for寫成一行,例如:
if (a > b) x++;
else y++;
或者是:
for (int i = 0; i < 10; i++)
dest = source;
雖然這在語法上沒有什麼問題,但當代碼數量增加,上下文的代碼變得複雜時,我們有可能會對if、while、for的結構體究竟包含哪些語句產生誤解。因此,我們建議,即使if、while、for結構的內容只有一條語句,也應該像處理多條語句那樣,寫在大括弧內。因此,剛才那兩段代碼就應該寫成下面