83
101

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# CODING GUIDELINES 2024

Last updated at Posted at 2024-06-25

C# CODING GUIDELINES 2024

目次


このドキュメントについて

命名規則、コーディング規則を遵守して生産性を向上させることを目的としています。

自分で書いたコードでも長い間メンテナンスしなければ他人のコードと同じです。
一定の規則に従い、読みやすく、バグの少ない、メンテナンスのしやすいコードを目指しましょう。
規約に従うことは、多くの問題を改善し、技術的負債を減らします。

本書は、以下のページを参考にしています。
Microsoft Learn / .NET / C# / コーディングスタイル / C# 識別子の名前付け規則と表記規則
Microsoft Learn / .NET / C# / コーディングスタイル / 一般的な C# のコード規則

以下のガイドラインは、過去のものなので、最新の事情を反映していませんが、大部分は適用できます。
Microsoft Learn / .NET / フレームワーク デザインのガイドライン / 名前付けのガイドライン

本書では MSDN と記述しますが、移行後の Microsoft Docs(2016), Microsoft Learn(2022) を含みます。

開発環境

開発環境は Visual Studio 2022(以下VS2022) を想定しています。
最新の環境の方が補完機能なども優れているので、生産性やコード品質が上がります。
できるだけ最新のものを使用しましょう。

Visual Studio 2022 リリース ノート

言語バージョン

最近の C# では、ターゲットフレームワーク(.NET, .NET Framework, .NET Standard)によって使用できる言語バージョンが違います。

例えば、.NET 8.0 をターゲットにすると、言語バージョンは C# 12 が使用できます。

Windows 7 SP1 や Windows Server 2012 で動かす必要があるなら .NET 6.0 をターゲットにすると比較的新しい C# 10 が使用できます。

ライブラリで、古い環境(Windows7 SP1, 初期Windows10)やクロスプラットフォーム(MAUIやUnity)を対象とする場合は、.NET Standard を指定することができます。
.NET Standard 2.0 をターゲットにすると、.NET Core 2.0 以降と.NET Framework 4.6.1 以降をサポートし、C# 7.3 を使用できます。
.NET Framework のサポートが必要なければ、.NET Standard 2.1 をターゲットにでき、.NET Core 3.0 以降、 C# 8.0 が使用できます。
ただし、.NET Standard では使用できる API が制限されています。

C# 言語のバージョン管理
.NET Standard のバージョン
C# の歴史

本書では、表記を短縮するため、下記を用いることがあります。
.NET Framework 4.8 は .NET 4.8、.NET Core 3.0 は Core 3.0、.NET Standard 2.1 は Std 2.1 と表記します。

開発環境とターゲットフレームワークと言語バージョン

開発環境 .NET Framework .NET Core .NET Standard 言語
VS.NET(2002) .NET Framework 1.0 -- -- C#1.0
VS2003 .NET Framework 1.1 -- -- C#1.1/1.2
VS2005 .NET Framework 2.0 -- -- C#2.0
VS2008 .NET Framework 3.0/3.5 -- -- C#3.0
VS2010 .NET Framework 4.0 -- -- C#4.0
VS2012 .NET Framework 4.5 -- -- C#5.0
VS2013 .NET Framework 4.5.1 -- -- --
VS2015 .NET Framework 4.6 -- -- C#6.0
VS2015 Update 1 .NET Framework 4.6.1 -- -- --
VS2015 Update 3 -- .NET Core 1.0 -- --
VS2017 15.0 .NET Framework 4.6.2 .NET Core 1.1 .NET Standard 1.0 C#7.0
VS2017 15.1 .NET Framework 4.7 -- -- --
VS2017 15.3 -- .NET Core 2.0 .NET Standard 2.0 C#7.1
VS2017 15.5 .NET Framework 4.7.1 -- -- C#7.2
VS2017 15.7 -- .NET Core 2.1 -- C#7.3
VS2019 16.0 -- .NET Core 2.2 -- --
VS2019 16.1 .NET Framework 4.7.2 -- -- --
VS2019 16.3 .NET Framework 4.8 .NET Core 3.0 .NET Standard 2.1 C#8.0
VS2019 16.4 -- .NET Core 3.1 -- --
VS2019 16.8 -- .NET 5.0 -- C#9.0
VS2022 17.0 -- .NET 6.0 -- C#10
VS2022 17.3 .NET Framework 4.8.1 -- -- --
VS2022 17.4 -- .NET 7.0 -- C#11
VS2022 17.8 -- .NET 8.0 -- C#12
Nov. 2024 -- .NET 9.0 -- C#13

OSとランタイム

.NET Framework 4.8(C# 8.0) または .NET 6.0(C# 10) をターゲットにすると、初期のWindows 10(1507, 1511)を除くほとんどの環境でサポートしています。

OS プリインストール インストール可能(min/max) .NET Core
Windows 7 SP1 .NET Framework 3.5 .NET Framework 4.6.1※/4.8 .NET 6.0
Windows 8.1 .NET Framework 4.5.1 .NET Framework 4.6.2/4.8 .NET 6.0
Windows 10 1507 .NET Framework 4.6 .NET Framework 4.6.2 Only --
Windows 10 1511 .NET Framework 4.6.1 .NET Framework 4.6.2 Only --
Windows 10 1607 .NET Framework 4.6.2 .NET Framework 4.7/4.8 .NET 6.0/8.0
Windows 10 1703 .NET Framework 4.7 .NET Framework 4.7.1/4.8 .NET 6.0/8.0
Windows 10 1709 .NET Framework 4.7.1 .NET Framework 4.7.2/4.8 .NET 6.0/8.0
Windows 10 1803 .NET Framework 4.7.2 .NET Framework 4.8 .NET 6.0/8.0
Windows 10 1903 .NET Framework 4.8 .NET Framework 4.8 .NET 6.0/8.0
Windows 10 20H2 .NET Framework 4.8 .NET Framework 4.8.1 .NET 6.0/8.0
Windows 11 .NET Framework 4.8 .NET Framework 4.8.1 .NET 6.0/8.0
Windows 11 22H2 .NET Framework 4.8.1 .NET Framework 4.8.1 .NET 6.0/8.0

※Windows 7 SP1 での .NET Framework 4.6.1 の動作には特定の制約があります。

Server OS プリインストール インストール可能(min/max) .NET Core
Windows Server 2008 R2 SP1 .NET Framework 3.5 .NET Framework 4.6.2/4.8 --
Windows Server 2012 .NET Framework 4.5 .NET Framework 4.6.2/4.8 .NET 6.0/8.0
Windows Server 2012 R2 .NET Framework 4.5.1 .NET Framework 4.6.2/4.8 .NET 6.0/8.0
Windows Server 2016 .NET Framework 4.6.2 .NET Framework 4.7/4.8 .NET 6.0/8.0
Windows Server 1709 .NET Framework 4.7.1 .NET Framework 4.7.2 Only .NET 6.0/8.0
Windows Server 1803/1809/2019 .NET Framework 4.7.2 .NET Framework 4.8 .NET 6.0/8.0
Windows 2022 .NET Framework 4.8 .NET Framework 4.8.1 .NET 6.0/8.0

.NET Framework のシステム要件
Windows に .NET をインストールする

古い .NET Framework SDK の導入

基本的には Visual Studio Installer で TargetPack をインストールします。

.NET Framework 4.5.1/4.5.2 は、開発者パックのオフラインインストーラーがダウンロードできます。
.NET Framework 4.5.1 のダウンロード
.NET Framework 4.5.2 のダウンロード

.NET Framework 4.0/4.5 のプロジェクトは NuGet で Microsoft.NETFramework.ReferenceAssemblies を参照します。
もしくは Visual Studio 2019 で TargetPack をインストールしておくと 2022 でもビルドできます。
Microsoft.NETFramework.ReferenceAssemblies に対してアプリをビルドする

.NET Framework 3.5/3.0/2.0 は Windows の機能の有効化から導入します。


命名規則

意味のある、わかりやすい、正しい名前付けを心がけてください。
正しい名前付けを心掛けることにより、コードが洗練されます。
正しい名前を付けるには、正しいクラス構造が必要だからです。

既存のコードを修正する場合は、そのスタイルに従ってください。
他のスタイルを混入させることは、品質の劣化となります。

英単語か日本語か

コメント以外は半角英数のみで記述し、英単語(アメリカ英語)を基本とします。
ローマ字綴りは可読性に問題があるので禁止とします。
日本語名が必要な場合は、コメントで説明します。

翻訳が難しい専門用語の場合は、プロジェクトで話し合って日本語をそのまま使用することがあります。
テストメソッドに関しては、プロジェクトで話し合って日本語を使用することがあります。

コードに日本語を使用するとコーディングが困難になります。
文字エンコーディングの違いからサードパーティ製ツールで文字化けします。
VS2013 から VS2015 にかけて中黒(U+30FB)問題がありました。

パスカルケースとキャメルケース

Pascal / Camel Case を基本とします。
クラス、メソッド、プロパティ、パブリックフィールド、定数、構造体、列挙体はパスカルケースとします。
プライベートフィールド、パラメータ変数、ローカル変数はキャメルケースとします。

✔️DO Pascal Case: PascalCaseName, DbContext, UserId, Sqlite, Grpc
✔️DO Camel Case: camelCaseName, dbContext, userId, sqlite, grpc
❌DO NOT Other Cases: ALLCAPS, CONSTANT_CASE_NAME, snake_case_name, kebab-case-name

二文字の名前(変更)

二文字の名前のみ大文字で表記する場合がありますが、先頭のみ大文字を優先します。

🔨変更
MSDN では二文字の頭字語に関しては大文字を推奨していますが、本書では採用しません。
DB-Db、Id-ID、 Ok-OK のどれも間違いやすいためです。
過去のMSDN では Id(Identifier), Ok(Okay) の二つのみ頭字語ではないと説明していました。

✔️DO 先頭のみ大文字: DbContext, IdManager, UiController, OsHelper
二文字が大文字の例(既存): System.IO, IPAddress, UIElement, OSVersion

省略形と表記ゆれ

単語は省略せずに記述します。
省略形は表記ゆれの要因になりますし、ドメイン領域によって意味が異なります。
自明な省略形の場合は、プロジェクトで話し合って使用します。

✔️DO 省略なし: Database, Windows, Temporary, Jpeg, Configuration
❌DO NOT 表記揺れ: Database/DataBase, Db/DB, Windows/Win/Window, Temporary/Temp/Tmp, Jpeg/Jpg, Configuration/Config/Conf/Cfg, Exception/Ex/E/Event
✔️CONSIDER 自明な省略形: App, Lib, Db, Sql, EFCore, Dto

スコープの短い局所変数では省略形を使用できます。
なるべく意味がわかりやすい省略形を優先します。
類推するための情報が足りなければコメントを残します。

局所変数の省略形
    // [❌DO NOT] u は意味が不明確、メソッドの中まで遡って初めて user だと判読できます。
    var u = this.Login(id, pwd);
    // [❌DO NOT] 型を明示することで変数宣言から判読できますが、
    // 変数の使用箇所から宣言個所まで戻って確認が必要です。
    User u = this.Login(id, pwd);
    // usr は類推できますが、変数名だけ見ると usr ディレクトリの可能性が残ります。
    var usr = this.Login(id, pwd);
    // [✔️DO] user 程度は省略せずに記述する方が読み手の負担が減ります。
    var user = this.Login(id, pwd);

    // 局所的な使用で、見通しがよい場合は判読できます。
    // 行数が多く、処理が複雑な場合は省略せずに書きます。
    if (obj is User u)
    {
        ...
    }

    // [❌DO NOT] i は意味が不明確です。
    var i = this.ReadPrimaryImage(path);
    // img なら類推できますが、他の略語や img 拡張子の可能性が残ります。
    var img = this.ReadPrimaryImage(path);
    // [✔️DO] image 程度は省略せずに記述する方が表記ゆれの可能性が減ります。
    var image = this.ReadPrimaryImage(path);
    // 文脈における形容詞(ここでは primary)の重要度によっては省略しません。
    var primaryImage = this.ReadPrimaryImage(path);
    // loaded までつけるのは冗長ですが、文脈によっては必要となります。
    var loadedPrimaryImage = this.ReadPrimaryImage(path);

    // w と h を一緒に使うと width と height だと類推できます。
    var w = primaryImage.Width;
    var h = primaryImage.Height;

    // Stream の文脈における buff と len は慣例的によく使用されます。
    var buff = new byte[256];
    var len = buff.Length;
    stream.Read(buff, 0, len);
    // [✔️CONSIDER] 他の行を見に行かないので、可読性が上がります。
    stream.Read(buffer, 0, buffer.Length);

名前空間とアセンブリ名

名前空間とアセンブリ名はパスカルケースです。
一意となるソリューション名を基点にディレクトリ構造を表現します。

プロジェクト構造例
repo/
└─ src/
    └─ SolutionName/
        ├─ SolutionName.sln
        ├─ Apps/
        │   └─ AppName/
        │       └─ AppName.csproj
        └─ Libs/
            └─ LibName/
                └─ LibName.csproj

デフォルト namespace は下記のようになります。
namespace SolutionName.Apps.AppName;
namespace SolutionName.Libs.LibName;

アプリケーションのアセンブリ名は、AppName.exe とします。
ライブラリのアセンブリ名は SolutionName.Libs.LibName.dll とします。

標準的に使われている接辞

  • インターフェースは I- を付けます。
  • 機能を表すインターフェースは -able を付けます。
  • 抽象クラスは -Base を付けます。
  • 派生クラスは元のクラス名を含めます。
  • 属性クラスは -Attribute を付けます。
  • 処理が中心のクラスは -er を付けます。
  • 非同期メソッドは -Async を付けます。
  • イベントハンドラーメソッドは On- または ControlName_On- から始めます。
  • 型パラメータは T または T- から始めます。

メソッド名は動詞句とする

インスタンスを <主語> とすると、メソッド名は <動詞> です。
変換を表すメソッドは、例外として To-As- を使います。
自然な英文として読めることが理想です。

✔️DO 自然なメソッド名: server.Restart(), enemy.MoveTo(target), enemy.Attack(target)
✔️DO 変換を表すメソッド名: obj.ToString(), list.AsSpan()
❌DO NOT 単一責任ではない: server.StopAndStart(), enemy.MoveToAndAttack(target)
❌DO NOT 動詞ではない: server.Name(), enemy.Hp()

論理値を表す名前は is-, has-, can- をつける

変数名、プロパティ名、メソッド名に当てはまります。

論理値を表す変数やプロパティの場合は、is-<過去形>, has-<過去分詞>, can-<現在形> を付けます。
<動詞> から始まるのはメソッド名ですが、論理値を表す名前に関しては例外として使用します。

✔️DO 変数名: bool isItemExist, isChanged, canSave, isSaved, hasSucceeded, isCompleted
✔️CONSIDER 考慮する: bool hasChanges, hasTransaction, hasSavedSuccessfully
❌AVOID 避ける: bool exists, existed, saved, hasSaved
❌DO NOT 非推奨: bool doesItemExist, itemsExists, doesSave, saves

プロパティ名の場合は、インスタンスを <主語> として Is-<名詞> で状態を示す表現が多いです。
過去の MSDN では、プロパティの場合は過去形を使うことが推奨されていましたが、イベント名と被るため現在では推奨されません。

✔️DO プロパティ名: bool IsRefreshing { get; }, HasError { get; }, CanExecute { get; }
✔️CONSIDER プロパティ名: bool ShouldRefresh { get; }, NeedsRefreshing { get; }
❌DO NOT 非推奨: bool Refreshing { get; }, Refreshed { get; }

メソッド名の場合は、Is-<名詞> もありますが、<動詞>-s の三単現(三人称単数現在形)を使います。

✔️DO メソッド名: bool IsNullOrEmpty(...), Exists(...), Contains(...)
❌DO NOT 非推奨: bool IsExisted(...), IsContained(...)

二重否定は避ける

論理値を返す名前は肯定形を基本とします。
条件式で否定が続くと読みにくくなります。

二重否定は避ける
    // [✔️DO] 肯定形
    if (isNull)
    if (!isNull)
    if (IsFound(obj))
    if (IsNullOrEmpty(obj))
    if (Exists(obj))
    if (Contains(obj))

    // [❌DO NOT] 二重否定
    if (!isNotNull)
    if (!IsNotFound(obj))
    if (!(IsNotNull(obj) && IsNotEmpty(obj)))

    // C#7.0(.NET4.6/Core1.0) からは is 演算子を使えます。
    if (obj is null)

    // C#9.0(.NET5.0) からは not 演算子を使えます。
    // !(否定演算子) よりも判読しやすくなります。
    if (obj is not null)

コレクションやリストの変数名は複数形とする

コレクションやリストの変数名は複数形とします。
-Collection-List とすると、型を書き換えたときに名前まで修正しなければなりません。
型名が重要な場合は、例外として名前に型名を含めます。

単数形: File file, Item item, byte singleByte
✔️DO 複数形: List<File> files, IEnumerable<Item> items, byte[] bytes
❌DO NOT 非推奨: List<File> fileList, IEnumerable<Item> itemList

単数形と複数形が異なる単語には注意が必要です。

  • Child - Children(子供)
  • Person - People(人)
  • Man - Men(男性)
  • Woman - Women(女性)
  • Index - Indexes(索引) - Indices(指数)
  • Axis - Axes(軸)
  • Analysis - Analyses(分析)

不可算名詞など複数形がないものは、例外として -s を無理やりつけます。

  • Series - Serieses
  • Information - Informations
  • Knowledge - Knowledges
  • Software - Softwares
  • Logic - Logics

Data は Datum の複数形ですが、単数形として使います。
Dataset は Data の集合体を意味します。
-Data とつけると冗長なのでなるべく使いません。

  • Data - Dataset, DataSet

英語の慣例として単位に -s を付けて表現します。
配列の表現と混同するのでなるべく単数形に言い換えます。

  • Milliseconds - Msec
  • Retries - RetryCount
  • Bytes - ByteCount

private なフィールドにはアンダースコアを付ける (変更、設定)

フィールドは基本的に private です。
private なフィールドは _-(アンダースコア)をつけて、キャメルケースとします。
private static の場合は s_- をつけます。
アンダースコアを付けることにより、フィールド変数とパラメータ変数の区別がつきやすくなります。
過去の MSDN ではアンダースコアを付けませんでしたが、現在はつけることが推奨されています。

✔️DO private: private string _userId;
✔️DO private static: private static string s_userId;
✔️CONSIDER [ThreadStatic]: [ThreadStatic] private static string t_userId;
❌DO NOT 非推奨: private string userId;

🔨変更
public または internal にする場合はプロパティにできないか検討します。
internalprivate と同じルールとしていることがありますが、クラス外に公開するため、本書では private より public に近い扱いとします。

✔️DO プロパティ: public string UserId { get; set; }
❌DO NOT public/internal: public string UserId;

🔧設定
VisualStudio では以下の手順でルールを登録できます。
ルールの重要度はプロジェクトに合わせて「リファクタ/提案事項/警告/エラー」を選択します。

  1. [ツール] > [オプション] を開きます。
  2. [テキスト エディター] > [C#] > [コード スタイル] > [名前指定] を開きます。
  3. 「Private Field」「Begins with _」というルールを追加します。
  4. 「Private Static Field」「Begins with s_」というルールを追加します。

構造体の public なフィールドは大文字から始める

構造体のフィールドは基本的に public です。
public なフィールドはパスカルケースとなります。
構造体の場合はプロパティにアクセスすると防衛的コピーを行うため、public なフィールドが必要となります。

C#7.0(.NET4.6/Core1.0) から使えるタプル(ValueTuple)は構造体で、各変数は public なフィールドになります。
そのため、パスカルケースを推奨しますが、規定とはしません。
タプルが使われるのは、スコープが短い範囲に限られるので、キャメルケースでも違和感がないためです。

構造体を扱う場合は値コピーとボクシングに注意が必要です。
構造体の使用

this. は省略しない (設定)

this. を明示する方が可読性が上がります。
コード内で一貫して this. を使用することで、読み手の理解を助けます。
ありとなしが混在すると誤読につながるので設定を入れて統一します。

マージ時に当該部分だけ読んで判断するための材料となります。
インスタンスメソッド、静的メソッド、ローカル関数の混同を避けることができます。
アンダースコアがなくてもインスタンス変数/ローカル変数/パラメータの混同を避けることができます。

✔️DO this: this._value = value, this.Id = id
❌DO NOT 非推奨: _value = value, Id = id, this.id = id

🔧設定
VisualStudio ではデフォルトで省略するようになっていますが、以下の手順で変更できます。

  1. [ツール] > [オプション] を開きます。
  2. [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
  3. this の優先」をすべて「優先する」に変更します。

static なメンバーはクラス名をつけて呼び出す

自クラスの静的メンバーを呼び出す場合は ClassName.StaticMember とする方が可読性が上がります。
this. を付けるのと同様に読み手の理解を助けます。
static class の場合は冗長なのでクラス名は省略します。

インスタンスメソッド、静的メソッド、ローカル関数の混同を避けることができます。
s_- のプレフィックスがなくてもインスタンス変数/静的変数の混同を避けることができます。

static メンバーの呼び出し
    // [✔️DO] ClassName を付けると static メンバーだとわかります。
    ClassName.StaticMethod();

    // [✔️DO] this を付けるとインスタンスメンバーだとわかります。
    this.InstanceMethod();

    // [❌DO NOT] どちらかわかりません。
    Method();

メンバーアクセス時は定義済みの型を使わない (設定)

C# 言語自体で定義されているプリミティブ型には bool, int, string, decimal, ... があります。
これは CLR 型へのエイリアスですが、メンバーアクセスに関しては CLR 型を明示すべきです。
言語仕様としての型と、メソッドを使用するためのクラスでは、文脈上の意味合いが違います。
また他のクラスの静的メソッドの呼び出し方とプリミティブ型を使う場合の一貫性がなくなります。

✔️CONSIDER CLR型: Boolean.TryParse(...), String.IsNullOrEmpty(...)
❌AVOID 非推奨: bool.TryParse(...), string.IsNullOrEmpty(...)

🔧設定
VisualStudio ではデフォルトで定義済みの型を使用するようになっていますが、以下の手順で変更できます。

  1. [ツール] > [オプション] を開きます。
  2. [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
  3. 「定義済みの型の設定」で「メンバーアクセス式の場合」を「フレームワークの型を優先する」に変更します。

名前空間とクラス名の重複は避ける

名前空間とクラス名が同じだと問題があります。
名前空間は表記揺れしないように省略形を避け、重複しないように複数形を用います。

✔️DO 重複しない: Xxx.Logging.Logger, Xxx.Configurations.Configuration
❌DO NOT 非推奨: Xxx.Logger.Logger, Xxx.Config.Config

列挙体のビットフィールドは複数形とする

列挙体、列挙値ともにパスカルケースです。
列挙体名は、基本は単数形ですが、[Flags](ビットフィールド) を表す場合は複数形とします。

列挙体を定義する場合は、None = 0 を定義します。
None = 0 は明示的に使用しなくても、初期値として使われる可能性があります。
エラー値を定義したい場合は、Invalid = -1 を検討します。

ビットフィールド
    [Flags]
    public enum Permissions
    {
        None = 0,
        Read = 1 << 0, // 1
        Write = 1 << 1, // 2
        Execute = 1 << 2, // 4
        ReadWrite = Read | Write, // 3
        All = Read | Write | Execute, // 7
        // シフト演算子でも表現できます。
        //All = ~(-1 << 3),
    }

C#7.0(.NET4.6/Core1.0) からは二進数リテラルを使えます。
視覚的にビット表現を確認できるので計算が不要になります。
イコールの位置を揃えたい場合はVS拡張の Productivity Power Tools - Align Assignments や Code alignment などを使います。
位置合わせのスペースを残すために #pragma プリプロセッサディレクティブでドキュメントフォーマットを無効化します。

2進数リテラル
    #pragma warning disable format
    [Flags]
    public enum Permissions
    {
        None      = 0b0000,
        Read      = 0b0001,
        Write     = 0b0010,
        Execute   = 0b0100,
        ReadWrite = 0b0011,
        All       = 0b0111,
    }
    #pragma warning restore format

定数は大文字から始める (変更)

定数は static readonly フィールドとし、private フィールドでも例外的にパスカルケースとします。
すべて大文字で表現する形式は強調しすぎるため禁止とします。

🔨変更
定数は const が推奨されていますが、本書では採用しません。
static readonly の実行時定数にできないか検討します。
コンパイル時定数より実行時定数の方が問題を減らせます。

✔️DO private/public static readonly: public static readonly string JsonContentType = "application/json";
❌AVOID private const: private const string JsonContentType = "application/json";
❌DO NOT 非推奨: public const string JsonContentType = "application/json";
❌DO NOT 非推奨: public const string JSON_CONTENT_TYPE = "application/json";

🔨変更
メソッド内のローカル定数は const ですが、本書では採用しません。
const にするとメソッド内でもパスカルケースで記述する必要があります。
読みやすさを考慮して、ローカル定数より通常の変数を使用します。
ローカル定数よりもグローバル定数にできないか検討します。

✔️DO 変数: var defaultMaxRetryCount = 5;
❌DO NOT 非推奨: const int DefaultMaxRetryCount = 5;

record のプライマリコンストラクタのパラメータは大文字から始める

C#9.0(.NET5) から record とプライマリコンストラクタを使用できます。
record のプライマリコンストラクタのパラメータはプロパティとして展開されます。
プロパティにはパスカルケースを使用するため、record のプライマリコンストラクタにはパスカルケースを使用します。

プライマリコンストラクタ
    // record のプライマリコンストラクタ
    public record Person(int Id, string Name);

    // 通常のコンストラクタ
    public class Person
    {
        public int Id { get; init; }
        public string Name { get; init; }

        // 通常のコンストラクタのパラメータ名はキャメルケースとします。
        public Person(int id, string name)
        {
            this.Id = id;
            this.Name = name;
        }
    }

    // C#12(.NET8.0) からはクラスと構造体でもプライマリコンストラクタが使えます。
    public class Person(string name, int age)
    {
        // プロパティに自動展開されないので代入します。
        public string Name { get; init; } = name;
        public int Age { get; init; } = age;
    }

コントロール名は大文字から始める (変更)

Windows Forms と XAML の両方に当てはまります。
コントロールはフィールドとして作成されますが、例外的にパスカルケースとします。

🔨変更
MSDN の規定がないのでフィールドの命名規則に従ってキャメルケースが用いられていました。
メソッド名はパスカルケースというルールがあったため、コードビハインドのイベントハンドラーメソッドの自動作成は先頭大文字にするという VisualStudio の設定があります。
本書ではコントロール名はパスカルケースを採用します。
コントロール名をパスカルケースにしておくと、イベントハンドラーメソッドもパスカルケースで作成されます。

✔️DO Pascal Case: LoginIdLabel, LoginIdTextBox, LoginPasswordTextBlock
❌DO NOT 非推奨: loginIdLabel, loginIdTextBox, loginPasswordTextBlock

ボタンは動作を表すため、動詞とするか、役割が明確な名前を付けます。

✔️DO ボタン名: SaveButton, CancelButton, ClearButton, OkButton
❌DO NOT 非推奨: UserButton, PasswordButton, TextButton

ListBox, ComboBox, DataGrid など複数項目を表示/選択するコントロールの場合は役割に複数形を用いて、複数の選択が可能なことを示します。
コントロール自体を複数形にするわけではないので、配列とは区別されます。

✔️DO Pascal Case: WaitingListBox, SexesComboBox, UsersDataGrid

🔨変更
ハンガリアン記法は一般的に推奨されていませんが、本書ではコントロール名に限り推奨します。
省略形を用いないとコントロールを区別するために冗長な型名をすべて記述することが多くなるためです。
ハンガリアン記法を使う場合はコントロール名の短縮形を統一しておき、表記ゆれを避けます。
MVVM の場合はコントロール名を付けることが少ないため、プロジェクトで採用するかは検討が必要です。

✔️CONSIDER ハンガリアン記法: BtnSave, BtnCancel, LstUsrs, LblUser, TxtUser
❌DO NOT 表記ゆれ: LstUsers, LvwUsers, LvUsers, CboSexes, CmbSexes, RdoFemale, RadFemale

付録: コントロールのハンガリアン記法


コーディング・レイアウト規則

.editorconfig には VisualStudio と ReSharper 特有の設定を含めることができます。
ルールを設定し、自動フォーマッタを活用してください。

  • タブ文字は使わず、スペース4つとします。
  • 1つの行には1つのステートメントのみを記述します。
  • 1つの行には1つの宣言のみ記述します。
  • 継続行には1タブ分(スペース4つ)のインデントを行います。
  • メソッドおよびプロパティの各定義の間には1つ以上の空白行を追加します。
  • 式(expression)に句(clause)を含めるときは括弧でくくります。

サンプル: .editorconfig の例

インデントはスペースを使う (設定)

インデントにはスペース4つを使います。
タブとスペースを修正するとコミットログが汚れるので混在は避けます。

参考元のコードや使用している別ツールからの混入が考えられます。
VS 拡張の Productivity Power Tools に含まれる Fix Mixed Tabs を入れておくと修正できます。

🔧設定
.editorconfig でインデントを設定できます。

.editorconfig
    indent_size = 4
    indent_style = space

ファイルの末尾に空行を入れる (設定)

ファイルの末尾に空行を入れておくと、コミットの Diff が整います。
基本的には1ファイル1クラスですが、関連の深いクラスは1ファイルにまとめることがあります。

ファイルの末尾に改行あり
    public interface ISample
    {
        ...
    }
+
+   public static class SampleExtensions
+   {
+       ...
+   }

ファイルの末尾に改行なし
    public interface ISample
    {
        ...
-   }
+   }
+
+   public static class SampleExtensions
+   {
+       ...
+   }

🔧設定
.editorconfig でファイル末尾の空行を設定できます。
行末の空白文字の削除も指定できます。
VS 拡張の Trailing Whitespace Visualizer も可視化できるのでおすすめです。

.editorconfig
    insert_final_newline = true
    trim_trailing_whitespace = true

最後の要素にもカンマを付ける

配列の列挙時などで最後の要素にもカンマを付けておくとコミットの Diff が整います。

最後の要素にカンマあり
    var people = new List<Person>
    {
        new Person{ Name = "Alice", Age = 8 },
        new Person{ Name = "Bob", Age = 2 },
        new Person{ Name = "Charlie", Age = 14 },
+       new Person{ Name = "David", Age = 35 },
    };
最後の要素にカンマなし
    var people = new List<Person>
    {
        new Person{ Name = "Alice", Age = 8 },
        new Person{ Name = "Bob", Age = 2 },
-       new Person{ Name = "Charlie", Age = 14 }
+       new Person{ Name = "Charlie", Age = 14 },
+       new Person{ Name = "David", Age = 35 },
    };

文字コードや改行コードを指定する (設定)

Windows 上で編集していると意図せず Shift-JIS や CRLF が紛れ込むことがあります。
文字コードや改行コードを指定しておくとよいでしょう。

🔧設定
.editorconfig で改行コードを指定できます。
文字エンコーディングに BOM ありを指定しておくことで日本語の文字化けを防ぎます。
Windows を対象とする場合は改行コードを CRLF(\r\n) とします。

.editorconfig
    charset = utf-8-bom
    end_of_line = crlf

🔧設定
Git の AutoCrLf は無効にしておきます。
設定を変更した場合は再クローンが必要です。

autocrlf
    git config --global core.autocrlf false

🔧設定
.gitattributes を使うと改行コードを強制できます。
開発途中で入れた場合は、クリーンアップが必要です。

.gitattributes
    * text=auto

    ##### Windows
    *.bat text eol=crlf
    *.cmd text eol=crlf
    *.ps1 text eol=crlf
    *.exe binary
    *.dll binary

    # Visual Studio
    *.sln text eol=crlf
    *.csproj text eol=crlf

    # .Net
    *.resx text eol=crlf
    *.settings text eol=crlf
    *.cs text eol=crlf

.NET Core/.gitattributes
GitHub Extension for Visual Studio/.gitattributes

メソッドの長さは1画面に収まる程度にする

長大なメソッドは読解に時間がかかるので、適切な長さのメソッドに分割します。
単一責任の原則を意識するとメソッドを短く保てます。
一般的に30行程度、長くても100行程度が目安とされています。

長い行は改行する

横スクロールが表示されると可読性が低下するため避けることが推奨されます。
モバイルなら65文字、デスクトップなら80文字、ワイドディスプレイなら100文字から120文字、最大でも140文字程度が目安とされています。

改行位置
    // 引数が多い場合は `,`(カンマ) の後で改行します。
    public class Person(
        int id,
        string name,
        DateTime? birthDate,
        Sex sex
    )
    {
        public int Id { get; set; } = id;
        public string Name { get; set; } = name;
        public DateTime? BirthDate { get; set; } = birthDate;
        public Sex Sex { get; set; } = sex;
    }

    // 型パラメタが長い場合は `where` の手前で改行します。
    public abstract class DbRepositoryBase<TEntity> : IDbRepository<TEntity>
        where TEntity : class
    {
        // メソッドのラムダ式が長い場合は `=>`(ラムダ演算子, アロー演算子) の後ろで改行します。
        public virtual TEntity? Get(Func<TEntity, bool> predicate) =>
            this._dbSet.FirstOrDefault(predicate);
    }

    // 継承が長い場合は `:`(コロン) の手前で改行します。
    public class PersonDbRepository(DbContext Context)
        : DbRepositoryBase<Person>(Context);

    // 宣言が長い場合は `.`(ピリオド) の後で改行します。
    var currentPerformanceCounterCategory = new global::System.
        Diagnostics.PerformanceCounterCategory();
    
    // ラムダ式の `{}` はメソッド宣言行の開始位置に揃えます。
    Action<string> printMessage = (msg) =>
    {
        Console.WriteLine(msg);
    };

メソッドチェーンの改行位置

LINQ やビルダーパターンのメソッドチェーンを記述する場合はピリオドを前置とします。

演算子の前置
    // C#3.0(.NET3.5) から LINQ が使えます。
    var oddNumbers = numbers
        ?.OfType<int>()
        .Where(x => (x % 2) != 0)
        .OrderBy(x => x)
        .ToList() ?? [];
        
    // ビルダーパターン
    var config = new DbConfigBuilder()
        .SetDbType(DbType.PostgreSQL)
        .SetHost("localhost")
        .SetPort(5432)
        .SetUsername("user")
        .SetPassword("password")
        .Build();

演算子の改行位置

演算子が多く、1行が長くなる場合は演算子の前後で改行することができます。
読みにくい場合は一時変数を用いて説明的な名前をつけることを検討します。

演算子の改行位置
    // C#7.0(.NET4.6/Core1.0) からは is 演算子を使えます。
    // if の場合は演算子の後で改行すると括弧の位置が揃います。
    if ((data is null) ||
        (data == DBNull.Value) ||
        (data is string s && s.Length == 0))

    // return の場合は演算子の前で改行すると括弧の位置が揃います。
    return (data is null)
        || (data == DBNull.Value)
        || (data is string s && s.Length == 0);

    // [✔️CONSIDER] 説明変数を用意すると可読性が上がります。
    var isNullOrDBNullOrEmpty = (data is null)
        || (data == DBNull.Value)
        || (data is string s && s.Length == 0);

    // is が使えない場合
    if ((data == null) ||
        (data == DBNull.Value) ||
        (String.TryParse(data, out string s) && s.Length == 0))

    // 三項演算子を改行する場合は演算子を前置とします。
    var result = condition
        ? "success"
        : "failure";

範囲を表す比較演算子の向き

比較演算子は、左を主とするのが一般的です。
範囲を扱う場合は、数学と同じように、比較演算子の向きを揃えることで可読性が上がります。

演算子の向き
    // 比較演算子の左を主とした場合
    if ((a >= 90 && a <= 180) ||
        (a >= 270 && a <= 360))

    // [✔️CONSIDER] 比較演算子の向きを揃えた場合
    if ((90 <= a && a <= 180) ||
        (270 <= a && a <= 360))

    // [✔️DO] インデックスを扱う場合は包含排他を意識して `<` とします。
    for (var i = 0; i < length; i++)

if の中括弧の省略はしない

if{ }(中括弧) は省略しません。
省略すると修正時やマージ時に問題となることがあります。

ifの中括弧
    // [✔️DO] 省略しないスタイルです。
    if (obj is null)
    {
        throw new InvalidOperationException();
    }

    // [✔️CONSIDER] 省略しない、1行にするスタイルです。
    // 複数のガード節並ぶ場合に使用します。
    if (obj is null) { throw new InvalidOperationException(); }

    // [❌AVOID] 省略する、1行にするスタイルです。
    // 1行が長くなるとエディタの設定によっては改行されることがあります。
    if (obj is null) throw new InvalidOperationException();

    // [❌DO NOT] インデントのみで区別するスタイルです。
    if (obj is null)
        throw new InvalidOperationException();

自動実装プロパティは1行にする

自動実装プロパティやプロパティ本体に式を使用する場合は1行で記述します。
プロパティはステートレスとして実装するのが理想です。

プロパティ
    // [✔️DO] 1行で記述するスタイルです。
    // C#3.0 から自動実装プロパティが使えます。
    // prop -> tab -> tab で補完できます。
    public bool IsRefreshing { get; set; };

    // バッキングフィールドを自分で実装する場合
    private bool _isRefreshing = false;
    public bool IsRefreshing
    {
        // C#6.0(.NET4.6/Core1.0) からは式本体を使えます。
        get => this._isRefreshing;
        set => this._isRefreshing = value;
    }

    // [❌AVOID] C# 初期の冗長な記述です。
    // 式が複雑になる場合に複数行で記述することがありますが、
    // 処理が複雑な場合はメソッドにできないか検討します。
    private bool _isRefreshing = false;
    public bool IsRefreshing
    {
        get
        {
            return this._isRefreshing;
        }
        set
        {
            this._isRefreshing = value;
        }
    }

    // C#11(.NET7.0) から required が使えます。
    public class Product
    {
        // C#9.0(.NET5.0) から init が使えます。
        public required string Name { get; init; }
        public required decimal Price { get; init; }
    }

    // required で初期化が必須となり、init でオブジェクト初期化子が使えます。
    var product = new Product
    {
        Name = "Unknown",
        Price = 0,
    };

    // C#6.0(.NET4.6/Core1.0) から getter のみのプロパティが定義でき、
    // 初期値を代入できるようになりました。
    public int MyProp { get; } = 0;
    public MyClass(int value)
    {
        // getter のみだとコンストラクタで設定できます。
        this.MyProp = value;
    }

    // getter のみの場合は、プロパティに式を記述できます。
    // アクセス毎に再評価されるので複雑な計算は避けます。
    public int MyProp => this._myPropValue;

    // setter のアクセスを制限したい場合は、private を指定できます。
    public bool MyProp { get; private set; };

空のコンストラクタは1行にする

空のコンストラクタは1行で記述します。
コードを折りたたんでも判別できるので展開する必要がなくなります。

コンストラクタ
    // ctor -> tab -> tab で補完できます。
    private MyConstructor() : base() { }

    // [❌AVOID] 折りたたむと中身があるように見えます。
    private MyConstructor() : base()
    {
    }

var は積極的に使う(変更、設定)

暗黙的型推論の var を積極的に使い、冗長な宣言を短縮することができます。

🔨変更
MSDN では右辺が明らかな場合や型が重要でない場合は var が推奨されています。
本書では型指定が重要でない場合は var とし、積極的な使用を推奨します。

暗黙的型推論
    // [✔️DO] var を使うことで記述が短くなります。
    var items = new List<Item>();

    // [❌DO NOT] var を使わないと記述が冗長になります。
    Dictionary<string, int> people = new Dictionary<string, int>();

🔧設定
VisualStudio ではデフォルトで明示的な型を使用するようになっていますが、以下の手順で変更できます。

  1. [ツール] > [オプション] を開きます。
  2. [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
  3. 「var を優先」ですべて「var を優先してください」に変更します。

var のツールチップヒントで型名を確認できます。
ReSharper を利用していると var に型名を表示することができます。

Dictionary<TKey, TValue>は型パラメータのコメントを残す

Dictionary<TKey, TValue> は変数名だけだと何の値が使われているかわかりません。
宣言時にコメントを残しておくことで読み手の負担が減ります。

Dictionaryの型パラメータへのコメント
    // [✔️DO] Dictionary の場合は、何を表すかをコメントで残します。
    // { GroupName, Average }
    var groupAverages = new Dictionary<string, decimal>();

    // ブロックコメントを利用することもできます。
    var personNameAges = new Dictionary<string /* Name */, int /* Age */>();

Visual Studio のクリーンアップ

VisualStudio では以下の手順でクリーンアップできます。
標準のクリーンアップ機能が使いにくい場合は、ReSharper の使用を推奨します。

  1. [分析] > [コードのクリーンアップ] > [クリーンアップ(プロファイル)の実行] を実行します。

クリーンアップの構成で適用するルールを選択できます。

以下のルールは C# ではないので除外しておきます。

  • ドキュメントのフォーマット (C++)
  • ファイルインクルードグラフの最適化 (C++)
  • #include ディレクティブを並べ替える (C++)
  • オブジェクト作成の基本設定を適用する (VB)
  • IsNot の基本設定を適用する (VB)

以下のルールは適用すると意図しない変更が発生する可能性があります。

  • 名前空間の基本設定を適用する
  • 名前空間に一致するフォルダーの基本設定を適用する
  • アクセシビリティ修飾子を追加します
  • 式/ブロック本体の基本設定を適用します
  • かっこの基本設定を適用する
  • 単一行のコントロール ステートメントに対する必須の波かっこを追加する
  • 未使用の値の基本設定を適用する: _(discard) の使用
  • var の基本設定を適用する

コード クリーンアップ設定


コメント規則

コードは実装を、コメントは意図を、コミットは変更理由を、チケットには現象や問題を記載します。
正しい名前と構造を持ったコードは、コメントがなくても読みやすいですが、要約(サマリー)を読む方が、簡単で間違いがありません。

  • ブロックコメント(/* */)は避け、行コメント(//)を使います。
  • インテリセンスに反映されるため、XMLコメントを使用します。
  • コード行の末尾ではなく別の行に記述します。
  • 文章となるように記述します。(句点で終わる)
  • コメント記号(//)とコメント文字の間には空白をひとつ挿入します。(コードは除く)

複数行コメントは使わない (設定)

ブロックコメント(/* */)は避け、行コメント(//)を使います。
ブロックコメントを使うとコミットの変更が2行のみで Diff できません。
マージ時にもコンフリクトが起きないため問題となります。

🔧設定
VisualStudio では、Ctrl + K, C でコードをコメントアウトできます。
Ctrl + / のショートカットに変更しておくと便利です。

  1. [ツール] > [オプション] を開きます。
  2. [環境] > [キーボード] を開きます。
  3. ショートカットを設定できます。

行末コメントは使わない

行末のコメントは編集の邪魔になるため避けます。

行末コメント
    // [✔️DO] コメントは別の行に記述します。
    var a = 0;

    var abcde = 0; // [❌AVOID] 行末コメントのパターンです。
    var a = 0;     // [❌DO NOT] インデントを揃えてもフォーマッタで消されます。

コードの内容を繰り返す行コメントは書かない

コードの内容を繰り返しただけの行コメントはノイズになります。
コメントには意図(why, what)を残すのがよいコメントの仕方です。
コメントにはコードに書けないことを残します。

初学者が理解のためにコメントを残す場合は学習用ブランチを分けるか、一括で削除できるようにしておきます。

不要なコメント
    // [❌DO NOT] 以下は内容を繰り返すコメントなので不要です。
    // 配列の長さを取得
    var len = files.Length;

    // [❌DO NOT] 以下はメソッドにして、メソッド名として説明するとよいです。
    // ユーザーの年齢を計算
    var age = DateTime.Now.Year - user.BirthDate.Year;
    if (DateTime.Now.DayOfYear < birthDate.DayOfYear)
    {
        age--;
    }

    // [✔️DO] 以下は有用なコメントです。
    // TODO: このメソッドのパフォーマンスを最適化する。
    // この処理はシングルスレッドで動作することを前提としています。

変更履歴のコメントは書かない

変更履歴のコメントはノイズになります。
変更履歴が積み重なると更に難解になります。
行ごとの変更履歴は git blame で確認できます。

前のコードを残しておく必要がある場合は別メソッドとすることを検討します。

履歴コメント
    // #2024/12/31 sato 修正開始
    // oldCode();
    // 2025/01/01 suzuki 修正開始
    // newCode();
    newCode2();
    // 2025/01/01 suzuki 修正終了
    // 2024/12/31 sato 修正終了

ドキュメンテーションコメント

ドキュメンテーションコメントを記述するとインテリセンスに表示できます。
初学者や途中から参加したメンバー、未来の自分の読解コストを削減します。
少なくとも public な部分にはコメントをつけることを推奨します。

ドキュメンテーションコメント
    /// <summary>
    /// メソッドの要約です。できるだけ簡潔に1行でまとめます。
    /// <para>段落分けが必要な場合は para(paragraph) タグを使います。</para>
    /// </summary>
    /// <param name="x">param(parameter) は引数 x の説明です。</param>
    /// <returns>
    /// true: メソッド成功です。<br />
    /// false: メソッド失敗です。<br />
    /// メソッド名から明らかな場合は returns タグを省略します。
    /// </returns>
    /// <remarks>
    /// 補足やコードの参考元などを記載します。
    /// </remarks>
    /// <example>
    /// 1行の場合は c タグを、複数行の場合は code タグを使います。
    /// 使い方:
    /// <c>var x = await MyMethodAsync(...);</c>
    /// <code>
    /// MyMethodAsync(...)
    ///     .ConfigureAwait(false);
    /// </code>
    /// </example>
    public virtual async Task<bool> MyMethodAsync(int x)
    {
        ...
    }

    /// <summary>
    /// プロパティの要約です。
    /// </summary>
    /// <value>プロパティの値の説明です。summary で説明される場合は省略します。</value>
    public bool Name { get; }

    // VS2015 から inheritdoc がサポートされました。
    // inheritdoc タグを使うと継承元のコメントを表示できます。
    /// <inheritdoc />
    public override Task<bool> MyMethodAsync(int x)
    {
        ...
    }

    /// <summary>
    /// 説明内のコードリンクは <see cref="MyClass.MyMethod{T}(T)"/> と記述します。<br />
    /// 関連情報として挙げる場合は seealso タグを使用します。
    /// </summary>
    /// <exception cref="System.ArgumentNullException">例外の説明です。</exception>
    /// <seealso cref="MyClass"/>
    /// <seealso cref="MyClass.MyMethod{T}(T)"/>
    /// <seealso href="http://www.google.com/"/>

プラクティス&イディオム

ファイルスコープ namespace を使う

C#10(.NET6) よりファイルスコープの namespace が使用できます。
インデントが減らせるので、できる限りファイルスコープ namespace を使います。

ファイルスコープ namespace
// [✔️DO] C#10(.NET6) からはファイルスコープの namespace が宣言できます。
namespace AbcSoftware.Apps.AbcApp;

// [❌DO NOT] 以前は必ずインデントが必要でした。
namespace DefSoftware.Apps.DefApp
{
    ...
}

ローカル変数の使いまわしはしない

ローカル変数を不必要に使い回すと適切な変数名でない可能性があります。
そのため誤読につながる可能性が高まります。
正しい値を把握するためにコードを読み解く必要が出てきます。
なるべく読み手に負担がない記述を心がけます。

変数の使いまわし
    // [❌DO NOT] 変数に何が入っているかコードを読む必要があります。
    Item item = Item.Empty;
    item = items.First();
    item = items.Last();

    // [✔️DO] 正確な名前を付けます。
    var firstItem = items.First();
    var lastItem = items.Last();

ローカル変数のスコープは小さくする

ローカル変数は使うときに宣言し、生存期間をできるだけ短くします。
スコープを最小とすることで可読性が向上し、無駄な処理がなくなります。
生存期間は短い方がよいですが、ループ内での不必要な変数宣言と new は避けます。

スコープは小さくする
    // あとで使います。
    var selectedItems = items.Where(x => x.Selected).ToList();

    // [✔️DO] 使う直前で宣言します。
    var hasChanged = items.Any(x => x.HasChanged);
    if (hasChanged)
    {
        ...
    }

    // [✔️DO] C#7.0(.NET4.6/Core1.0) からは out パラメータ使用時に宣言できます。
    // value のスコープは if の外になります。
    if (Int32.TryParse(obj, out var value))
    {
        ...
    }

    // [❌DO NOT] selectedItems を宣言したところが離れています。
    try
    {
        if (selectedItems.Length > 0)
        {
            ...
        }
    }
ループ内の new は避けます。
    // [❌DO NOT] ループ内での不必要な new は避けます。
    while (true)
    {
        var list = new List();
        ...
        list.Add(item);
        ...
    }

    // [✔️DO] ループの外で宣言します。
    var list = new List(max);
    while (true)
    {
        ...
        list.Add(item);
        ...
        list.Length = 0;
    }

マジックナンバーは使わない

冗長なようでもマジックナンバーの代わりに正しい名前を持つ説明変数を用意します。
その値を設定した意図(why, what)をコメントに残しておくとコードの理解を助けます。
文脈から意味が明確な場合は不要です。

説明変数
    // 定数にする場合
    public static class Constants
    {
        // 一般的なタイムアウトの初期値が長すぎるため短いものを使います。
        public static readonly int DefaultTimeoutSec = 5;
    }

    // 文脈から意味が読み取れる場合
    var mean = (a + b) / 2;
    var millisec = sec * 1000;

    // 既存のファイルが存在する場合は追記します。
    var isFileAppend = true;
    using var writer = new StreamWriter(path, isFileAppend);

    // 説明変数を用意する代わりに名前付き引数にできます。
    using var writer = new StreamWriter(path, append: false);

    // [❌DO NOT] 直接的なリテラルは避けます。
    var priceWithTax = price * 1.10d;

    // [❌DO NOT] この 10 は何を表すか読み取れません。
    for (var i = 0; i < 10; i++)
    {
        ...
    }

インクリメントは別の行にする

インクリメントを式の中に使うと、考慮する事項が増えます。
別の行にすることで、前置または後置の区別を意識する必要がなくなります。

インクリメント
    // [✔️CONSIDER] インクリメントは別の行にします。
    while (i < numbers.Length)
    {
        numbers[i] = 0;
        i++;
    }

    // [❌AVOID] 前置と後置を間違えると IndexOutOfRangeException が発生します。
    while (i < numbers.Length)
    {
        numbers[++i] = 0; // 例外発生
    }

    // ループしない場合は式中でインクリメントすることがあります。
    messages[i++] = "Hello, world!";
    messages[i++] = "Welcome to the system.";
    messages[i++] = "Error: Invalid input.";
    messages[i++] = "Goodbye!";
    messages[i++] = "Thank you for visiting.";

三項演算子を使う

単純な条件は三項演算子を使うことで可読性が上がります。

三項演算子
    // [✔️DO] 単純な条件は三項演算子を使用します。
    status = isActive ? "Active" : "Inactive";

    // if だと冗長になります。
    if (isActive)
    {
        status = "Active";
    }
    else
    {
        status = "Inactive";
    }

    // [❌DO NOT] 複雑な条件のときは三項演算子は使いません。
    status = isAdmin
        ? isActive
            ? "Active Admin"
            : "Inactive Admin"
        : isActive
            ? "Active User"
            : "Inactive User";

switch 式を使う

複雑な分岐は switch 式を使うと可読性が上がります。
より複雑な分岐は if-else 式を使います。
読みやすさを重視して使用する構文を決定します。

switch式
    // C#8.0(.NET4.8/Core3.0) からは switch 式が使えます。
    // 戻り値とデフォルトケースを強制できます。
    string status = user switch
    {
        // C#8.0(.NET4.8/Core3.0) からはプロパティパターンマッチが使えます。
        { IsAdmin: true, IsActive: true } => "Active Admin",
        { IsAdmin: true, IsActive: false } => "Inactive Admin",
        { IsAdmin: false, IsActive: true } => "Active User",
        _ => "Inactive User"
    };

    // メソッドとして処理に名前を付けると理解しやすくなります。
    public string GetUserStatus(User user)
    {
        // 従来の switch 文は短いコードを実行するときに使います。
        // 見通しが悪い場合は if-else 文にします。
        switch (user)
        {
            // C#7.0(.NET4.6/Core1.0) からは型パターンと when 句が使えます。
            case User u when u.IsAdmin && u.IsActive:
                Debug.WriteLine($"User: {user.Id}, {user.Name}");
                return "Active Admin";
            case User u when u.IsAdmin && !u.IsActive:
                return "Inactive Admin";
            case User u when !u.IsAdmin && u.IsActive:
                return "Active User";
            default:
                return "Inactive User";
        }
    }

    public string ProcessUserAction(UserAction action)
    {
        string result;

        // 処理が複雑な場合や判定順が重要な場合は if-else 文を使います。
        if (action == UserAction.Login)
        {
            this.LogLoginAttempt();
            result = "User logged in";
        }
        else if (action == UserAction.Logout)
        {
            this.LogLogoutAttempt();
            result = "User logged out";
        }
        else if (action == UserAction.SignUp)
        {
            this.SendWelcomeEmail();
            result = "User signed up";
        }
        else
        {
            result = "Unknown action";
        }

        return result;
    }

    // C#8.0(Core3.0) からはメソッドに式本体を指定できます。
    // 4 case を超える場合は if より switch の方が最適化されます。
    public string GetSeason(int month) => month switch
    {
        12 => "Winter",
        1 => "Winter",
        2 => "Winter",
        3 => "Spring",
        4 => "Spring",
        5 => "Spring",
        6 => "Summer",
        7 => "Summer",
        8 => "Summer",
        9 => "Fall",
        10 => "Fall",
        11 => "Fall",
        _ => "Unknown",
    };
    
    // C#9.0(.NET5.0) からは比較演算子パターンマッチが使えます。
    // パターンマッチの範囲指定は可読性がよくないですが、参考のために載せています。
    public string GetSeason(int month) => month switch
    {
        >= 3 and <= 5 => "Spring",
        >= 6 and <= 8 => "Summer",
        >= 9 and <= 11 => "Fall",
        12 or 1 or 2 => "Winter",
        _ => "Unknown"
    };

unsigned 型は使わない

uint すなわち System.UInt32 は CLS(Common Language Specification) に準拠していません。
public な部分は uint ではなく、int を使用します。
公開されない内部でビット演算をする場合は unsigned 型を使用できます。

例えば uintint で比較すると long に拡張されて判定されます。
不必要に uint を使うと効率が悪くなる場合があります。

unsigned 型はどうしても必要な場合のみ使用します。

言語への非依存性、および言語非依存コンポーネント

const と列挙体はコンパイル時定数

const と列挙体はコンパイル時定数です。
外部のアセンブリで使用してコンパイルすると、exe と dll ができますが、それぞれで埋め込まれます。
その状態でコンパイル時定数を変更し、dll のみコンパイルしなおしても exe は古い情報のままです。
外部のアセンブリへ変更を反映するには全体の再コンパイルが必要です。

その他に再コンパイルが必要な事例がいくつかあります。

メソッドのデフォルト引数を追加した場合はソースコードレベルの互換性があります。
実際には呼び出し側の引数にデフォルト値が渡されるようコンパイルされます。

デフォルト引数の追加
    // 変更前
    void MyMethod(string a)
    {
        ...
    }

    // 変更後
    void MyMethod(string a, string b = "")
    {
        ...
    }

フィールドとプロパティを切り替えた場合はソースコードレベルの互換性があります。
プロパティの実態はメソッドですので、アセンブリレベルでは全然違います。

フィールドとプロパティの書き換え
    // 変更前
    public string MyMember = "";

    // 変更後
    public string MyMember { get; set; } = "";

Null 許容参照型

C#8.0(.NET4.8/Core3.0) からは Null 許容参照型を有効にできます。
コンパイル時に null チェックが行われるため、null チェックのガード節は不要となります。
一般に公開するライブラリの場合は参照元のアプリケーションに依存するため、Null許容参照型を有効にするかの検討が必要です。

csproj で nullable の指定
    <PropertyGroup>
        <LangVersion>8.0</LangVersion>
        <Nullable>enable</Nullable>
    </PropertyGroup>
cs ファイルで nullable の指定
    // ファイル毎にも設定できます。
    // 有効にすると、T? としないと null 許容できません。
    #nullable enabled

    // 無効にすると従来通り、参照型に null が入ります。
    #nullable disable

Null 許容参照型を有効にしていても、構造体と配列においては null が設定される可能性があります。
null 許容参照型 - 既知の落とし穴

EFCore7.0 より古い場合は DbSet<T> には Set<T>null! を設定します。
Null 許容参照型の使用 - DbContext と DbSet

LINQ の Nullable を解除したいときは OfType を使う

Null 許容参照型を有効にしていても Nullable の値を扱う必要があります。
OfType<T>() を利用すると簡単に非 Null 値を取得できます。

Nullableの解除
    var items = new List<int?>();

    // [✔️DO] OfType で int? から null を除外して int にできます。
    var max = items.OfType<int>().Max();

    // null を除外してから int にしています。
    var max = items.Where(x => x.HasValue).Select(x => x.Value).Max();

    //  [❌DO NOT] Nullable<T> は構造体なので、HasValue と Value は必ず参照できます。
    var max = items.Where(x => x?.HasValue ?? false).Select(x => x!.Value).Max();

早期リターンは積極的に使う

早期リターンはネストを減らすことができます。

ガード節とスローヘルパー
    public bool Method(object x, string y)
    {
        // C#7.0(.NET4.6/Core1.0) からは is が使えます。
        // ガード節は多数並ぶことが多いので1行で記述します。
        if (x is null) { return false; }
        if (String.IsNullOrEmpty(y)) { return false; }

        // それ以前
        if (x == null) { return false; }

        // [❌DO NOT] 早期リターンを行わない場合はインデントが深くなります。
        if (x is not null)
        {
            ...
        }

        ...
    }

    public void Method(object x)
    {
        // C#7.0(.NET4.6/Core1.0) からは ?? や throw式 が使えます。
        // [❌DO NOT] トリッキーな記述なので、後述のスローヘルパーを使います。
        _ = x ?? throw new ArgumentNullException(nameof(x));

        // [✔️DO] .NET6.0(C#10) から使えます。
        ArgumentNullException.ThrowIfNull(x);

        // .NET Community Toolkit で使えます。
        Microsoft.Toolkit.Diagnostics.Guard.NotNull(x, nameof(x));

        if (x is null)
        {
            // 直接 throw を行うとメソッドのインライン展開が阻害されるのでメソッド呼び出しで回避します。
             Microsoft.Toolkit.Diagnostics.ThrowHelper.ThrowArgumentNullException(nameof(x));
        }

        // [❌DO NOT] .NET Framework 4 で使えますが、.NET Core では使えません。
        Contract.Requires<ArgumentNullException>(x != null, nameof(x));
        
        ...
    }

よく使う例外

各メソッドで問題がある場合は、下記例外を使用します。
早期リターンのガード節でチェックします。

ArgumentNullException: 引数が null だった場合に投げます。
ArgumentOutOfRangeException: 引数が範囲外だった場合に投げます。
ArgumentException: 上記以外の引数の例外に使います。

InvalidOperationException: 異常な手順であるなど、メソッド呼び出しが不適切な場合に投げます。
NotImplementedException: メソッドスタブを作成した直後の未実装時に投げます。
NotSupportedException: クロスプラットフォームなどで使えない機能があるときに投げます。実行してみるまでわからないのであまりよくありません。

標準例外型の使用

フロー制御に例外は使わない

例外は管理された goto です。
そのためフロー制御に例外を使用すると可読性と保守性が下がります。
ただし、非同期のキャンセル例外はフロー制御に使用されます。

キャンセル例外
    static async void Main()
    {
        try
        {
            var cts = new CancellationTokenSource();
            var task = Task.Run(() => DoWork(cts.Token), cts.Token);
            cts.Cancel();
            await task;
        }
        catch (OperationCanceledException ex)
        {
            // キャンセル例外処理
        }
    }

    // C#7.1(Core2.0) からは default 式が使えます。
    // default はコンパイル時に決定される既定値になります。
    static void DoWork(CancellationToken token = default)
    {
        // 例外で終了するパターン
        while (true)
        {
            token.ThrowIfCancellationRequested();
            ...
        }

        // 例外を発生させないパターン
        while (token.CanBeCancelled && !token.IsCancellationRequested)
        {
            ...
        }
    }

catch 時の throw の使い分け

throw exでリスローとするとスタックトレースが上書きされます。
その場で処理するべきか、呼び出し元へ伝えるべきかで使い分けます。

スローの使い分け
    catch (XxxException)
    {
        // 原因を呼び出し元にそのまま伝える場合
        throw;
    }
    catch (YyyException ex)
    {
        // 原因の詳細がそれ以上必要ない場合
        // 長すぎるスタックトレースが解析の邪魔になる場合
        // 余計な情報を外に漏らさないようにする場合
        throw ex;
    }

catch しない例外

復帰可能な例外は適切に処理し、それ以外はキャッチしない、またはリスローします。
最終的に未処理例外としてキャッチし、ログに記録しておけば修正の手がかりになります。
例外を握りつぶし、ログへの記録もリスローもない場合は修正が困難です。

特に次の例外はコーディングのミスなので、キャッチせず、コードの修正を心がけます。
NullReferenceException
IndexOutOfRangeException

GUIアプリケーションを落とさないためには、イベントハンドラーメソッド内でキャッチし、ユーザーにエラーメッセージを表示します。
可能なら例外報告用のフォームなどを用意しておきます。

未処理例外
    // WPF のメインスレッド(UI スレッド、ディスパッチャースレッド)の未処理例外
    Application.Current.DispatcherUnhandledException += (s, e) =>
    {
        e.Handled = true;
        ...
    };

    // タスクの GC 時の未処理例外
    TaskScheduler.UnobservedTaskException += (s, e) =>
    {
        e.SetObserved();
        ...
    };

    // ドメイン内のあらゆる未処理例外
    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
    {
        ...
    };

catch-all

すべての例外をキャッチするには、catch all を使います。

catch-all
    catch (Exception)
    {
        // CLS準拠例外
    }
    catch
    {
        // CLSに準拠してない例外
        // Native DLL で発生する例外、悪意のあるコードなど
    }

リストの作成

開発ターゲットに合わせて様々な方法でリストを作成できます。

リストの作成
    public void Method(List<Item>? items)
    {
        // C#12(.NET8) からは [](コレクション式) が使えます。
        int[] numbers = [1, 2, 3];

        // 空のコレクションを作成できます。
        foreach (var item in items ?? [])
        {
            ...
        }

        // C#9(.NET5) からはターゲットから型推論できます。
        items ??= new();

        // C#8.0(.NET4.8/Core3.0) からは ??=(Null 合体代入) が使えます。
        items ??= new List<Item>();
    }

    // C#8.0(.NET4.8/Core3.0) 以前で Null 許容参照の設定が無効の場合は、
    // `null` ではなく、なるべく空のコレクションを返します。
    // Null 許容参照が有効で `List<Item>?` となっている場合は、`null` にする意図があります。
    public List<Item> Method()
    {
        ...
        return items ?? [];
    }

    // C#3.0 からはコレクション初期化子が使えます。
    var people = new List<Person>
    {
        new Person{ Name = "Alice", Age = 8 },
        new Person{ Name = "Bob", Age = 2 },
        new Person{ Name = "Charlie", Age = 14 },
    };

    // C#3.0 から var と匿名型が使えます。
    var people = new[]
    {
        new { Name = "Alice", Age = 30 },
        new { Name = "Bob", Age = 25 },
        new { Name = "Charlie", Age = 35 },
    };

    // C#2.0 でジェネリクスが使えるようになりましたが、別途追加する必要がありました。
    List<int> numbers = new List<int>();
    numbers.Add(1);
    numbers.Add(2);
    numbers.Add(3);

    // それ以前は配列の初期化のみできました。
    int[] numbers = { 1, 2, 3 };

    // 省略しない場合
    int[] numbers = new int[] { 1, 2, 3 };

    // 領域の確保のみ
    int[] numbers = new int[3];

引数でコレクションを受けるときは抽象度の高いものを使う

メソッドの引数に IEnumerable<T> を指定することによって、配列が変化しないことを示せます。
IEnumerable<T> は列挙可能なことを示しますが、IList<T> は追加・削除が可能です。
.NET 4.5以降は IReadOnlyList<T> も使えます。

IEnumerable<T> は特性として遅延評価となります。

コレクションの引数
    // [✔️DO] 抽象度の高いメソッド引数
    public void DoAnything(IEnumerable<T> items) { ... }

    // 直接的なメソッド引数
    public void DoAnything(List<T> items) { ... }

ループで削除したいときは RemoveAll() を使う

ループ中にコンテナから項目を削除することはできません。
そのため別のコンテナを用意して移し替えなければいけません。
RemoveAll() を利用することで記述を簡単にできる可能性があります。

コンテナからの削除
    // [✔️DO] items自身から削除できます。
    var removeCount = items.RemoveAll(x => x.IsRemoving);

    // C#3.0 から LINQ が使えます。
    var newItems = items.Where(x => !x.IsRemoving).ToList();

    // [❌AVOID] LINQ を使用しない場合
    var newItems = new List<Item>();
    foreach (var item in items)
    {
        if (!item.IsRemoving)
        {
            newItems.Add(item);
        }
    }

文字列補間

文字列補間を使うと可読性が上がります。

文字列補間
    // [✔️DO] C#10(.NET6.0) からはハンドラパターンに展開され効率化されます。
    // [✔️DO] C#6.0(.NET4.6/Core1.0) から文字列補間が使えます。
    var now = DateTime.Now;
    var s = $"[{now:yyyy/MM/dd HH:mm:ss}] {msg} (pid: {processId}, tid: {threadId})";

    // 複合書式指定も使えます。
    var b = 255;
    var hex = $"0x{b:X4}"; // 0x00FF

    // 文字列補間と逐語的文字列リテラルは同時に使えます。
    var filePath = $@"C:\Logs\User_{userId}_{now:yyyyMMdd}.log";

    // [❌DO NOT] 逐語的文字列リテラルがないと \(バックスラッシュ) が二重になります。
    var filePath = $"C:\\Logs\\User_{userId}_{now:yyyyMMdd}.log";

    // [❌DO NOT] それ以前
    var s = String.Format("[{0:s}] {1} (pid: {2}, tid: {3})", now, msg, processId, threadId);

ヒアドキュメント

生文字列リテラルや逐語的文字列リテラルを使うとヒアドキュメントとして扱えます。

ヒアドキュメント
    // [✔️DO] C#11(.NET7.0) からは生文字列リテラルが使えます。
    // 終了引用符より左のスペースは削除されます。
    var sql = """
        SELECT
          u.user_name
          , o.order_date
        FROM
          users u
          INNER JOIN orders o
            ON o.user_id = u.user_id
        WHERE
          u.user_id = @user_id
          AND o.order_id = @order_id
        ;
        """;

    // [❌DO NOT] ヒアドキュメントを使わない場合は可読性が悪く、修正に手間がかかります。
    var sql = new StringBuilder()
        .AppendLine("SELECT")
        .AppendLine("  u.user_name")
        .AppendLine("  , o.order_date")
        .AppendLine("FROM")
        .AppendLine("  users u")
        .AppendLine("  INNER JOIN orders o")
        .AppendLine("    ON o.user_id = u.user_id")
        .AppendLine("WHERE")
        .AppendLine("  u.user_id = @user_id")
        .AppendLine("  AND o.order_id = @order_id")
        .AppendLine(";")
        .ToString();

    #region sql
    // 逐語的文字列リテラル
    // C# の初期から使えますが、改行もそのまま認識されるためSQL文などの記述に使用できます。
    // インデントのスペースも認識してしまうため、左寄せする必要がありました。
    // インデントがないため region ディレクティブで隠します。
    var sql = @"
SELECT
  u.user_name
  , o.order_date
FROM
  users u
  INNER JOIN orders o
    ON o.user_id = u.user_id
WHERE
  u.user_id = @user_id
  AND o.order_id = @order_id
;
";
    #endregion sql

データベースの列名

データベースでよく使われるイディオムがあります。
作成日時を表す列名を created_at とします。
作成日を表す列名を created_on とします。

PostgreSQLの例
CREATE TABLE access_logs (
  access_log_id BIGSERIAL NOT NULL
  , access_log TEXT DEFAULT '' NOT NULL
  , access_user_id INTEGER DEFAULT 0 NOT NULL
  , access_user_name TEXT DEFAULT '' NOT NULL
  , access_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
  , session_id TEXT DEFAULT '' NOT NULL
  , ip_address TEXT DEFAULT '' NOT NULL
  , machine_name TEXT DEFAULT '' NOT NULL
  , access_json JSONB DEFAULT '{}' NOT NULL
);

アクセスログなどは量が膨大となるので JOIN を避けるため user_name 列を用意しておきます。
また JSON 型を用意しておくことで、任意の内容を構造化して記録できます。

C# の DateTime は値がないことを表すため DateTime? と Nullable で宣言します。
そのため、データベース側の必須ではない日時列は NOT NULL 制約をかけないようにします。
それ以外の列はなるべく NOT NULL を付けます。

イベントの購読

開発ターゲットに合わせて様々な記述方法があります。

イベントの登録
    // .NET4.5/Core3.1 ReactiveProperty
    this.IsRefreshing
        .Subscribe(x => 
        {
            ...
        })
        .AddTo(this._disposables);

    // C#3.0 ラムダ式(メソッドグループへの変換)
    // イベントの解除の必要がなければ積極的に使用できます。
    this.Click += (s, e) =>
    {
        ...
    };

    // [❌DO NOT] C#2.0 匿名メソッド
    this.Click += delegate(object s, EventArgs e)
    {
        ...
    };

    // C#11(.NET7.0) からメソッドグループの変換時にキャッシュされ、つけ外しの効率が上がります。
    // C#2.0 メソッドグループへの変換
    // Event名 += -> tab -> tabで補完できます。
    this.Click += this.Button_OnClick;
    this.Click -= this.Button_OnClick;

    // [❌DO NOT] それ以前
    this.Click += new EventHandler(this.Button_OnClick);

イベントの購読は解除する

イベントの購読を解除しないと参照が残るため GC(ガーベージコレクタ) で解放されなくなります。
イベント元とイベントハンドラメソッドのインスタンスの生存期間が異なる場合は注意が必要です。
手動でイベントを登録した場合は、手動で解除します。

Dispose パターンを使ってイベントを解除します。
Dispose パターン

Rx(Reactive) の文脈では IDisposable を Composite パターンで扱う CompositeDisposable を使います。

リソースの解放は using を使う

ファイルの読み書き、データベースの接続、ネットワークの読み書きなどは外部リソースを使用します。
外部リソースを扱う場合は必ず Dispose() する必要があるので using パターンを使います。

using
    // C#8.0(.NET4.8/Core3.0) から using ステートメントが使えます。
    // 親スコープを抜けるときに Dispose されます。
    using var reader = new StreamReader("example.txt");
    var content = reader.ReadToEnd();

    // 以前より使える using 句はスコープを明示できるため、ブロックで解放したいときに有効です。
    using (var conn = new SqlConnection(connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand("SELECT * FROM Users", conn))
        using (var reader = cmd.ExecuteReader())
        {
            ...
        }
    }

    // using を使わないパターン
    try
    {
        client = new TcpClient(hostname, port);
        networkStream = client.GetStream();
        reader = new StreamReader(networkStream);
        writer = new StreamWriter(networkStream);
        ...
    }
    finally
    {
        // C#6.0(.NET4.6/Core1.0) からは ?.(Null 条件演算子) が使えます。
        writer?.Dispose();
        reader?.Dispose();
        networkStream?.Dispose();
        client?.Close();
    }

Dispose() 後は null を設定する

オブジェクトは Dispose() 後すぐにメモリから開放されるわけではありません。
タイミングによっては問題なくアクセスできてしまします。
null を設定することにより危険なアクセスを避けることができます。
?.(Null 条件演算子) と組み合わせることで、二重に Dispose() することを回避できます。

一部のリソース(EXCEL の COM オブジェクトなど)は、明示的に null を設定しないと解放されません。

Dispose後にnull
    finally
    {
        stream?.Dispose();
        stream = null;
    }

ファイナライザ(デストラクタ)は使わない

ファイナライザは C#8.0 以前はデストラクタと呼ばれていましたが、C++ のデストラクタとは動作が異なるため誤解を避けるためにファイナライザと呼ばれるようになりました。

.NET5 以降はアプリケーションの終了時にファイナライザは呼ばれなくなりました。
外部リソースのソケット閉じ忘れによるコネクション数不足にならないようにきちんと Dispose() します。

きちんと Dispose() している場合はファイナライザは不要です。
ファイナライザがあるとファイナライズキューに追加され、Gen 1 になるので解放が遅くなります。

以前は、高い信頼性が求められる場合、Dispose() 忘れのセーフティとしてファイナライザが使われることがありました。
ファイナライザがある場合は Dispose() したときに、ファイナライズが不要という GC.SuppressFinalize(this) を呼び出しておきます。

ファイナライザ
public class MyClass: IDisposable
{
    ~MyClass()
    {
        this.Dispose(disposing: false);
    }

    ...
}

ファイナライザー (C# プログラミング ガイド)

イベントハンドラメソッドから非同期処理を行う

非同期処理を行う場合は、基本的にイベントハンドラメソッドで async void となります。

OnClosing, OnClosed などのイベントハンドラメソッドでは、await を待たずにアプリケーションを終了してしまうため、 イベントの遅延が必要です。
e.GetDeferral() を呼び出すことで回避できます。

The perils of async void
非同期プログラミングのシナリオ

readonly なフィールドとプロパティをメソッドで初期化する

readonly なフィールドはコンストラクタで初期化しなければなりません。
複雑な初期化の場合は Initialize() メソッドで分離したい場合があります。
out パラメータで渡すことにより値を保証できます。

readonlyの初期化
    public class MyClass
    {
        private readonly int _readonlyField1;

        // getter のみのプロパティはコンストラクタで初期化できます。
        public int ReadonlyProperty1 { get; }
        public int ReadonlyProperty2 { get; }

        public MyClass()
        {
            this.Initialize(
                out this._readonlyField1,
                out var prop1
            );

            // プロパティは out で渡せないので変数を介します。
            this.ReadonlyProperty1 = prop1;

            // 初期化用のメソッドを用意しておくことも有効です。
            this.ReadonlyProperty2 = this.InitializeProp2Value();
        }

        private void Initialize(
            out int field1,
            out int prop1
        )
        {
            // 複雑な初期化
            ...
        }
    }

副作用のあるメソッド

副作用のないメソッド(純粋関数)は、入力に対して必ず同じ結果を返します。
そうでない場合は何かが変更される、副作用のあるメソッドと呼ばれます。

Get など軽くて単純な動作が期待される名前のメソッドに副作用を持たせてはいけません。
return で結果を返す場合は、副作用がないようにします。
Update などは何かを更新するという副作用があることを連想させます。

引数に対して結果を反映したい場合は、構造体と同様に ref をつけて明示的に変化することを示せます。
クラスは参照を渡す(参照の値渡し)ため、ref でなくてもプロパティへの変更を反映できますが、ref とすることで読み手に伝わります。

refの例
// メソッド名から判断できますが、ref がある方が明示できます。
void RefreshNumbers(ref List<int> numbers)
{
    // numbers に変更を加えます。
    ...
}

プロパティの指標

プロパティは Get/Set のような軽量で、副作用を持たない場合に使います。
処理に時間がかかる場合や、呼び出し毎に結果が変わる場合はメソッドにできないか検討します。
DateTime.Now, Environment.TickCount などは公式実装のプロパティですが、不適切な例です。

プロパティで時間のかかる処理を行いたい場合は、OnPropertyChanged イベントにすることを検討します。
MVVM 以外でも Reactive 系ライブラリや INotifyPropertyChanged を使うことで OnPropertyChanged を実装できます。

サンプル: ReactivePropertyのサンプル(一方向)

宣言的に記述する

手続き的に書くよりも、宣言的に書いた方が理解しやすく、バグが減らせます。

宣言的な記述
    // [✔️DO] 宣言的な記述
    var total = numbers.Sum();

    // [❌DO NOT] 手続き的な記述
    var total = 0;
    foreach (var i = 0; i < numbers.Length; i++)
    {
        total += numbers[i];
    }

    // [✔️DO] 宣言的な記述
    var numberString = number switch
    {
        1 => "One",
        2 => "Two",
        3 => "Three",
        _ => "Unknown"
    };

    // [❌DO NOT] 手続き的な記述
    string numberString = "";
    if (number == 1)
    {
        numberString = "One";
    }

    // [✔️DO] 宣言的な記述
    var person = new Person
    {
        Name = "Alice",
        Age = 1,
    };

    // [❌DO NOT] 手続き的な記述
    var person = new Person();
    person.Name = "Alice";
    person.Age = 1;

record の機能

recordrecord struct をプライマリコンストラクタで宣言すると以下のコードが自動的に実装されます。

  • 読み取り専用プロパティの実装(record struct だとミュータブル)
  • Constructor の実装(初期化用)
  • Deconstruct の実装(タプルへの分解)
  • Clone の実装(専用特殊メソッド、シャローコピー)
  • ToString の実装(Person { Id = ..., Name = ... } という書式)
  • Equals の実装(全プロパティ値の比較)
  • GetHashCode の実装(ハッシュ値)

注意点としては、record 型という名前に反して DB のレコードに使用するには向いていません。
各プロパティが { get; init; } で宣言されるので読み取り専用となるため書き換えができません。
DB 用に使用するにはいくつかの工夫が必要です。

record 型は with 式を使うことができます。

サンプル: recordで実装されるクラスのサンプル


パフォーマンスティップス

早すぎる最適化は無駄になるだけでなく、可読性と保守性が犠牲になるため行ってはなりません。
最適化を始めるまえに、クリーンで読みやすいコードにすることで修正が簡単になります。

小手先の最適化よりも全体を見て処理の見直しが効果的です。
手法やアルゴリズムを変えるだけで何百何千倍もの差が出ることがあります。

時間のかかる処理がループ内に含まれる場合はコードスメルです。
一般的に時間のかかる処理は以下の順番です。

  • ネットワーク I/O
  • データベース I/O
  • ディスク I/O
  • メモリアクセス
  • CPU

計測について

少ない修正で最大の効果を得るには計測が必要です。
どれくらい改善されたかを確認するためにも計測を行います。

よほどシビアに速度を求めない限りは Stopwatch で十分です。

計測用メソッドに [Conditinal("DEBUG")] の属性を付けると、デバッグ時のみコールされます。
リリースビルドのときはコンパイルされますが、呼び出しが回避されます。

AOP で各メソッドに属性をつけるだけで計測を追加することができます。
RealProxy クラスによるアスペクト指向プログラミング

DI しているならインターセプターで全メソッドに計測を追加可能です。
メソッド呼び出しの回数と処理時間を記録できれば把握しやすくなります。

ホットコードパスを見つけ出すには VisualStudio のプロファイラを使用します。
プロファイリング ツールの最初の確認 (C#、Visual Basic、C++、F#)

マイクロベンチマークを取りたい場合は BenchmarkDotNet を使用します。
正確なベンチマークにはインライン展開の抑制と DynamicPGO の無効化が必要です。
Visual Studio で BenchmarkDotNet データを分析する

インライン化の抑制
    public int NoInliningProp
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get;
    }
csproj
    <PropertyGroup>
        <!-- DynamicPGOを抑制 -->
        <TieredPGO>false</TieredPGO>
    </PropertyGroup>

最新の実行ランタイムを使用する

最新のランタイムを使用するだけでパフォーマンスが改善されます。
.NET Framework より .NET Core を使います。
.NET Core であれば、最新の .NET への追従は簡単です。
.NET 6 でかなりの高速化がされていますが、.NET 7、.NET 8 ではさらに高速化されています。
.NET9は11月に正式リリースですが、RCになったので仕様は確定されました。

Performance Improvements in .NET 9
Performance Improvements in .NET 8
Performance Improvements in .NET 7
Performance Improvements in .NET 6

ユーザーの操作をブロックしない

非同期処理の async/await を使って UI スレッドをブロックしないようにします。
直接パフォーマンスに寄与することではありませんが、画面がフリーズすることはユーザーにとってストレスです。
I/O がある場合は非同期 I/O のメソッドが提供されていないか確認します。

I/O 以外で、時間のかかる CPU バウンドな処理は Task.Run() にできないか検討します。

アクティブインジケーター、ローディングサークル、プログレスバーの表示もストレスを軽減します。
時間のかかる処理はキャンセルできるようにしておくことも必要です。

スケルトン表示と Lazy ロードも有効です。
量が多い場合は SemaphoreSlim(maxConcurrency) などで同時実行数を制限します。

起動時間の改善

起動時に時間がかかってしまう場合は遅延読み込みを使います。
起動時に必要ない処理は、起動してから非同期(並列)で読み込みます。

初めてアクセスがあったときにインスタンスを作成する遅延初期化も有効です。

起動時間を早めるためには R2R(ReadyToRun) や NativeAOT も有効です。
R2R はいわゆる通常の AOT(Ahead-Of-Time) コンパイルで実行ランタイムが必要です。
NativeAOT は単独で実行可能なバイナリ形式となります。

起動時間を改善できない場合は、スプラッシュスクリーンの表示を検討します。

NativeAOT について

NativeAOT は IL をネイティブコードに変換します。
起動時間の高速化、トリミングによるバイナリサイズの減少、使用メモリの減少、に効果があります。
リフレクションなど実行時に動的に生成する機能は使えません。
LINQ の JIT 最適化もなくなります。

.NET 7 からコンソールアプリで NativeAOT を使えます。
.NET 8 から ASP.NET Core で NativeAOT を使えます。
.NET 8 から MAUI iOS の NativeAOT(実験的) 対応があります。
.NET 9 から UWP で NativeAOT を使えます。
Windows App SDK 1.6 から NativeAOT に対応しました。
.NET Community Toolkit 8.3 から NativeAOT に対応しました。

Limitations in the .NET Native AOT deployment model
ネイティブ AOT の ASP.NET Core サポート
What’s new in Windows App SDK 1.6
.NET Community Toolkit 8.3 is here! NativeAOT, .NET 8 enhancements, and more!

ネットワークアクセスの改善

ネットワークに関しては設計段階の問題も影響してきます。
回線速度は幅があるため、遅い想定で設計する必要があります。

効果的な施策としてはローカル側で適切にキャッシュすることです。
繰り返し表示する変更のない過去データなどはキャッシュできます。
キャッシュはメモリリークの要因になるので、適切に運用します。

サイズの大きい画像などはローカルの一時ストレージに保存します。
画像は適切にシュリンクされているか確認します。

HTTP の場合はレスポンスキャッシュを検討します。
高負荷やスケーラビリティが必要な場合は分散キャッシュを検討します。

先読みして事前準備を非同期で行っておくことも効果的です。
他の行動をとられると無駄になるので、適用箇所は慎重に判断します。

通信量が多い場合は圧縮できるか検討します。
HTTP の場合は gzip 圧縮を有効にできます。
RPC の場合は通信内容に合わせてシリアライザを変更できないか検討します。
バイナリを扱う場合はJSON用のシリアライザよりバイナリ用のシリアライザが効率的です。

TCP/IP は低速開始です。
通信が安定しない場合は速度がリセットされます。
TCP は ACK を必要とするためレイテンシが大きいと低速になります。
他のプロトコルが使えないか検討します。

HttpClient は毎回作成しないようにします。
HttpClient の使用に関するガイドライン

データベースアクセスの改善

クエリ結果を元に、ループで問い合わせるN+1問題は対処します。

不要な列を取得していないか確認します。
列数は少なくとも行数が多い場合や呼び出し回数が多い場合は影響が大きくなります。
できるだけ最適化された DTO(Data Transfer Object) クラスを用意します。

データベースの設定で遅いクエリを監視できるようにします。
スロークエリがあった場合は実行計画を確認します。
インデックスがなくフルスキャンになっている箇所は対処します。
カーディナリティが低いカラムへのインデックスは注意が必要です。
インデックスショットガンとならないようにします。

適切にフィルタリングされていない箇所は対処します。
オプティマイザの統計情報が古い場合は更新を指示します。
DBによってはオプティマイザへのヒントを指示できます。

データの局所性を理解し、クラスタ化インデックスを適切に定義します。
大規模なデータの場合はパーティショニングを検討します。

パラメタライズドクエリだとデータベースのキャッシュが効きやすくなります。
SQLインジェクション対策にもなりますので積極的に使います。
固定値でしか効かない最適化もあるので、使用頻度によっては固定値で書きます。

データ量が多い場合はページネーションにできないか検討します。
ページネーションにはオフセット法とシーク法があります。
また UI の仮想化が利用可能かどうかも検討します。
ページネーションしない場合はチャンクで複数回取得することを検討します。

変更の少ないマスタなどのデータはローカル側のキャッシュを永続化しておくことを検討します。
データの変更があるかは最終更新日時の比較だけで済みます。

排他制御やロック待ちが発生している場合は対応します。
読み取りしかしないことが保証されている場合は IsolationLevel を READ UNCOMMITTED にできます。

リアルタイムで分析業務を行いたい場合はレプリケーションを検討します。
レプリカ側ならテーブルロックするような全件検索も実施できます。

ディスクアクセスの改善

巨大なファイルを扱う場合は、すべてメモリにロードするのではなく、MemoryMappedFile にできないか検討します。
巨大なファイルをシーケンシャルに処理していく場合は、非同期ストリーム(await foreach)が使えます。

大きなファイルを扱う場合は非同期 I/O を使います。
小さなファイルを多量に扱う場合はオーバーヘッドが大きくなるので、非同期 I/O にはしません。
ファイルへの非同期アクセス

メモリに余裕がある場合は、MemoryStream にしてからファイルへ書き込むと改善する可能性があります。

多量のバイト配列が繰り返し必要な場合は ArrayPoolStackalloc を検討します。
MemoryStream の場合は RecyclableMemoryStream を検討します。

繰り返し使うファイルはメモリにキャッシュしてしまいます。

ネットワークでも使いますが System.IO.Pipelines を使うと高性能なバッファ管理ができます。
ReadOnlySequence<byte> を使うためアロケーションなしで処理できます。

メモリアクセスの改善

スタックとヒープの違いと GC を理解します。

GC の改善

余計なヒープを多量に確保してしまうと GC の負荷が高くなります。

基本的には手動で GC を呼び出す必要はありません。
Gen0 のスペースは広いので、多量のオブジェクトを解放したあとは、任意のタイミングで GC を呼び出しておくとよい場合があります。

85KB を超えるオブジェクトは Gen2 に登録されます。
Gen2 の GC はフル GC と呼ばれ負荷が高いです。
任意のタイミングでフル GC をしておき、処理落ちが発生しにくい状況を作ります。
Windows システムの大きなオブジェクト ヒープ

GCモードを制御することもできます。
GC - 待機モード

不要なヒープの回避

不要なヒープを確保しないようにします。
宣言時に new List<T>() して、すぐにメソッドの戻り値を入れるなどはやりがちです。

ループ内で変数を宣言し new() している場合は、ループの外に出せないか検討します。
最適化で解消されることも多いですが、明示した方が確実です。

文字列の改善

文字列型は参照型ですが、イミュータブルです。
そのためループ内で文字列の連結をすると、そのたびにヒープの確保が発生するので StringBuilder を使います。

できるだけ文字列補間($"{ }")を使います。

大文字小文字を区別しない比較は str.ToLower() を使うよりも、バイト単位で比較できる OrdinalIgnoreCase を使います。
ロケールが必要な場合は InvariantCultureIgnoreCase(Cロケール) で比較できないか検討します。

空文字の判定は if (str == "") よりも if (str.Length == 0) を使います。

.NET で扱う文字列は UTF-16 です。
JSON ファイル用で UTF-8 を使う場合は UTF-8 文字列リテラル("あいうえお"u8)の使用を検討します。

不要なボクシングの回避

値型はスタック領域にありますが、ボクシングされるとヒープ領域に確保されます。

構造体を扱うときは、特にボクシングが発生しやすいので注意が必要です。

文字列フォーマット使用時に値型を渡すとボクシングが発生しやすいです。
文字列補間($"{ }")が String.Format(string, params object[]) に変換される場合はボクシングします。
.NET6 以降であればボクシングを避ける最適化が行われます。

System.Enumenum の基底クラスです。
メソッド引数で enumEnum で受けるとボクシングが発生します。
C#7.3 以降であれば、ジェネリックと型パラメータ制約で回避できます。

メソッド引数で構造体をインターフェースで受けるとボクシングされます。
ジェネリックを使うと回避できます。

非ジェネリックを使うとボクシングされます。
object とするのではなく、ジェネリックを使います。

構造体の使用

次の内容を満たす場合のみ構造体にできないか検討します。

  • プリミティブ型のように単一で何かをあらわす場合
  • インスタンスのサイズが 16 Byte 未満
  • ボックス化が必要ない場合
  • 値が変更されない場合

構造体は値型なのでスタック領域に作成されます。
その代わり値コピーが発生するため、 16 Byte を超えると効率が悪くなります。
構造体はプロパティやメソッドを呼ぶだけで防衛的コピーが行われます。

C#7.2(.NET4.7.1/Core2.1) からは readonly struct が使えます。
全てのフィールドを readonly として宣言する必要がありますが、プロパティやメソッド呼び出しによる防衛的コピーを回避できます。

C#7.2(.NET4.7.1/Core2.1) からパラメータの in 修飾子も使えるようになったので、メソッドへ構造体を渡す場合のコピーを回避できます。
readonly struct と合わせて利用するとコピーを減らせます。

C#10(.NET6.0) からは record struct が使えます。
IEquatable<T>GetHashCode() を自動実装するため、同値比較とディクショナリキーへの使用でのボクシングを回避できます。

構造体を扱う場合は readonly record struct にできないか検討します。

Win32API やネイティブ DLL を利用するために構造体を利用することがありますが、通常のクラスでも [StructLayout(LayoutKind.Sequential)] を指定できます。
つまりインスタンスのサイズや用途によって構造体かクラスを使い分けます。

CPUアクセスの改善

CPU がマルチコアになり、シングルスレッドでは性能を伸ばせなくなりました。
非同期(並列)や並列処理を行い、コアを使い切るようにします。
async/await(WhenAll())、TPL(ParallelFor())、PLINQ(AsParallel()) の使用を検討します。
並列書き込みがある場合は自前でロックするか、内部的にロック機構のあるスレッドセーフなクラスが必要です。

SIMD命令を使えないか検討します。
SimdLinq

例外処理はなるべく使わない

例外処理、スタックトレースの取得は重いので、可能なら避けます。
例外で検知するしかない処理もありますが、なるべく例外を制御に使わないようにします。

例えば、例外の発生する Parse() よりも TryParse() を使います。
キャストも isas を優先し、括弧によるキャストはなるべく避けます。

最適化

コアライブラリを記述する場合はクリーンで効率的な方がよいですが、通常のアプリケーションは読みやすさ、書きやすさを優先します。
読みやすさを損なわない範囲であれば、積極的に適用します。
微々たる速度アップのためにメンテナンス性を損なう必要はありません。

局所的に最適化する場合はコメントを併記して意図を説明します。

メソッドのインライン化

メソッドのインライン展開を意識します。
IL が32バイト以下の制限があります。
SharpLab を使うと簡単に IL を確認できます。
SharpLab

ループと例外処理があるとインライン化が阻害されるので、該当箇所だけ静的メソッドやローカルファンクションに分離します。
virtual メソッドもインライン化が阻害されます。
sealed は継承されないので、最適化の対象となります。
メソッドとプロパティへの sealed は冗長ですので、クラスに適用します。
継承が必要ないクラスには sealed を積極的に使います。

ループの最適化

ループは最適化がされやすいです。
ローカル変数などマルチスレッドの安全性が確保されている場合に最適化されます。

基本的に MoveNext() をする foreach よりも、添え字でアクセスする for が高速です。
foreach を使わず for を使えということではありません。
通常は、LINQ や foreach で読みやすさと書きやすさを優先します。
高速化が必要なときは Span<T> にできないか検討します。

for (var i = 0; i < array.length; i++) と配列のlengthを条件に使うと境界値チェックが外れます。
var len = array.length; としたものを使うと最適化されません。
foreach (var a in array)for に最適化されます。

List<T>Span<T> にすると境界値チェックが外れます。
System.Runtime.InteropServices.CollectionsMarshal.AsSpan(); という拡張メソッドで変換できます。
foreach (var s in span) は最適化されるので配列の for よりも高速です。
メソッドの引数として IEnumerable<T> で受けた場合は、実態が Array<T>List<T> だったら Span<T> にキャストして処理すると高速になります。

List<T> などは初めから使う予定のキャパシティを確保しておくと効率的です。
初回確保時は4で、そのあとは足りなくなれば倍々に増えていき、移し替えが発生します。

LINQ の最適化

空かどうかの判定は、Length, Count, IsEmpty を優先し、存在しないときは Any() を使います。
Count() は全評価 O(n) なので使いません。

ToList(), ToArray() を使うと即時評価できます。
Where() によるフィルタリング後に、複数のパターンで LINQ を評価したい場合は、ToList() します。
同様のパターンとして LINQ のあとに複数回 foreach するなら、ToList() します。
そうでなければ複数回すべての評価が繰り返されます。
適切に ToList() を行いますが、過度な ToList() はメモリが無駄に消費され GC が誘発されます。

foreach では IEnumerable<T> より List<T> の方が速いです。

ToList() したなら、Span<T> にできないかも検討します。
LINQ で Span<T> が使えるサードパーティ製のライブラリはいくつか存在します。

ラムダ式で変数のキャプチャがあると暗黙的なクラスが作成されます。
ローカル関数の引数で渡すとキャプチャを避けることができます。
static ローカル関数にするとキャプチャがあるとコンパイルエラーにできます。
ラムダ式はインスタンスメソッドに最適化されているので、静的メソッドを直接渡すと遅くなります。

変数のキャプチャで、フィールド変数をキャプチャする場合は、インスタンスの参照もキャプチャされます。
インスタンスのキャプチャはメモリリークの要因にもなります。
ローカル変数にすることで、直接的なキャプチャを避けることができます。

時間がかかる場合は PLINQ(AsParallel()) の使用を検討します。

非同期の最適化

await すると、ステートマシンのクラスが暗黙的に作成され、ラップされます。
実行時間が非常に短いメソッドは非同期にするとオーバーヘッドの方が大きくなります。
そのため、await Task.Delay(1) は 1ms よりかかります。

Task が作成されると同期コンテキスト(SynchronizationContext)はキャプチャされます。
.ConfigureAwait(false) を設定すると、同期コンテキスト(メインスレッド)へのコンテキスト切り替えが不要になり、キャプチャを避けることができます。
特に ASP.NET Core API の場合は、可能であればすべての非同期メソッドには .ConfigureAwait(false) を設定します。

非同期の Task は参照型なのでヒープです。
Task よりも ValueTask にできないか検討します。

LINQ 内で await する場合はできるだけ即時評価します。
await せずにタスクだけ作成し、あとから await Task.WhenAll() で待つことを検討します。
.NET6 以降は System.Linq.AsyncSelectAwait() が使えます。

ループ内で await するより、Task.Run() でループ全体を非同期にできないか検討します。

UI スレッド内で Thread.Sleep() すると、現在のスレッドが止まるため、await Task.Delay() を使います。

ビットトリック

いくつかの演算は、数式通り計算するよりも速く計算することができます。
CPU命令や最適化の影響をうけるため、基本的にはコンパイラに任せます。
IL時点で最適化されることもありますし、実行時にJITで最適化されるものもあります。

ビットトリックを使ってループ中の分岐命令を削減できる場合は効果的です。

定数畳み込み
    var y = x / 5;
    var y = x * (1 / 5.0); // 定数畳み込み
    var y = x * 0.2; // コンパイル時最適化
剰余演算
    var normal_rem = a % b;

    var div = a / b;
    var perform_rem = a - b * div;
奇数偶数判定
    var isOdd = ((x & 1) == 1);
    var isEven = ((x & 1) == 0);
ビットシフト
    var y = x * 2;
    var y = x << 1;

    var y = x / 2;
    var y = x >> 1;
ビットマスク
    var x = 50;
    var n = 32; // 2の冪乗
    var y = x % n;
    var y = x & (n - 1);

      11_0010 (50)
    & 01_1111 (31)
    --------------
      01_0010 (18)
Swap
    void Swap(ref int a, ref int b)
    {
        a ^= b;
        b ^= a;
        a ^= b;
    }
ビットカウント
    // 1ビットずつ数えると遅いです。
    int CountBit(uint x)
    {
        int count = 0;
        while (x != 0)
        {
            // MSBが1であればカウントを増やす
            count += (int)(x & 1);
            // 右シフト
            x >>= 1;
        }
        return count;
    }

    // CPU命令が最速です。
    var bitCount = BitOperations.PopCount(x);

その他の最適化

static readonly のままでも const 同様に実行時最適化の対象となります。

ループはアンローリングをすると判定回数を削減できます。

interface 型からのメソッド呼び出しだと vtable を参照するので通常のメソッド呼び出しより処理がかかります。
interface のデフォルト実装メソッドの場合は、privatesealed の場合のみ直接呼出しとなります。
abstract の実装メソッドの場合は直接呼出しです。

リフレクションは基本的に遅い処理なので、例えばメンバーの一覧を取得した場合はキャッシュできないか検討します。
特に、Type をキーとする場合は、Static Type Caching が使えます。

Console.Write() が遅いのはバッファがなく毎回 AutoFlush されるからです。
AutoFlush をオフにして、バッファを確保した StreamWriter を使用すると改善されます。

通常の lock を避けて、Interlockedvolatile やメモリバリアを使ってロックフリーにできないか検討します。

ネイティブを使う

C# は開発生産性を重視した言語です。
コンパイル時に様々な安全対策が自動的に組み込まれます。
コンパイル時に自動的に生成されるので、ネイティブな言語に比べると処理が遅くなります。
シンプルな書き心地を実現するために、ソースジェネレーターも利用されています。
極限まで速度が必要な部分はネイティブの処理を呼び出すことを検討します。

csbindgen を使うと Rust 言語を C 形式(extern "C")で簡単に DllImport できます。
csbindgen

開発環境の改善

デバッグは繰り返し行うため、少しでも早い方が効率的です。

Visual Studio やリポジトリの場所は、ウイルススキャンの除外設定をしておきます。
リポジトリが HDD にある場合は SSD にできないか検討します。

プロジェクト数が多いと起動やコンパイルに時間がかかります。
変更が少ないライブラリが多くなってきた場合は、ローカル NuGet にできないか検討します。
参照するソースコード量が減ることで、起動時の分析時間も削減できます。

不要な拡張がある場合は無効化しておきます。
開発環境が貧弱な場合は、IncrediBuild の導入を検討します。

Visual Studio 上で使わないウィンドウを開いている場合は閉じます。

.NET Framework の場合は、同一コードの場合は、同一バイナリにするオプション(決定論的ビルド)を指定します。
Directory.Build.props ファイルを作成しておくと全プロジェクトに適用できます。

Directory.Build.props
    <Project>
        <PropertyGroup>
            <Deterministic>true</Deterministic>
        </PropertyGroup>
    </Project> 

開発環境が仮想マシン上にある場合、CPU の割り当てを増やすときは、Visual Studio の設定も変更します。
Visual Studio はインストール時のコア数に従ってビルド時のスレッドを決定しています。

🔧設定
VisualStudio では以下の手順でビルド時のCPUの割り当てを増やせます。

  1. [ツール] > [オプション] を開きます。
  2. [プロジェクトおよびソリューション] > [ビルドして実行] を開きます。
  3. 「平行してビルドするプロジェクトの数」を変更します。

付録

アーキテクチャ

シンプルさと便利さは、複雑性のトレードオフです。

テストは実施したいのでMVVMは採用します。
IHostを利用するならDIはそのまま使えます。
DIが使えるならインターセプターで開発時のログを強化できます。
どうせならRepositoryパターンにしてDBアクセスをDIすればクリーンアーキテクチャを実現できます。
そこまでやれば移植性もある程度確保できます。
将来、新しい UI が登場した場合でも移行できる可能性が高まります。

コードビハインド(Windows Forms)

コードビハインドは、UI デザインとコードを分離する手法です。
UI と密接に結びついているため、テストが困難です。
直感的なので修正などは容易ですが、コードが肥大化しやすい傾向にあります。
アーキテクチャとしての複雑性は低いですが、適切な切り分けを自力で行わなければなりません。
適切な切り分けができる場合は次のステップを検討します。

コードビハインド(WPF)

MVVM は学習曲線が急なので、WPF のコードビハインドの開発にステップアップすることも検討します。
Windows Forms と変わらない書き心地で XAML の強力な表現力を手に入れることができます。
簡単な表現であれば Windows Forms のように Owner Draw する必要がなくなります。

C# の便利な機能への習熟

C# は日々進化しているため、新しい機能への習熟が必要です。

LINQ
ラムダ式を使って、ループを宣言的に表現できます。
簡単に書けてしまうため、間違った使い方をするとパフォーマンスへの影響がでます。
IEnumerable の遅延実行への理解が必要です。

非同期メソッド
Network I/O, DB, File I/O, ループ処理, などの時間のかかる処理を UI スレッドを止めることなく行えます。
async/await の特性は呼び出し元へ伝播するため、イベントハンドラーメソッドから適用する必要があります。
メインスレッドとスレッドプール実行への理解が必要です。

MVVM(Model-View-ViewModel)

MVVM は UI とイベントとロジックを分離することでテスト容易性を高める手法です。
コードビハインドより複雑性が増しますが、サードパーティのライブラリ(ReactiveProperty) を用いて簡単に View と ViewModel をバインドできます。
緩いレイヤードアーキテクチャなので、モデル内の適切な分割は必要ですが、クリーンアーキテクチャに移行しやすいです。

View: UI層(XAMLなど)、ViewModelをバインドします。
ViewModel: ViewとModelの接続層、データの変換やUIの状態を持ち、ユーザーのアクションを処理します。
Model: データ定義やビジネスロジックなど、View と ViewModel に属さないものはすべて Model です。

MVVM で実装しておくと Windows Forms/WPF/WinUI3 の UI 部分の移植性が確保できます。
ただし、インフラストラクチャ(ストレージ)が分離されていないので、MVVM だけではクロスプラットフォームには対応できません。
クリーンアーキテクチャまでいくとある程度の移植性が確保できます。

DI(Dependency Injection: 依存挿入)

DI は DIP(Dependency Inversion Principle: 依存逆転の原則) を実現するための手段です。
抽象(Inferface、抽象クラス)に依存することにより、疎結合を実現できます。
より複雑性が増しますが、呼び出して使うだけなら難しくありません。

.NET の汎用実行基盤(IHost) には IServiceProvider という DIコンテナの仕組みがあります。
ASP.NET Core, Worker Service, MAUI のテンプレートで使えますが、Forms や WPF にも適用可能です。

例えば ILogger に依存していれば、アプリケーションごとにロガー(NLogやSerilogなど)を切り替えて使うことが可能です。
EntityFrameworkCore で直接 SQL 文を書かずに使用していれば SQLServer/PostgreSQL/SQLite/Oracle の切り替えも可能です。

AOP(Aspect Oriented Programming: アスペクト指向プログラミング)

AOP はクロスカッティング関心事を分離して、コードの可読性や保守性を向上させる手法です。
ログ、セキュリティ、モニタリング、エラーハンドリング、キャッシュなどに利用できます。

例えば Proxy パターンを使用すると、メソッドの前後に開始と終了のログを仕込むことができます。
DI と Proxy パターンを組み合わせることでより柔軟な対応が可能です。

クリーンアーキテクチャ

クリーンアーキテクチャは、ビジネスロジックとインフラストラクチャを分離することでモジュール性を高める手法です。
さらに複雑性が増しますが、移植性が高まり、変更に強くなります。

ソフトウェアを同心円状のレイヤード構造で表現し、依存関係を内向きにすることでロジックとインフラの分離を図ります。
クリーンアーキテクチャを使わない場合は、UI側からインフラ側まで中心を横断する一方向の依存関係となります。
外周のインフラ側にある DB アクセス部分を DI することで、依存関係を逆転して、内向きにできます。

クロスプラットフォーム開発である MAUI の場合に採用するとスマートです。
外周のプラットフォーム部分をインターフェースに依存するようにして、依存関係を逆転します。

Repository パターン

Repository パターンは、ロジックとデータアクセスを分離する手法です。
DDD(Domain-Driven Design: ドメイン駆動設計) で紹介され広く使われるようになりました。
EntityFramework でも使われることが多いです。

UnitOfWork パターン

UnitOfWork パターンはトランザクションを抽象化します。
複数のリポジトリをまとめて操作し、トランザクションを一貫性のあるものに保ちます。
Repository パターンとともに使われることが多いです。


UI フレームワーク

過去の Windows Forms の資産を生かす、または低スペックマシン向けなら Windows Forms にします。
高解像度に対応したい、またはテストを書かないような小規模なアプリの場合は WPF(コードビハインド) を採用します。
自動テストを書きたい場合は WPF(MVVM) を採用します。
モダンなコントロールを使いたい場合は WinUI 3 を検討します。
クロスプラットフォームのアプリの場合は MAUI を検討します。
小、中規模な Web アプリケーションの場合は Blazor Server です。
大規模 Web アプリケーションの場合は ASP.NET Core API と React などを組み合わせます。

Windows Forms (2002-)

ルックアンドフィールは古いですが、低スペックのマシンでも軽快に動作します。

.NET Framework 4.7 以降に高 DPI のサポートが開始され、徐々に改善されました。
Owner Draw で独自描画とかやり始めると高解像度対応に難が出ます。

.NET 8 からは DataBindings が使用でき、MVVM を導入できます。

WPF (2006-)

ルックアンドフィールが改善され、GPU描画がサポートされています。
高解像度へも対応し、相対配置によるレスポンシブ対応ができるようになりました。
柔軟になった分、メモリ消費が多く、パフォーマンスが悪化しやすくなります。
2024年現在、Windows Forms とのパフォーマンスの差異はあまり感じなくなりました。

WPF 以降は XAML がメインストリームとなり、UWP/WinUI/MAUI でも使われています。
MVVM という新しいアーキテクチャが使え、自動テストを書けるようになりました。
もちろん昔ながらのコードビハインドでも記述できます。

WPF-Samples
.NET Community Toolkit
MVVM-Samples

WinUI (UWP 2015-, WinUI2 2018-, WinUI3 2021-)

ルックアンドフィールがさらに改善され、ダークモードにも対応しています。
Project Reunion で UWP から分離され、WinUI 3 となり単独で利用できるようになりました。
サンドボックスで動作していた名残りか、ウィンドウの扱いやネイティブな機能にやや難があります。
ウィンドウサイズを変える場合などは Windows App SDK の API を使用します。

WinUI 3 Gallery
Windows Community Toolkit

デフォルトでは MSIX パッケージ形式となりますので、EXE 形式にしたい場合はプロジェクトの構成を変更します。
Windows App SDK をフル活用する場合はパッケージ化が必要です。

csproj
<PropertyGroup>
- <OutputType>AppContainerExe</OutputType>
- <TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net6.0-windows</TargetFramework>
  <UseWinUI>true</UseWinUI>
- <GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
</PropertyGroup>

MAUI (2022-)

Xamarin の後継です。
2024年現在、なんとか実用できるようになってきましたが、まだまだ開発中です。
多少の不具合に目をつぶれば、クロスプラットフォームを簡単に構築できるのは魅力です。
主に Android と iPhone を対象に共通のデザインを作成できます。
プラットフォーム依存の部分は依然として存在するので、Android と iPhone の知識は必要です。
C# の資産を生かしつつ、Android もしくは iPhone の薄いラッパーとしても使用できます。
Xamarin の資源も生かすことができます。

Windows の場合は画面の解像度が違うので、別で作成することになります。
MVVM とクリーンアーキテクチャで作っておけば、Model は共通化できます。

.NET MAUI Samples
.NET MAUI Community Toolkit

その他の選択肢としては、Avalonia (2011-) や Uno Platform (2018-) などがあります。
Avalonia は日本語入力の扱いに問題があります。

ASP.NET Core Blazor Server (2018-)

ASP、ASP.NET WebForm、ASP.NET MVC、ASP.NET Core Razor Pages を経て、Blazor Server になりました。
バックエンドをAPIとして別に作る場合は ASP.NET Core API が使えます。

Blazor ではコンポーネント単位の表示更新ができるため SPA(Single Page Application) が記述しやすくなっています。

Blazor にはいくつか種類があります。

  • Blazor Server: WEB アプリケーションです。サーバーとの接続が維持されるため不特定多数の使用には向いていません。
  • Blazor WASM: WebAssemblyを使ったWEBアプリケーションです。起動時にランタイムとアプリケーションのロード時間がかかります。
  • Blazor United: Server と WASM を両方使えます。併用する場合は起動時間のストレスはなくなりますが、ブリッジするためのコードが必要です。
  • Blazor Hybrid: WebView でネイティブアプリに組み込めます。起動時間のかかるWASMでも常駐アプリ化できます。

WEBアプリケーションとすることでWEBフロントエンドの豊富な資源が使えます。
特にUIフレームワークのデザインやアニメーションなどの自由度が高いです。

Microsoft FluentUI


プログラミング原則、格言

  • DRY
    DRY(Don't repeat yourself)とは、アプリケーションに必要な「情報」は、重複を避けるべきという考え方です。
    重複があると変更漏れや作業量、コード量の増大につながります。
    DRY原則に反しているシステムをWET(Write Every Twice)なシステムと呼びます。

  • OAOO
    OAOO (Once and only once) とは、コードの機能、ふるまいの重複を避けるべきという考え方です。
    特に「実装」の重複を避けることに焦点を当てています。

  • KISS
    KISS(Keep it simple, stupid)とは、コードや設計を可能な限りシンプルに保つべきという考え方です。
    過度な複雑性を避け、分かりやすく、保守しやすいコードを書くことを目的としています。

  • YAGNI
    YAGNI(You aren't gonna need it)とは、将来の要件を過剰に見込んで無駄な機能や複雑性を追加しないという考え方です。
    必要になるときが来たらその時に実装すればよいというアプローチです。

  • SDP
    安定依存の原則(SDP: Stable-dependencies principle)とは、パッケージ間の依存関係の指針です。
    変更が少なく被参照が多い安定したパッケージが、変更が多い不安定なパッケージに依存してはいけません。
    OCPやDIPに従い抽象化することによりSDPに則した構造となります。

  • SOLID
    SRP, OCP, LSP, ISP, DIPの頭文字からSOLIDとまとめて呼ばれます。

  • SRP
    単一責任の原則(SRP: Single responsibility principle)とは、クラスの変更理由はひとつでなければならないという考え方です。
    責任という観点で見ることが重要です。

  • OCP
    オープン・クローズドの原則(OCP: Open/closed principle)とは、拡張に対して開いていて、修正に対して閉じていなければならないという考え方です。
    抽象化を行い、継承していくことで機能の拡張ができます。
    抽象化しておくと、実装を差し替えるだけでインターフェイスには影響を与えません。
    抽象化は柔軟性と複雑性のトレードオフとなります。

  • LSP
    リスコフの置換原則(LSP: Liskov substitution principle)とは、スーパークラスとサブクラスは置換できなければならないという考え方です。
    サブクラスでオーバーライドされた結果、動作が意図しないものに変わってしまってはいけません。

  • ISP
    インターフェイス分離の原則(ISP: Interface segregation principle)とは、インターフェイスをシンプルに保つための指針です。
    複数のクライアントにおいて、片方のクライアントから参照しないメソッドがある場合は、インターフェースを分離します。

  • DIP
    依存関係逆転の原則(DIP: Dependency inversion principle)とは、依存関係を切り離す抽象化の手法です。
    上位のクラスは下位のクラスに依存するべきではなく、どちらもインターフェイスに依存させます。

  • PIE
    PIE(Program intently and expressively)とは、意図を明確に表現するコードを書くということです。
    書きやすさより読みやすさを重視します。

  • SLA, SLAP
    SLA(Single level of abstraction principle)とは、抽象化レベルを揃えるという考え方です。

  • PLS, PLA, POLA
    驚き最小の原則(PLS: Principle of least surprise)とは、最も自然に思えるものを選択すべきだとする考え方です。

  • ループバックチェック
    名前可逆性という命名に関する指針があります。
    名前は、そのもととなった内容の説明文を復元できなければならないということです。

  • 行ってはならない
    パフォーマンスチューニング、最適化に関する2つの格言です。
    行ってはならない(Don't do it.)
    まだ行ってはならない(Don't do it yet.)
    早すぎる最適化は、無駄になるだけでなく、コードを複雑にしてしまいます。

  • 推測するな、計測せよ
    「計測なくして改善なし」とも言われます。
    どこがボトルネックなのかをはっきりさせてから速度の改善に取り組むという指針です。

  • 求めるな、命じよ
    求めるな、命じよ(TdA: Tell, don't ask.)とは、オブジェクト指向の設計指針です。
    手続き型では、情報を取得して自分で処理をします。
    オブジェクト指向型では、オブジェクトに処理を命じます。

  • デメテルの法則
    デメテルの法則(LoD: Law of Demeter)とは、依存関係を排除する指針となります。
    最小知識の原則(PLK: Principle of least knowledge)や知らないヤツには話しかけない(Don't talk to strangers.)とも呼ばれます。
    オブジェクト自身、オブジェクトに渡されたインスタンス、オブジェクト内部で生成したインスタンス以外には依存してはいけません。
    例えば「あるメソッド」から「別クラスのインスタンス」を取得した場合、「別クラス」への依存関係が発生してしまいます。
    取得するのではなく、そのオブジェクトを持つクラスへ処理を命じます。

  • オペランドの原則
    メソッドの引数にはオペランドのみを指定するべきという指針です。
    この原則に従うとオプションを追加してもインターフェイスを変更する必要がありません。

  • 割れた窓の法則
    割れ窓理論をソフトウェア開発に当てはめた言葉です。
    コードが清潔で美しく保たれている場合、開発者はソレを汚さないよう細心の注意を払うことになります。

  • ボーイスカウト・ルール
    ボーイスカウトには「自分のいた場所は、そこを出ていくときは、来た時よりもキレイにしなければならない」というルールがあります。
    小さな洗練を継続させることで、コードがよりよい方向へ向かっていきます。

  • UNIX哲学
    UNIXの設計哲学です。
    「1. Small is beautiful.」から始まる19の定理からなります。

コントロールのハンガリアン記法

ChatGPT 4o に作ってもらいました。
精査していません。

Forms = Windows Forms
WUI3 = WinUI 3

共通コントロール

Abbr. Controls Framework
btn Button Forms, WPF, WUI3, MAUI
chk CheckBox Forms, WPF, WUI3, MAUI
cbo ComboBox Forms, WPF, WUI3, MAUI
dtp DateTimePicker Forms
dp DatePicker WPF, WUI3, MAUI
tmpck TimePicker WUI3, MAUI
lbl Label Forms, WPF, WUI3, MAUI
lnk LinkLabel Forms
mtxt MaskedTextBox Forms
tblk TextBlock WPF, WUI3, MAUI
txt TextBox Forms, WPF, WUI3, MAUI
rtxt RichTextBox Forms, WPF, WUI3
rdo RadioButton Forms, WPF, WUI3, MAUI
nud NumericUpDown Forms, WPF

共通コントロール(MAUIのみ)

Abbr. Controls Framework
ent Entry MAUI
edt Editor MAUI
ibtn ImageButton MAUI
swt Switch MAUI
sbar SearchBar MAUI
sld Slider MAUI
stp Stepper MAUI

リスト、データ

Abbr. Controls Framework
dgv DataGridView Forms
dg DataGrid WPF, WUI3
lst ListBox Forms, WPF, WUI3
lvw ListView Forms, WPF, WUI3, MAUI
tvw TreeView Forms, WPF, WUI3
cal MonthCalendar Forms
cal Calendar WPF, WUI3, MAUI

レイアウト

Abbr. Controls Framework
flp FlowLayoutPanel Forms
grp GroupBox Forms, WPF, WUI3
pnl Panel Forms, WPF, WUI3
splt SplitContainer Forms
tab TabControl Forms, WPF, WUI3
ti TabItem WPF, WUI3
tlp TableLayoutPanel Forms
grd Grid WPF, WUI3, MAUI
gsp GridSplitter WPF, WUI3

ページ、ビュー(MAUIのみ)

Abbr. Controls Framework
brd Border MAUI
cont ContentControl MAUI
dock DockPanel MAUI
abs AbsoluteLayout MAUI
bind BindableLayout MAUI
flex FlexLayout MAUI
hsl HorizontalStackLayout MAUI
stl StackLayout MAUI
vsl VerticalStackLayout MAUI
ctp ContentPage MAUI
flyp FlyoutPage MAUI
navp NavigationPage MAUI
tabp TabbedPage MAUI
bvw BlazorWebView MAUI
box BoxView MAUI
carvw CarouselView MAUI
colvw CollectionView MAUI
contvw ContentView MAUI
swpvw SwipeView MAUI
tblvw TableView MAUI
wbvw WebView MAUI
scvw ScrollView MAUI

メニュー、ステータス

Abbr. Controls Framework
cms ContextMenuStrip Forms
ctx ContextMenu WPF, WUI3
mnu MenuStrip Forms
mnu Menu WPF, WUI3
sts StatusStrip Forms
status StatusBar WPF, WUI3
tls ToolStrip Forms
tool ToolBar WPF, WUI3
tsc ToolStripContainer Forms
ni NotifyIcon Forms
tip ToolTip Forms, WPF, WUI3

ダイアログ

Abbr. Controls Framework
colDlg ColorDialog Forms, WPF, WUI3
fntDlg FontDialog Forms, WPF, WUI3
openDlg OpenFileDialog Forms, WPF, WUI3
saveDlg SaveFileDialog Forms, WPF, WUI3
folderDlg FolderBrowserDialog Forms, WPF, WUI3

非UIコンポーネント

Abbr. Controls Framework
bgw BackgroundWorker Forms, WPF
fsw FileSystemWatcher Forms, WPF, WUI3
err ErrorProvider Forms
hlp HelpProvider Forms
tmr Timer Forms
-- BindingSource Forms

その他

Abbr. Controls Framework
prg ProgressBar Forms, WPF, WUI3, MAUI
pic PictureBox Forms
cvs Canvas WPF, MAUI
expnd Expander WPF, MAUI
elps Ellipse WPF, MAUI
img Image WPF, MAUI
media MediaElement WPF
ai ActivityIndicator MAUI
gvw GraphicsView MAUI
indvw IndicatorView MAUI
line Line WPF, MAUI
path Path WPF, MAUI
pck Picker MAUI
poly Polygon WPF, MAUI
pline Polyline WPF, MAUI
rec Rectangle WPF, MAUI
refvw RefreshView MAUI
rrec RoundRectangle MAUI

サンプル

.editorconfig の例

.editorconfig
# 上位ディレクトリから .editorconfig 設定を継承する場合は、以下の行を削除します
root = true

# C# ファイル
[*.cs]

#### コア EditorConfig オプション ####
indent_size = 4
indent_style = space
charset = utf-8-bom
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true

#### .NET コーディング規則 ####
# this. と Me. の設定
dotnet_style_qualification_for_event = true:error
dotnet_style_qualification_for_field = true:error
dotnet_style_qualification_for_method = true:error
dotnet_style_qualification_for_property = true:error

# 言語キーワードと BCL の種類の設定
dotnet_style_predefined_type_for_locals_parameters_members = true:error
dotnet_style_predefined_type_for_member_access = false:error

#### C# コーディング規則 ####
# var を優先
csharp_style_var_elsewhere = true
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true


#### 命名スタイル ####
# 名前付けルール
dotnet_naming_rule.private_field_should_be_begins_with__.severity = suggestion
dotnet_naming_rule.private_field_should_be_begins_with__.symbols = private_field
dotnet_naming_rule.private_field_should_be_begins_with__.style = begins_with__

dotnet_naming_rule.private_static_field_should_be_begins_with_s_.severity = suggestion
dotnet_naming_rule.private_static_field_should_be_begins_with_s_.symbols = private_static_field
dotnet_naming_rule.private_static_field_should_be_begins_with_s_.style = begins_with_s_

# 記号の仕様
dotnet_naming_symbols.private_field.applicable_kinds = field
dotnet_naming_symbols.private_field.applicable_accessibilities = private
dotnet_naming_symbols.private_field.required_modifiers = 

dotnet_naming_symbols.private_static_field.applicable_kinds = field
dotnet_naming_symbols.private_static_field.applicable_accessibilities = private
dotnet_naming_symbols.private_static_field.required_modifiers = static

# 命名スタイル
dotnet_naming_style.begins_with__.required_prefix = _
dotnet_naming_style.begins_with__.required_suffix = 
dotnet_naming_style.begins_with__.word_separator = 
dotnet_naming_style.begins_with__.capitalization = camel_case

dotnet_naming_style.begins_with_s_.required_prefix = s_
dotnet_naming_style.begins_with_s_.required_suffix = 
dotnet_naming_style.begins_with_s_.word_separator = 
dotnet_naming_style.begins_with_s_.capitalization = camel_case

ReactivePropertyのサンプル(一方向)

SimpleModel.cs
    public class SimpleModel
    {
        // 一方向バインド用(VM → M)
        public int Id { get; set; }
    }
ViewModelBase.cs
    // ViewModel では ReactiveProperty を使うので INotifyPropertyChanged は不要ですが、
    // DataContext にいれるものは INotifyPropertyChanged を実装しておかないとメモリリークします。
    public class ViewModelBase :  INotifyPropertyChanged, IDisposable
    {
        #region INotifyPropertyChanged
    // フィールド 'PropertyChanged' が割り当てられていますが、値は使用されていません
    #pragma warning disable CS0414

        public event PropertyChangedEventHandler? PropertyChanged = null;

        // 使えないよう Obsolete を設定しておきます。
        // このメソッドを定義する代わりに CS0414 を抑制してもよいです。
        [Obsolete("This method is not intended to be used. Use ReactiveProperty instead.", true)]
        protected virtual void DoNotUse_OnPropertyChanged(string propertyName) =>
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    #pragma warning restore CS0414
        #endregion INotifyPropertyChanged

        #region IDisposable

        private bool _disposed = false;

        public void Dispose()
        {
            if (!this._disposed)
            {
                this.Disposables.Dispose();
                this._disposed = true;
            }
            GC.SuppressFinalize(this);
        }

        #endregion IDisposable

        // Compositeパターンの変数には複数形を用います。
        protected System.Reactive.Disposables.CompositeDisposable Disposables { get; } = new();
    }
SimpleViewModel.cs
    // INotifyPropertyChanged, IDisposable は ViewModelBase で実装済みですが、
    // 慣例として実装しているインターフェイスを列挙します。
    public class SimpleViewModel : ViewModelBase, INotifyPropertyChanged, IDisposable
    {
        private readonly SimpleDbService _service;
        private readonly ILogger _logger;

        private readonly SimpleModel _model = new();

        // フィールドにしないように注意します。
        public ReactivePropertySlim<int> Id { get; } = new();
        public AsyncReactiveCommand SaveCommand { get; } = new();

        // DB アクセス用のサービスは DI コンテナでコンストラクタインジェクションします。
        public SimpleViewModel(
            SimpleDbService service,
            ILogger logger)
        {
            this._service = service;
            this._logger = logger;

            #region Initialize Rx

            // 一方向バインド(VM → M)は ReactivePropertySlim を使います。
            // Subscribe したら Dispose が必要になります。
            this.Id.Subscribe(x => this._model.Id = x)
                .AddTo(this.Disposables);

            this.SaveCommand.Subscribe(async _ => await this.SaveAsync())
                .AddTo(this.Disposables);

            #endregion Initialize Rx
        }

        private async Task SaveAsync()
        {
            // イベントの発生元に近い箇所で try-catch しておくと強制終了は防げます。
            try
            {
                // 実際の処理はモデル側のロジック層で行います。
                // DIで取得していますが、静的メソッドとすることもあります。
                var isSaved = await this._service.SaveAsync(this._model, out var errorMessage);
                // ILogger に対する拡張メソッドを用意しておくと便利です。
                this._logger.WarnIf(!isSaved, errorMessage);
                // エラー内容の表示処理など
                ...
            }
            catch (Exception ex)
            {
                this._logger.Error(ex);
                ...
            }
        }
    }

record で実装されるクラスのサンプル

実際に SharpLab などでデコンパイルすると確認できます。

record型
    public record Person(int Id, string Name);
record型の自動実装
public class Person : IEquatable<Person>
{
    // 型の判別用です。
    protected virtual Type EqualityContract => typeof(Person);
 
    // record や readonly record struct だとイミュータブルになります。
    public int Id { get; init; }
    public string Name { get; init; }

    // record struct だとミュータブルになります。
    //public int Id { get; set; }
    //public string Name { get; set; }

    // コンストラクタ
    public Person(int Id, string Name) =>
        (this.Id, this.Name) = (Id, Name);

    // タプルの分解が使えるようになります。
    public void Deconstruct(out int Id, out string Name) =>
        (Id, Name) = (this.Id, this.Name);

    // コピーコンストラクタ
    protected Person(Person original)
    {
        this.Id = original.Id;
        this.Name = original.Name;
    }

    // 実際にはアクセスできない名前です。シャロ―コピーです。
    public virtual Person Clone() => new Person(this);

    // 中身がわかる文字列を返します。
    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.Append("Person");
        sb.Append(" { ");
		if (this.PrintMembers(sb))
		{
			sb.Append(' ');
		}
        sb.Append(" }");
        return sb.ToString();
    }
 
    protected virtual bool PrintMembers(StringBuilder sb)
    {
        sb.Append("Id");
        sb.Append(" = ");
        sb.Append(this.Id.ToString());
        sb.Append(", ");
        sb.Append("Name");
        sb.Append(" = ");
        sb.Append(this.Name);
        return true;
    }
 
    // record struct でも object へボクシングしなくてもハッシュコードを計算します。
    public override int GetHashCode()
    {
        ...
    }
 
    #region IEquatable<Person>

    // プロパティの全値比較になります。
    public virtual bool Equals(Person other) => (object)other != null
        && this.EqualityContract == other.EqualityContract
        && EqualityComparer<int>.Default.Equals(this.Id, other.Id)
        && EqualityComparer<string>.Default.Equals(this.Name, other.Name);

    public override bool Equals(object obj) => this.Equals(obj as Person);
 
    public static bool operator !=(Person left, Person right) =>
        !(left == right);
    public static bool operator ==(Person left, Person right) =>
        (object)left == right || (left is not null && left.Equals(right));
    // record struct だと object へボクシングしなくても比較できます。
    //public static bool operator ==(Person left, Person right) =>
    //    left.Equals(right);
    
    #endregion IEquatable<Person>
}

おすすめのツール

  • SharpLab: WEB でデコンパイルしたコードやILを確認できます。
  • ILSpy: VisualStudio のデバッガーに統合されているデコンパイラです。
  • dotnet-trace: メソッド単位の処理時間を出力できます。グラフィカルで見やすい Speedscope 形式や Chromium 形式にも変換できます。
  • PerfView, PerfCollect: CPU 使用率、メモリと GC の動作などを分析できます。動作が軽いので本番環境でも使用できます。
  • PerfMon(イベントモニター): Windowsに標準搭載されている分析ツールです。
  • WinDbg, WinDbg Preview: Windows SDK に含まれている解析ツールです。強力なダンプ解析が行えます。
  • SysInternals: いろいろな便利ツールがあります。たとえば PsPing を使えば ICMP がブロックされていても TCP のポートへ向けて疎通確認ができます。
  • Snoop: WPF のビジュアルツリーの調査ができます。
  • Swagger UI: REST API の確認ができます。
  • Postman: REST、gRPC、GraphQL のリクエストを作成して、確認できます。
  • Fiddler: HTTP の通信内容をキャプチャするツールです。
  • Wireshark: ネットワークのパケット解析ツールです。

おすすめのVS拡張機能

VisualStudio には便利な拡張機能がたくさんあります。
有効に活用し、開発効率を上げましょう。

  • ReSharper(有料): 詳細なコーディングルールが強力です。新機能への対応が早くヒントが豊富です。開発者の強い味方です。
  • StyleCop.Analyzers: 拡張ではなく Microsoft.CodeAnalysis.NetAnalyzers と同様の NuGet パッケージです。詳細なコーディングスタイルルールを設定できます。
  • Productivity Power Tools: 便利機能の詰め合わせです。タブスペース混在を修正するツール(Fix Mixed Tabs)や空行を狭く表示するツール(Shrink Empty Lines)が便利です。
  • Trailing Whitespace Visualizer: 末尾の余計なスペースを削除でき、余計なコミットがなくなります。
  • VSColorOutput / Output enhancer: 出力に色が付いて判別しやすくなります。
  • Viasfora: 対になる括弧に色が付いて判別しやすくなります。
  • File Icons: ソリューションエクスプローラーにアイコンがつくので判別しやすくなります。
  • Open in Visual Studio Code: VSCode で開きたいときに便利です。
  • SwitchStartupProject: フロントエンドとバックエンドを協働させたい場合など、スタートアップ対象の切り替えが簡単にできます。

ReSharper を導入した場合、エディタ上でどう変化するかが紹介されています。
First steps with ReSharper

参考資料


83
101
1

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
83
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?