c#代码规范
约定的目的
为代码创建一致的外观, 以确保读取器专注于内容而非布局. 使得读取器可以通过基于之前的经验进行的假设更快地理解代码. 便于复制, 更改和维护代码. 展示 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#中, record和class都用于定义对象, 但它们有不同的用途和特性. 使用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允许你使用更简洁的语法来定义对象和其属性. 如上例所示, 只需要一行代码即可定义一个包含Name和Age属性的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!");