dotnet/runtime C 编码风格规范:20 条规则详解与 .editorconfig 自动执行机制
2026/9/16 21:03:10 网站建设 项目流程

dotnet/runtime C# 编码风格规范:20 条规则详解与 .editorconfig 自动执行机制

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

本篇技术文章以 dotnet/runtime 仓库官方的 C# 编码风格指南 为主体,完整覆盖其 20 条命名、括号、类型引用与语句格式规则,并结合仓库根目录 .editorconfig 配置与 eng/formatting/format.sh 自动化脚本,展示每条规则在仓库中如何通过 Roslyn/EditorConfig 体系被机器可执行地落地。读完本文后,你能够按 runtime 仓库的标准写出合规的 C# 代码,并借助dotnet format与 Git pre-commit 钩子让格式检查变成自动化流程。

总体原则:以 Visual Studio 默认风格为基准

runtime 仓库 C# 编码风格的第一条总则是"use Visual Studio defaults"(遵循 Visual Studio 默认设置)。在此总则之下,官方指南给出了 20 条具体规则。指南同时强调两条执行层面的机制:

  1. 仓库根目录提供了.editorconfig文件(.editorconfig),使支持 EditorConfig 的 IDE 能够按照上述规则自动格式化 C# 代码;
  2. 仓库使用dotnet format工具(dotnet SDK 内置的 dotnet-format 工具)确保代码库风格随时间推移保持一致,该工具会自动修复代码使其符合指南。

指南中第 9 条规则本身也是一条"元规则":如果某个文件的既有风格与这些指南不一致(例如私有成员命名为m_member而非_member),该文件的既有风格优先。这意味着在修改历史代码时应先观察文件局部惯例,而非机械套用全局规则。

括号、缩进与空白控制(规则 1、2、7、8、18)

规则 1:Allman 风格括号

runtime 使用 Allman 风格括号:每个左大括号单独起一行。单行语句块可以省略大括号,但该块必须独占一行并正确缩进,且不能嵌套在已使用大括号的其他语句块中。唯一的例外是:using语句允许嵌套在另一个using语句内——嵌套using从下一行开始、保持相同缩进级别即可,即使内层using包含一个受控代码块。

规则 2:4 空格缩进,禁用 Tab

统一使用 4 个空格缩进,禁止使用 Tab 字符。这在 .editorconfig 中以indent_style = spaceindent_size = 4全局生效(作用于所有文件)。

规则 7 与 8:空行与游离空格

  • 任意位置最多只允许一个空行,例如类型成员之间不允许出现连续两个空行;
  • 避免出现游离的空格(spurious free spaces),例如if (someVar == 0)中括号与内容之间多余的空格。指南建议在使用 Visual Studio 时可开启"显示空白"(View White Space,快捷键 Ctrl+R, Ctrl+W)辅助发现这类问题。

规则 18:单语句 if 的括号约定

这是指南中最细致的一条格式规则,具体分三层:

  • 永远不使用单行形式,例如不允许if (source == null) throw new ArgumentNullException("source");
  • 使用大括号始终被接受,且以下情况必须使用if/else if/.../else复合语句中任何一块使用了大括号,或某个语句体跨越多行;
  • 省略大括号仅限于:与if/else if/.../else复合语句关联的所有块的体都放在单行上时。

该规则在 .editorconfig 中有对应的机器可读配置:csharp_prefer_braces = true:silent表示建议总是使用大括号(静默提示级别),csharp_preserve_single_line_statements = false:none表示格式化时不保留单行语句形态。

命名规范(规则 3、12、13、15、20)

规则 3:字段前缀与 readonly 约定

字段命名是 runtime 风格中最具辨识度的一点:

字段类别前缀大小写示例
实例私有/内部字段_camelCaseprivate int _count;
静态私有/内部字段s_camelCaseprivate static readonly ... s_loadedInDefaultContext
线程静态字段t_camelCaseprivate static ThreadLocal<T> t_cache;
公共字段无(谨慎使用)PascalCase

其他要点:

  • 能用readonly就用readonly
  • 作用于静态字段时,readonly必须写在static之后,即static readonly而非readonly static
  • 公共字段应谨慎使用,使用 PascalCase 且不加前缀。

仓库源码可以印证这一约定,例如 ComActivator.cs 中:

private static readonly Dictionary<string, AssemblyLoadContext> s_assemblyLoadContexts = new Dictionary<string, AssemblyLoadContext>(StringComparer.InvariantCultureIgnoreCase); ... private static readonly HashSet<string> s_loadedInDefaultContext = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);

.editorconfig 用三条命名规则(dotnet_naming_rule)把前缀约定变成了可执行检查:静态字段要求s_前缀 + camelCase(L66-L74),private/internal 实例字段要求_前缀 + camelCase(L76-L83),dotnet_style_readonly_field = true:suggestion(L94)则建议将只读字段声明为readonly

规则 12 与 13:常量与方法命名

  • 所有常量局部变量与字段使用PascalCase。唯一例外是互操作(interop)代码:常量值应与所调用的目标代码的名称和值完全匹配,此时可以保留目标侧的命名;
  • 所有方法名使用 PascalCase,局部函数同样适用

对应地,.editorconfig 定义了constant_fields_should_be_pascal_case命名规则,对const修饰的字段要求 pascal_case 风格。

规则 15:字段声明位置

字段应声明在类型声明的顶部,先于构造方法与其他成员。

规则 20:主构造函数的参数命名

主构造函数(primary constructor)的参数应像普通参数一样命名:使用 camelCase 且不加_前缀。例如:

// 正确 public ObservableLinkedList(IEnumerable<T> items) { ... } // 错误:不应加下划线前缀 public ObservableLinkedList(IEnumerable<T> _items) { ... }

但如果类型本身不够小、参数使用位置不易一眼看清,则应把主构造函数参数赋值给带_前缀的字段,例如private readonly IEnumerable<T> _items = items;

类型引用与 var 的使用(规则 10、11)

规则 10:var 只在类型右侧显式命名时允许

仓库只允许(不强制)使用var的情形是:右侧显式写出了类型,通常因为new或显式类型转换。例如:

// 允许 var stream = new FileStream(...); // 不允许:右侧类型不可见 var stream = OpenStandardInput();

指南补充了 target-typednew()的对应约定:只能用于左侧显式写出类型的变量定义或字段定义语句中,例如FileStream stream = new(...);可以,而stream = new(...);(类型在更早的行声明过)不可以。

.editorconfig 用三条设置表达这一规则的可机器检查近似:

csharp_style_var_for_built_in_types = false:suggestion csharp_style_var_when_type_is_apparent = false:none csharp_style_var_elsewhere = false:suggestion

规则 11:使用语言关键字而非 BCL 类型

类型引用与方法调用都使用语言关键字而非 BCL 类型名:用int, string, float而非Int32, String, Single;例如写int.Parse而非Int32.Parse。对应 .editorconfig:

dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion

成员可见性、密封与 this.(规则 4、5、19)

  • 规则 4:尽量避免this.,除非绝对必要。.editorconfig 对字段、属性、方法、事件四类成员均设置dotnet_style_qualification_for_* = false:suggestion,即出现this.限定时会给出建议级提示;
  • 规则 5总是显式写出可见性修饰符,即使它是默认值——写private string _foo而非string _foo。可见性应是第一个修饰符,例如public abstract而非abstract public。.editorconfig 的csharp_preferred_modifier_order定义了完整的修饰符顺序:public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
  • 规则 19:除非派生确实需要,所有 internal 与 private 类型都应声明为staticsealed。与任何实现细节一样,未来若需要派生,届时可以再调整。

using 指令、nameof 与 Unicode 转义(规则 6、14、16、17)

  • 规则 6(using 指令位置与排序):命名空间导入应写在文件顶部、namespace声明之外,并按字母序排序——唯一的例外是System.*命名空间必须放在所有其他命名空间之上。对应 .editorconfig 的csharp_using_directive_placement = outside_namespace:suggestiondotnet_sort_system_directives_first = true
  • 规则 14(nameof 优先):只要可行且相关,一律使用nameof(...)替代字符串字面量"..."(如异常参数名、事件名、绑定反射字符串等场景);
  • 规则 16(Unicode 转义):源码中需要包含非 ASCII 字符时,使用 Unicode 转义序列(\uXXXX)而非字面字符,因为字面非 ASCII 字符偶尔会被某些工具或编辑器弄乱;
  • 规则 17(goto 标签缩进):使用goto标签时,标签的缩进应比当前缩进少一级。.editorconfig 的csharp_indent_labels = one_less_than_current正是这一规则的机器表达。

示例文件:ObservableLinkedList 示范代码

官方指南给出了两个示例文件片段,展示上述规则的组合运用。以下是文档中的完整示例(带...省略号,为风格示范摘录):

ObservableLinkedList`1.cs:

using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using Microsoft.Win32; namespace System.Collections.Generic { public partial class ObservableLinkedList<T> : INotifyCollectionChanged, INotifyPropertyChanged { private ObservableLinkedListNode<T> _head; private int _count; public ObservableLinkedList(IEnumerable<T> items) { if (items == null) throw new ArgumentNullException(nameof(items)); foreach (T item in items) { AddLast(item); } } public event NotifyCollectionChangedEventHandler CollectionChanged; public int Count { get { return _count; } } public ObservableLinkedListNode AddLast(T value) { var newNode = new LinkedListNode<T>(this, value); InsertNodeBefore(_head, node); } protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { NotifyCollectionChangedEventHandler handler = CollectionChanged; if (handler != null) { handler(this, e); } } private void InsertNodeBefore(LinkedListNode<T> node, LinkedListNode<T> newNode) { ... } ... } }

ObservableLinkedList`1.ObservableLinkedListNode.cs:

using System; namespace System.Collections.Generics { partial class ObservableLinkedList<T> { public class ObservableLinkedListNode { private readonly ObservableLinkedList<T> _parent; private readonly T _value; internal ObservableLinkedListNode(ObservableLinkedList<T> parent, T value) { Debug.Assert(parent != null); _parent = parent; _value = value; } public T Value { get { return _value; } } } ... } }

从这段示范中可以看到指南规则的实际组合:using指令在命名空间之外且System.*在最前(规则 6);字段_head_count_parent_value采用_camelCase前缀并放在类型顶部(规则 3、15);_parent/_value使用readonly(规则 3);if (items == null)单语句体独占一行且因体为单行而省略大括号(规则 1、18);throw new ArgumentNullException(nameof(items))使用nameof(规则 14);构造方法参数items不带下划线(规则 20 的思想);所有方法均为 PascalCase(规则 13)。

规则在 .editorconfig 中的逐条落地

.editorconfig 位于仓库根目录(root = true,标记为最顶层 EditorConfig 文件),是上述指南的"可执行版本"。下表汇总关键规则与配置的对应关系:

指南规则.editorconfig 配置位置
规则 1:Allman 括号csharp_new_line_before_open_brace = all.editorconfig#L26
规则 1:else/catch/finally 另起一行csharp_new_line_before_else/catch/finally = true.editorconfig#L27-L29
规则 1/18:建议总是使用大括号csharp_prefer_braces = true:silent.editorconfig#L88
规则 2:4 空格缩进indent_style = space/indent_size = 4.editorconfig#L11-L12
规则 3:readonly 建议dotnet_style_readonly_field = true:suggestion.editorconfig#L94
规则 3:s_静态字段前缀static_fields_should_have_prefix命名规则.editorconfig#L66-L74
规则 3:_camelCase私有/内部字段camel_case_for_private_internal_fields命名规则.editorconfig#L76-L83
规则 4:避免this.dotnet_style_qualification_for_* = false:suggestion.editorconfig#L45-L49
规则 5:修饰符顺序csharp_preferred_modifier_order.editorconfig#L43
规则 6:using 在命名空间外、System 优先csharp_using_directive_placement = outside_namespace/dotnet_sort_system_directives_first = true.editorconfig#L86-L87
规则 10:var 使用限制csharp_style_var_*三条设置.editorconfig#L52-L54
规则 11:语言关键字优先dotnet_style_predefined_type_for_*.editorconfig#L55-L56
规则 12:常量 PascalCaseconstant_fields_should_be_pascal_case命名规则.editorconfig#L58-L64
规则 17:标签缩进少一级csharp_indent_labels = one_less_than_current.editorconfig#L40

需要注意 severity 语义::suggestion表示在 IDE 中以"建议"呈现,:silent表示保留但不主动提示,:none表示关闭该诊断。例如规则 10 中csharp_style_var_when_type_is_apparent = false:none意味着"允许类型明显处使用 var,但不提示",与指南"只允许、不强制"的措辞一致。

除 C# 外,.editorconfig 还规定了仓库其他文件的格式基线:

  • C/C++*.cpp, *.h, *.in):curly_bracket_next_line = trueindent_brace_style = Allman(L176-L179),与 C# 的 Allman 风格保持一致;
  • 生成代码_AssemblyInfo.cs.notsupported.csAsmOffsets.cs)标记generated_code = true(L20-L21);
  • 项目文件.csproj.slnx.resx等 XML 类文件)使用 2 空格缩进(L181-L205);JSON/YAML 同样 2 空格(L204-L205);
  • 行尾约定.sh脚本使用 LF,.cmd/.bat使用 CRLF(L207-L211);
  • 许可头模板file_header_template指定了 MIT 许可头文本(L165-L166),dotnet format据此统一文件头。

自动执行:dotnet format 与 Git pre-commit 钩子

指南声明风格后,仓库还建立了让风格"自动保持"的工具链,具体做法详见 Code Formatting Tools 指南。

dotnet format 工具

C#/VB 代码依赖 Roslyn 对 EditorConfig 的内置支持,在多数 IDE 中无需额外工具即可自动格式化;同时也可以用 dotnet SDK 的dotnet format命令做批处理格式化。dotnet format会读取.editorconfig并自动修复代码,使其长期符合上述指南。

pre-commit 钩子脚本 format.sh

eng/formatting/format.sh 是仓库提供的提交前格式化脚本,其完整逻辑为:

#!/bin/sh LC_ALL=C # Select files to format NATIVE_FILES=$(git diff --cached --name-only --diff-filter=ACM "*.h" "*.hpp" "*.c" "*.cpp" "*.inl" | sed 's| |\\ |g') MANAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM "*.cs" "*.vb" | sed 's| |\\ |g') exec 1>&2 if [ -n "$NATIVE_FILES" ]; then # Format all selected files echo "$NATIVE_FILES" | xargs "./artifacts/tools/clang-format" -style=file -i # Add back the modified files to staging echo "$NATIVE_FILES" | xargs git add fi if [ -n "$MANAGED_FILES" ]; then # Format all selected files echo "$MANAGED_FILES" | dotnet format whitespace --include - --folder # Add back the modified files to staging echo "$MANAGED_FILES" | xargs git add fi exit 0

从脚本实现看,它只对本次暂存区中新增/修改/改名(ACM)的文件做格式化,分两条路径:

  1. 原生代码.h/.hpp/.c/.cpp/.inl):调用./artifacts/tools/clang-format -style=file -i-style=file表示使用仓库内的.clang-format配置文件,因此工具版本必须与仓库约定一致——这些工具需先运行 eng/formatting/download-tools.sh(Windows 上为download-tools.ps1)下载到artifacts/tools目录;
  2. 托管代码.cs/.vb):调用dotnet format whitespace --include - --folder,即只对空白类格式做规范化(不改动分析器级样式),并把结果重新git add回暂存区。

启用方式

按 Code Formatting Tools 指南 的说明,在本地克隆中创建.git/hooks/pre-commit文件,内容为:

#!/bin/sh ./eng/formatting/format.sh

然后确认文件可执行(chmod +x .git/hooks/pre-commit);若git config core.hooksPath指向了别处,需要用git config core.hooksPath .git/hooks指回。由于 Git for Windows 附带 Git Bash,该脚本在 Windows 与非 Windows 平台均可工作。

在 IDE 层面,Visual Studio Code 可通过.vscode/settings.json中的"editor.formatOnSave": true开启保存即格式化;Visual Studio 则可在Tools > Options下配置"语句结束/块结束时格式化",配合上述 Git 钩子即可获得完整的自动化体验。

其他语言与脚本的风格约定

指南最后一节说明:对 C# 之外的语言,当前最好的建议就是一致性(consistency)

  • 编辑现有文件时,新代码与改动应与该文件内的既有风格保持一致
  • 新建文件时,应符合所在组件的风格
  • 若是一个全新组件,任何被广泛接受的合理风格都可以;
  • 脚本文件可参考 Microsoft 官方脚本博客中的 PowerShell 技巧与最佳实践条目。

小结

runtime 仓库的 C# 编码风格可以概括为三层:20 条显式规则定义命名、括号、类型引用与语句格式(遵循 Visual Studio 默认风格为总纲);仓库根目录的 .editorconfig把可机器表达的规则转化为命名规则与格式开关,让 IDE 与 Roslyn 格式化引擎自动执行;dotnet format+ format.sh pre-commit 钩子则保证即便开发者不手动格式化,提交前的代码依然保持仓库统一风格。三者配合,使得风格约束从"文档约定"变成了"流水线事实"。

参考文档与文件:

  • 编码风格指南原文:docs/coding-guidelines/coding-style.md
  • 格式化工具指南:docs/coding-guidelines/code-formatting-tools.md
  • 全局格式配置:.editorconfig
  • 提交前格式化脚本:eng/formatting/format.sh、eng/formatting/download-tools.sh
  • 风格示例源码:src/coreclr/System.Private.CoreLib/src/Internal/Runtime/InteropServices/ComActivator.cs

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询