1. 前言 最近重溫了《Framework Design Guidelines》。 《Framework Design Guidelines》中文名稱為《.NET設計規範 約定、慣用法與模式》,簡介如下: 數千名微軟精銳開發人員的經驗和智慧,最終濃縮在這本設計規範之中。與上一版相比,書中新增了許多評 ...
1. 前言
最近重溫了《Framework Design Guidelines》。
《Framework Design Guidelines》中文名稱為《.NET設計規範 約定、慣用法與模式》,簡介如下:
數千名微軟精銳開發人員的經驗和智慧,最終濃縮在這本設計規範之中。與上一版相比,書中新增了許多評註,解釋了相應規範的背景和歷史,從中你能聆聽到微軟技術大師Anders Hejlsberg、Jeffrey Richter和Paul Vick等的聲音,讀來令人興味盎然。
當年第一次讀時茅塞頓開,現在重溫還是獲益良多。雖是一本十年前的書,仍是值得推薦給初學者閱讀的一本好書。
2. 常見被違反的規範
今年升級一個核心代碼從很久以前的代碼改寫過來的軟體,各種不符合C#代碼規範的代碼讓我感到難以維護;去年系統工程師退休前留給我們的一個代碼更是讓我受到會心一擊。我使用C#多年來見到過很多不規範的代碼,於是試著參考書中的規範,列出其中一些來常見的錯誤以及一些問題。
2.1 命名
要把PascalCasing用於由多個單詞構成的名字空間、類型以及成員的名字。
要把camelCasing用於參數的名字。
不要使用匈牙利命名法。
也就是說參數要用camelCasing,其它所有能讓使用者看到的地方,包括命名空間、類名稱、屬性、函數等都要都要使用PascalCasing。(除非是ex、e、i等約定俗成的用法,或者其他特殊情況如工業標準、商標、歷史問題、遺留代碼、調用非托管代碼等。)
由於習慣問題,現在還經常見到匈牙利命名法如btnOk、strPwd,應修改為OkButton和Password。
要在命名欄位時使用PascalCasing大小寫風格。(適用於靜態公有欄位和靜態受保護欄位)
不要提供共有的或受保護的實例欄位。
.NET Core有更詳細的# Coding Style:
We use
_camelCase
for internal and private fields and usereadonly
where possible. Prefix internal and private instance fields with_
, static fields withs_
and thread static fields witht_
. When used on static fields,readonly
should come afterstatic
(e.g.static readonly
notreadonly static
). Public fields should be used sparingly and should use PascalCasing with no prefix when used.
雖然寫得很複雜,但我建議只有private的欄位、常量欄位和靜態只讀欄位。能被外部修改的欄位是危險的,所以欄位應該只有如下幾種形式:
private readonly string _id;
private string _userName;
private static bool s_valid = false;
public const int MaxValue = 0x7fffffff;
public static readonly Color Red = new Color(0x000FF);
要在命名資源鍵(Resource Key)時使用PascalCasing大小寫風格。
可是,我不覺得微軟自己有遵循這個規範啊。
總的來說,框架中除了函數的參數外所有可見的部分都應該使用PascalCasing風格,因為資源通常可以以屬性的方式被使用,所以資源的Key應該使用Pascal。可能因為很多時候資源的生成方式都是internal所以很多人都不遵守這個規範。
要在命名異常消息的資源時遵循下麵的命名約定。
資源標識符應該是異常的類型名加上一個簡短的異常標識符:
ArgumentExceptionIllegalCharacters
ArgumentExceptionInvalidName
ArgumentExceptionFileNameIsMalfrmed
我覺得這條規範也適用於一般的錯誤信息,我常常見到CreateUserErrorMessage1
、CreateUserErrorMessage2
這種資源名稱,改成CreateUserErrorInvalidUserName
、CreateUserErrorInvalidPassword
會比較好。
避免在命名基類時使用“Base”尾碼 -- 如果公共API中會用到這個類。
但是微軟自己的框架中就一大堆啊?不過這些都不常用,給一般用戶的API最好還是要遵守這條規範。
要用肯定性的短語(CanSeek而不是CantSeek)來命名布爾屬性。如果有幫助,還可以有選擇地給布爾屬性添加“Is”、“Can”或“Has”等首碼。
我覺得dont-首碼真的挺常見的,.NET Core的源碼里能搜出一大堆。無論如何我還是建議用肯定性的短語,否定性短語讓人混淆。
2.2 屬性
要在下列情況中使用方法而不要使用屬性
- 該操作比欄位訪問要慢記個數量級。
- 該操作返回一個數組。
這條規範有很多種情況,我只列出常見的兩種容易犯錯的情況。
第一種情況在WPF尤其常見,因為對XAML來說可以用於綁定的屬性好用很多,所以很多應該是方法的地方都使用屬性實現。
第二種情況在老代碼里很常見,別說返回數組,把數組做成全局變數大家一起複用都很常見,也許是因為當年記憶體很貴?
2.3 枚舉
要用單數名詞來命名枚舉類型,除非它表示的是位域(bit field)。
要用複數名詞來命名錶示位域的枚舉類型,這樣的枚舉類型也稱為標記枚舉(flag enum)。
不要給枚舉類型的名字添加“Enum”尾碼。
不要給枚舉類型的名字添加“Flag”或“Flags”尾碼。
不要給枚舉類型值的名字添加首碼。
//bad
public enum ImageMode
{
ImageModeBitmap,
ImageModeGrayScale,
ImageModeRgb,
}
//good
public enum ImageMode
{
Bitmap,
GrayScale,
Rgb,
}
枚舉的規範挺多的,但即使不特別提出來,參考.NET Framework中的枚舉也能很好地遵守這些規範。
2.4 集合
不要在公共API中使用ArrayList或List
。
不要在公共API中使用Hashtable或Dictionary<TKey,Tvalue>。
這些類型的設計目的是為了用於內部實現,應該使用Collection
要在公共API中優先使用集合,避免使用數組。
不要提供可設置的集合屬性。
要用Collection或其子類--如果屬性或返回值表示可讀寫的集合。
要用ReadOnlyCollection或其子類,在少數情況下用IEnumerable ,如果屬性或返回值表示只讀的屬性。
總的來說就是不要讓集合被人不明不白地修改了。現在我在處理的遺留代碼既使用數組作為屬性,又可Get和Set,畢竟是從很久以前一路修改過來的,當時的開發者應該也沒想到這些代碼現在會讓人這麼困擾吧。
要用描述集合中項目短語的複數形式來命名集合屬性,而不要使用短語的單數形式加“List”或“Collection”尾碼。
例如,要用Items、Objects,而不用ItemList、ObjectCollection。
2.5 異常
不要在框架代碼中捕獲System.Exception或System.SystemException,除非打算重新拋出。
不要在框架的代碼捕獲具體類型不確定的異常(比如System.Exception、System.SystemException,等等)時,把錯誤吞了。
總之不要捕獲System.Exception和System.SystemException,要讓用戶知道哪裡發生了問題。無論是不是框架的代碼,把異常吞了的做法都很讓人困擾,除非有充分的理由。
不要正常的控制流中使用異常,如果能夠避免的話。
很常見到捕獲了System.Exception做跳轉分支,以及明明有TryParse卻還是用TryCatch的代碼。
要在捕獲並重新拋出異常時使用空的throw語句。這是保持異常調用棧不變的最好方法。
總有人喜歡把異常封裝一下,然後就把異常類型改變,StackTrace或InnerException弄丟。
不要拋出System.Exception與System.SystemException。
2.6 事件
要用受保護的虛方法來觸發事件。
要讓觸發事件的受保護的方法帶一個參數,該參數的類型為事件參數類,該參數的名字應該為e。
public event EventHandler ContentRendered;
protected virtual void OnContentRendered(EventArgs e);
上面是WPF中Window類的代碼,WPF的各個控制項都有很好地執行這個規範,但自定義控制項及其它控制項庫則不是。
要用object作為事件處理函數的第一個參數的類型,並將其命名為sender。
要用System.EventArgs或其子類作為事件處理函數的第二個參數的類型,並將其命名為e。
同樣是DataContextChanged事件,WPF有遵循規範,但UWP則不然。我可以理解只有FrameworkElement會觸發DataContenxtChanged事件所以用FrameworkElement作為sender的類型,但將這個理論延伸到所有事件顯然不合適,到底UWP是怎麼回事?
//WPF
private void MainWindow_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
//UWP
private void MainPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
throw new NotImplementedException();
}
要用動詞或動詞短語來命名時間。
這樣的例子包括Clicked、Painting、DroppedDown,等等。
要用現在時和過去時來賦予事件名以之前和之後的概念。
例如,在視窗關閉之前發生的close事件應該命名為Closing,而在視窗關閉之後發生的應該命名為Closed。
所以WPF中Button的Click事件一直讓我很困擾,Xamarin改為Clicked就好多了。
還有一點比較困擾的是事件處理函數的命名,常常見到同一個類存在以下命名方式:
Loaded += OnLoaded;
_inlineBackButton.Click += OnInlineBackButtonClicked;
SizeChanged += MasterDetailsView_SizeChanged;
我一向比較喜歡用On-首碼加事件名稱的命名方式,因為這樣方便查找。但VisualStudio預設給的就是第三種,即“變數名+下劃線+事件名稱”的命名方式。這也很讓人困擾,不過反正不是給別人看的,隨意些也無所謂了。
3. 一些想法,關於XAML元素的命名
我不記得有在哪裡見過XAML上元素命名的規範(只看到XamlName語法),總之就是要符合C#的的通用命名規範。我個人建議XAML上元素使用PascalCasing,原因如下:
- 保持統一,基本上XAML中所有標簽都使用PascalCasing。
- UWP預設控制項模板也使用PascalCasing,下麵是UWP和WPF中ScrollViewer ControlTemplate的對比:
<!--UWP-->
<ScrollContentPresenter x:Name="ScrollContentPresenter"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}" />
<Grid Grid.RowSpan="2"
Grid.ColumnSpan="2" />
<ScrollBar x:Name="VerticalScrollBar"
Grid.Column="1"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Value="{TemplateBinding VerticalOffset}"
ViewportSize="{TemplateBinding ViewportHeight}"
HorizontalAlignment="Right" />
<ScrollBar x:Name="HorizontalScrollBar"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableWidth}"
Orientation="Horizontal"
Grid.Row="1"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Value="{TemplateBinding HorizontalOffset}"
ViewportSize="{TemplateBinding ViewportWidth}" />
<Border x:Name="ScrollBarSeparator"
Grid.Row="1"
Grid.Column="1"
Opacity="0"
Background="{ThemeResource ScrollViewerScrollBarSeparatorBackground}" />
<!--WPF-->
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
CanContentScroll="{TemplateBinding CanContentScroll}"
CanHorizontallyScroll="False"
CanVerticallyScroll="False"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
Grid.Row="0" />
<ScrollBar x:Name="PART_VerticalScrollBar"
AutomationProperties.AutomationId="VerticalScrollBar"
Cursor="Arrow"
Grid.Column="1"
Maximum="{TemplateBinding ScrollableHeight}"
Minimum="0"
Grid.Row="0"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
ViewportSize="{TemplateBinding ViewportHeight}" />
<ScrollBar x:Name="PART_HorizontalScrollBar"
AutomationProperties.AutomationId="HorizontalScrollBar"
Cursor="Arrow"
Grid.Column="0"
Maximum="{TemplateBinding ScrollableWidth}"
Minimum="0"
Orientation="Horizontal"
Grid.Row="1"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
ViewportSize="{TemplateBinding ViewportWidth}" />
在WPF中TemplatePart的命名常會使用PART_
首碼,這種古老的習慣現在還常常可以見到。Blend for VisualStudio已經移除“部件”視窗,使用PART_
首碼可以標識控制項模板中的TemplatePart,基於這種理由也可以接受這種命名方式。
4. 結語
雖然很古老,但我還是把這本書推薦給初學者。docs.microsoft.com上有Framework Design Guidelines的文檔,但比書上精簡了很多,而且沒有來自微軟技術大師的評註,還是書好看,可惜09年出了第二版以來再沒有更新過,裡面一些規範也已經過時(如花括弧的用法)。
VisualStudio有很多工具可以用於規範代碼,好代碼是管出來的——.Net中的代碼規範工具及使用 這篇文章是很好的參考。也可以參考dotnet core 編程規範,林德熙(lindexi)的博客里有它的翻譯。