目录
1.简介... 2
2.适用范围... 2
3.规范目的... 2
4.代码组织与风格... 2
4.1 Tab键... 2
4.2 缩进... 3
4.3空行... 3
4.4函数长度... 3
4.5行宽... 3
4.6{ “,”} 3
5.文件命名... 3
6.命名... 4
6.1基本约定... 4
6.2程序集命名... 4
6.3命名空间命名... 4
6.4类和接口命名... 5
6.5方法命名... 5
6.7变量命名... 5
6.8组件名称缩写列表... 5
6.9数据库缩写列表... 7
6.10其他命名... 8
7.类型设计... 8
7.1类型和命名空间... 8
7.2类型和接口选择... 9
7.3属性和方法的选择... 9
7.4类设计... 9
7.5枚举设计... 9
8.成员设计... 10
8.1方法重载... 10
8.2属性设计... 11
8.3构造函数设计... 11
8.4字段设计... 11
8.5参数设计... 11
8.6扩展性设计... 12
8.7异常处理... 12
9.代码注释... 12
9.1代码注释约定... 12
9.2文件头部注释... 13
9.3方法注释... 14
9.4代码行注释... 15
9.5变量注释... 15
10.参考文档... 15
术语:
PasalCasing:标识符的第一个单词的字母大写;
CamelCasing:标识符的第一个单词的字母小写。
1.简介
本规范为一套编写高效可靠的 C# 代码的标准、约定和指南。它以安全可靠的软件工程原则为基础,使代码编写得易于理解、维护和增强,同时提高生产效率。
2.适用范围
- 本规范主要以C#为开发语言的原则性规范;
- 适用人员:软件工程师;
- 适用产品:各类软件。
3.规范目的
编码规范背景:
1. 一个软件的生命周期中,80%的花费在于维护;
2. 几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护;
3. 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码。为了执行规范,每个软件开发人员必须一致遵守编码规范:
- 使用统一编码规范的主要原因,是使应用程序的结构和编码风格标准化,以便于阅读和理解这段代码;
- 好的编码约定可使源代码严谨、可读性强且意义清楚,与其它语言约定相一致,并且尽可能的直观,这样对于后期维护升级非常重要。
4.代码组织与风格
4.1 Tab键
换行要使用一个为四个空格的Tab。
4.2 缩进
一个代码块内同级代码统一使用相同的Tab长度。
4.3空行
适当增加空行,增加代码块之间的可读性。
在类与类、接口与接口之间添加两行空行。
以下情况可以适当添加一行空行:
方法之间、局部变量和适当语句之间、不同功能逻辑块之间等情况。
4.4函数长度
每个方法有效代码(不包括注释和空行)不要超过50行,做到方法职责单一。
4.5行宽
每行代码和注释不要超过70个字符或屏幕的宽度,如超过则应换行,换行后的代码应该缩进一个Tab。
4.6{ “,”}
开括号“{”要放在代码块的所有者的下一行,单起一行;
闭括号“}”要单独放在代码块的最后一行,单起一行。
5.文件命名
- 文件命名原则是更容易区分不同的文件类型和作用。
在文件名前增加三字符的前缀,前缀字母一律为小写,例如:
一个窗体文件可以增加frm前缀,frmImportData.cs
- 文件主体名必须用名词或动名词,且主体名必须是单词首字大写的方式表示
例如:
数据导入的窗体可以命名为frmImportData.cs
- 文件名必须采用在不影响原意表达时尽量采用单词缩写的形式命名,以达到文件名简洁明了的命名目的。
- 文件名要和类名匹配
例如,对于类HelloWorld, 相应的文件名应为
HelloWorld.cs (或, HelloWorld.vb)
6.命名
6.1基本约定
标示符 |
命名类型 |
示例 |
命名空间 |
Pascal |
namespace Com. SMSoft.ProductionCenter |
类型 |
Pascal |
public class Student |
接口 |
Pascal |
public interface ICreateTable |
方法 |
Pascal |
public void UpdateData() |
属性 |
Pascal |
public int Length{ get; set; } |
事件 |
Pascal |
public event EventHandler ChangedEvent; |
枚举值 |
Pascal |
FileEnun{Open; Close; } |
非私有字段 |
Pascal |
public string FieldName; |
私有字段 |
Camel |
private string fieldName; |
参数 |
Camel |
public void UpdateData(string fieldName) |
局部变量 |
Camel |
string fieldName; |
6.2程序集命名
公司域名(SMSoft)+ 项目名称 + 模块名称(可选),例如:
中心系统程序集:SMSoft.ProductionCenter;
中心系统业务逻辑程序集:SMSoft. ProductionCenter.Business;
6.3命名空间命名
命名空间应使用解决方案的名称,每个项目应设置一个二级命名空间,并以项目名命名。
采用和程序集命名相同的方式:公司域名(SMSoft)+ 项目名称 + 模块名称。 另外,一般情况下建议命名空间和目录结构相同。例如:
中心系统:SMSoft.ProductionCenter;
中心系统下的用户控件:SMSoft.ProductionCenter.UserControl;
中心系统业务逻辑:SMSoft. ProductionCenter.Business;
中心系统数据访问:SMSoft. ProductionCenter.Data;
6.4类和接口命名
类的名字要用名词,避免使用单词的缩写,除非它的缩写已经广为人知,
如TCP、HTTP、SQLHelper。
接口的名字要以字母I开头。保证对接口的标准实现名字只相差一个“I”前缀,例如对ICompare的标准实现为Compare;
泛型类型参数的命名:命名要为T或者以T开头的描述性名字,例如:
public class List<T>
public class MyClass<TSession>
对同一项目的不同命名空间中的类、接口要避免命名重复。避免引用时的冲突和混淆。
6.5方法命名
方法名一般采用:动词+名词,表达要做什么。
如果方法返回一个成员变量的值,方法名一般为Get+成员变量名;
如果方法返回的值是bool变量,一般以Is作为前缀或Try前缀;
如果方法修改一个成员变量的值,方法名一般为:Set + 成员变量名。
6.7变量命名
按照使用范围来分,我们代码中的变量的基本上有以下几种类型:
类的公有变量(受保护同公有);类的私有变量;方法的参数变量;方法内部使用的局部变量。这些变量的命名规则基本相同。区别如下:
- 类的公有变量按通常的方式命名,无特殊要求。例如WorkerName;
- 类的私有变量采用两种方式均可:采用加“m”前缀更好区别局部变量。
例如mWorkerName,wokrerName;
- 方法的参数变量采用camalString,例如workerName;
- 方法内部的局部变量采用camalString,例如workerName;
不要用_或&作为第一个字母;
尽量要使用短而且具有意义的单词;
单字符的变量名一般只用于生命期非常短暂的变量。i,j,k,m,n一般用于integer;c,d,e 一般用于characters;s用于string
如果变量是集合,则变量名要用复数。例如表格的行数,命名应为:RowsCount;
命名组件要采用匈牙利命名法,所有前缀均应遵循同一个组件名称,参考缩写列表。
6.8组件名称缩写列表
缩写的基本原则是取组件类名各单词的第一个字母,如果只有一个单词,则去掉其中的元音,留下辅音。缩写全部为小写。
组件类型 |
简写 |
标准命名举例 |
Label |
lbl |
lblMessage |
LinkLabel |
llbl |
llblToday |
Button |
btn |
btnSave |
TextBox |
txt |
txtName |
MainMenu |
mmnu |
mmnuFile |
CheckBox |
chk |
chkStock |
RadioButton |
rbtn |
rbtnSelected |
GroupBox |
gbx |
gbxMain |
PictureBox |
pic |
picImage |
Panel |
pnl |
pnlBody |
DataGrid |
dgrd |
dgrdView |
ListBox |
lst |
lstProducts |
CheckedListBox |
clst |
clstChecked |
ComboBox |
cbo |
cboMenu |
ListView |
lvw |
lvwBrowser |
TreeView |
tvw |
tvwType |
TabControl |
tctl |
tctlSelected |
DateTimePicker |
dtp |
dtpStartDate |
HscrollBar |
hsb |
hsbImage |
VscrollBar |
vsb |
vsbImage |
Timer |
tmr |
tmrCount |
ImageList |
ilst |
ilstImage |
ToolBar |
tlb |
tlbManage |
StatusBar |
stb |
stbFootPrint |
OpenFileDialog |
odlg |
odlgFile |
SaveFileDialog |
sdlg |
sdlgSave |
FoldBrowserDialog |
fbdlg |
fgdlgBrowser |
FontDialog |
fdlg |
fdlgFoot |
ColorDialog |
cdlg |
cdlgColor |
PrintDialog |
pdlg |
pdlgPrint |
6.9数据库缩写列表
数据类型 |
数据类型简写 |
标准命名举例 |
Connection |
con |
conNorthwind |
Command |
cmd |
cmdReturnProducts |
Parameter |
parm |
parmProductID |
DataAdapter |
dad |
dadProducts |
DataReader |
dtr |
dtrProducts |
DataSet |
dst |
dstNorthWind |
DataTable |
dtbl |
dtblProduct |
DataRow |
drow |
drowRow98 |
DataColumn |
dcol |
dcolProductID |
DataRelation |
drel |
drelMasterDetail |
DataView |
dvw |
dvwFilteredProducts |
6.10其他命名
每行要只有一个声明或一条语句,变量名要避免块内部的变量与它外部的变量名相同。
除了for循环外,声明要放在块的最开始部分。for循环中的变量声明可以放在for语句中。如:
for(int i = 0; I < 10; i++)
{ }
if-else,if-elseif语句,任何情况下,都应该有“{”,“}”;
switch语句,每个switch里都应包含default子语句;
7.类型设计
确保每个类型由一组定义明确相互关联的成员组成,而不是一些无关功能的随机集合。
7.1类型和命名空间
要用命名空间把类型组织成相关域的层次结构。例如:
界面层:SMSoft.ProductionCenter;
业务逻辑层:SMSoft.ProductionCenter.Business;
数据访问层:SMSoft.ProductionCenter.Data;
避免过深的命名空间;
避免太多的命名空间;
7.2类型和接口选择
要优先采用类而不是接口。
接口的缺点在于语义变化时改变困难。注意接口并不是协定,把协定和实现分开并非一定用接口实现,用基类和抽象类同样可以表达;
建议使用抽象类而不是接口来解除协定与实现间的偶合;
要定义接口,来实现类似多重继承的效果;
精心定义接口的标志是一个接口只做一件事情。关键是接口的协定需要保持不变, 如果一个接口包含太多功能,那么这个胖接口产生变化的机会就会大得多。
7.3属性和方法的选择
基本原则是方法表示操作,属性表示数据。如果其他各方面都一样,优先使用属性而不是方法。
要使用属性,如果该成员表示类型的逻辑attribute
如果属性的值存储在内存中,而提供属性的目的仅仅是为了访问该值,要使用属性而不要使用方法
如果该操作每次返回的结果不同,那么要使用方法。
如果该操作比访问字段慢一个或多个数量级,要使用方法。
如果该操作有严重的副作用,要使用方法。
7.4类设计
抽象类:
不要在抽象类中定义公有的或内部受保护的构造函数。因为抽象类无法实例化,所以这种设计会误导用户;
要为抽象类定义受保护的构造函数或内部构造函数;
静态类:
静态类是一个只包含静态成员的类,它提供了一种纯面向对象设计和简单性之间的一个权衡,广泛用来提供类似于全局变量或一些通用功能。
要少用静态类。静态类应该仅用作辅助类、工具类;
避免把静态类当作杂物箱。每个静态类都应该有其明确目的;
不要在静态类中声明或覆盖实例成员;
7.5枚举设计
要用枚举来加强那些表示值的集合的参数,属性以及返回值的类型性;
要优先使用枚举而不是静态常量
8.成员设计
方法,属性,事件,构造函数以及字段等统称为成员。
1) 一个方法只完成一个任务。不要把多个任务组合到一个方法中,即使那些任务非常小。
2) 使用C#的特有类型,而不是System命名空间中定义的别名类型。
3) 别在程序中使用固定数值,用常量代替。
4) 避免使用很多成员变量。声明局部变量,并传递给方法。不要在方法间共享成员变量。如果在几个方法间共享一个成员变量,那就很难知道是哪个方法在什么时候修改了它的值。
5) 别把成员变量声明为 public 或 protected。都声明为 private 而使用 public/protected 的属性
6) 不在代码中使用具体的路径和驱动器名。 使用相对路径,并使路径可编程。
7) 应用程序启动时作些“自检”并确保所需文件和附件在指定的位置。必要时检查数据库连接。出现任何问题给用户一个友好的提示。
8) 如果需要的配置文件找不到,应用程序需能自己创建使用默认值的一份。
9) 如果在配置文件中发现错误值,应用程序要抛出错误,给出提示消息告诉用户正确值。
10) DataColumn取其列时要用字段名,不要用索引号。
例: 正确DataColumn[“Name”]
不好 DataColumn[0]
11) 在一个类中,字段定义全部统一放在class的头部、所有方法或属性的前面。
12) 在一个类中,所有的属性全部定义在一个属性块中,用region折叠:
13) 文件读取、数据连接方法最后要手动清理。
8.1方法重载
避免在重载中随意的给参数命名。如果两个重载中的某个参数表示相同的输入,那么该参数的名字应该相同。
避免使重载成员的参数顺序不一致。在所有的重载中,同名参数应该出现在相同的位置。
较短的重载应该仅仅调用较长的来实现。另外,重载如果需要扩展性,把最长重载做成虚函数。
要允许可选参选为null。这样做是为了避免调用者调用之前需要检查参数是否null。
8.2属性设计
如果不应该让调用方法改变属性值,要创建只读属性;
不要提供只写属性;
要为所有的属性提供合理的默认值,这样可以确保默认值不会导致漏洞或效率低的代码;
要允许用户以任何顺序来设置属性的值;
避免在属性的获取方法抛出异常。
属性的获取方法应该是个简单的操作,不应该有任何的条件。如果一个获取方法会抛出异常,那么可能它更应该设计为方法。
8.3构造函数设计
建议提供简单的构造函数,最好是默认构造函数。简单的构造函数增强易用性;
考虑扩展性,如果构造函数设计的不自然,建议用静态的工厂方法来替代构造函数;
要把构造函数的参数用作设置主要属性的便捷方法。如果构造函数参数仅用来设置属性,应和属性名称相同。仅有大小写的区别;
要在构造函数中做最少的工作。任何其他处理应该推迟到需要的时候;
要在类中显示的声明公用的默认构造函数,如果这样的构造函数是必须的。
如果没有显示默认构造函数,填加有参数构造函数时往往会破坏已有使用默认构造函数的代码;
避免在对象的构造函数内部调用虚成员。这样在扩展设计的时候会导致难以理解的现象;
8.4字段设计
不要提供公有的或受保护的字段。代之以属性来访问字段;
要只用常量字段来表示永远不会改变的量。否则会导致兼容性问题。
要用公有的静态只读字段来定义预定义的对象实例。
8.5参数设计
要用类结构层次中最接近基类类型来作为参数的类型,同时要保证该类型能够提供成员所需的功能。
要设计一个集合遍历的方法,那么参数应该是IEnbumerable为参数,而不应该是IList,这样方法具有更强的适应性。
不要使用保留参数。如果将来需要更多的参数,那么可以增加重载成员。
8.6扩展性设计
如果没有恰当理由,不要把类密封起来。这些理由包括:
A)类为静态类;
B)类的受保护成员保存了高度机密信息;
C)类继承了许多虚成员,逐个密封的代价太高,不如密封整个类;
D)不要在密封类中声明保护成员或虚成员,因为无法覆盖其实现;
建议用保护成员用于高级定制。它提供了扩展性,同时也避免了公用接口过于复杂;
不要使用虚成员,除非有合适的理由;
建议只有在绝对必须的时候才用虚成员提供扩展性,并使用Template Method模式;
要优先使用受保护的虚成员,而不是公有虚成员。公有成员通用调用受保护的虚成员的方式来提供扩展性;
8.7异常处理
异常的思想是只对错误采用异常处理:逻辑和编程错误,设置错误,被破坏的数据,资源耗尽,等等。通常的法则是系统在正常状态下以及无重载和硬件失效状态下,不应产生任何异常。异常处理时可以采用适当的日志机制来报告异常,包括异常发生的时刻;
一般情况下不要使用异常实现来控制程序流程结构;
发生异常时,给出友好的消息给用户,但要精确记录错误的所有可能细节,包括发生的时间,和相关方法,类名等。
要通过抛出异常的方式来报告操作失败。如果成员无法成功地完成它应该做的任务,那么应该抛出异常;
不要“捕捉了异常却什么也不做“。如果隐藏了一个异常,你将永远不知道异常到底发生了没有。
优先考虑使用System命名空间中已有的异常,而不是自己创建新的异常类型;
要使用最合理,最具针对性的异常。例如,对参数为空,应抛出 System.ArgutmentNullException,而不是System.ArgutmentException。
所有外部资源都必须显式释放。例如:数据库连接对象、IO对象等。
9.代码注释
9.1代码注释约定
1. 所有的方法和函数都应该以描述这段代码的功能的一段简明注释开始(方法是干什么)。这种描述不应该包括执行过程细节(它是怎么做的),因为这常常是随时间而变的,而且这种描述会导致不必要的注释维护工作,甚至更糟—成为错误的注释。代码本身和必要的嵌入注释将描述实现方法。
2. 当参数的功能不明显且当过程希望参数在一个特定的范围内时,也应描述传递给过程的参数。被过程改变的函数返回值和全局变量,特别是通过引用参数的那些,也必须在每个过程的起始处描述它们。
9.2文件头部注释
以一个物理文件为单元的都需要有模块头部注释,例如:C#中的.cs文件。
用于每个模块开头的说明,主要包括:(粗体字为必需部分,其余为可选部分)
1. 文件名称(File Name):此文件的名称
2. 功能描述(Description):此模块的功能描述与大概流程说明
3. 数据表(Tables):所用到的数据表,视图,存储过程的说明,如关系比较复杂,则应说明哪些是可擦写的,哪些表为只读的。
4. 作者(Author):
5. 日期(Create Date):
6. 参考文档(Reference)(可选):该档所对应的分析文档,设计文檔。
7. 引用(Using) (可选)﹕开发的系统中引用其它系统的Dll、对象时,要列出其对应的出处,是否与系统有关﹙不清楚的可以不写﹚,以方便制作安装档。
8. 修改记录(Revision History):若档案的所有者改变,则需要有修改人员的名字、修改日期及修改理由。
9. 分割符:*************************** (前后都要)
9.3方法注释
- C# 提供一种机制,使程序员可以使用含有XML 文本的特殊注释语法为他们的代码编写文档。在源代码文件中,具有某种格式的注释可用于指导某个工具根据这些注释和它们后面的源代码元素生成XML。具体应用当中,类、接口、属性、方法必须有<summary>节,另外方法如果有参数及返回值,则必须有<param>及<returns>节点。示例如下:
/// <summary>
/// 方法作用
/// </summary>
/// <param name=””></param>
/// <returns></returns>
- 事件不需要头注解,但包含复杂处理时(如:循环/数据库操作/复杂逻辑等),应分割成单一处理函数,事件再调用函数。
- 所有的方法必须在其定义前增加方法注释。
- 方法注释采用 /// 形式自动产生XML标签格式的注释。
标记 |
说明 |
备注 |
<c> |
提供了一种将说明中的文本标记为代码的方法 |
|
<code> |
提供了一种将多行指示为代码的方法 |
|
<example> |
可以指定使用方法或其他库成员的示例。一般情况下,这将涉及到 <code> 标记的使用。 |
|
<exception> |
对可从当前编译环境中获取的异常的引用。 |
|
<include> |
得以引用描述源代码中类型和成员的另一文件中的注释。 |
|
<list> |
用于定义表或定义列表中的标题行。 |
|
<para> |
用于诸如<summary>、<remarks> 或 <returns> 等标记内,使您得以将结构添加到文本中。 |
|
<param> |
应当用于方法声明的注释中,以描述方法的一个参数。 |
|
<paramref> |
提供了一种指示词为参数的方法。 |
|
<permission> |
得以将成员的访问记入文档。 |
|
<remarks> |
用于添加有关某个类型的信息,从而补充由 <summary> 所指定的信息。 |
|
<returns> |
应当用于方法声明的注释,以描述返回值。 |
|
<see> |
得以从文本内指定链接。 |
|
<seealso> |
对可以通过当前编译环境进行调用的成员或字段的引用。 |
|
<summary> |
应当用于描述类型或类型成员。 |
|
<value> |
得以描述属性。 |
- 在公用类库中的公用方法需要在一般方法的注释后添加作者、日期及修改记录信息,统一采用XML标签的格式加注,标签如下:
<Author> </Author> 作者
<CreateDate></CreateDate> 建立日期
<RevisionHistory> 修改记录
<ModifyBy> </ModifyBy> 修改作者
<ModifyDate> </ModifyDate> 修改日期
<ModifyReason> </ModifyReason> 修改理由
<ModifyBy> </ModifyBy> 修改作者
<ModifyDate> </ModifyDate> 修改日期
<ModifyReason> </ModifyReason> 修改理由
</RevisionHistory>
<LastModifyDate> </LastModifyDate> 最后修改日期
9.4代码行注释
- 如果处理某一个功能需要很多行代码实现,并且有很多逻辑结构块,类似此种代码应该在代码开始前添加注释,说明此块代码的处理思路及注意事项等。
- 注释从新行增加,与代码开始处左对齐。
- 双斜线与注释之间以空格分开。
- 定义变量时需添加变量注释,用以说明变量的用途。
- Class级变量应以采用 /// 形式自动产生XML标签格式的注释,
- 方法级的变量注释可以放在变量声明语句的后面(也可以放在前一行),与前后行变量声明的注释左对齐,注释与代码间以Tab隔开。
9.5变量注释
- 定义变量时需添加变量注释,用以说明变量的用途。
- Class级变量应以采用 /// 形式自动产生XML标签格式的注释,
- 方法级的变量注释可以放在变量声明语句的后面(也可以放在前一行),与前后行变量声明的注释左对齐,注释与代码间以Tab隔开。
10.参考文档
本规范很多内容都参考了这本《.NET设计规范》,书中对规范背后的背景和原则做了深入讨论。另外也参考了很多博客园的博客。有不对之处或有更好的建议欢迎拍砖,请邮件联系645817606@qq.com。
磨刀不误砍柴工