[书籍]重温《Framework Design Guidelines》

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 use readonly where possible. Prefix internal and private instance fields with _, static fields with s_ and thread static fields with t_. When used on static fields, readonly should come after static (e.g. static readonly not readonly 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

我觉得这条规范也适用于一般的错误信息,我常常见到CreateUserErrorMessage1CreateUserErrorMessage2这种资源名称,改成CreateUserErrorInvalidUserNameCreateUserErrorInvalidPassword会比较好。

避免在命名基类时使用“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、IEnumerable、或IDictionary<TKey,Tvalue>。

在公共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,原因如下:

  1. 保持统一,基本上XAML中所有标签都使用PascalCasing。
  2. 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)的博客里有它的翻译

posted @ 2019-03-27 09:00  dino.c  阅读(2258)  评论(2编辑  收藏  举报