Skip to Content
C#C# 编码指南

C# 编码指南

常量(Constants)

  • 能声明为 const 的变量和字段应始终声明为 const
  • 如果无法使用 constreadonly 可以作为合适的替代方案。
  • 优先使用命名常量而非魔法数字(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.Concatoperator+

  • 通常,使用最易读的方式,特别是对于日志和断言消息。
  • 注意,链式 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 的等价物。

迭代时从容器中删除元素

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