THE ELEMENTS OF C# STYLE

|前言

  程序员其实艺术家,灵动的双手如行云流水般在键盘上创造着生命的奇迹,我认为代码是有灵魂的。同一个模块,在每个程序员手中所缔造出来的是不相同的。

   最终,这个模块或者实现了最初的业务,但是回过头看看你的作品,你会认为她是你的艺术品,还是她就是一坨Code?

   好吧,为了普及C#是最美的编程语言这种思想,我决定写这篇【THE ELEMENTS OF C# STYLE】,欢迎吐槽!

|一般原则 

1. 坚持最小惊奇原则
   简单性(Simplicity)
   用简单的类和简单的方法满足用户期望
   清晰性(Clarity)
   确保每个类、接口、方法、变量和对象都有清晰的目的,并阐明何时、何处、如何使用它们
   完整性(Completeness)
   提供任一可能的用户期望找到和使用的最小功能,创建完整的文档,描述所有特性和功能
   一致性(Consistency)
   相似实体的外观和行为应该相同,不同实体的外观和行为应该不同,应该尽可能制定和遵守相关标准
   健壮性(Robustness)
   对软件中可能出现的错误和异常做出预测,并将解决方法记入文档,不要隐藏错误,也不要等着用户去发现错误
2. 第一次就做对
3. 记录所有规范的行为

|格式    

1. 第一次就做对

1.1. 使用快捷键 Ctrl + K + D 来格式化 .CS 代码
1.2. 在流程控制语句中使用语句块花括号
但若控制体中为以下单句语句,可以不使用花括号
return, break, continue, throw, yield return, goto

      if(a<2) return;

2. 类的组织

2.1.使用 .CS 文件右键菜单中顶部的Organize Usings-> Remove and Sort 来整理 using 指令
2.2.使用 #region 将源代码组织到不同区域中

   推荐使用ReSharper的 File Structure 窗口方便的管理代码结构(快捷键Ctrl + Alt + F)

   一般可以考虑按以下类型和访问级别组织类成员:
      #region [Fields]
      #region [Embedded Types]
          … 这里放public 成员
      #region [Protected]
      #region [Private Methods]
    区域尽量不要嵌套多层
2.3.单独声明每个变量和特性(每行只放一个)
不建议的书写方式:

      int a;int b;int c;

            

|命名  

1. 一般原则   

1.1. 使用有意义的名称
      应该使用对读代码的人来说有意义的名称,避免使用单个字符或太一般性的名称(特别是在方法内部实现中)
1.2. 根据含意而非类型来命名
      类型信息一般可从其用法和智能感知中看出来,如:使用Customer 而不是CustomerClass对于UI控件的命名可以加上控件的类型

      如:CustomerNameLabel、 FeedBackButton


1.3. 使用熟悉的名称(目标领域通用的术语)
1.4. 不要用大小写来区分名称
      虽然编译器区分大小写,但这样会影响代码可读性,如:XMLStream和XmlStream
1.5. 避免使用过长的名称
      对象的名称应当足以描述其目的,如果名称过长,可能说明该实体企图实现的功能太多,此时应当考虑是否需要重构
1.6. 加上元音 – 使用完整的单词
      不要通过去除元音来缩短名称,这样会降低代码可读性,有时还可能产生歧义
      如:不要使用 public Msg AppendSig(),而应该使用 public Message AppendSignature()
1.7. 不要出现拼写错误,第一次就拼对!

2.缩略形式    

2.1、除非全称太长,否则不建议使用缩略形式
     不要使用不必要的缩略词,这样会影响可读性,如:使用Attachment 而不是Atta
     如果必须使用缩略词,请使用广为使用和接受的缩略词,如:Api、App、Zip
2.2. 像普通词一样书写缩略词
     不要全都大写,如:XmlString 而不是 XMLString

3.类型和常量

3.1. 建议使用Pascal写法给命名空间、类、结构、属性、枚举、成员常量及方法命名
     每个单词首字母都大写,如:DataManipulator
3.2. 建议使用名词命名类、结构和属性
     如: NetworkManager、 public string NetworkType { set; get; }
3.3. 建议用复数形式书写集合名称
     集合对象的名称应该有能反映集合中对象类型的复数形式,
     差:List<Shape> shapeList = new …
     好:List<Shape> shapes = new …
3.4. 建议给抽象基类加上Base 后缀
     如:public abstract class AccountBase
     public class PersonalAccount: AccountBase
3.5、使用单个大写字母命名泛型参数
    单个泛型参数使用<T>,多个泛型参数将T作为前缀,如:<TKey, TValue>
3.6、给实现了一种设计模式的类添加模式名称
    如:public class MessageFactory

4.枚举

4.1. 用单数形式为(一般)枚举命名
    如:public enum EnhancementMode
4.2. 用复数形式为位域枚举命名
    如,定义时:    

      public enum Languages
      {
         English = 1,
         ChineseSimplify = 1 << 1,
         ChineseTraditional = 1 << 2,
         …
      }

          使用时(通常与位或操作符一起使用): 

    var languages = Languages.English | Languages.ChineseSimplified | Languages.ChineseTraditional

 5.接口 

5.1. 用大写字母 I 作为接口名称的前缀(IInterface)名
5.2. 使用名词或形容词给接口命名
       若接口声明了对象提供的服务,可用名词命名,如:public interface IMessageListener
       若接口描述了对象的能力,可使用带-able或-ible后缀的形容词命名
       如:public interface IReversible

6.属性 

6.1. 根据Getter和Setter的值来为属性命名
      如对于一个取得过期日期的属性,将其命名为ExpirationDate
6.2. 避免冗长的属性名称
      如:使用Icollection Customers,而不用Icollection CustomerCollection
6.3.用能体现布尔值特性的名称给bool类型的属性命名
      视含意添加Is、Has等前缀,如:bool IsOpen, bool HasCompleted

7.方法 

7.1. 使用Pascal写法为方法命名
    每个单词首字母都大写,如:ParseSecondsFrom1970
7.2. 避免冗长的方法名
    操作类名对象的类的成员方法名中不需要再带有该类对象名
    如:在Book 类中,使用方法Open(),而不是OpenBook()

8.变量和参数 

8.1. 使用Camel写法为变量和参数命名
    第一个单词的首字母小写,后面每个单词的首字母大写,如:lastName
8.2. 用名词命名变量和参数
8.3. 给字段(Fields)名称加上下划线前缀_,使之与其他变量区分开来
    如:_lastName
8.4. 用一系列标准名称为“一次性”变量和参数命名
   •循环变量int i, j, k
   •object o, obj
   •string s, str
   •Exception e, ex
   •EventArgs args

9.特性 

9.1. 给自定义特性加上Attribute后缀
    定义时:
    public class MyCustomAttribute: Attribute { … }
    使用时:
    [MyCustom]
    public class CertainClass{ … }

10.事件、异常 

10.1. 事件的名称应当包括对象及动作的描述
     如:public event EventHandler<MessageReceivedEventArgs> MessageReceived
10.2. 事件对应的参数类型应当添加EventArgs后缀,并继承自EventArgs类
     public class MessageReceivedEventArgs: EventArgs
    {
       //…
     }
10.3. 事件所在的类中应当有一个名为OnEventName()的方法用于触发事件
10.4. 给自定义异常类型添加Exception 后缀
     public MyCustomException: Exception
    {
      //…
    }

|文档 

1.一般原则

1.1. 为使用接口的人编写软件接口文档
    编写代码中公共接口的文档,可以让别人正确、高效地理解和使用接口
    编写文档注释的首要目的是在服务的提供者(supplier)和客户(client)之间定义一种编程契约(programming contract)
    与方法相关的文档应该描述与该方法的调用者有依赖的行为的诸多方面,而不应试图描述其实现细节
1.2.为维护者编写代码实现文档
    总是假定会有完全不熟悉你的代码的人要阅读和理解你的代码
1.3.保持注释和代码同步
    修改代码时,也要确保更新相关注释
    代码和文档一起构成软件产品,应对其同等重视
   “When the code and the comments disagree, both are probably wrong.”–Norm Schryer, Bell Labs
1.4.尽早编写软件元素的文档
在实现之前或实现过程中编写软件元素的文档,勿拖延至软件将近完成才编写,因为对代码太过熟悉或太过厌烦,在项目结尾做的文档往往缺少细节
如果在实现之前编写软件参考文档,则可以使用该文档为受托实现软件的开发人员定义需求
1.5.考虑全世界的读者

2.API 

1.尽量使用C#内建的文档机制
   使用<summary>等标签编写文档,之后VS可自动生成API文档
   使用NDoc等工具可自动生成可满足各种需求各种格式的文档
   常用标签:

   

   Ctrl + Shift + F1

  

3.内部代码

3.1. 只在需要帮助别人理解代码的时候才添加内部注释
3.2. 解释代码为什么要这样做
3.3. 避免使用C风格的注释块 /* … */
3.4. 使用单行注释 // 描述实现细节
      特定变量或表达式的目的
      实现层面的设计决定
      复杂算法的来源资料
      缺陷的修正或变通方法
      以后可能做优化或加工的代码
     任何所知的问题、局限或缺陷
3.6. 使用关键词标出待完成工作、未解决问题、错误和缺陷修正

 

|编程

1.类型

1.1.使用内建的C#类型别名
    别名更加简洁易读,而且具有关键字语法着色
    例如:使用int,而不是System.Int32;string,而不是String;object,而不是Object
1.2. 避免使用内联字面值
   不要:if (size > 45) 
   而是:const int limit = 45; if (size > limit) 
   这样不但改善了可读性,而且便于在一个位置修改
1.3.避免不必要的值类型装箱拆箱

 

1.4. 使用标准形式书写浮点字面值
   差:const double foo = 0.000042;
   好:const double foo = 4.2e-5;
1.5. 将“值”语义定义为struct(而不是class)
   选用struct表示其实例在堆栈上创建
   注意struct不能继承和派生(是密封的),而且没有默认的构造器
1.6. 避免使用代价较高的隐藏式字符串分配
   代码if (str1.ToUpperCase() == str2.ToUpperCase()) 将产生2次字符串分配
   可以优化为if (string.Compare(str1, str2, true))
1.7. 采用更高效的空字符串检测方法
   差:if (str == “”)
   好:string.IsNullOrEmpty(str) / string.IsNullOrWhiteSpace(str)
1.8. 只在必要时使用可空值类型
   因为需要装箱拆箱,所以请确认只在有必要的情况下使用可空值类型

2.语句和表达式 

2.1. 在复杂表达式中使用括号,而不要依赖操作符优先级
   差:varj = 10 * 2 << 1 + 20;
   好:varj = (10 * (2 << 1)) + 20;
2.2. 不要将布尔值与true 或false 进行相等比较
   if (popup.IsOpen == true) 这没有必要!应当总是直接判断布尔值if (popup.IsOpen)
2.3. 使用静态方法object.Equals() 测试引用类型等同
   因为某个类型的实例方法Equals() 和操作符 == 可能已被重写
   if (object.Equals(str1, str2))

3.类

 

3.1. 定义小类和小方法
   较小的类和方法易于设计、编码、测试、编写文档、阅读、理解和使用,较小的类通常拥有更少的方法,能表达更简单的概念
   尽量将每个类的接口(指对外暴露的功能)限制在提供必要功能所需的最少法之内
   尽量将较大的方法拆分出一些较小的私有方法,即使这些小方法只被调用一次,因为切割开的代码更容易阅读和重用
   另外,CLR可以对小方法进行更好的代码优化
3.2. 声明所有成员的访问级别
   不要假设别人会记得默认的访问级别,应当全部写出,并按原则上public, protected, private 的顺序排列
3.3. 合理避免使用internal 声明
   internal成员通常标志着不好的设计,因为它绕过了访问限制,而且隐藏了类和成员之间的关系
   你能说出protected internal 的访问级别吗?
   仅在以下情况下考虑使用internal:
   •某工具类仅提供给同程序集中的其他类使用,不需要暴露给程序集的使用者
   •需要防止派生类获得特定父类方法,但又同时允许特定辅助类等访问这些方法

4.字段、属性、方法

4.1. 声明所有字段为private,使用属性提供访问
将实现细节看作私有信息,降低在实现改动时对依赖类的影响,并且让数据持续有效
4.2. 只为简单、低成本、顺序无关的访问使用属性
即不应有副作用、开销大、与访问顺序有关
4.3. 方法中避免传递过多参数
如果某个方法需要大量参数,就应该考虑是否要重新设计了,参数太多标志着方法做了太多的事,或者数个相关参数可以被抽象到另一个类里

5.异常

5.1. 使用返回码(return code)报告预期的状态改变
5.2. 使用异常强迫获得编程契约
    •前置条件异常(pre-conditional exception):参数无效或调用方法时相关对象处于无效状态
    •后置条件异常(post-conditional exception):方法产出结果无效或在返回前相关对象处于无效状态
    与方法调用者有关时才抛出异常,如果是内部逻辑错误,则应使用断言(Debug.Assert)
5.3. 不要静默地接受或忽略非预期的运行时错误
    不要使用空的catch 块消除异常
5.4. 只捕获能处理的异常
    合理避免使用通用的catch (Exception ex) { … }
5.5.使用try…finally… 代码块或using 语句管理资源
5.6.尽可能抛出最具体的异常
    NullReferenceException, ArgumentOutOfRangeException, FileNotFoundException
5.7.按照异常类型的特殊性级别排列catch 块
    先捕获更特殊的异常
5.8.不要在finally 块中抛出异常
    在finally 块中抛出异常会导致同时激活多个异常,很难处理

6.效率 

6.1.使用懒惰求值和懒惰初始化(lazy evaluation & lazy initialization)
   原则:
   在需要结果之前,不要进行复杂的计算
   总是在最靠近嵌套边缘的地方执行计算
   如果可能,缓存结果
   在需要对象之前,不构造对象(可能需要加锁来防止并发初始化)
6.2.重用对象以避免再次分配
   缓存并重用频繁创建且生命周期有限的对象
   使用访问器而不是构造器来重新初始化对象
   使用工厂模式来封装缓存和重用对象机制
6.3.避免创建不必要的对象
   特别是新对象生命周期较短,或者构造后从来不引用时,尤其需要注意
   在知道自己需要什么之前,避免创建对象

 

6.4.让CRL处理垃圾回收
   一般而言,避免调用GC.Collect() 方法,让垃圾回收器自行动作
   在多数情况下,垃圾回收器的优化引擎比你更善于判断执行回收的最佳时机
6.5.到最后再进行优化
   优化的第一原则:
   不要优化
   优化的第二原则(只针对专家):
   还是不要优化
   在确认需要优化之前,不要花时间做优化
   确实要做优化时,应采用80-20原则:平均而言,系统中20%的代码使用80%的资源,确保从这20%的代码开始优化

|推荐

神器Resharper非常好用,推荐安装,让你爱上Coding!

 

posted @ 2015-07-08 11:14  弗雷德瑞克杨  阅读(821)  评论(5编辑  收藏  举报