跳到正文
Qiita · Unity 开发社区· 来源· tsukino_·· 3 小时前AI 评分1

Microsoft 推荐的 C# 编码规范总结

【C#】Microsoft推奨コーディング規約まとめ

正文 · AI 翻译

在进行团队开发时,为了保持代码的一致性,制定编码规范是必要的工作,但对于刚学习的语言来说,可能会在风格上感到困惑。

本文介绍了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