Development Specification

 

DotNet开发规范

一、代码规范

  • 默认值使用属性控制而不是在业务代码中处理

  • 集合类型不允许为空

    原因:在代码质量未保证以及新的语法糖(order.Details?.Any())未普及前,可避免绝大多数的空指针异常。而且可以减少很大部分nullable的判断及其类型转换。

  • 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。

  • 所有的覆写方法,必须加Override关键字。

    原因:添加Override关键字,编译器可判断是否覆写成功,另外抽象类如果对方法名进行修改,实现类可以马上编译报错。

  • 方法参数如果不是相同业务含义,禁止使用params关键字。另外提倡直接使用集合来传递相同业务含义的参数而不是用params关键字。

  • 禁止使用过时的类和方法。接口提供方要明确过时接口,接口调用方有义务考证过时方法的新实现。

    说明:如new MongoClient(connStr).GetServer().GetDatabase(database)官方标记为过时,则要使用new MongoClient(connStr).GetDatabase(database)来代替。

    【正例】

    public MongoDbService(MongoDbConfig config)
    {
        var connStr =
            $"mongodb://{config.UserName}:{config.Password}@{config.Server}:{config.Port}/{config.Database}";
        InitMember(connStr, config.Database);
    }
      
    private void InitMember(string connStr, string database)
    {
        var client = new MongoClient(connStr);
        MongoDatabase = client.GetDatabase(database);
    }
    
  • 内部服务不要覆盖异常信息并以自定义信息替换

    说明:会导致原有异常被覆盖并导致要查看并调试代码才能排查问题,并且会覆盖异常信息的堆栈跟踪,如果是为了友好提示则由框架封装处理(MVC、WebApi、TopShelf等)

    【反例】

    try
    {
        var result = PostRequestHelper.Post(postUrl, paramDic);
        var aeOrderDetail = JsonHelper.JsonHelper.JsonToObject<AliExpressResultModel>(result);
        return aeOrderDetail.Success;
    }
    catch (WebException e)
    {
        var message = SalesPlatformException.ParserException(e);
        throw new Exception(message);
    }
    
  • 优先使用已存在的功能,禁止自己另外实现一套逻辑。

    【正例】

    var sqlNames = string.Join(",", names);
    

    【反例】

    var sqlNames = string.Empty;
    foreach(var i = 0; i < names.Length ; i++)
    {
        sqlNames += name[i];
        if(i < names.Length - 1)
            sqlNames += ",";
    }
    
  • 字符串拼接使用StringBuilder,直接使用字符串拼接会占用字符串常量池并导致无效的字符串常量池占用。

    【正例】

    var msg = new StringBuilder("以下账号未激活:");
    var unactivatedUsers = unauthorizedUsers.Concat(blockedUsers);
    msg.Append(string.Join(",",users.Select(p => p.Name).ToList()))
    

    【反例】

    var msg = "以下账号未激活:";
    msg += string.Join(",", unauthorizedUsers.Select(p => p.Name).ToList());
    msg += ",";
    msg += string.Join(",", blockedUsers.Select(p => p.Name).ToList());
    
  • 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读。

  • 类内方法定义顺序依次是:属性 > 公有方法 > 保护方法 > 私有方法。

    说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现。

  • 关联的属性和字段放一起。

    【正例】

    private Guid _id;
    public Guid Id
    {
        get => _id;
        private set => _id = value;
    }
      
    private string _name;
    public string Name
    {
        get => _name;
        private set => _name = value;
    }
    
  • 可为静态方法的方法,就声明为静态方法。

    说明:调用静态方法可以不用实例化对象。

  • 类成员与方法访问控制从严:

    • 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。

    • 类非 static 成员变量并且与子类共享,必须是 protected。

    • 类非 static 成员变量并且仅在本类使用,必须是 private。

    • 类 static 成员变量如果仅在本类使用,必须是 private。

    • 若是 static 成员变量,必须考虑是否为 readonly 甚至 const。

    • 类成员方法只供类内部调用,必须是 private。

    • 类成员方法只对继承类公开,那么限制为 protected。

      说明:任何类、方法、参数、变量,严控访问范围。过宽泛的访问范围,不利于模块解耦。

  • 多个互斥的状态直接使用枚举来设计,而不是用多个布尔值来描述。

  • 禁止出现任何魔法数字,代之以有名字的枚举或者布尔值

    说明:所魔法数字,在编程领域指的是莫名其妙出现的数字。数字的意义必须通过阅读代码才能推断出来。

    【反例】

    var deductTime = order.IsBangGoodCustomer == 1 ? jjDate.AddHours(1) : pkgTime;
      
    switch(i)
    {
        case 0: doSomething;
        case 1: doSomething;
        case 2: doSomething;
        case 3: doSomething;
    }
    
  • 用于计算的浮点优先使用decimal,因为用double会出现精度丢失问题。但在性能测试中double比decimal快一个数量级,因此要考虑业务的具体需要选择使用。

    例子:

    var a = 3;
    var b = 0.03;
    var c = 0.03;
    var result = a + b + c;
    

    result的结果为3.0599999999999996而不是预想中的3.06

    性能测试结果(100万次循环):

    Type Time CPU Cycles
    int 20ms 53,130,304
    long 73ms 224,594,996
    float 13ms 41,020,292
    double 23ms 72,120,164
    decimal 405ms 1,234,385,590
  • 主键只允许三种类型,GUID、snowflake生成的long以及流水号,禁止使用RDB的自增整型作为主键。

    原因:使用RDB的自增整型存在单点问题,难以做到高可用和横向扩展,甚至可能要等到持久化后才可以获取到Id,在分布式系统中难以用于进行幂等处理。通常流水号存在业务需要因此允许以RDB的自增作为种子生成来使用,snowflake的long类型可分布式生成,但要考虑集群中机器号的设计。如果使用GUID类型,禁止使用系统的Guid.NewId()生成,必须使用GuidHelper来获取,因为针对不同RDB的GUID处理,GuidHelper生成的有序GUID可在对应的RDB上提供Int级别的性能。详情

  • 业务对象尽量使用DateTimeOffset而不是DateTime,但是DTO或者PO对象由于序列化器/持久化的技术选型,可能并不支持DateTimeOffset(如protobuf不支持DateTimeOffset,MySql也不支持DateTimeOffset类型),这里要针对具体的技术进行选择。

    原因:DateTime本身存在设计缺陷,如以下代码:

    var d = DateTime.Now;
    var d2 = d.ToUniversalTime();
    d == d2 //false
    d.Equals(d2);  //false
    

    d和d2本质上是不同时区的同一时间,两者应该相等,但DateTime并没有保存时区信息,而是通过DateTimeKind来表达本地时间和UTC时间,并且当这个枚举为Unspecified时则更有歧义。 在微软的文档中已经建议使用DateTimeOffset来代替DateTime。DateTimeOffset 实际上是一个基于UTC的偏移量,同时保存了时区的信息,这有助于我们统一系统中的时间格式以及时间判断。 但是要注意,某些序列化方式不支持DateTimeOffset,因此在DTO/PO等类型中可以使用DateTime或者Timaspan来进行处理,而聚合(DO)中还是尽量使用DateTimeOffset。

二、集合处理

  • 熟练掌握lambda表达式和linq,优先使用lambda表达式。

    说明:lambda表达式将声明式语言引入到C#中,使代码更加优雅健壮。lambda后微软出于商业利益想实现一种类sql语法的语法糖(一开始甚至和sql写法一样是 select u.Name from user,但出于智能提示的考虑改为现在的写法 from u in user select u.Name),因此linq和lambda是功能等价的。但实际上lambda链式编程的写法更加简单易读书写方便,linq相对于lambda的优势在于类sql语法比较容易写出查询数据库的表达式语句。但在CQS的设计下Q端查询数据库使用dapper甚至ado.net等更加简单快捷,因此linq使用的范围更加狭窄。

  • 使用SelectMany代替双重foreach取值

    【正例】

    var kidNodeIds = categories.SelectMany(p => p.KidNodes.Select(g => g.Id));
    

    【反例】

    var kidNodeIds = new List<Guid>();
    foreach(var category in categories)
        foreach(var kidNode in category.KidNodes)
            kidNodeIds.Add(kidNode.Id);
    
  • 了解IEnumerable和IQueryable的区别,IQueryable中不许出现任何函数调用

    说明:IQueryable的数据源千差万别,除了RDB、NoSql甚至可以是WebApi等远程调用,表达式树的解析完全依赖于provider的实现,因此如果在表达式中写函数调用,编译不会报错,但可能会报运行时异常(如NotSupportException)

    【反例】

    var query = dataContext.User;
    var result = query
        .Where(u => u.Name == name.ToLower() || u.CategoryId == CategoryId.ToString());
    
  • 禁止无用的查询字段以及排序字段

    索引的创建以及维护是需要成本的,当一个查询中使用了多余的字段,要么影响其查询速度,要么为了此查询功能而污染了索引的设计并导致拖慢整个系统的响应。

    在一个完整的系统中,数据库往往最容易成为瓶颈,最难以横向扩展,造成影响也是最大的。因此对于数据库我们要避免滥用其功能,如在一个非分页的查询中使用数据库排序。

    【反例】

    var result = dbContext<Order>
        .Where(order => order.Status == OrderStatus.Commited)
        .OrderBy(order => order.ImportTime)
        .ToList();
    

三、并发处理

  • 获取单例对象要线程安全。在单例对象里面做操作也要保证线程安全。

    说明:资源驱动类、工具类、单例工厂类都需要注意。

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

    说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

  • 在一些存在线程安全的地方,可以考虑用线程安全的集合代替非线程安全的集合。

    说明:如用ConcurrentDictionary代替Dictionary。

  • 禁止使用静态字段作为本地缓存,如果使用了则必须实现订阅刷新功能。

    说明:任何服务以及站点都要站在集群的角度考虑,而不能自以为现在只发布一个Host就随便实现。特别是站点项目,虽然在IIS上只发布了一个站点,但如果设置了多个工作进程,每个进程间实际上是单独的Host,除了静态字段外单例也只是在同一个Host中起作用。

四、控制语句

  • 在一个 switch 块内,每个 case 要么通过 break/return 来终止,要么注释说明程序 将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。

  • 尽量少用else,甚至可以考虑反转if来减少层级嵌套,如果if else超过3层请使用状态设计模式。

    【反例1】

    if(_conn == null)
    {
        lock (LockObj)
        {
            DoSomething...
            if(_conn == null)
                _conn = factory.CreateConnection();
            return _conn;
        }
    }
    

    【正例1】

    if (_conn != null) return;
    lock (LockObj)
    {
        DoSomething...
        return _conn ?? (_conn = factory.CreateConnection());
    }
    

    【反例2】

    if(order.Details.Count == 0)
    {
        DoSomething1...
        DoSomething2...
        DoSomething3...
    }
    else
        continue;
    

    【正例2】

    if(order.Details.Count > 0) continue;
    DoSomething...
    DoSomething2...
    DoSomething3...
    
  • 出于性能考虑,获取数据库连接、事务和try catch操作等操作尽量移至循环体外处理。

  • 事务范围内只存在事务操作,禁止直接将业务逻辑整个包括在业务范围内。

    说明:使用事务的原则是快进快出,事务代码尽量少,无脑将代码丢事务里不仅会造成事务执行时间超长,而且也会严重降低并发能力。

    【反例】

    using(var trans = new Transaction)
    {
        try
        {
            dao.Insert;
            httpClient.QueryRemoteResult();
            dao.Update;
            trans.Commit();
        }
        catch
        {
            trans.RollBack();
        }
    }
    

五、注释规范

* 未完成的逻辑要加上注释//TOTO

【正例】

```CSharp
//TODO:the submit function has not compelted
doSomething...
```
  • 属性以及方法要添加文档注释以提供智能提示,方法参数、泛型以及返回值等也要在文档注释中添加对应的param tag。

    【正例】

    /// <summary>
    /// 更新一个实体
    /// </summary>
    /// <typeparam name="T">泛型类型</typeparam>
    /// <param name="entity">持久化对象</param>
    /// <returns>受影响行数</returns>
    public long Update<T>(T entity)
    
  • 如果代码过长,单行注释要独立一行。上下滚动要比左右滚动更适合代码阅读。

六、其它

  • 禁止使用web.config或者app.config等配置文件进行个性化配置

    原因:nuget等程序在更新dll版本时会自动编辑项目对应的web.config和app.config,如果将个性化配置也写到里面那么每次发布时都要检查配置文件是否有改动。因此个性化配置应写在自己新建的config文件中,让web.config和app.config可以直接无忧发布。

  • 禁止将本机开发环境配置推送到VCS中。

  • 自定义配置如果有改动,则要发邮件给测试、运维进行测试环境和生产环境的针对性修改。

  • 所有文件保存的操作都必须有按照业务进行定期删除的机制。

七、命名规范

  • 命名空间以及类名都使用UpperCamelCase

    【正例】

    FinanceDomain、OrderDetail
    
  • Interface使用IUpperCamerCase

    【正例】

    IRespository、ICommand
    
  • 方法、属性以及事件使用UpperCamelCase

    【正例】

    public User GetUserById(Guid userId)
    public List<OrderDetail> OrderDetails {get;set;}
    public event EventHandler<BasicDeliverEventArgs> Received;
    
  • 局部变量使用lowerCamelCase

    【正例】

    var user = respository.GetUserById(1);
    
  • 局部常量使用lowerCamelCase

    【正例】

    const string directoryChar = @"\";
    
  • 参数使用lowerCamelCase

    【正例】

    public Order GetOrderById(string orderId)
    
  • 非私有字段使用UpperCamelCase

    【正例】

    protected RabbitMqProxy RabbitMq = new RabbitMqProxy();
    
  • 私有字段使用_lowerCamelCase

    【正例】

    private string _name;
    
  • 静态私有字段使用_lowerCamelCase

    【正例】

    private static string _connStr;
    
  • 非私有常量字段使用UpperCamelCase

    【正例】

    public const double KgToOz = 35.2739619;
    
  • 私有常量字段使用UpperCamelCase

    【正例】

    private const double Pi = 3.1415;
    
  • 非私有静态只读字段使用UpperCamelCase

    【正例】

    public const string SystemName = "飞特物流管理系统";
    
  • 私有静态只读字段使用UpperCamelCase

    【正例】

    private static readonly string Table = typeof(T).Name;
    
  • 枚举成员使用UpperCamelCase

    【正例】

    public enum AppType
    {
        WebApi = 0,
        Mvc = 1,
        Rpc = 2
    }
    
  • 局部函数UpperCamelCase

    【正例】

    void GetResult()
    {
        using (var httpResponse = httpRequest.GetResponse())
        using (var respStream = httpResponse.GetResponseStream())
        using (var reader = new StreamReader(respStream, Encoding.UTF8))
            strResponse = reader.ReadToEnd();
    }
    
  • 对象成员禁止使用匈牙利命名法,原因是OOP编程中不需要知道成员基于什么数据结构(Dictionary或者List),使用匈牙利命名法会使得代码更为臃肿。

    原因:实际上匈牙利命名法源自微软内部一位匈牙利程序员Charles Simonyi,最初被叫做“应用型匈牙利命名法”(Apps Hungarian),因为它是在“应用程序部”(Applications Division)中使用的,例如 Word 和 Excel 。在 Excel 中xl代表“相对于布局的水平坐标”,而xw表示“相对于窗口的横向坐标”,这两者都是同样的整形但明显不能互换。使用匈牙利命名法有助于程序员发现这种编译器无法发现的错误。因此匈牙利命名法中的type并不是我们通常认为的数据结构,但软件界历史原因这已经积重难返导致匈牙利命名法现在变成“系统性匈牙利命名法”。详细参考:https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/

    【正例】

    var orderDetails = order.Details;
    

    【反例】

    var lstDetail = order.LstDetail;
    
  • 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

    说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使是纯拼音命名方式也要避免使用。

  • 特性类命名要以Attribute结尾;异常类命名要使用Exception结尾;测试类命名要以它要测试的类的名称开头,以Test结尾;抽象类名要使用Abstract开头。

  • 杜绝完全不规范的缩写,避免望文不知意。以下例子后两者是通过长期耗费开发人员精力才最终约定成俗,但新开发人员接手时依然要培训或者自己看代码内部消化,原因是这些缩写完全没有可读性。

    【反例】

    fdId,ctId,ciId(countryId),ptId(postTypeId)
    
  • 如果使用了设计模式,建议在类名中体现出具体模式

    【正例】

    public class RdbFactory;
    public class RabbitMqProxy;
    
  • 代表数量的属性使用Quantity而不是Count命名,因为Quantity是名词,而Count含有计算这种动词的含义。

  • 时间类型,精度在日期级别的以Date结尾,精度在时分秒级别的以Time结尾。

  • 使用long类型进行赋值时,要使用大写的L而不能是小写的l,因为小写l容易和数字1混淆造成误解

    【正例】

    var snowflakeId = 3L;
    

    【反例】

    var snowflakeId = 3l;
    
  • 禁止使用类名来进行冗余的属性命名。

    说明:变量名规范,根本不需要根据属性名来判断其属于什么类。另外统一命名为Id还可以便于一些底层架构封装进行统一处理(如以DO为参数类型的仓储类;自动管理新增、修改时间以及数据版本的仓储父类)。

    【正例】

    public Guid Id { get; set;}
    public string Name { get; set;}
    

    【反例】

    public Guid UserId { get; set;}
    public string UserName { get; set;}
    
  • 除了以VO为目标对象,禁止使用如AutoMapper等映射工具。

    • 字段名耦合,要想自动转换必须字段名一致,而各层模型有不同的命名规范,虽然可以通过单一设置来解决,但如果数量多的话和手写hardCode的工作量没多大区别,而且引入了学习映射工具设置的负担。

    • 失去智能提示,如果修改了一个字段名,但在转换设置中没有写,会导致数据无法转换并引发极大的业务风险,而且代码量多的时候要搜索针对某类型是否使用映射代码进行排查也是一个很大的工作量,提高了维护成本。

    • VO只用于展现数据,不会对数据本身造成影响,因此允许使用。

八、格式规范

  • 所有类成员(属性、方法)之间都要相隔一行空行,方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。

    说明:可以让成员间的界限更加清晰。

  • 缩进使用制表符,禁止使用空格。(制表符设置为4个空格)

  • 无效的using以及声明/实例禁止存在。

  • 不允许连续多个空行影响代码阅读。

  • 使用var而不是显式地指定变量类型。

    原因:可以让代码更加简洁,在重构的时候可以减少代码的变动从而减少文件的签出降低冲突以提高可维护性。如果认为使用var导致可读性差,那是因为变量名或者方法名没起好。

  • 方法参数在定义和传入时,多个参数逗号后必须添加空格。

    【正例】

    public void SendCommand(string commandName, string body, bool isDeadLetter = false)
    SendCommand(exchange, body, isDeadLetter);
    
  • 任何运算符左右必须加一个空格

    说明:运算符包括赋值运算符=、逻辑运算符&&,加减乘除符号以及三目运算符等。

    【正例】

    var i = 1;
    return order.Details.Any() && order.Completed;
    return _virtualHost ?? (_virtualHost = "");
    
  • 除了缩进,禁止连续空格,缩进以四个空格为标准。

    说明:会让人怀疑你的职业素养……

    【反例】

    var  order.TraceId = traceId;
    
  • 如果大括号内容为空,则直接写成{}不需要换行;如果非空则分为两种情况:

    • 如果内容只有一行,则不需要大括号,直接相对上一行缩进四个空格来写代码。

    • 如果内容不止一行,则左右花括号独立一行。