【Google C#编码规范】

Google C# 编码规范


此编码规范适用于 Google 内部开发的 C# 代码,并且是 Google C# 代码的默认风格。它在风格上与 Google 的其他语言规范(如 Google C++ 风格和 Google Java 风格)保持一致。

格式化指南


命名规则

命名规则遵循微软的 C# 命名指南。在微软的命名指南未明确规定的情况下(例如私有和局部变量),规则取自 CoreFX C# 编码指南。

规则概要:

代码
  • 类、方法、枚举、公共字段、公共属性、命名空间的名称: PascalCase。
  • 局部变量、参数的名称:
    camelCase。
  • 私有、受保护、内部和受保护内部字段和属性的名称:
    _camelCase。
  • 命名约定不受 const、static、readonly 等修饰符的影响。
  • 对于大小写,“word” 是指没有内部空格的任何写法,包括首字母缩写词。例如,使用 MyRpc 而不是 MyRPC。
  • 接口的名称以 I 开头,例如 IInterface。
文件
  • 文件名和目录名使用 PascalCase.
    例如 MyFile.cs。
  • 在可能的情况下,文件名应与文件中的主类的名称相同.
    例如 MyClass.cs。
  • 总体上,优先选择一个核心类一个文件。
组织
  • 修饰符按照以下顺序出现:
    public, protected, internal, private, new, abstract, virtual, override, sealed, static, readonly, extern, unsafe, volatile, async。
  • 命名空间的 using 声明放在顶部,在任何命名空间之前。using 的导入顺序是按字母顺序排列,除了始终放在第一位的 System 导入。
  • 类成员排序:
    • 按照以下顺序对类成员进行分组:
      • 嵌套类、枚举、委托和事件。
      • 静态、const 和 readonly 字段。
      • 字段和属性。
      • 构造函数和析构函数。
      • 方法。
    • 在每个组内,元素应按以下顺序排列:
      • Public。
      • Internal。
      • Protected internal。
      • Protected。
      • Private。
    • 在可能的情况下,将接口实现组合在一起。
空格规则

从 Google Java 风格发展而来。

  • 每行最多一个语句。
  • 每个语句最多一个赋值。
  • 缩进为 2 个空格,不使用制表符。
  • 列限制:100。
  • 在打开括号之前不换行。
  • 在关闭括号和 else 之间不换行。
  • 即使是可选的,也要使用括号。
  • if/for/while 等后面以及逗号后要有空格。
  • 在开放括号之前和关闭括号之后不要有空格。
  • 在一元运算符和其操作数之间不要有空格。其他运算符的运算符和每个操作数之间要有一个空格。
  • 从 Google C++ 风格指南发展而来的换行规则,为了与 Microsoft 的 C# 格式化工具兼容进行了轻微修改:
    • 一般情况下,换行缩进为 4 个空格。
    • 带有括号的换行(例如列表初始化、lambda、对象初始化等)不计为换行。
    • 对于函数的定义和调用,如果参数不能都在一行上,应将它们分成多行,每个后续行与第一个参数对齐。如果没有足够的空间,可以将参数放在后续行上,并缩进四个空格。
    • 以下代码示例说明了这一点。
示例
using System;                                       // `using` 放在顶部,超出命名空间之外。

namespace MyNamespace {                             // 命名空间使用 PascalCase。
                                                    // 命名空间后缩进。

  public interface IMyInterface {                   // 接口以 'I' 开头
    public int Calculate(float value, float exp);   // 方法使用 PascalCase
                                                    // ...并在逗号后有空格。
  }

  public enum MyEnum {                              // 枚举使用 PascalCase。
    Yes,                                            // 枚举值使用 PascalCase。
    No,
  }

  public class MyClass {                            // 类使用 PascalCase。
    public int Foo = 0;                             // 公共成员变量使用
                                                    // PascalCase。
    public bool NoCounting = false;                 // 鼓励使用字段初始化程序。
    private class Results {
      public int NumNegativeResults = 0;
      public int NumPositiveResults = 0;
    }
    private Results _results;                       // 私有成员变量使用
                                                    // _camelCase。
    public static int NumTimesCalled = 0;
    private const int _bar = 100;                   // const 不影响命名
                                                    // 约定。
    private int[] _someTable = {                    // 容器初始化程序使用 2
      2, 3, 4,                                      // 个空格缩进。
    }

    public MyClass() {
      _results = new Results {
        NumNegativeResults = 1,                     // 对象初始化程序使用 2 个空格
        NumPositiveResults = 1,                     // 缩进。
      };
    }

    public int CalculateValue(int mulNumber) {      // 开括号之前不换行。
      var resultValue = Foo * mulNumber;            // 局部变量使用 camelCase。
      NumTimesCalled++;
      Foo += _bar;

      if (!NoCounting) {                            // 一元运算符后没有空格,而
                                                    // 'if' 后有空格。
        if (resultValue < 0) {                      // 即使是可选的,也要使用括号,以及
                                                    // 比较运算符周围的空格。
          _results.NumNegativeResults++;
        } else if (resultValue > 0) {               // 在括号和 else 之间没有换行。
          _results.NumPositiveResults++;
        }
      }
      return resultValue;
    } 
    public void ExpressionBodies() {
      // 对于简单的 lambda 表达式,如果可能的话,一行内完成,无需括号或大括号。
      Func<int, int> increment = x => x + 1;

      // 闭合大括号与包含开括号的行上的第一个字符对齐。
      Func<int, int, long> difference1 = (x, y) => {
        long diff = (long)x - y;
        return diff >= 0 ? diff : -diff;
      };

      // 如果在连续换行之后定义,缩进整个主体。
      Func<int, int, long> difference2 =
          (x, y) => {
            long diff = (long)x - y;
            return diff >= 0 ? diff : -diff;
          };

      // 内联 lambda 参数也遵循这些规则。如果包含 lambda,则最好在参数组前面加一个换行。
      CallWithDelegate(
          (x, y) => {
            long diff = (long)x - y;
            return diff >= 0 ? diff : -diff;
          });
    }

    void DoNothing() {}                             // 空块可能是简洁的。

    // 如果可能的话,通过将换行与第一个参数对齐来包装参数。
    void AVeryLongFunctionNameThatCausesLineWrappingProblems(int longArgumentName,
                                                             int p1, int p2) {}

    // 如果将参数行与第一个参数对齐不合适或难以阅读,则在新行上使用 4 个空格缩进包装所有参数。
    void AnotherLongFunctionNameThatCausesLineWrappingProblems(
        int longArgumentName, int longArgumentName2, int longArgumentName3) {}

    void CallingLongFunctionName() {
      int veryLongArgumentName = 1234;
      int shortArg = 1;
      // 如果可能的话,通过将换行与第一个参数对齐来包装参数。
      AnotherLongFunctionNameThatCausesLineWrappingProblems(shortArg, shortArg,
                                                            veryLongArgumentName);
      // 如果将参数行与第一个参数对齐不合适或难以阅读,则在新行上使用 4 个空格缩进包装所有参数。
      AnotherLongFunctionNameThatCausesLineWrappingProblems(
          veryLongArgumentName, veryLongArgumentName, veryLongArgumentName);
    }
  }


常量
  • 应始终将可以声明为 const 的变量和字段声明为 const。
  • 如果不可使用 const,则 readonly 可以是一个合适的替代方案。
  • 更倾向于使用命名常量而不是魔法数字。
IEnumerable vs IList vs IReadOnlyList
  • 对于输入,尽量使用最具限制性的集合类型,例如 IReadOnlyCollection / IReadOnlyList / IEnumerable,当输入应该是不可变的时。
  • 对于输出,如果将返回的容器的所有权传递给所有者,请优先选择 IList 而不是 IEnumerable。如果不转移所有权,请选择最具限制性的选项。
生成器 vs 容器
  • 根据最佳判断进行选择,注意:
    • 生成器代码通常不如填充容器易读。
    • 如果结果将被懒处理,例如不需要全部结果时,生成器代码可能性能更好。
    • 将生成器代码通过 ToList() 直接转换为容器将比直接填充容器性能差。
    • 多次调用生成器代码将比多次迭代容器慢得多。
属性风格
  • 对于单行只读属性,在可能的情况下,更倾向于使用表达体属性 (=>)。
  • 对于其他情况,请使用旧的 { get; set; } 语法。
表达体语法

例如:

int SomeProperty => _someProperty
  • 明智地在 lambda 和属性中使用表达体语法。
  • 不要在方法定义上使用。这将在 C# 7 上线后进行审查,该版本大量使用此语法。
  • 与方法和其他代码块一样,将闭合与包含开括号的行上的第一个字符对齐。参见示例代码以获取示例。
结构体和类
  • 结构体与类非常不同:

    • 结构体始终通过值传递和返回。
    • 对返回的结构体的成员赋值不会修改原始结构体
      例如,transform.position.x = 10 不会将 transform 的 position.x 设置为 10;position 这里是通过值返回 Vector3 的属性,因此这只是设置原始副本的 x 参数。
  • 几乎总是使用类。

  • 考虑使用结构体,当类型可以像其他值类型一样对待时,例如,如果该类型的实例小而通常是短暂的,或者通常嵌入在其他对象中。好的例子包括 Vector3、Quaternion 和 Bounds。

  • 请注意,此指导可能会因团队而异,例如,性能问题可能会强制使用结构体。

Lambda vs 命名方法
  • 如果 lambda 不是微不足道的(例如,超过几个语句,不包括声明),或者在多个地方重复使用,则它可能应该是一个命名方法。
字段初始化器
  • 通常鼓励使用字段初始化器。
扩展方法
  • 仅当原始类的源不可用时,或者更改源不可行时,才使用扩展方法。
  • 仅当添加的功能是一个适合添加到原始类源的“核心”一般功能时,才使用扩展方法。
    • 注意: 如果我们有被扩展的类的源代码,并且原始类的维护者不希望添加该功能,则最好不要使用扩展方法。
  • 只将扩展方法放入到可在任何地方使用的核心库中 - 仅在某些代码中可用的扩展会成为可读性问题。
  • 请注意,使用扩展方法始终会使代码变得模糊,因此在不添加它们的一方面错误。
ref 和 out
  • 对于不是输入的返回值,请使用 out。
  • 将 out 参数放在方法定义的所有其他参数之后。
  • ref 应该很少使用,只有在必须改变输入时才使用。
  • 不要将 ref 用作传递结构体的优化。只有在提供的容器需要被替换为完全
  • 不同的容器实例时才需要使用 ref。
LINQ
  • 一般而言,优先选择单行 LINQ 调用和命令式代码,而不是长链式的 LINQ。混合命令式代码和大量链式 LINQ 常常难以阅读。
  • 优先选择成员扩展方法而不是 SQL 风格的 LINQ 关键字 - 例如,更喜欢 myList.Where(x) 而不是 myList where x。
  • 避免对于超过单个语句的任何内容使用 Container.ForEach(…)。
数组 vs 列表
  • 一般而言,对于公共变量、属性和返回类型,更倾向于使用 List<> 而不是数组(请记住上文关于 IList / IEnumerable / IReadOnlyList 的指导)。
  • 当容器的大小可以更改时,请使用 List<>。
  • 当容器的大小在构造时是固定且已知的时候,请使用数组。
  • 对于多维数组,更倾向于使用数组。
  • 注意:
    • 数组和 List<> 都表示线性、连续的容器。
    • 类似于 C++ 的数组 vs std::vector,数组具有固定的容量,而 List<> 可以添加元素。
    • 在某些情况下,数组可能更高效,但总体而言 List<> 更灵活。
文件夹和文件位置
  • 与项目保持一致。
  • 在可能的情况下,更倾向于使用扁平结构。
元组作为返回类型的使用
  • 一般而言,优先使用命名的类类型而不是 Tuple<>,特别是在返回复杂类型时。
字符串插值 vs String.Format() vs String.Concat vs operator+
  • 一般而言,使用最易于阅读的方法,特别是对于日志记录和断言消息。
  • 请注意,链式 operator+ 连接会更慢,并导致显著的内存波动。
  • 如果性能是一个问题,对于多次字符串连接,使用 StringBuilder 会更快。
using
  • 通常情况下,不要使用 using 对长类型名进行别名。这通常是 Tuple<> 需要转换为类的迹象。
    • 例如,using RecordList = List<Tuple<int, float>> 应该更可能成为一个命名类。
  • 请注意,using 语句仅限于文件范围,因此使用类型别名对外部用户不可用。
对象初始化语法

例如:

var x = new SomeClass {
  Property1 = value1,
  Property2 = value2,
};
  • 对象初始化语法适用于“普通数据”类型。
  • 避免在具有构造函数的类或结构中使用此语法。
  • 如果拆分成多行,请将缩进设置为一个块级别。
命名空间命名
  • 一般而言,命名空间不应超过 2 层深。
  • 不要强制文件/文件夹布局与命名空间匹配。
  • 对于共享的库/模块代码,请使用命名空间。对于叶子“应用程序”代码,例如 unity_app,命名空间是不必要的。
  • 新的顶级命名空间名称必须是全局唯一且可识别的。
结构体的默认值/对结构体的 null 返回
  • 更倾向于返回一个“成功”的布尔值和一个结构体的输出值。
  • 在性能不是问题,且结果代码更易读的情况下(例如,链式空条件运算符 vs 深层嵌套的 if 语句),可接受可空结构体。
  • 注意:
    • 可空结构体很方便,但强化了通常要避免的“null 表示失败”的模式。如果有足够的需求,我们将在将来调查 StatusOr 等等。
在迭代时从容器中删除元素

C#(与许多其他语言一样)没有提供明显的机制来在迭代时从容器中删除项。有几个选择:

  • 如果只需删除满足某些条件的项,则推荐使用 someList.RemoveAll(somePredicate)。
  • 如果需要在迭代中执行其他工作,则 RemoveAll 可能不足够。一个常见的替代模式是在循环外部创建一个新容器,将要保留在新容器中的项插入其中,并在迭代结束时将原始容器与新容器交换。
调用委托
  • 调用委托时,使用 Invoke() 并使用空条件运算符 - 例如,SomeDelegate?.Invoke()。这清楚地标记了调用点为“正在调用的委托”。空检查简洁且对线程竞争条件具有稳健性。
var 关键字
  • 如果使用 var 有助于通过避免嘈杂、显而易见或不重要的类型名称提高可读性,则鼓励使用 var。

  • 鼓励使用:

    • 当类型明显时 - 例如,var apple = new Apple();,或者 var request = Factory.Create();
    • 对于仅直接传递给其他方法的短暂变量 - 例如,var item = GetItem(); ProcessItem(item);
  • 不鼓励使用:

    • 当与基本类型一起工作时 - 例如,var success = true;
    • 当与编译器解析的内置数值类型一起工作时 - 例如,var number = 12 * ReturnsFloat();
    • 当用户明显受益于知道类型时 - 例如,var listOfItems = GetList();
属性
  • 属性应出现在它们相关联的字段、属性或方法的上方一行,与成员之间用换行符隔开。
  • 多个属性之间应用换行符分隔。这样可以更轻松地添加和删除属性,并确保每个属性易于搜索。
参数命名

源自 Google C++ 风格指南。
当函数参数的含义不明显时,请考虑以下解决方案之一:

  • 如果参数是文字常量,并且相同的常量在多个函数调用中以一种默认情况下认为它们相同的方式使用,则使用命名常量使该约束条件明确,并确保它成立。
  • 考虑更改函数签名以使用枚举参数替换布尔参数。这将使参数值自我描述。
  • 用命名变量替换大型或复杂的嵌套表达式。
  • 考虑使用命名参数来清晰地表示调用点上的参数含义。
  • 对于具有多个配置选项的函数,请考虑定义一个单一的类或结构来保存所有选项,并传递该实例。这种方法有几个优点。选项在调用点通过名称引用,从而澄清它们的含义。它还减少了函数参数数量,使函数调用更易于阅读和编写。作为额外的好处,当添加另一个选项时,调用点无需更改。

考虑以下示例:

  • 不推荐
// 不好 - 这些参数是什么意思?
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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值