3、代码约定
概述
一般情况下,代码使用以下格式:
- 使用四个空格缩进。 不要使用选项卡。
- 一致地对齐代码以提高可读性。
- 方法函数代码量应尽量少,当函数因为逻辑过长时应考虑提取封装
- 禁止 为了便利大量的复制代码,仅仅是为了小幅度更改。
- 适当情况下,可将长语句分解为多行以提高清晰度。
- 大括号
{}都单独占用一行,大括号与当前缩进级别对齐,包括if-elsetry-catch方法类命名空间 - 遵循
Visual Studio工具默认的Format格式化即可,写完代码记得ctrl k d
正例
public Sample()
{
public Sample()
{
if(true)
{
}
else
{
}
}
}
反例
public Sample()
{
public Sample(){
if(true){
}else{
}
}
}
变量声明
不要在同一行内放置一句以上的代码语句。 这会使得调试器的单步调试变得更为困难。
正例
var a = 1;
var b = 2;
反例
int a = 1, b = 2;
代码分块
当代码过于复杂有多个逻辑步骤时,应该使用 #region...#endregion 标记单个逻辑步骤,方便折叠,便于阅读。
空行
空行是为了将逻辑上相关联的代码分块,以便提高代码的可阅读性。 在以下情况下使用一个空行:
- 当接口和类定义在同一文件中时,接口和类的定义之间。
- 当枚举和类定义在同一文件中时,枚举和类的定义之间。
- 当多个类定义在同一文件中时,类与类的定义之间。
- 方法与方法之间。
- 方法中变量声明与语句之间。
- 方法中不同的逻辑块之间。
- 方法中的返回语句与其他的语句之间。
- 属性与方法、属性与字段、方法与字段之间。
- 语句控制块之后,如if、for、while、switch。
- 注释与它注释的语句间不空行,但与其他的语句间空一行。
if语句
- 条件语句判断不应层次过深,比如超过3层嵌套,就应该利用面向对象特性、设计模式等方法进行抽象
- 对于简单的业务逻辑,应优先使用三目运算,禁止简单判断也使用大量
if... - 应使用防御式编程,即优先判断错误条件,穷举完逻辑上可能错误以后直接
return(应尽量少的出现else逻辑块),再书写正确的业务逻辑,禁止return和else同时出现
正例
//判断num 是否等于 3
var num = 2;
if(num == 3)
{
return num;
}
throw new Exception("num不等于3");
反例
//判断num 是否等于 3
var num = 2;
if(num == 3)
{
return num;
}
else // 这里的else应该去掉,多余的代码
{
throw new Exception("num不等于3");
}
- 所有的if 语句都应该有 大括号
{},不管单行,还是多行
正例
if(true)
{
return true;
}
反例
if(true)
return true;
for、foreach语句
- 循环优势在于计算,而不是处理数据(涉及IO的操作,一般情况不建议写在循环中,例如文件读取、数据读取等等)
for (int i = 0; i < 100; i++)
{
//Code
}
foreach (var item in itemList)
{
//Code
}
switch语句
- 最后的
default必须存在
switch (Codition)
{
case 1:
//Code
break;
default://不可缺失
break;
}
using语句
- 对于封装的类一般都需要继承
IDispose接口,有利于垃圾回收器的及时回收,使用Using是良好习惯。 - 通过使用
using语句简化你的代码。 如果具有try-finally语句(该语句中finally块的唯一代码是对Dispose方法的调用),请使用using语句代替。
正例
using (Font arial = new Font("Arial", 10.0f))
{
byte charset2 = arial.GdiCharSet;
}
反例
Font bodyStyle = new Font("Arial", 10.0f);
try
{
byte charset = bodyStyle.GdiCharSet;
}
finally
{
if (bodyStyle != null)
{
((IDisposable)bodyStyle).Dispose();
}
}
- 不要使用新的
using语法,即去掉大括号{}的写法
正例
using (Font arial = new Font("Arial", 10.0f))
{
byte charset2 = arial.GdiCharSet;
}
反例
using Font normalStyle = new Font("Arial", 10.0f);
byte charset3 = normalStyle.GdiCharSet;
new 运算符
- 使用对象实例化的简洁形式之一,如以下声明中所示。
var firstExample = new ExampleClass();
- 使用对象初始值设定项简化对象创建,如以下示例中所示。
正例
var thirdExample = new ExampleClass
{
Name = "Desktop",
ID = 37414,
Location = "Redmond",
Age = 2.3
};
反例
var fourthExample = new ExampleClass();
fourthExample.Name = "Desktop";
fourthExample.ID = 37414;
fourthExample.Location = "Redmond";
fourthExample.Age = 2.3;
LINQ
- 所有的
LINQ查询参数统一使用Lambda表达式,禁止使用from子句 Lambda表达式的传入参数为方法首字母,如customers.Where(w=>w.City == "Seattle").Select(s=>s.Name).OrderBy(o=>o.City);里面的wsoLinq表达式中的方法过多时,应适当按照方法换行,一个方法独占一行
正例
var seattleCustomers = customers.Where(w=>w.City == "Seattle").Select(s=>s.Name);
反例
var seattleCustomers = from customer in customers
where customer.City == "Seattle"
select customer.Name;
字符串
- 使用字符串内插来连接短字符串,如下面的代码所示。
string displayName = $"{nameList[n].LastName}, {nameList[n].FirstName}";
- 若要在循环中追加字符串,尤其是在使用大量文本时,请使用
System.Text.StringBuilder对象。
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
var manyPhrases = new StringBuilder();
for (var i = 0; i < 10000; i++)
{
manyPhrases.Append(phrase);
}
//Console.WriteLine("tra" + manyPhrases);
事件处理
- 使用
lambda表达式定义稍后无需移除的事件处理程序:
正例
public Form2()
{
this.Click += (s, e) =>
{
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
};
}
反例
public Form1()
{
this.Click += new EventHandler(Form1_Click);
}
void Form1_Click(object? sender, EventArgs e)
{
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}
异常处理
- 避免空异常(Null Reference Exception): 在访问对象的属性或方法之前,应始终检查对象是否为
null,以避免空引用异常。 - 不要捕获通用异常:避免捕获通用的
Exception类型,而是捕获特定的异常类型,以便更好地理解和处理异常情况,对于整个代码调用链路不管在哪里,至少需要有一个异常捕获,来记录明确的错误日志或者处理成友好的报错信息等。
try
{
// 一些可能引发异常的代码
}
catch (SpecificException ex)
{
// 处理特定异常
}
- 使用
finally块:在try-catch块中,如果需要无论是否发生异常都要执行某些代码,可以使用finally块。
try
{
// 一些可能引发异常的代码
}
catch (SpecificException ex)
{
// 处理特定异常
}
finally
{
// 这里的代码会在try或catch块完成后执行
}
- 不要滥用异常: 异常处理是相对昂贵的操作,因此不应该用于正常的控制流。只有在真正发生错误时才应该引发异常。
编程技巧(重要!)
- 充分发挥面向对象的思想,封装、继承、多态等特性
- 禁止使用面向过程的编程思想,这会导致函数方法因为逻辑复杂性代码量变得过长,不利于阅读和重构(通常称为猪大肠代码)
- 禁止在没有思考清楚逻辑之前,进行代码书写
- 代码书写过程中时刻谨记变量、对象等Null引用问题,该校验校验并抛出异常等信息,禁止使用可能为Null的变量进行逻辑判断和处理。
- 对于不再使用的方法和接口不应删除,应标记过时并写明替代方法,举例
- 禁止程序中使用已过时的方法,特别是Framework中的API方法。
- 禁止在类的构造方法中写业务逻辑,如有对象初始化什么的,应在此类中添加类名+Init的方法,并且进行异常捕获记录日志,再在构造方法中调用。
- 禁止在使用共享变量/缓存集合进行非线程安全操作,即要加锁才可以进行更新操作,对于只读的可以不考虑。特别是做后端服务的一定要考虑多线程问题。
- 针对多线程中加锁问题,如果可以不加就不加,如果需要加锁应该控制最小粒度,即如果加锁变量可以完成的,绝不加锁方法,加锁意味着性能损耗问题。
推荐
- 应尽量以安全的代码实现需求,即应该是书写在编译时就可以发现问题的代码并修正(硬编码),而不是在运行时再去发现(软编码,反射等)
- 在需求分析时尽量考虑全部可能情况,但不要为了有可能存在的需求进行过度的设计和编码实现。往往过度的设计会增加软件复杂度和实现周期,并且有可最后一点作用都没有。
- 工具类方法即可能会复用的,业务无关性的方法封装,推荐使用扩展方法定义并实现。
- 通用业务即可能会复用的业务方法,应提取抽象成类和方法,进行拆分归类,目的就是解耦。软件工程中解耦做的越好,不管是以后扩展还是重构都会省时省力,方便调整。
以下是摘录网络上的推荐:
类成员与方法访问控制从严:
- 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private 。
- 工具类不允许有 public 或 default 构造方法。
- 类非 static 成员变量并且与子类共享,必须是 protected 。
- 类非 static 成员变量并且仅在本类使用,必须是 private 。
- 类 static 成员变量如果仅在本类使用,必须是 private 。
- 若是 static 成员变量,必须考虑是否为 const。
- 类成员方法只供类内部调用,必须是 private 。
- 类成员方法只对继承类公开,那么限制为 protected 。
说明: 任何类、方法、参数、变量,严控访问范围。过宽泛的访问范围,不利于模块解耦。思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 Service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,如果无限制的到处跑,那么你会担心的
日志记录
- 禁止混乱无意义的日志输出,日志记录,应在入口,出口,有标志性的地方,如果是调试程序的临时日志记录,在调整完毕以后必须删除,应充分使用日志级别进行隔离。大量日志记录非常会损耗服务的整体性能,拖慢服务并且带来日志占用磁盘空间,日志清理的问题。
- 不允许个性化的日志记录,应统一格式,对于第三方接口调用记录建议格式为:
- 本程序名->调用程序名:接口名
- 本程序名->调用程序名:URL地址和入参
- 本程序名->调用程序名:出参
- 本程序名->调用程序名:异常(应记录堆栈信息,会标明具体发生异常时的详细信息)
- 应理解
AOP面向切面编程的设计模式,对于封装代码、解耦模块非常有好处。
