要作为一名合格的开发者,最基本的素质就是要做到编码规范,从小我们就接受教导“字如其人”,而写代码亦是如此,良好的代码风格,彰显了个人的工作素养。而良好的代码规范,能够帮助我们进行更好的团队协作,它能方便代码的交流和维护;不会影响编码的效率,不与大众习惯冲突;使代码更美观、阅读更方便;使代码的逻辑更清晰、更易于理解。
那为什么要整理这个规范呢?
最近社区群里有在讨论C#的编码规范,而网络上也没有一些全面的规范文档,所以我就结合微软官方、Resharper和stylecop的规范,以及曾经网路上的一些规范,整理一套全面的编码规范吧。
任何的标准都可能众口难调,整理的这套编码规范目的在于帮助开发者们提高开发效率,减少代码中的bug,并增强代码的可维护性和可读性,同时,提升代码的执行效率等。万事开头难,接受一个不熟悉的规范可能在初期会有一些棘手和困扰,但是这些不适应很快便会消失,它所带来的好处和优势很快便会显现,特别是在当您接手他人代码时,这也是一劳永逸的事情。
规范目的
一个软件的生命周期中,80%的花费在于维护;
几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护;
编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码。为了执行规范,每个软件开发人员必须一致遵守编码规范;
使用统一编码规范的主要原因,是使应用程序的结构和编码风格标准化,以便于阅读和理解这段代码;
好的编码约定可使源代码严谨、可读性强且意义清楚,与其它语言约定相一致,并且尽可能的直观。
高质量的代码往往具有如下特质
易懂性 – 代码必须易读且简单明确的。它们必须能展示出重点所在,且代码应该做到易于重用,不可包含多余代码,它们必须带有相应文档说明。
正确性 – 代码必须正确展示出其要告知使用者的重点。代码必须经过测试,且可以按照文档描述进行编译和正确运行。
一致性 – 代码应该按照一致的编程风格和设计来保证代码易读。 同样的,不同代码之间也应当保持一致的风格和设计,让使用者能够很轻松的结合使用它们。一致性能将我们代码库优良的品质形象传递给使用者,展示出我们对于细节的追求。
流行性 – 代码应当展示现行的编程实践,例如使用 Unicode,错误处理,防御式编程以及可移植性。代码应当使用当下推荐的运行时库和API函数,以及推荐的项目和生成设置。
可靠性 – 代码必须符合当地法律,隐私和政策标准和规范。不允许展示入侵性或低质的编程实践,不允许永久改变机器状态。所有的安装和执行过程必须是可以被撤销的。
安全性 – 代码应该展示如何使用安全的编程实践 :例如最低权限原则,使用运行时库函数的安全版本,以及SDL推荐的项目设置。
合理使用编程实践,设计和语言特性决定了示例代码是否可以很好满足上述特性。
目录:
1.基本代码格式
1.1 缩进
一定不要使用制表符。不同的编辑器使用不同的空格来生成制表符 ,这就带来了格式混乱。所有代码都应该使用4个空格来表示缩进。
配置Visual Studio 文字编辑器,以空格代替制表符 。
1.2 花括号
花括号一定独占一行。关于花括号的格式问题,也是很多人争论的点,有些人习惯于Java的左花括号跟在圆括号后面,但微软官方推荐的是花括号应该独占一行,不与任何语句并列一行。
左花括号 “{” 放于关键字或方法名的下一行并与之对齐。左花括号 “{” 要与相应的右花括号 “}”对齐。
错误的示范:
public static void Main(string[] args) {
}
正确的示范:
public static void Main(string[] args)
{
}
if、while、do语句后一定要使用{},即使{}号中为空或只有一条语句。如:
if (somevalue == 1)
{
somevalue = 2;
}
右花括号 “}” 后建议加一个注释以便于方便的找到与之相应的 {。如:
while(1)
{
if(valid)
{
} // if valid
else
{
} // not valid
} // end forever
例外的,类的自动属性花括号与代码合占一行:
public string Name{get;set;}
1.3 using排序
引入的命名空间应该按照字母音序排列,这样做的目的在于方便在引入的多个命名空间中直接快速的找到命名空间。
1.4 换行
当表达式超出或即将超出显示器一行显示范围的时候,遵循以下规则进行换行:
1.在逗号后换行。
2.在操作符前换行。
规则1优先于规则2。
当以上规则会导致代码混乱的时候自己采取更灵活的换行规则。
1.5 空行
空行是为了将逻辑上相关联的代码分块,以便提高代码的可阅读性。
在以下情况下使用两个空行:
当接口和类定义在同一文件中时,接口和类的定义之间。
当枚举和类定义在同一文件中时,枚举和类的定义之间。
当多个类定义在同一文件中时,类与类的定义之间。
在以下情况下使用一个空行:
方法与方法、属性与属性之间。
方法中变量声明与语句之间。
方法与方法之间。
方法中不同的逻辑块之间。
方法中的返回语句与其他的语句之间。
属性与方法、属性与字段、方法与字段之间。
语句控制块之后,如if、for、while、switch。
注释与它注释的语句间不空行,但与其他的语句间空一行。
1.6 空格
在以下情况中要使用到空格:
关键字和左括符 “(” 应该用空格隔开。如:while (true)
注意在方法名和左括符 “(” 之间不要使用空格,这样有助于辨认代码中的方法调用与关键字。
多个参数用逗号隔开,每个逗号后都应加一个空格。
除了 . 之外,所有的二元操作符都应用空格与它们的操作数隔开。一元操作符、++及–与操作数间不需要空格。如:
a += c + d;
a = (a + b)/(c*d);
while (d++ == s++)
{
n++;
}
PrintSize("size is " + size + "\n");
语句中的表达式之间用空格隔开。如:for (expr1; expr2; expr3)
1.7 文件定义
通常情况下,一个cs文件只能定义一个类、接口、枚举、结构体,特殊情况可将多个类定义在同一cs文件,如代码生成器生成的代码或紧密关联的两个class。
其次类名应该与cs文件名保持一致,以便于通过文件名查找类名,比如UserInfo类应该在UserInfo.cs文件里。
语句
一定不要在同一行内放置一句以上的代码语句。 这会使得调试器的单步调试变得更为困难。
错误示范:
a = 1; b = 2;
正确示范:
a = 1;
b = 2;
2.命名规范
2.1 基本命名规范
一定要为各种类型,函数,变量,特性和数据结构选取有意义的命名。其命名应该能直接反映其作用。所谓自注释的代码就是好代码。
名称应该说明“什么”而不是“如何”。通过避免使用公开基础实现(它们会发生改变)的名称,可以保留简化复杂性的抽象层。例如,可以使用 GetNextStudent(),而不是 GetNextArrayElement()。
不应该在标识符名中使用不常见的或有歧义的缩短或缩略形式的词。比如,使用 “GetTemperature” 而不是 “GetTemp”,Temp到底是Temperature的缩写还是Temporary的缩写呢。对于公共类型或大家都知道的缩写,则可以使用缩略词,如:线程过程,窗口过程,和对话框过程函数,为“ThreadProc”, “DialogProc”, “WndProc” 等使用公共后缀。
一定不要使用下划线,连字号,或其他任何非字母数字的字符。
不要使用计算机领域中未被普遍接受的缩写。
在适当的时候,使用众所周知的缩写替换冗长的词组名称。例如,用 UI 作为 User Interface 缩 写,用 OLAP 作为 On-line Analytical Processing 的缩写。
在使用缩写时,对于超过两个字符长度的缩写请使用 Pascal 命名法或驼峰命名法。例如使用 HtmlButton 或 HTMLButton;但是,应当大写仅有两个字符的缩写,如:System.IO,而不是 System.Io。
不要在标识符或参数名称中使用缩写。如果必须使用缩写,对于由多于两个字符所组成的缩写请使用驼峰命名法。
2.2 命名原则是:
选择正确名称时的困难可能表明需要进一步分析或定义项的目的。使名称足够长以便有一定的意义,并且足够短以避免冗长。唯一名称在编程上仅用于将各项区分开。表现力强的名称是为了帮助人们阅读;因此,提供人们可以理解的名称是有意义的。不过,请确保选择的名称符合适用语言的规则和标准。
2.3 推荐的命名法
Pascal命名法:将标识符的首字母和后面连接的每个单词的首字母都大写。可以对三字符或更多字符的标识符使用Pascal 命名法。例:BackColor
驼峰命名法:标识符的首字母小写,而每个后面连接的单词的首字母都大写。例:backColor
2.4 不推荐的命名法❌
匈牙利命名法:匈牙利命名法是一名匈牙利程序员发明的,这种命名法的基本原则是:变量名=属性+类型+对象描述,如:m_bFlag: m表示成员变量,b表示布尔,合起来为:“某个类的成员变量,布尔型,是一个状态标志”。
一定不要在.NET中使用匈牙利命名法 (例如,不要在变量名称内带有其类型指示符)。
2.5 几点是推荐的命名方法:
避免容易被主观解释的难懂的名称,如AnalyzeThis(),或者属性名 Temp。这样的名称会导致多义性。
在类属性的名称中包含类名是多余的,如 User.UserName。而是应该使用 User.Name,“.”即中文的“的”的意思。
只要合适,在变量名的末尾或开头加计算限定符(Avg、Sum、Min、Max、Index)。
在变量名中使用互补对,如 min/max、begin/end 和 open/close。
布尔变量名通常应该包含 Is,这意味着True/False 值,如 fileIsFound,若单词的意义本身已经包含是非的情况,可省略Is,如Exist。
在命名状态变量时,避免使用诸如 Flag 的术语。状态变量不同于布尔变量的地方是它可以具有两个以上的可能值。比如订单状态不应该是OrderFlag,而是使用更具描述性的名称,OrderStatus。
即使对于可能仅出现在几个代码行中的生存期很短的变量,仍然需要使用有意义的名称。仅对于短循环索引使用单字母变量名,如 i 或 j。
定义方法名通常使用动词,接口通常使用形容词,类名、属性名、字段名、参数名通常使用名词。
2.6常用的命名规范
-
命名严禁使用拼音与英文混合的方式,更不允许直接使用中文。正确的英语拼写和语法可以让阅读者易于理解,避免歧义。注意:即使纯拼音命名的方式也要避免采用。
正例:name / order / baidu / alibaba 等国际通用的名称可视为英文。
反例:zhekou(折扣)/Shuliang(数量)/ int 变量=1 -
类名使用Pascal命名法,某些情况例外:DTO/UID等模块功能缩写或接口定义IInterface。
正例: UserDTO / XmlService / TFlowInfo /TTouchInfo /IUserService
反例: userDto / XMLService / tflowInfo / ttouchInfo -
方法名、参数名、成员变量、局部变量都统一使用驼峰命名法,必须遵从驼峰形式。
正例: name / getUserInfo() / userId -
常量的命名使用Pascal命名法,单词力求语义表达要完整,不要嫌名字长。
正例:MaxStockCount
反例:Max_Count -
抽象类命名推荐使用Base结尾,异常类命名使用Exception结尾,测试类命名以它要测试的类的名称开始,以Test结尾。杜绝不规范的缩写,避免望文不知义。
反例:NotFoundException 缩写命名为 NotFoundEx
-
为了达到代码自注释的目标,任何自定义编程元素在命名时,尽量使用完整的单词组合来表达其意思。
反例:int a的随意命名方式 -
如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:public class OrderFactory / public class LoginProxy -
接口和实现类的命名须遵循以下两点规则:
(1)对于Service和DAO类,暴露出来的服务一定是接口。正例:CacheService实现自ICacheService接口。
(2)如果形容能力的接口名称,取对应的形容词为接口名(一般为-able结尾),正例:IDisposeable接口。 -
枚举类的成员名称使用Pascal命名法,枚举为特殊的类,成员均为常量,并为其显式指定枚举值,防止将来在中间插入枚举变量导致枚举值混乱,同时最好为没个枚举值打上Description标签。
正例:
public enum PaymentStatus : sbyte
{
/// <summary>
/// 进行中
/// </summary>
[Description("进行中")]
Processing = 1,
/// <summary>
/// 成功
/// </summary>
[Description("成功")]
Succeed = 2
}
2.7 各层命名规约:
A) Service/DAO 层方法命名规约
1)获取单个对象的方法用 Get 做前缀。
2)获取多个对象的方法用List做后缀,如:GetOrdersList。
3)获取统计值的方法用 Count 做后缀。
4)添加或更新的方法用 Save或Add。
5)删除的方法用 Remove/Delete。
6)修改的方法用 Update。
B) 领域模型命名规约
1) 实体对象:如UserInfo,UserInfo即为数据库表名。
2) 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
3) 展示对象:xxxViewModel,xxx 一般为网页名称。
2.8命名空间和程序集命名
命名空间名称采用Pascal命名法,且首字符大写。命名空间名称尽量反映其内容所提供的整体功能,一般以“域名.项目名.模块名”的命名方式。如:Masuit.MyBlogs.Models。
其次命名空间应该与文件夹层级结构保持一致,比如在项目Masuit.MyBlogs.Core中,Masuit.MyBlogs.Core.Infrastructure.Services则表示该命名空间位于项目的Masuit.MyBlogs.Core/Infrastructure/Services文件夹下。
2.9 常见的标识符的命名规范
2.10 全局变量
尽量少用全局变量。 为了正确的使用全局变量,一般是将它们作为参数传入函数。永远不要在函数或类内部直接引用全局变量,因为这会引起一个副作用:在调用者不知情的情况下改变了全局变量的状态。这对于静态变量同样适用。如果您需要修改全局变量,您应该将其作为一个输出参数,或返回其一份全局变量的拷贝。
变量的声明和初始化
一定是在最小的,包含该局部变量的作用域块内声明它。一般情况,如果语言允许,就仅在使用前声明它们,否则就在作用域块的顶端声明。
void MyMethod()
{
int int1 = 0;
if (condition)
{
int int2 = 0;
...
}
}
避免不同层次间的变量重名,如:
int count;
...
void MyMethod()
{
if (condition)
{
int count = 0; // 避免
...
}
...
}
一定要在声明变量时初始化它们的默认值,降低被抛空引用异常的概率。
在语言允许的情况下,将局部变量的声明和初始化或赋值置于同一行代码内。这减少了代码的垂直空间,确保了变量不会处在未初始化的状态。
错误示范:
string str;
str="123";
正确示范:
string str="123";
一行只建议作一个声明,并按字母顺序排列。如:
int level; // 推荐
int size; // 推荐
int x, y; // 不推荐
2.11 常量定义
不允许任何未经预先定义的常量直接出现在代码中。
反例:string name =”tflow” + userId;
在long 或者 float赋值时,数值后使用大写L或者F,特别是L不能写成小写的l,小写的l容易跟数字1混淆,造成误解。
不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。因为大而全的常量类,杂乱无章,使用查找才能定位到常量,不利于理解和维护。
正例:缓存相关的常量放在类CacheConstants下;系统配置常量放ConfigConstants下。
如果变量值仅在一个固定范围内变化用 enum 类型来定义。
一定要将那些永远不会改变值定义为常量字段。编译器直接将常量字段嵌入调用代码处。所以常量值永远不会被改变,且并不会打破兼容性。
public class Int32
{
public const int MaxValue = 0x7fffffff;
public const int MinValue = unchecked((int)0x80000000);
}
一定要为预定义的对象实例使用public static readonly字段 。如果有预定义的类型实例,也将该类型定义为public static readonly。举例,
public class ShellFolder
{
public static readonly ShellFolder ProgramData = new ShellFolder("ProgramData");
public static readonly ShellFolder ProgramFiles = new ShellFolder("ProgramData");
...
}
结语
以上,只是规范,不是规定,所以不是强制要求一定要这样做,大家自取所需就好了。
如有遗漏,欢迎大家随时补充。
如有不合理之处,也接受大家的批评指正。