C#编码规范

命名约定

任何标识符的名字都应该可以简单、清楚、正确的表示出该标识符的作用。同时我们要将尽可能多的信息装入到标识符的名字当中去,以便读代码的人可以快速的读懂代码。

PascalCasing(帕斯卡命名法)

PascalCasing包含一到多个单词,每一个单词第一个字母大写,其余字母均小写。例如:HelloWorld、UserName等。

所有命名空间名称、类、接口、函数、属性、枚举等名称的命名,使用 Pascal 风格。

//命名空间
namespace Soft.Service
//类
public class UserService
//接口
public interface IUserService
//函数
public string GetUser()
//属性
public string UserName { get; set; }
//枚举
public enum StockTypeEnum

camelCasing(驼峰命名法)

camelCasing包含一到多个单词,第一个单词首字母小写,其余单词首字母大写。例如:name、productID等。

参数与变量的命名使用camelCasing.

//参数
public string GetUser(int id)
//变量
var name = "xyc";
var user = new User();

UPPER_CAPS

UPPER_CAPS包含一到多个单词,每个单词的所有字母都大写,单词与单词之间用”_”连接。

该风格目前在c#中只用于const常量。

//常量
public const string MAX_VALUE  = "1000";

私有变量的命名

Private 的私有变量使用下划线”_”+camelCasing的大小写规则,以便快速确认该变量的作用域。

//私有变量
private int _userId;

首字母缩写词的大小写

首字母缩写词是由一个短语的首字母组成的,如Xml(ExtensibleMarkuLaguage),IO(Input and Output)。它和单词缩写是有区别的,单词缩写仅仅是把一个单词的长度变短。

a. 把两个字母的首字母缩写词全部大写,除非它是camelCasing的第一个单词。

//两个字母缩写
using System.IO;
public void StartIO(Stream ioStream)

b. 由三个或以上的字母组成的首字母缩写词,只有第一个字母大写,如Xml,Html.除非首字母是camelCasing标识符的第一个单词。

//三个以上个字母缩写
using System.Xml;
public void ProcessXmlNode(XmlNode xmlNode)

复合词的大小写

不要把复合词中的首字母大写,复合词本身就是一个单词,复合词要当成一个单词来处理。

如endpoint, callback,metadata,namespace等都是正确的写法

不要使用匈牙利命名法

匈牙利命名法是指用小写形式的数据类型缩写来作为变量名的前缀。如:strName,intCount。

这种命名法在C和C++时代很流行,可以帮助程序员记住自己的类型。

但在C#中需要禁用,除非你有足够的理由,因为:

a. C#都是强类型的,现在的IDE(如Visual Studio)可以自动的检测出当前变量的类型以及类型错误

b. 开发初期经常需要修改变量的类型,使用匈牙利命名法维护很困难。

尽量不要使用单词缩写,除非它是众所周知的

命名选择

名字一定要能够表达出标识符的含意

标识符名字必须要表达出该标识符的意义,绝对不可以使用无意义的v1,v2…vn之类的命名。

public static void CloneChars(char[] cl1, char[] cl2)
{
     for (var i = 0; i < cl1.Count(); i++)
     {
          cl2[i] = cl1[i];
     }
}

代码的调用者不看这函数是无法知道cl1还是cl2是要拷贝的char数组,他必须进到这个函数去看完整个逻辑才可以调用。而且在看的过程中cl2[i] = cl1[i]; 也需要他花几秒钟来思考是做什么的。

如果改成有意义的名字: source 和target那么这个方法调用者一看名字就知道使用方法了。

public static void CloneChars(char[] source, char[] target)
{
     for (var i = 0; i < source.Count(); i++)
     {
         target[i] = source[i];
     }
}

选择意义单一明确的名字

在命名时要使用专业的单词,避免使用“空洞”的单词

public class BinaryTree
{
     public int Size { get; set; }
}

看到这行代码你想到Size会返回什么,树的高度,节点数?

我们可以使用更单一明确的词来告诉读者这个方法的具体含义,如

public class BinaryTree
{
     public int Height { get; set; }
     public int NodesNum { get; set; }
}

用不会产生歧义的名字

在给标识符命名时,一定不能产生歧义,代码中的很多错误都是由于命名时的歧义造成的。例如:

public const int CART_TOO_BIG_LIMIT = 10;
if (ShoppingCart.Count() >= CART_TOO_BIG_LIMIT)
{
     LogError("Too many items in cart.");
}

这段代码有个很经典的”大小差一缺陷“。在判断购物车物品上限时,我是应该使用 “>“还是应该使用”>=”,我是无法从代码中判断出来的,所以这个地方很容易出现bug.如果我们换成MAX_ITEMS_IN_CART, 那我马上就可以判定出这里要使用“>“

不要卖弄风骚

使用最常用,众所周知的单词。不要在代码命名时卖弄你的学识,要让你的代码快速准确的表达出你的想法才是真正的牛人。

public static string ConvertXml2Html (string sourcePath)

有些人在看到这个方法的时候怎么想也想不明白这个2是做什么用的,是把一个Xml文件变成两个Html?

熟悉英语文化的人可能知道这是To的俚语表达。如果你不能保证所有阅读你代码的人都知道2是To的缩写。那么请使用ConvertXmlToHtml命名

特定场景下的命名最佳实践

要让接口的名字以字母I开头

如IComponet,IDisposable 大家一看就知道是接口。

派生类的末尾使用基类名称

例如,从 Stream 继承的 Framework 类型以 Stream 结尾,从 Exception 继承的类型以 Exception 结尾。

泛型类型参数的命名

a. 使用描述性的名字来命名泛型类型参数,并且在前面加上T前缀如下面都是很好的命名

public delegate TOutput Converter<TInput, TOutput>TInput from);

b. 如果只有一个类型参数,可以只用一个字母T来表示泛型

public class Nullable<T>
public class List<T>

c. 如果泛型参数有约束,那么需要在泛型类型参数名中需要显示出该约束

public interface ISessionChannel<TSession> where TSession:ISession

要用动词和动词短语命名方法

属性的命名

a. 要用名词、名词短语或形容词来命名属性

b. 要用描述集合中具体内容的短语的复数形式来命名属性集合,而不要用短语的单数形式加”List“、”Array”或”Collection“后缀

class BinaryTree
{
     //好的命名
     public NodeCollection Nodes { get; set; }
     //坏的命名
     public NodeCollection NodesCollection { get; set; }
}

c. 要用肯定性的短语命名布尔属性。最好在前面选择性的加入”Is“、”Can“、”Has“等前缀。

CanSeek比CantSeek和Seekable都更准确和容易理解。

字段的命名

a. 禁止使用实例的公有字段和受保护字段,请使用属性代替。

b. 要使用名词、名词短语或形容词命名字段

c. 不要给字段加前缀如“g_”、”s_”来表示静态字段。因为字段和属性是非常相似的,所以要遵循相同的命名规范。

注释

注释毫无疑问是让别人以最快速度了解你代码的最快途径,但写注释的目的绝不仅仅是”解释代码做了什么“,更重要的尽量帮助代码阅读者对代码了解的和作者一样多。

当你写代码时,你脑海里会有很多有价值的信息,但当其他人读你代码时,这些信息已经丢失,他们所见到的只是眼前代码。

注释约定

如果IDE提供注释格式,则尽量使用IDE提供的格式,否则使用//来注释。类、属性和方法的注释在Visual Studio中都使用输入///自动生成的格式。

类的注释约定

 /// <summary>
/// 类说明
/// <summary>
public class BinaryTree

属性的注释约定

/// <summary>
/// 属性说明
/// </summary>
public int NodesCount { get; private set; }

方法的注释约定

/// <summary>
/// 方法说明
/// </summar>
/// <param name="parentNode">参数说明</param>
/// <returns>返回值说明</returns>
public int ComputeChildNodesCount(BinaryNode parentNode)

代码间注释约定

  • 将注释放在单独的行上,而非代码行的末尾。
  • 当单行注释太长时,换行显示,如下面的示例所示。
// The following declaration creates a query. It does not run
// the query.
  • 不要在注释周围创建格式化的星号块。
/***************************************************
 *  代码块注释1
 *	代码块注释2
 *	......
 *	代码块注释10
 *	代码块注释11
***************************************************/

常用注释标识的约定

这里约定下以后团队常用几种注释标识及含义:

//TODO:  我还没有处理的事情

//FIXME: 已知的问题

//HACK:  对一个问题不得不采用比较粗糙的解决方案

//XXX:   危险!这里有重要的问题

请团队成员自行在Visual Studio中配置FIXME和XXX为高优先级的Comments.

Steps: 工具->选项->环境->Task List->名称->添加->确定

配置完成后,我们就能在视图-任务列表(Ctrl+w,t)窗口中的Comments选项中看到代码中存在的任务了。

需要的注释

记录你对代码有价值的见解

你应该在代码中加入你对代码这段代码有价值的见解注释。

//出乎意料的是,对于这些数据用二叉树比哈希表要快40%

//哈希运算的代价比左右比要大的多

这段注释会告诉读者一些重要的性能信息,防止他们做无谓的优化。

为代码中的不足写注释

代码始终在演进,并且在代码中肯定会有不足。

要把这些不足记录下来以便后来人完善。

如当代码需要改进时:

//TODO:尝试优化算法

如当代码没有完成时:

//TODO:处理JPG以外的图片格式

你应该随时把代码将来该如何改动的想法用注释的方式记录下来。这种注释给读者带来对代码质量和当前状态的宝贵见解,甚至会给他们指出如何改进代码的方向。

对意料之中的疑问添加注释

当别人读你的代码的时候,有些部分可能让他们有这样的疑问:“为什么要这样写?”你的工作就是要给这些部分加上注释。

// 因为Connection的创建很耗费资源和时间,而且需要多线程访问,
// 所以使用多线程单例模式

公布可能的陷阱

当为一个函数或者类写注释时,可以这样的问自己:”这段代码有什么出人意料的地方吗?会不会被无用?“。基本上说就是你需要未雨绸缪,预料到别人使用你代码时可能遇到的问题。如:

//XXX: 因为调用外部邮件服务器发送邮件,所以耗时较长,请使用异步方法调用以防止UI卡死。
public void SendEmail(string to, string subject, string body)

对于代码块总结性地注释

对于代码块的总结性注释可以使读者在深入细节之前就能得到该代码块的主旨,甚至有时候都可以直接跳过该代码块,从而可以快速准确的把握代码。

如读者看到:

//下面代码使用了二分查找算法来快速的根据用户Id找到相应用户

那么他就可以快速理解下面代码的逻辑,否则自己看二分查找还是要用些时间的。

不需要的注释

阅读注释会占用阅读真实代码的时间,并且每条注释都会占用屏幕上的空间。所以我们约定所加的注释必须是有意义的注释,否则不要浪费时间和空间。

别要不要写注释的核心思想就是:不要为那些能快速从代码本身就推断的事实写注释。

不要为了注释而注释

有些人可能以前的公司对于注释要求很高,如“何时写注释”章节中的要求。所以很多人为了写注释而注释。

在没有特殊要求的情况下我们要禁止写下面这种没有意义的注释。

/// <summary>
/// The class definition for BinaryTree
/// </summary>
public class BinaryTree
{
      /// <summary>
      /// Total counts of the nodes
      /// </summary>
      public int NodesCount { get; private set; }
      /// <summary>
      /// All the nodes in the tree
      /// </summary>
      public List<BinaryNode> Nodes { get; set; }
      /// <summary>
      /// Insert a node to the tree
      /// </summary>
      /// <param name="node">the node you want insert into the tree</param>
      public void InsertNode(BinaryNode node){}
}

不要用注释来粉饰糟糕的代码

写注释常见的动机之一就是试图来使糟糕的代码能让别人看懂。对于这种“拐杖式注释”,我们不需要,我们要做的是把代码改的能够更具有”自我说明性“。

记住:“好代码>坏代码+好注释”

注释掉的代码不要吝惜删掉

直接把代码注释掉是非常令人讨厌的做法。

其他人不敢删掉这些代码。他们会想代码依然在这一定是有原因的,而且这段代码很重要,不能删除。而且每个阅读代码的人都会去看一下这些被注释掉的代码中是否有他们需要注意的信息。

这些注释掉的代码会堆积在一起,散发着腐烂的恶臭。

如何写好注释

精确描述方法的行为

注释一定要精确的描述方法的行为。避免由于注释不准确而造成的误调用。

如你写了一个方法统计文件中的行数

//Return the number of lines in this file
public long CountLinesInFile(string fileName)

上面的注释不是很精确,因为有很多定义行的方式,下面几种情况这个方法的返回值无法根据注释快速的判断出来。

a. “”(空文件)——0或1行?

b. “hello”——0或1行?

c. “hello\n”——1或2行?

d. “hello\n\r world\r”——2、3或4行?

假设该方法的实现是统计换行符的(\n)的个数,下面的注释就要比原来的注释更好些。

//Count how many newline symbols('\n') are this file

这条注释包含更多的信息。读者可以知道如果没有换行符,这个函数会返回0。读者还知道回车符(\r)会被忽略。

用输入输出例子来说明特殊的情况

一个精挑细选的例子比千言万语还要有效,而且更加直白有效,阅读速度更快。

/// 
/// Remove the suffix/prefix of charsToRemove from the input source
/// 
public string StripPrefixAndSuffix(string source, string charsToRemove)

这条注释不是很精确,因为它不能回答下面的问题

a. 是只有按charsToRemove中顺序的字符才会被移除,还是无序的charsToRemove也会被移除?

b. 如果在开头和结尾有多个charsToRemove会怎样?

而一个好例子就可以简单直白的回答这些问题:

/// 
/// Example: StripPrefixAndSuffix("abbayabbazbaba","ab") returns "yababz"
///

适当的使用具名函数

假设你看见这样的函数调用:。

  ConnectMailServer(100,false);

假设函数是这样的:

  public void ConnectMailServer(int timeout, bool useEncryption)

那我们可以这样使用具名函数:。

  ConnectMailServer(timeout: 100, useEncryption: false);

更新代码时记得更新注释

再好的注释也会随着内容的更改而变得越来越没有意义,有时候甚至会对读者造成误导,产生不必要的bug。所以在更改代码后,记得要更新所更改代码的注释,使其表达最新代码的含意。

只有能让别人读懂的注释才是合格的注释

当自己不确定自己的注释是否合格时,请周围的同事读下你的注释,看他读完注释后说出的想法是否是你想要表达的,是否有信息遗漏和误解等。

语言准则

以下各节介绍 C# 遵循以准备代码示例和样本的做法。

String 数据类型

  • 使用 + 运算符来连接短字符串,如下面的代码所示。
string displayName = nameList[n].LastName + ", " + nameList[n].FirstName;
  • 若要在循环中追加字符串,尤其是在使用大量文本时,请使用 StringBuilder 对象。
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
var manyPhrases = new StringBuilder();
for (var i = 0; i < 10000; i++)
{
    manyPhrases.Append(phrase);
}
//Console.WriteLine("tra" + manyPhrases);

隐式类型的局部变量

  • 当变量类型明显来自赋值的右侧时,或者当精度类型不重要时,请对本地变量进行隐式类型化
// When the type of a variable is clear from the context, use var 
// in the declaration.
var var1 = "This is clearly a string.";
var var2 = 27;
var var3 = Convert.ToInt32(Console.ReadLine());
  • 当类型并非明显来自赋值的右侧时,请勿使用 var
// 无法一眼看出变量类型时,请使用显式类型
int var4 = ExampleClass.ResultSoFar();
  • 请勿依靠变量名称来指定变量的类型。 它可能不正确。
// Naming the following variable inputInt is misleading. 
// It is a string.
var inputInt = Console.ReadLine();
Console.WriteLine(inputInt);
  • 避免使用 var 来代替 dynamic
  • 使用隐式类型化来确定 for 和 foreach 循环中循环变量的类型。

下面的示例在 for 语句中使用隐式类型化。

var syllable = "ha";
var laugh = "";
for (var i = 0; i < 10; i++)
{
    laugh += syllable;
    Console.WriteLine(laugh);
}

下面的示例在 foreach 语句中使用隐式类型化。

foreach (var ch in laugh)
{
    if (ch == 'h')
        Console.Write("H");
    else
        Console.Write(ch);
}
Console.WriteLine();

无符号数据类型

  • 通常,使用 int 而非无符号类型。 int 的使用在整个 C# 中都很常见,并且当你使用 int 时,更易于与其他库交互。

数组

  • 当在声明行上初始化数组时,请使用简洁的语法。
// 推荐语法. 此处不能用var代替string[].
// 推荐语法. 此处不能用var代替string[].
string[] vowels1 = { "a", "e", "i", "o", "u" };


// 显式实例化可以用var.
var vowels2 = new string[] { "a", "e", "i", "o", "u" };

// If you specify an array size, you must initialize the elements one at a time.
var vowels3 = new string[5];
vowels3[0] = "a";
vowels3[1] = "e";
// And so on.

委托

  • 使用简洁的语法来创建委托类型的实例。
// First, in class Program, define the delegate type and a method that  
// has a matching signature.

// Define the type.
public delegate void Del(string message);

// Define a method that has a matching signature.
public static void DelMethod(string str)
{
    Console.WriteLine("DelMethod argument: {0}", str);
}

// In the Main method, create an instance of Del.

// 推荐语法:创建Del委托的实例
Del exampleDel2 = DelMethod;

// The following declaration uses the full syntax.
Del exampleDel1 = new Del(DelMethod);

异常处理中的 try-catch 和 using 语句

  • 对大多数异常处理使用 try-catch 语句。
static string GetValueFromArray(string[] array, int index)
{
    try
    {
        return array[index];
    }
    catch (System.IndexOutOfRangeException ex)
    {
        Console.WriteLine("Index is out of range: {0}", index);
        throw;
    }
}
  • 通过使用 C# using 语句简化你的代码。 如果具有 try-finally 语句(该语句中 finally 块的唯一代码是对 Dispose 方法的调用),请使用 using 语句代替。
// 此处finally语块仅仅是释放对象
Font font1 = new Font("Arial", 10.0f);
try
{
    byte charset = font1.GdiCharSet;
}
finally
{
    if (font1 != null)
    {
        ((IDisposable)font1).Dispose();
    }
}


// 可以用using语块代替
using (Font font2 = new Font("Arial", 10.0f))
{
    byte charset = font2.GdiCharSet;
}

&& 和 || 运算符

  • 若要通过跳过不必要的比较来避免异常并提高性能,请在执行比较时使用 &&(而不是 &),使用 || (而不是 |),如下面的示例所示。
Console.Write("Enter a dividend: ");
var dividend = Convert.ToInt32(Console.ReadLine());

Console.Write("Enter a divisor: ");
var divisor = Convert.ToInt32(Console.ReadLine());

// If the divisor is 0, the second clause in the following condition
// causes a run-time error. The && operator short circuits when the
// first expression is false. That is, it does not evaluate the
// second expression. The & operator evaluates both, and causes 
// a run-time error when divisor is 0.
if ((divisor != 0) && (dividend / divisor > 0))
{
    Console.WriteLine("Quotient: {0}", dividend / divisor);
}
else
{
    Console.WriteLine("Attempted division by 0 ends up here.");
}

New 运算符

  • 隐式类型化时,请使用对象实例化的简洁形式,如下面的声明所示。
var instance1 = new ExampleClass();

上一行等同于下面的声明。

ExampleClass instance2 = new ExampleClass();
  • 使用对象初始值设定项来简化对象创建。
// Object initializer.
var instance3 = new ExampleClass { Name = "Desktop", ID = 37414, 
    Location = "Redmond", Age = 2.3 };

// Default constructor and assignment statements.
var instance4 = new ExampleClass();
instance4.Name = "Desktop";
instance4.ID = 37414;
instance4.Location = "Redmond";
instance4.Age = 2.3;

事件处理

  • 如果你正定义一个稍后不需要移除的事件处理程序,请使用 lambda 表达式。
public Form2()
{
    // You can use a lambda expression to define an event handler.
    this.Click += (s, e) =>
        {
            MessageBox.Show(
                ((MouseEventArgs)e).Location.ToString());
        };
}

// Using a lambda expression shortens the following traditional definition.
public Form1()
{
    this.Click += new EventHandler(Form1_Click);
}

静态成员

  • 通过使用类名称调用静态成员:ClassName.StaticMember。 这种做法通过明确静态访问使代码更易于阅读。 请勿使用派生类的名称限定基类中定义的静态成员。 编译该代码时,代码可读性具有误导性,如果向派生类添加具有相同名称的静态成员,代码可能会被破坏。

LINQ 查询

  • 对查询变量使用有意义的名称。 下面的示例为位于西雅图的客户使用 seattleCustomers
var seattleCustomers = from cust in customers
                       where cust.City == "Seattle"
                       select cust.Name;
  • 使用别名确保匿名类型的属性名称都使用 Pascal 大小写格式正确大写。
var localDistributors =
    from customer in customers
    join distributor in distributors on customer.City equals distributor.City
    select new { Customer = customer, Distributor = distributor };
  • 如果结果中的属性名称模棱两可,请对属性重命名。 例如,如果你的查询返回客户名称和分销商 ID,而不是在结果中将它们保留为 Name和 ID,请对它们进行重命名以明确 Name 是客户的名称,ID 是分销商的 ID。
var localDistributors2 =
    from cust in customers
    join dist in distributors on cust.City equals dist.City
    select new { CustomerName = cust.Name, DistributorID = dist.ID };
  • 在查询变量和范围变量的声明中使用隐式类型化。
var seattleCustomers = from cust in customers
                       where cust.City == "Seattle"
                       select cust.Name;
  • 对齐 from 子句下的查询子句,如上面的示例所示。
  • 在其他查询子句之前使用 where 子句,以确保后面的查询子句作用于经过减少和筛选的数据集。
var seattleCustomers2 = from cust in customers
                        where cust.City == "Seattle"
                        orderby cust.Name
                        select cust;
  • 使用多行 from 子句代替 join 子句以访问内部集合。 例如,Student 对象的集合可能包含测验分数的集合。 当执行以下查询时,它返回高于 90 的分数,并返回得到该分数的学生的姓氏。
// Use a compound from to access the inner sequence within each element.
var scoreQuery = from student in students
                 from score in student.Scores
                 where score > 90
                 select new { Last = student.LastName, score };

格式

格式的统一使用可以使代码清晰、美观、方便阅读。为了不影响编码效率,在此只作如下规定:

长度

a. 一个文件最好不要超过1000行(除IDE自动生成的类)。

一个文件必须只有一个命名空间,严禁将多个命名空间放在一个文件里。

一个文件最好只有一个类。

如果超过1000行,考虑拆分类将类按照功能拆分。

b. 一个方法的代码最好不要超过100行,如果超过考虑将里面的逻辑封装成函数,可以使用VS的“重构-提取方法”功能:

空格、空行

空行的使用以使代码清晰为基本原则。空行影响程序的运行,但可以使代码看起来清晰,增加可读性,因此可以适当的使用。

a. 方法与方法之间有两个空行

b. 函数内部变量声明与函数内部逻辑之间有一个空行。

c. 函数内部一个逻辑完成后要有一个空行,然后再写下一个逻辑

换行

换行掌握的原则是不要使一行代码特别长,以方便读者快速阅读。

a. 方法与方法之间有两个空行

b. 函数内部变量声明与函数内部逻辑之间有一个空行。

c. 函数内部一个逻辑完成后要有一个空行,然后再写下一个逻辑

根据代码类型划分区域

在class中尽量根据字段,属性,构造函数,私有方法,公有方法等类型,划分区域,相同的类型放一起。

使用VS的格式化文档功能

一个类或者方法编写完成后,必须使用Visual Studio 自带的“编辑-高级-设置文档的格式(Ctrl+K,Ctrl+D)”的功能进行排版后才能签入代码.

发表评论

邮箱地址不会被公开。