0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Posted at

チーム制作を行うとき、コードの一貫性を保つためにコーディング規約を決定することは必要な作業ですが、勉強したての言語の場合はスタイルに迷うことでしょう。

本記事では、C#に関して、開発元のMicrosoftが推奨するコーディングスタイルを紹介します。

前提知識

  • プログラミングに関する基礎を理解している

このコーディング規則について

このページのガイドラインは、Microsoftが自社のドキュメントやサンプルコードを書くときの規則です。C#の言語仕様として決められているものではありません。

また、ページ内でもこれが絶対のリストではなく、よくある規則の例ですという位置づけになっています。

なので、プロジェクトに合わせて取捨選択してOKです。

一貫性や可読性など、プロジェクトに合わせて規約を決めましょう。

言語ガイドライン

まずは全体方針をまとめます。

  • 新しい言語機能と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)で何でも受けてしまうと、本来気づくべきバグまで握りつぶしてしまいます。

そして、ログだけ出して再スローするならthrow ex;ではなくthrow;と書きます。スタックトレースが保たれるためです。

次にusingです。finallyの中でDisposeを呼んでいるだけなら、try-finallyではなくusingに置き換えます。

さらに、波かっこが不要な新しい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()にするか、どちらか片方だけでOKです。

ただし、変数の型がインターフェースや基底クラスの場合は、new()は使えません。

また、プロパティの設定はオブジェクト初期化子にまとめます。newしたあとに1行ずつ代入していく書き方より短く、「初期化の一塊」として読めます。

イベント処理

あとで解除する必要がないイベントハンドラーは、ラムダ式で書きます。

TitleScreen.cs
startButton.Click += (sender, e) => StartGame();

逆に、-=で購読を解除したいときは、ラムダ式だと外せないので、名前付きのメソッドにしておく必要があります。

ゲームだとシーン遷移で購読解除を忘れがちになることが注意点です。

静的メンバー

staticメンバーは、必ず定義したクラス名で呼びます。

派生クラス名で呼ぶと、コンパイルは通るのですが、読む人が派生クラス側に定義があるのかと誤解します。

また、あとから派生クラスに同名のstaticメンバーが追加されたときに、呼び出し先が意図せず変わってしまう危険もあります。

LINQ クエリ

クエリ構文を使う場合の規則です。

  • クエリ変数にはわかりやすい名前をつける
  • 匿名型のプロパティ名はPascalCaseにする
  • 名前が衝突してあいまいなら、CustomerNameのように付け直す
  • クエリ変数や範囲変数にはvarを使う
  • whereは他の句より先に書く
  • 内部のコレクションを展開するときは、joinではなくfromを複数書く
AliveEnemies.cs
var aliveEnemies = from enemy in enemies
                   where enemy.Hp > 0
                   orderby enemy.Name
                   select enemy;

whereを先に置くのは、先に絞ってからあとの処理を行うためです。並べ替えてから絞るより効率がよくなります。

2つのコレクションを結合して、名前がかぶるときの例です。

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でOK(for (var i = 0; ...))
  • foreachのループ変数:varは使わない(要素の型が見えにくいため)
Loop.cs
foreach (Enemy enemy in enemies)
{
    enemy.Update();
}

ちなみに、MicrosoftのドキュメントはブラウザでMouseOverしても型が見えないので、読むだけで型がわかることを重視しています。Visual Studioで型を確認しながら書くなら、もう少し緩めてもよいかと思います。

ファイルスコープ名前空間

ほとんどのファイルでは名前空間は1つだけなので、ファイルスコープ名前空間(C# 10)を使います。

Player.cs
namespace MyGame.Characters;

public class Player
{
    // ...
}

波かっこが1段減るので、インデントが浅くなります。

using は名前空間の外側に書く

usingディレクティブは、名前空間宣言の外側に置きます。

名前空間の内側に置くと、名前解決が今いる名前空間からの相対になり、あとから追加した名前空間のせいで、触っていないコードが突然ビルドエラーになることがあります。

ページの例では、Azureのusingを名前空間の内側に書いていたところ、依存先がCoolStuff.Azureという名前空間を追加した結果、Azureが先にそちらに解決されてしまい、エラーになっていました。

ゲーム開発でも、ライブラリやプラグインを追加した途端に、名前が衝突して動かなくなるケースがあります。外側に書くのが無難です。

スタイルのガイドライン

見た目の規則です。

  • インデントはスペース4つ(タブは使わない)
  • 波かっこはAllmanスタイル(開きも閉じも独立した行)
  • 1行に書く文は1つ、宣言も1つ
  • メソッドとプロパティの間には空行を入れる
  • 条件式は、かっこで区切りを明確にする
Combat.cs
if ((attack > defense) && (target.IsAlive))
{
    ApplyDamage(target);
}

なお、ページには「1行65文字以内」という規則もありますが、これはモバイルでのドキュメント表示を意識したものです。普通のプロジェクトで守る必要はありません。

コメント

  • 短い説明は//の1行コメントにする
  • 長い説明は/* */を避ける
  • コメントは行の末尾ではなく別の行に書く
  • //のあとにスペースを1つ入れる
  • メソッドやクラス、パブリックメンバーにはXMLコメントを使う

Microsoftのサンプルでは、さらにコメントは大文字で始めてピリオドで終えるという規則もあります。

ただしこれは英語での話なので、日本語で書く場合は、プロジェクト内で統一できていればよいと思います。

総括

本記事では、Microsoft Learnの**.NET コーディング規則 - C#**を、ゲーム開発のサンプルに置き換えながら整理しました。

  • 規則は絶対ではなく、取捨選択してよい
  • ツール(.editorconfigとコード分析)で自動化する
  • varは、右辺から型がわかるときだけ
  • 例外は、処理できるものだけを具体的な型で受ける
  • usingディレクティブは名前空間の外側に書く
  • 新しい機能(生の文字列、コレクション式、requiredなど)を積極的に使う

個人開発だと、ついレビューなしで好き勝手に書いてしまいがちですが、未来の自分は他人です。

規則を決めて、.editorconfigに任せてしまうのが楽でおすすめです。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?