Microsoft 推荐的 C# 编码规范总结
【C#】Microsoft推奨コーディング規約まとめ
在进行团队开发时,为了保持代码的一致性,制定编码规范是必要的工作,但对于刚学习的语言来说,可能会在风格上感到困惑。
本文介绍了C#的开发方Microsoft推荐的编码风格。
前提知識
- 具备编程基础知识的人
关于此编码规则
本页面的指南是微软在撰写自身文档和示例代码时所遵循的规则。它并非C#语言规范中规定的标准。
此外,页面中也明确指出这并非绝对列表,而是常见规则的示例。
因此,可以根据项目需求自行取舍。
请根据项目制定一貫性和可読性等规范。
语言指南
首先总结整体方针。
- 积极使用新的语言功能和
C#版本 - 避免旧的写法
- 只捕获能够处理的异常(不要直接接收
Exception) - 使用具体的异常类型,并赋予有意义的消息
- 集合操作请使用
LINQ - 涉及I/O的处理请使用
async/await - 注意死锁,必要时使用
ConfigureAwait - 类型请使用语言关键字书写,如
String而非string、Int32而非int - 使用
int而非无符号类型(便于与其他库连接) - 编写能让读者理解的简洁代码
简而言之,就是在采用新写法的同时,保持简洁、对读者友好的叙述。
文字列
拼接短字符串时,推荐使用字符串插值而非+。
Player.cs
string status = $"{player.Name} HP: {player.Hp}/{player.MaxHp}";
要点在于不使用位置指定(即不使用{0}的类型),而是直接嵌入表达式。
在循环中拼接字符串时,应使用StringBuilder。因为字符串是不可变的,每次使用+=都会创建一个新的字符串实例。
LogBuilder.cs
var log = new StringBuilder();
for (var i = 0; i < 10000; i++)
{
log.Append("Enemy defeated. ");
}
对于多行字符串或包含大量转义字符的字符串,请使用原始字符串字面量(C# 11)。由于无需对"和\进行转义,直接在代码中书写JSON时会非常方便。
EnemyJson.cs
var enemyJson = """
{
"name": "Slime",
"hp": 30
}
""";
构造函数与初始化
record类型的主构造函数参数应采用PascalCase命名法。
EnemyData.cs
public record EnemyData(string Name, int Hp);
class和struct的主构造函数参数使用camelCase命名。
这里虽然有点复杂,但记住record的情况下参数会直接成为属性PascalCase,而class的情况下只是普通参数camelCase,这样就不容易出错了。
另外,如果想强制初始化属性,请使用required属性而不是构造函数。
Enemy.cs
public class Enemy
{
public required string Name { get; init; }
public int Hp { get; init; } = 10;
}
如果不指定Name,会导致编译错误。能够在编译时防止忘记初始化,这一点非常令人愉快。
数组与集合
集合的初始化应使用集合表达式(C# 12)。
Inventory.cs
string[] itemNames = ["Potion", "Ether", "Antidote"];
List<int> scores = [100, 250, 400];
数组和List采用相同的写法,这一点很好。
委托
建议不使用自定义委托类型,而是使用Func<>和Action<>。
DamageCalc.cs
Func<int, int, int> calcDamage = (atk, def) => Math.Max(1, atk - def);
Action<string> log = message => Console.WriteLine($"[LOG] {message}");
log($"Damage: {calcDamage(30, 12)}");
如果存在返回值,就记住Func<>;如果不存在,就记住Action<>。
如果无论如何都要创建自定义委托类型,则在实例化时使用简写语法(不直接使用new Del(Method),而是将Method直接赋值)。
异常处理(try-catch 与 using)
异常处理使用try-catch来编写。关键点在于仅捕获能够处理的异常,并且以具体类型进行接收。
SaveLoader.cs
static string LoadSaveData(string path)
{
try
{
return File.ReadAllText(path);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"セーブデータが見つかりません: {ex.FileName}");
throw;
}
}
catch (Exception)如果全盘接收,反而会掩盖原本应该发现的bug。
因此,如果只是记录日志后重新抛出异常,应写为throw;而不是throw ex;,因为这样可以保留堆栈跟踪信息。
接下来是using。如果只是在Dispose中调用了finally,则应替换为using而非try-finally。
此外,使用无需花括号的新式using声明。
SaveLoader.cs
using var reader = new StreamReader(path);
var firstLine = reader.ReadLine();
由于在作用域结束时会自动Dispose,嵌套减少,代码更加简洁。
关于 && 和 ||
&&在左侧为false时不会评估右侧(短路求值)&会始终评估两侧
在实际开发中,条件判断通常使用&&和||。
Battle.cs
if (count != 0 && total / count > 10)
{
Console.WriteLine("平均ダメージが高いです");
}
当count等于0时,如果满足&&,则不会执行右侧的除法运算。若改为&,则会因0除零而引发异常。
new 运算符
当类型明确时,new可以简洁地书写。
Spawner.cs
var slime = new Enemy { Name = "Slime" };
Enemy goblin = new() { Name = "Goblin" };
只需将左侧设为var或右侧设为new()即可,二者选其一。
但是,如果变量的类型是接口或基类,则不能使用new()。
此外,属性设置会汇总到对象初始值设定项中。与在new之后逐行赋值的方式相比,这种写法更短,并且可以被阅读为“初始化的一个整体”。
事件处理
对于不需要稍后取消订阅的事件处理程序,使用lambda表达式编写。
TitleScreen.cs
startButton.Click += (sender, e) => StartGame();
相反,如果想在-=中取消订阅,由于无法通过lambda表达式进行移除,因此必须使用命名方法。
在游戏开发中,需要注意在场景切换时容易忘记取消订阅。
静态成员
static成员必须使用定义的类名进行调用。
如果使用派生类名调用,虽然编译可以通过,但读者会误以为定义存在于派生类一侧。
此外,如果之后在派生类中添加了同名的static成员,调用目标可能会意外改变,存在风险。
LINQ 查询
使用查询语法时的规则。
- 为查询变量赋予易于理解的名字
- 匿名类型的属性名应设为
PascalCase - 如果名称冲突导致歧义,请像
CustomerName那样重新命名 - 查询变量或范围变量请使用
var -
where应优先于其他子句编写 - 展开内部集合时,请多次使用
from而非join
AliveEnemies.cs
var aliveEnemies = from enemy in enemies
where enemy.Hp > 0
orderby enemy.Name
select enemy;
将where放在前面,是为了先进行筛选再进行后续处理。这样比先排序再筛选效率更高。
这是将两个集合合并时名称发生冲突的示例。
DropTable.cs
var drops = from enemy in enemies
join item in items on enemy.DropId equals item.Id
select new { EnemyName = enemy.Name, ItemName = item.Name };
如果保持Name和Name不变,就会分不清哪个是哪个。
此外,LINQ中关于var的规则优先于接下来要介绍的一般性var规则。这是因为会出现匿名类型或嵌套泛型类型。
var(隐式类型的局部变量)
varは便利ですが、使いどころが大事です。
使うのは、右辺を見れば型がわかるとき。
var message = "ゲームを開始します";
var enemy = new Enemy { Name = "Slime" };
var maxLevel = 99;
new、明示的なキャスト、リテラルの代入なら、型は明らかと見なされます。
使わないのは、右辺から型が読み取れないときです。
int score = CalculateScore();
また、変数名を型の手がかりにしないことも大事です。inputIntと名前をつけても、実際はstringということが起こり得ます。型は型で示し、変数名には意味(iterationsなど)を持たせるのが吉です。
加えて、varの代わりにdynamicを使うのは避けます。dynamicは本当に実行時に型を決めたいときだけ使いましょう。
关于循环,说明如下。
for的循环变量:使用var即可(for (var i = 0; ...))-
foreach的循环变量:var不使用(因为难以看出元素的类型)
Loop.cs
foreach (Enemy enemy in enemies)
{
enemy.Update();
}
顺便一提,Microsoft的文档在浏览器中MouseOver也看不到类型,因此这里重视仅靠阅读就能看懂类型。如果是在Visual Studio中一边确认类型一边编写,那么稍微放宽一些也可以。
文件作用域命名空间
在大多数文件中,由于只有一个命名空间,因此使用文件作用域命名空间(C# 10)。
Player.cs
namespace MyGame.Characters;
public class Player
{
// ...
}
大括号减少一层,缩进变浅。
using 应写在命名空间外部
using指令应放在命名空间声明的外部。
如果将其放在命名空间内部,名称解析将变为相对于当前所在命名空间,并且由于后续添加的命名空间,可能会导致未修改的代码突然产生编译错误。
在页面示例中,Azure的using原本写在命名空间内部,但由于依赖项引入了名为CoolStuff.Azure的命名空间,导致Azure优先解析到了该命名空间,从而引发了错误。
在游戏开发中,添加库或插件后,也常会出现因名称冲突而导致程序无法运行的情况。因此,将其写在外层更为稳妥。
样式指南
外观规范。
- 缩进使用4个空格(不使用制表符)
- 大括号采用Allman风格(开括号和闭括号各自独占一行)
- 一行只写一句,声明也只写一个
- 在方法和属性之间加入空行
- 条件表达式用括号明确分隔
Combat.cs
if ((attack > defense) && (target.IsAlive))
{
ApplyDamage(target);
}
此外,页面还有“每行不超过65个字符”的规则,但这是为了在移动端显示文档而考虑的。普通项目中没有必要遵守。
评论
- 简短说明应作为
//的单行注释。 - 避免在长说明中使用
/* */。 - 注释不应写在行的末尾,而应另起一行。
-
在
//之后添加一个空格。 - 对方法、类或公共成员使用
XML注释。
Microsoft的示例中还有一条规则:注释应以大写字母开头并以句号结尾。
不过这是针对英语的情况,如果用日语书写,只要在项目内保持统一即可。
総括
本文以Microsoft Learn的**.NET 编码规则 - C#**为基础,结合游戏开发的示例进行了整理。
- 规则并非绝对不可违背,可以酌情取舍
- 通过工具(
.editorconfig和コード分析)进行自动化 -
var适用于仅当右侧能够推断出类型时 - 例外情况是,仅接收能够处理的特定类型
using指令应写在命名空间的外部- 积极使用新功能(原始字符串、集合表达式、
required等)
在个人开发中,很容易在没有审查的情况下随心所欲地编写代码,但未来的自己就是他人。
制定规则并交给.editorconfig处理既轻松又推荐。
来源:Qiita · Unity 开发社区 · qiita.com