這是1年多以前我在公司所做講座的講義,現在與園友們分享,歡迎拿去使用、一起討論。文中有若干思考題,對園友們是小菜一碟。另有設計模式講義一篇,隨后發布。
博文上了首頁,感謝博客園團隊推薦,也感謝所有園友的支持。
經園友提醒,我們推薦新手使用ReSharper插件,它可以自動提示不符合代碼規范的地方。
1 概述
1.1 意義
“.NET平台開發規范”包括兩方面內容:代碼規范、最佳實踐。
(1) 規范和優美的代碼風格有助於思路清晰。
(2) 規范和優美的代碼有助於團隊溝通交流。
(3) 規范和優美的代碼有助於學習近20年(后設計模式時代)現代面向對象思想發展的精華。
.NET平台是繼Java平台之后又一偉大的帶集大成面向對象類庫的虛擬機平台。.NET平台及其主力語言C#吸取了Java平台及Java語言的全部優點和精華(包括開發規范),並逐漸發展和獨創出了鮮明的、先進的特色。
眾所周知,C#最初全盤照搬了Java。但時至今日,Java語言准備采用C#中的lambda語法。Java平台是個優秀的平台(類庫、虛擬機),但Java語言卻日益獲得差評,於是JVM上的其他語言越來越多地取代了Java,如Groovy,Scala,JRuby等。
.NET平台上的語言數量比Java平台有過之而無不及,但與之截然不同的是,C#始終在.NET中保持絕對優勢地位。C#也許不是.NET上最優秀的語言,但已經足夠優秀來進行生產。而Java語言卻稱不上足夠優秀,因為有語言專門用來彌補它的不足,最典型的就是Scala。而Scala解決的問題,如函數式編程、並發編程,在C#中早有解決方案。
毫不誇張地說,.NET是現代面向對象開發思想和實踐的一個制高點,而C#則是編程語言發展趨勢(聲明性、動態性、並發性)的風向標。
即使你是一名C++開發人員,你也能從中獲益。
(4) 規范是微軟內部秘笈,優美是秘笈指導下的修煉。
這些規范和最佳實踐多來源於微軟負責CLR、BCL、C#/VB團隊的成員。它們體現了.NET和C#設計人員的智慧結晶。例如,他們會告訴你哪些功能已經被哪些新功能取代而不建議使用,而新功能新方法總是以優雅的方式解決舊功能存在的問題,會給人啟發,很值得回味,使用新功能收獲的不僅是方便快捷,還有水平層次的提升。
1.2 后設計模式時代大事記
- Python/Ruby等動態面向對象語言的興起
- Java的誕生
- .NET的誕生
- Boost庫的發展
- 函數式編程融入面向對象語言
- 其他門派面向對象語言JavaScript和Objective-C的繁榮
- 新C++標准
1.3 參考資料和延伸閱讀
本教程主要綜合以下資料中的觀點和我公司開發實踐寫成:
- 《C#編程風格》(The Elements of C# Style)
- 《.NET設計規范:約定、慣用法與模式(第二版)》(Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition)
- Effective C#: 50 Specific Ways to Improve Your C#, 1st and 2nd Edition
- More Effective C#: 50 Specific Ways to Improve Your C#
2 代碼規范
2.1 空白
- 用空行分隔邏輯塊(函數、條件塊、循環塊等)
- 運算符空格(使用IDE自動設置)
- 命名空間導入,在不同公司或不同庫的命名空間之間空一行。
2.2 代碼塊
- 切分長語句為多行語句
- 左花括號也要單獨成行,與右花括號在同一鉛垂線上,與上一級花括號縮進一個制表位。
- 始終使用語句塊
正 |
誤 |
if (a == null) { return; } |
if (a == null) return; |
- 使用#region標記代碼區域,便於折疊
2.3 命名
- 使用有意義的名稱,如 IsDepartmentReadyToOfferJob
- 使用完整單詞,帕斯卡(Pascal)和駱駝(Camel)寫法
正 |
誤 |
NumberOfArgumentsIn |
nargin |
- 根據含義而非類型命名
正 |
誤 |
ParcelName |
strParcelName |
- 不要用大小寫區分名稱
- 像普通詞一樣書寫縮略詞:XmlReader; System.Linq
- 用復數形式書寫集合名稱
- 習慣性后綴(前綴):
抽象基類 |
-Base |
接口 |
I- |
異常 |
-Exception |
事件參數 |
-EventArgs |
事件委托 |
-EventHandler |
特性 |
-Attribute |
泛型類型參數 |
T- |
- 給實現一種設計模式的類加上模式名稱,例如:工廠模式 - xxxFactory
- 泛型參數的命名:使用單個大寫字母,如T, U等。需要時,寫成TKey, TValue等
- 枚舉的命名:普通枚舉用單數名詞,位域枚舉用復數名詞。
- 接口的命名:使用I-加名詞為聲明服務的接口命名,使用I-加形容詞為描述能力的接口命名。(例:IEnumerable和IEnumerator)
[思考題1]
從右邊找出與左邊項目相關的接口。
資源回收 |
|
a |
IFormattable |
Convert.ToString() |
|
b |
IEquatable |
重載關系運算符 |
|
c |
IConvertible |
if (textbox1.Text == "Hello") |
|
d |
IClonable |
Double.ToString() |
|
e |
IDisposible |
foreach循環 |
|
f |
IComparable |
from p in q select p; |
|
g |
IEnumerable |
復制數組元素 |
|
h |
IQueryable |
- 用能體現布爾值特征的名稱給布爾類型命名:IsXxx,HasXxx,CanXxx
- 方法的命名:用動[賓]結構、帕斯卡寫法
- 變量和方法參數命名:用名詞性結構、駱駝寫法
- 成組的私有字段、公共屬性和構造函數形參:_item, Item, item。見3.6節代碼示例
- 習慣中允許使用的標准“一次性”變量名(只限這幾個):
i, j, k |
循環變量(類型:System.Int32) |
o |
System.Object |
s |
System.String |
e |
事件實參(基類:System.EventArgs) |
ex |
異常(基類:System.Exception) |
g |
System.Drawing.Graphics |
x, y, z |
Lambda表達式的形參 |
- 命名空間的命名:公司名.產品名.技術/模塊名。將相互依賴的類型放在同一命名空間下。不要污染框架命名空間(例如在System命名空間中添加類型)。
2.4 注釋
- 使用內建XML注釋文檔機制
- 內部注釋:盡量少用,避免行末注釋
- 不用C風格注釋塊/**/
- 在右花括號、else后面加注釋輔助閱讀
1 |
2 |
if (a > 0) { } else // a <= 0 { } |
} // end of foreach |
2.5 字面量
- 避免使用數值字面量。使用常量表示一個值,使用枚舉明確指示一個狀態。
- 使用枚舉代替布爾值。
- 使用標准形式書寫浮點字面量。不必寫f,d(一般只用double),但要對整數寫上小數點。用科學計數法時注意小數點前只能有一位數,且不能是0。
- 字符串字面量應該總是賦給變量使用。用string.Empty代替空串字面量。
2.6 其他
- 不要依賴運算符優先級。應當加上括號。
- 單獨聲明每個變量,不要寫int a, b。
- 給switch語句加上default
- for循環變量聲明在循環作用域內部,foreach循環不要再聲明循環變量。或用框架函數代替循環,它們的思想是一致的,見最佳實踐。
3 最佳實踐
3.1 使用屬性
- 屬性是函數成員,沒有存儲空間。例:屬性不能做ref/out參數。
- 將成員字段全部私有,使用屬性來獲取,不需修改就不設set。
- 屬性應簡單、低成本、無副作用、與調用順序無關。
- 屬性返回集合應該總是只讀。(用戶代碼可以清空集合、再添加元素,但不允許使對象引用改變)
- 屬性返回其他引用類型也應該盡量考慮只讀,用戶代碼依然可以修改成員。
- 特殊的屬性:索引器
[思考題2]
string s = "abcdeeg"; s[5] = 'f';
以上代碼在C++和C#中是否都合法?是否都有效?
3.2 使用不可變值類型
- 為“值”語義使用struct。所謂值語義就是一種狀態、屬性,例如點(坐標)、顏色(分量)等,多具有短小數據量的特點。
- 為struct的字段添加readonly修飾符,成為不可變(Immutable)類型。成員函數返回新實例,而不是修改this實例。
- 值類型的成員字段只應使用基本CTS類型。
3.3 轉換
- 用as和is運算符代替顯式引用轉換
- 基本類型的轉換要使用System.Convert類
- 避免裝箱和拆箱
3.4 字符串
[思考題3]檢測一個字符串str是否以"abc"開頭,有兩種方法(不使用StartsWith):
str.SubString(0, 3) == "abc"; str.IndexOf("abc") == 0;
請問哪種方法好,為什么?
CLR中的字符串是不可變類型,字符串分配是一項昂貴的操作。因此我們有以下最佳做法:
- 用string.Empty代替空串字面量。
- 用str.Length == 0或string.IsNullOrEmpty(str)檢測空串,后者還兼容空引用。
- 反復使用的字符串應當創建變量多次引用而不是每次都使用字面量(每出現一次字面量就要重新分配一次字符串)。
- 不分大小寫的比較應當用string.Compare(str1, str2, true)而不是str1.ToUpper() == str2.ToUpper()。
- 考慮使用string.Format()方法而不是字符串拼接來輸出結果。
- 大量拼接字符串要使用StringBuilder類。
3.5 使用查詢表達式代替循環
函數式編程為C#帶來了高可讀性的聲明式語法。循環是一種命令式語法,在聲明式語法中相應的是對集合的mapping操作。
以下代碼生成前100個完全平方數:
var squares = Enumerable.Range(1, 100).Select(x => x * x);
返回int數組:squares.ToArray();
返回List<int>集合:squares.ToList();
以下代碼對集合foos中每個元素執行DoSomething()成員方法:
foos.ForEach(x => x.DoSomthing());
以下代碼選出foos中大於100的元素:
var q = foos.Where(x => x > 100);
points中點按到原點距離排序:
var q = points.OrderBy(x => x.DistTo(new Point(0, 0)));
foos中平方最小的數:
double minSq = foos.Min(x => x * x); double min = foos.Distinct().Single(x => x * x == minSq);
foos中的數是否有大於100的(是否都大於100)
foos.Any(x => x > 100); foos.All(x => x > 100);
3.6 返回多個值
- 在Matlab中可以用矩陣返回多個值,這就是元組(Tuple)的概念。當函數需要返回多個值時,應當使用元組而不是使用輸出參數。.NET 4.0中提供了Tuple泛型類,位於System命名空間。在3.5以下版本可以考慮自己實現。
public class Tuple<T1, T2> { private T1 _item1; private T2 _item2; public T1 Item1 { get { return _item1; } } public T2 Item2 { get { return _item2; } } public Tuple(T1 item1, T2 item2) { _item1 = item1; _item2 = item2; } } public class TupleList<T1, T2> : List<Tuple<T1, T2>> { public void Add(T1 item1, T2 item2) { base.Add(new Tuple<T1, T2>(item1, item2)); } }
- 在任何時候都應避免使用ref/out傳遞參數,尤其對引用類型(禁止引用的引用)。嘗試改進你的設計。
3.7 對象初始化
Button btn = new Button { Text = "Click me!", BackColor = Color.FromArgb(255, 255, 255) }; btn.Click += (sender, e) => MessageBox.Show("Hello!");
3.8 小函數
- 將大函數分拆成小函數。這樣做的好處有:
- 合並重復代碼,便於維護
- 增加函數層級,便於調試
- 方法參數也不宜過多,否則也應考慮拆分。
3.9 using塊和finally
以下代碼編譯成相同IL:
// 1. Example Using clause: using (myConnection = new SqlConnection(connString)) { myConnection.Open(); } // 2. Example Try / Catch block: try { myConnection = new SqlConnection(connString); myConnection.Open(); } finally { myConnection.Dispose(); }
3.10 使用泛型集合
- 不要使用非泛型的ArrayList,這會帶來不必要的裝箱和拆箱。使用System.Collections.Generic命名空間中的容器類型。
- 代替數組的首選:List<T>。如被用戶代碼獲取應該以只讀屬性形式提供。如果要真的使集合不可修改,可用ReadOnlyCollection<T>(System.Collections.ObjectModel)
- 集合做參數禁止ref/out傳遞(造成引用的引用)。值傳遞即可達到修改集合元素的目的。
- 在多個函數間返回、傳遞的集合考慮用接口類型IEnumerable<T>和yield關鍵字。連續的LINQ語句考慮不急於調用ToList或ToArray。
- 其他常用集合:Dictionary<TKey, TValue>; SortedSet<T>(.NET 4)
3.11 充分利用BCL
- BCL中提供了很多常用算法,並且還在不斷擴充。例如,.NET 4增加了System.Numerics命名空間,目前包括BigIntegar和Complex類,而在bcl.codeplex.com上已經放出了BigRational類的候選版和源代碼。
- 盡可能只依賴BCL,減少第三方依賴。
- 對字符串的操作都有簡單的方法可以調用。用這些方法不僅能更方便省時、穩定高效,而且能使你的代碼趨向於可讀性強的聲明式風格。
- 盡可能只用系統定義的委托,而不是自己定義委托類型。系統定義的委托包括:各種泛型參數數目的Func, Action; 一個Predicate<T>;各種EventHandler。以下是一個高階函數的應用實例:
Func<Color, EventHandler> changeControlColor = x => (sender, e) => (sender as Control).BackColor = x; button1.Click += changeControlColor(Colors.Red);
3.12 其他
- 使用虛函數和多態而不是頻繁使用引用轉換
- 創建枚舉0值表示未初始化、無效、未指定或默認
- 總是在派生類構造函數的初始化列表中列出所有基構造函數
- 總是重寫ToString()方法