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)的博客里有它的翻译。