C# 编码指南
常量(Constants)
- 能声明为
const的变量和字段应始终声明为const。 - 如果无法使用
const,readonly可以作为合适的替代方案。 - 优先使用命名常量而非魔法数字(Magic Number)。
IEnumerable 与 IList 与 IReadOnlyList
- 对于输入,使用尽可能严格的集合类型,例如在方法输入应为不可变时,
使用
IReadOnlyCollection/IReadOnlyList/IEnumerable作为方法的输入。 - 对于输出,如果将返回容器的所有权传递给调用者,优先使用
IList而非IEnumerable。如果不转移所有权,优先使用最严格的选项。
生成器(Generator)与容器(Container)
- 请根据实际情况做出最佳判断,同时注意以下几点:
- 生成器代码通常比直接填充容器的可读性差。
- 如果结果将被延迟处理(例如不需要所有结果时),生成器代码可能性能更好。
- 通过
ToList()直接转换为容器的生成器代码,性能不如直接填充容器。 - 多次调用的生成器代码会比多次迭代容器慢得多。
属性(Property)风格
- 对于单行只读属性,尽可能使用表达式主体属性(Expression Body Property)(
=>)。 - 对于其他所有情况,使用较旧的
{ get; set; }语法。
表达式主体(Expression Body)语法
例如:
int SomeProperty => _someProperty- 谨慎地在 Lambda 表达式和属性中使用表达式主体语法。
- 不要在方法定义中使用。当 C# 7 正式发布后会重新审视此规则, 因为该版本大量使用了这种语法。
- 与方法和其他有作用域的代码块一样,右花括号与包含左花括号的行的第一个字符对齐。 请参阅示例代码。
结构体(Struct)和类(Class):
-
结构体与类有很大不同:
- 结构体始终按值传递和返回。
- 对返回的结构体的成员赋值不会修改原始值——例如
transform.position.x = 10不会将 transform 的 position.x 设置为 10;这里的position是一个按值返回Vector3的属性,因此这只是设置了原始值副本的 x 参数。
-
几乎总是应该使用类。
-
当类型可以被视为其他值类型时,考虑使用结构体——例如,当该类型的实例较小且通常 生命周期较短,或者通常嵌入在其他对象中时。好的例子包括 Vector3、Quaternion 和 Bounds。
-
请注意,此指导可能因团队而异,例如性能问题可能迫使使用结构体。
Lambda 表达式与命名方法
- 如果 Lambda 表达式不简单(例如超过几条语句,不包括声明),或者在多处复用, 则应该使用命名方法。
字段初始化器(Field Initializer)
- 通常鼓励使用字段初始化器。
扩展方法(Extension Method)
- 仅在原始类的源代码不可用时,或者修改源代码不可行时,才使用扩展方法。
- 仅在添加的功能是”核心”通用功能,且适合添加到原始类的源代码中时,
才使用扩展方法。
- 注意——如果我们拥有被扩展类的源代码,而原始类的维护者不想添加该功能, 则最好不要使用扩展方法。
- 仅将扩展方法放入随处可用的核心库中——仅在某些代码中可用的扩展会成为可读性问题。
- 请注意,使用扩展方法总是会使代码变得不那么透明,因此倾向于不添加它们。
ref 和 out
- 对于不同时作为输入的返回值,使用
out。 - 将
out参数放在方法定义中所有其他参数之后。 ref应很少使用,仅在需要修改输入时使用。- 不要将
ref作为传递结构体的优化手段。 - 不要使用
ref将可修改的容器传入方法。ref仅在需要将提供的容器 替换为一个完全不同的容器实例时才需要。
LINQ
- 通常,优先使用单行 LINQ 调用和命令式代码,而非长链式 LINQ。 混合命令式代码和重度链式 LINQ 通常难以阅读。
- 优先使用成员扩展方法而非 SQL 风格的 LINQ 关键字——例如优先使用
myList.Where(x)而非myList where x。 - 对于超过单条语句的内容,避免使用
Container.ForEach(...)。
Array 与 List
- 通常,对于公共变量、属性和返回类型,优先使用
List<>而非数组 (请记住上面关于IList/IEnumerable/IReadOnlyList的指导)。 - 当容器大小可以改变时,优先使用
List<>。 - 当容器大小固定且在构造时已知时,优先使用数组。
- 对于多维数组,优先使用数组。
- 注意:
- 数组和
List<>都表示线性的、连续的容器。 - 类似于 C++ 中数组与
std::vector的关系,数组具有固定容量, 而List<>可以动态添加元素。 - 在某些情况下数组性能更好,但通常
List<>更灵活。
- 数组和
文件夹和文件位置
- 与项目保持一致。
- 尽可能使用扁平结构。
使用元组(Tuple)作为返回类型
- 通常,优先使用命名类类型而非
Tuple<>,特别是在返回复杂类型时。
字符串插值(String Interpolation) 与 String.Format() 与 String.Concat 与 operator+
- 通常,使用最易读的方式,特别是对于日志和断言消息。
- 注意,链式
operator+拼接会更慢,并导致大量内存消耗。 - 如果性能是关注点,对于多次字符串拼接,
StringBuilder会更快。
using
- 通常,不要使用
using为长类型名创建别名。这通常表明Tuple<>需要转换为一个类。- 例如
using RecordList = List<Tuple<int, float>>应该改为一个命名类。
- 例如
- 请注意,
using语句仅在文件范围内有效,因此用途有限。 类型别名对外部用户不可用。
对象初始化器(Object Initializer)语法
例如:
var x = new SomeClass {
Property1 = value1,
Property2 = value2,
};- 对象初始化器语法适用于”普通旧数据”(Plain Old Data)类型。
- 避免对带有构造函数的类或结构体使用此语法。
- 如果需要拆分为多行,缩进一个块级别。
命名空间(Namespace)命名
- 通常,命名空间层级不应超过 2 层。
- 不要强制文件/文件夹布局与命名空间匹配。
- 对于共享库/模块代码,使用命名空间。对于叶子”应用程序”代码,
如
unity_app,不需要命名空间。 - 新的顶级命名空间名称必须全局唯一且易于识别。
结构体的默认值/null 返回
-
优先返回一个”成功”布尔值和一个结构体
out值。 -
当性能不是关注点,且生成的代码可读性显著提高时(例如链式空条件运算符 对比深层嵌套的 if 语句),可以接受使用可空结构体(Nullable Struct)。
-
注意:
- 可空结构体很方便,但强化了 Google 倾向于避免的”null 即失败”的通用模式。
如果有足够的需求,我们将来会研究
StatusOr的等价物。
- 可空结构体很方便,但强化了 Google 倾向于避免的”null 即失败”的通用模式。
如果有足够的需求,我们将来会研究
迭代时从容器中删除元素
C#(与许多其他语言一样)没有提供在迭代时从容器中删除元素的明显机制。 有以下几种选择:
- 如果只需要删除满足某个条件的元素,推荐使用
someList.RemoveAll(somePredicate)。 - 如果在迭代中需要进行其他操作,
RemoveAll可能不够用。一种常见的替代模式是 在循环外创建一个新容器,将需要保留的元素插入新容器,并在迭代结束后 用新容器替换原始容器。
调用委托(Delegate)
- 调用委托时,使用
Invoke()并使用空条件运算符(Null Conditional Operator)—— 例如SomeDelegate?.Invoke()。这在调用点清楚地标记了”正在调用一个委托”。 空值检查简洁且能防止线程竞态条件。
var 关键字
-
鼓励使用
var,前提是它通过避免冗余的、显而易见的或不重要的类型名来提高可读性。 -
鼓励使用的情况:
- 当类型显而易见时——例如
var apple = new Apple();,或var request = Factory.Create<HttpRequest>(); - 对于仅直接传递给其他方法的临时变量——
例如
var item = GetItem(); ProcessItem(item);
- 当类型显而易见时——例如
-
不鼓励使用的情况:
- 当使用基本类型时——例如
var success = true; - 当使用编译器推断的内置数值类型时——例如
var number = 12 * ReturnsFloat(); - 当用户明确知道类型会受益时——例如
var listOfItems = GetList();
- 当使用基本类型时——例如
特性(Attribute)
- 特性应出现在其关联的字段、属性或方法的上一行,与成员之间用换行分隔。
- 多个特性应用换行分隔。这便于添加和删除特性,并确保每个特性易于搜索。
参数命名
源自 Google C++ 风格指南。
当函数参数的含义不明显时,考虑以下补救措施之一:
- 如果参数是字面常量,且相同的常量在多个函数调用中以默认它们相同的方式使用, 请使用命名常量使该约束明确,并确保其成立。
- 考虑修改函数签名,将
bool参数替换为enum参数。这将使参数值具有自描述性。 - 将大型或复杂的嵌套表达式替换为命名变量。
- 考虑使用 命名参数(Named Arguments) 在调用点阐明参数含义。
- 对于具有多个配置选项的函数,考虑定义一个单独的类或结构体来保存所有选项, 并传递该类型的实例。这种方法有几个优点。选项在调用点按名称引用, 这阐明了它们的含义。它还减少了函数参数数量,使函数调用更易于阅读和编写。 作为额外的好处,添加新选项时不需要更改调用点。
请看以下示例:
// 不好的写法 - 这些参数是什么?
DecimalNumber product = CalculateProduct(values, 7, false, null);对比:
// 好的写法
ProductOptions options = new ProductOptions();
options.PrecisionDecimals = 7;
options.UseCache = CacheUsage.DontUseCache;
DecimalNumber product = CalculateProduct(values, options, completionDelegate: null);Last updated on