September 1, 2023
By: Kevin

c#代码规范

  1. 约定的目的
  2. 命名约定
    1. 帕斯卡拼写法
    2. 驼峰式大小写
    3. 编写方法参数时, 请使用驼峰式大小写.
    4. 其他命名约定
    5. 项目中的约定
  3. 布局约定
  4. 注释约定
  5. 语言准则
    1. 字符串数据类型
    2. 隐式类型本地变量
    3. 无符号数据类型
    4. 数组
    5. 委托
    6. try-catch 和 using 语句正在异常处理中
    7. && 和 || 运算符
    8. new 运算符
    9. 事件处理
    10. 静态成员
    11. LINQ 查询
  6. 语言版本
    1. 使用Global Using
    2. 使用Implicit Using
    3. 使用namespace后加分号
    4. 使用record优于class
    5. 自动属性初始化器(Auto-Property Initializers)
    6. 只读自动属性(Getter-Only Auto-Properties)
    7. 字符串插值(String Interpolation)
    8. 空条件运算符(Null-Conditional Operators)
    9. nameof 表达式
    10. 表达式主体成员(Expression-Bodied Members)
    11. 使用静态导入(Using Static)

约定的目的

为代码创建一致的外观, 以确保读取器专注于内容而非布局. 使得读取器可以通过基于之前的经验进行的假设更快地理解代码. 便于复制, 更改和维护代码. 展示 C# 最佳做法.

命名约定

编写 C# 代码时需要考虑几个命名约定. 在下面的示例中, 在使用 protected 和 protected internal 元素时, 还需遵守与标记了 public 的元素相关的任何指南 - 所有这些元素都旨在对外部调用方可见.

帕斯卡拼写法

**命名 class, record 或 struct 时, 使用 pascal 大小写(" PascalCasing" ). **

public class DataService
{
}

public record PhysicalAddress(
    string Street,
    string City,
    string StateOrProvince,
    string ZipCode);

public struct ValueCoordinate
{
}

**命名 interface 时, 使用 pascal 大小写并在名称前面加上前缀 I. 这可以清楚地向使用者表明这是 interface. **

public interface IWorkerQueue
{
}

**命名类型的 public 成员(例如字段, 属性, 事件, 方法和本地函数)时, 请使用 pascal 大小写. **

public class ExampleEvents
{
    // A public field, these should be used sparingly
    public bool IsValid;

    // An init-only property
    public IWorkerQueue WorkerQueue { get; init; }

    // An event
    public event Action EventProcessing;

    // Method
    public void StartEventProcessing()
    {
        // Local function
        static int CountQueueItems() => WorkerQueue.Count;
        // ...
    }
}

**编写Record时, 对参数使用 pascal 大小写, 因为它们是记录的公共属性. **

public record PhysicalAddress(
    string Street,
    string City,
    string StateOrProvince,
    string ZipCode);

驼峰式大小写

**命名 private 或 internal 字段时, 使用驼峰式大小写(" camelCasing" ), 并且它们以 _ 作为前缀. **

public class DataService
{
    private IWorkerQueue _workerQueue;
}

提示:

在支持语句完成的 IDE 中编辑遵循这些命名约定的 C# 代码时, 键入 _ 将显示所有对象范围的成员. 使用为 private 或 internal 的static 字段时 请使用 s_ 前缀, 对于线程静态, 请使用 t_.

public class DataService
{
    private static IWorkerQueue s_workerQueue;

    [ThreadStatic]
    private static TimeSpan t_timeSpan;
}

编写方法参数时, 请使用驼峰式大小写.

public T SomeMethod<T>(int someNumber, bool isValid)
{
}

其他命名约定

在不包括 using 指令的示例中, 使用命名空间限定. 如果你知道命名空间默认导入项目中, 则不必完全限定来自该命名空间的名称. 如果对于单行来说过长, 则可以在点 (.) 后中断限定名称, 如下面的示例所示.

var currentPerformanceCounterCategory = new System.Diagnostics.
    PerformanceCounterCategory();

项目中的约定

namespace的命名使用

主要考虑是避免冲突 <公司名>.(<模块>|<技术领域划分>)[.<功能>][.<子功能>] 的划分方式, 使用帕斯卡命名 Ccss.HardwareConnector.HardWare

布局约定

  • 使用默认的代码编辑器设置(智能缩进, 4 字符缩进, 制表符保存为空格). 有关详细信息, 请参阅选项, 文本编辑器, C#, 格式设置.

  • 每行只写一条语句.

  • 每行只写一个声明.

  • 如果连续行未自动缩进, 请将它们缩进一个制表符位(四个空格).

  • 在方法定义与属性定义之间添加至少一个空白行.

  • 使用括号突出表达式中的子句, 如下面的代码所示.

    if ((val1 > val2) && (val1 > val3))
    {
        // Take appropriate action.
    }
    

注释约定

  • 项目总代码注释率 > 30%

  • 将注释放在单独的行上, 而非代码行的末尾.

  • 以句点结束注释文本

  • 在注释分隔符 (//) 与注释文本之间插入一个空格, 如下面的示例所示

    //  The following declaration creates a query. It does not run
    // the query.
    
  • 请勿在注释周围创建格式化的星号块

  • 请确保所有的命名控件, 接口, 公共类, 公共成员都有的 XML 注释 注释, 从而提供有关其行为的适当说明.

    在 C# 中, /// XML 注释是一种特殊的注释方式, 通常用于生成文档和为代码提供更丰富的上下文信息

    方法注释: 描述方法的作用, 参数, 返回值以及可能抛出的异常

    /// <summary>
    /// 计算两个整数的和.
    /// </summary>
    /// <param name="a">第一个整数. </param>
    /// <param name="b">第二个整数. </param>
    /// <returns>两个整数的和. </returns>
    public int Add(int a, int b)
    {
        return a + b;
    }
    

    类和接口注释:说明类或接口的目的和使用方法

    /// <summary>
    /// 这个类用于执行基本的数学运算.
    /// </summary>
    public class MathOperations
    {
        // ...
    }
    

    属性注释: 描述属性的用途和行为.

    /// <summary>
    /// 获取或设置用户名.
    /// </summary>
    public string Username { get; set; }
    

    使用 XML 注释是编写可维护, 易于理解和文档化的代码的良好实践

语言准则

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

字符串数据类型

  • 使用字符串内插来连接短字符串, 如下面的代码所示.

    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);
    

隐式类型本地变量

  • 当变量类型明显来自赋值的右侧时, 或者当精度类型不重要时, 请对本地变量进行隐式类型化.

    var var1 = "This is clearly a string.";
    var var2 = 27;
    
  • 当类型并非明显来自赋值的右侧时, 请勿使用 var. 请勿假设类型明显来自方法名称. 如果变量类型为 new 运算符或显式强制转换, 则将其视为明显来自方法名称.

    int var3 = Convert.ToInt32(Console.ReadLine());
    int var4 = ExampleClass.ResultSoFar();
    
  • 请勿依靠变量名称来指定变量的类型. 它可能不正确. 在以下示例中, 变量名称 inputInt 会产生误导性. 它是字符串.

    var inputInt = Console.ReadLine();
    Console.WriteLine(inputInt);
    
  • 避免使用 var 来代替 dynamic. 如果想要进行运行时类型推理, 请使用 dynamic. 有关详细信息, 请参阅使用类型 dynamic(C# 编程指南).

  • 使用隐式类型化来确定 for 循环中循环变量的类型. 下面的示例在 for 语句中使用隐式类型化.

    var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
    var manyPhrases = new StringBuilder();
    for (var i = 0; i < 10000; i++)
    {
        manyPhrases.Append(phrase);
    }
    //Console.WriteLine("tra" + manyPhrases);
    
  • 不要使用隐式类型化来确定 foreach 循环中循环变量的类型. 在大多数情况下, 集合中的元素类型并不明显. 不应仅依靠集合的名称来推断其元素的类型.

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

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

    备注 注意不要意外更改可迭代集合的元素类型. 例如, 在 foreach 语句中从 System.Linq.IQueryable 切换到 System.Collections.IEnumerable 很容易, 这会更改查询的执行.

无符号数据类型

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

数组

当在声明行上初始化数组时, 请使用简洁的语法. 在以下示例中, 请注意不能使用 var 替代 string[].

string[] vowels1 = { "a", "e", "i", "o", "u" };

如果使用显式实例化, 则可以使用 var.

var vowels2 = new string[] { "a", "e", "i", "o", "u" };

委托

使用 Func<> 和 Action<>, 而不是定义委托类型. 在类中, 定义委托方法.

public static Action<string> ActionExample1 = x => Console.WriteLine($"x is: {x}");

public static Action<string, string> ActionExample2 = (x, y) =>
    Console.WriteLine($"x is: {x}, y is {y}");

public static Func<string, int> FuncExample1 = x => Convert.ToInt32(x);

public static Func<int, int, int> FuncExample2 = (x, y) => x + y;

使用 Func<> 或 Action<> 委托定义的签名来调用方法.

ActionExample1("string for x");

ActionExample2("string for x", "string for y");

Console.WriteLine($"The value is {FuncExample1("1")}");

Console.WriteLine($"The sum is {FuncExample2(1, 2)}");

如果创建委托类型的实例, 请使用简洁的语法. 在类中, 定义委托类型和具有匹配签名的方法.

public delegate void Del(string message);

public static void DelMethod(string str)
{
    Console.WriteLine("DelMethod argument: {0}", str);
}

创建委托类型的实例, 然后调用该实例. 以下声明显示了紧缩的语法.

Del exampleDel2 = DelMethod;
exampleDel2("Hey");

以下声明使用了完整的语法.

Del exampleDel1 = new Del(DelMethod);
exampleDel1("Hey");

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 语句代替. 在以下示例中, try-finally 语句仅在 finally 块中调用 Dispose.

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 charset2 = font2.GdiCharSet;
}

使用不需要大括号的新 using 语法 :

using Font font3 = new Font("Arial", 10.0f);
byte charset3 = font3.GdiCharSet;

&& 和 || 运算符

若要通过跳过不必要的比较来避免异常并提高性能, 请在执行比较时使用 &&(而不是 &)和 ||(而不是 |), 如下面的示例所示.

Console.Write("Enter a dividend: ");
int dividend = Convert.ToInt32(Console.ReadLine());

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

if ((divisor != 0) && (dividend / divisor > 0))
{
    Console.WriteLine("Quotient: {0}", dividend / divisor);
}
else
{
    Console.WriteLine("Attempted division by 0 ends up here.");
}

如果除数为 0, 则 if 语句中的第二个子句将导致运行时错误. 但是, 当第一个表达式为 false 时, && 运算符将发生短路. 也就是说, 它并不评估第二个表达式. 如果 divisor 为 0, 则 & 运算符将同时计算这两个表达式, 这会导致运行时错误.

new 运算符

  • 使用对象实例化的简洁形式之一, 如以下声明中所示. 第二个示例显示了从 C# 9 开始可用的语法.

    var instance1 = new ExampleClass();
    
    ExampleClass instance2 = new();
    

    前面的声明等效于下面的声明.

    ExampleClass instance2 = new ExampleClass();
    
  • 使用对象初始值设定项简化对象创建, 如以下示例中所示.

    var instance3 = new ExampleClass { Name = "Desktop", ID = 37414,
        Location = "Redmond", Age = 2.3 };
    

    下面的示例设置了与前面的示例相同的属性, 但未使用初始值设定项.

    var instance4 = new ExampleClass();
    instance4.Name = "Desktop";
    instance4.ID = 37414;
    instance4.Location = "Redmond";
    instance4.Age = 2.3;
    

事件处理

如果你正在定义一个稍后不需要删除的事件处理程序, 请使用 lambda 表达式.

public Form2()
{
    this.Click += (s, e) =>
        {
            MessageBox.Show(
                ((MouseEventArgs)e).Location.ToString());
        };
}

Lambda 表达式缩短了以下传统定义.

public Form1()
{
    this.Click += new EventHandler(Form1_Click);
}

void Form1_Click(object? sender, EventArgs e)
{
    MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}

静态成员

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

LINQ 查询

  • 对查询变量使用有意义的名称. 下面的示例为位于西雅图的客户使用 seattleCustomers.

    var seattleCustomers = from customer in customers
                           where customer.City == "Seattle"
                           select customer.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 customer in customers
        join distributor in distributors on customer.City equals distributor.City
        select new { CustomerName = customer.Name, DistributorID = distributor.ID };
    
  • 在查询变量和范围变量的声明中使用隐式类型化.

    var seattleCustomers = from customer in customers
                           where customer.City == "Seattle"
                           select customer.Name;
    
  • 对齐 from 子句下的查询子句, 如上面的示例所示.

  • 在其他查询子句前面使用 where 子句, 确保后面的查询子句作用于经过缩减和筛选的一组数据.

    var seattleCustomers2 = from customer in customers
                            where customer.City == "Seattle"
                            orderby customer.Name
                            select customer;
    
  • 使用多行 from 子句代替 join 子句来访问内部集合. 例如, Student 对象的集合可能包含测验分数的集合. 当执行以下查询时, 它返回高于 90 的分数, 并返回得到该分数的学生的姓氏.

    var scoreQuery = from student in students
                     from score in student.Scores!
                     where score > 90
                     select new { Last = student.LastName, score };
    

语言版本

公司项目均基于dotnet 6.0, c# 语言版本10, 我们鼓励使用比较新的语言特性

使用Global Using

所有使用global标记的using, 都是全局的, 用于减少复杂的引入.

gloabal using System;
gloabal using System.Collections.Generic;
gloabal using System.Linq;
gloabal using System.Text;
gloabal using System.Threading.Tasks;
gloabal using static System.Console;

原理上, 是会生成一个文件HardwareConnector.GlobalUsings.g.cs

// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

也可以在系统配置中把自己需要的using语句配置进去

<ItemGroup>
  <Using Remove="System.Threading" />
  <Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>

使用Implicit Using

dotnet6.0以后的系统都会在.csproj文件中加入这一配置

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
  </PropertyGroup>
</Project>

不同类型的项目会有不同的默认引入, 比如Console类型的项目会引入以下的内容

System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks

使用namespace后加分号

使用c#的namespace语法

namespace abc;

而不是

namespace abc {

}

使用record优于class

在C#中, recordclass都用于定义对象, 但它们有不同的用途和特性. 使用record可能有以下优势:

不可变性(Immutability)

  • record默认是不可变的, 这意味着一旦创建, 其状态就不能改变. 这有助于编写更安全和可预测的代码.

值语义(Value Semantics)

  • record提供了内置的值相等性支持. 如果你有两个具有相同值的record实例, 它们将被视为相等.
record Person(string Name, int Age);
var person1 = new Person("Alice", 30);
var person2 = new Person("Alice", 30);
Console.WriteLine(person1 == person2);  // 输出 true

简洁性(Conciseness)

  • record允许你使用更简洁的语法来定义对象和其属性. 如上例所示, 只需要一行代码即可定义一个包含NameAge属性的Person记录.

解构(Deconstruction)

  • record类型自动支持解构, 这意味着你可以轻易地将其成员解构到单独的变量中.
var (name, age) = person1;

With 表达式(With Expressions)

  • 对于record对象, 你可以方便地通过with表达式创建一个新的, 值稍有变化的实例.
var olderPerson = person1 with { Age = 31 };

性能

  • 因为record是不可变的, 它们通常更容易进行优化, 尤其是在多线程环境中.

在 C# 6.0 中, 引入了许多新特性, 这些特性旨在简化代码, 增加可读性和提高编写效率. 在编程规范中, 合理利用这些新特性是非常重要的. 以下是一些关键的 C# 6.0 新特性, 以及如何在编程中有效地使用它们:

自动属性初始化器(Auto-Property Initializers)

可以直接在属性声明中初始化自动属性, 而不需要在构造函数中进行初始化. 这简化了属性初始化的代码.

public class Person
{
    public string Name { get; set; } = "Unknown";
    public int Age { get; set; } = 0;
}

只读自动属性(Getter-Only Auto-Properties)

C# 6.0 允你创建只读的自动属性, 这些属性只有 getter 没有 setter. 这对于定义不变的属性非常有用.

public class Person
{
    public string Name { get; }
    public Person(string name)
    {
        Name = name;
    }
}

字符串插值(String Interpolation)

字符串插值是一种新的字符串格式化方法, 它使得插入变量或表达式到字符串中更加直观和易读.

string name = "World";
string greeting = $"Hello, {name}!";

空条件运算符(Null-Conditional Operators)

空条件运算符(?.)允许你以线程安全的方式访问成员和元素, 同时自动检查 null 值, 从而减少冗余的 null 检查.

string length = person?.Name?.Length.ToString();

nameof 表达式

nameof 关键字用于获取变量, 类型或成员的字符串名称. 这在引发异常或进行属性更改通知时非常有用, 因为它避免了硬编码字符串.

throw new ArgumentNullException(nameof(person));

表达式主体成员(Expression-Bodied Members)

对于单行方法和只读属性, 可以使用表达式主体定义简化代码.

public override string ToString() => $"Name: {Name}, Age: {Age}";

使用静态导入(Using Static)

这允许导入一个类的静态成员, 而不是整个类, 可以直接使用静态成员, 而不用指定类名.

using static System.Console;
...
WriteLine("Hello, World!");
Tags: c# .NET style