C# CODING GUIDELINES 2024
目次
- このドキュメントについて
- 開発環境
-
命名規則
- 英単語か日本語か
- パスカルケースとキャメルケース
- 二文字の名前(変更)
- 省略形と表記ゆれ
- 名前空間とアセンブリ名
- 標準的に使われている接辞
- メソッド名は動詞句とする
- 論理値を表す名前は is-, has-, can- をつける
- 二重否定は避ける
- コレクションやリストの変数名は複数形とする
- private なフィールドにはアンダースコアを付ける (変更、設定)
- 構造体の public なフィールドは大文字から始める
- this. は省略しない (設定)
- static なメンバーはクラス名をつけて呼び出す
- メンバーアクセス時は定義済みの型を使わない (設定)
- 名前空間とクラス名の重複は避ける
- 列挙体のビットフィールドは複数形とする
- 定数は大文字から始める (変更)
- record のプライマリコンストラクタのパラメータは大文字から始める
- コントロール名は大文字から始める (変更)
- コーディング・レイアウト規則
- コメント規則
-
プラクティス&イディオム
- ファイルスコープ namespace を使う
- ローカル変数の使いまわしはしない
- ローカル変数のスコープは小さくする
- マジックナンバーは使わない
- インクリメントは別の行にする
- 三項演算子を使う
- switch 式を使う
- unsigned 型は使わない
- const と列挙体はコンパイル時定数
- Null 許容参照型
- LINQ の Nullable を解除したいときは OfType を使う
- 早期リターンは積極的に使う
- よく使う例外
- フロー制御に例外は使わない
- catch 時の throw の使い分け
- catch しない例外
- catch-all
- リストの作成
- 引数でコレクションを受けるときは抽象度の高いものを使う
- ループで削除したいときは RemoveAll() を使う
- 文字列補間
- ヒアドキュメント
- データベースの列名
- イベントの購読
- イベントの購読は解除する
- リソースの解放は using を使う
- Dispose() 後は null を設定する
- ファイナライザ(デストラクタ)は使わない
- イベントハンドラメソッドから非同期処理を行う
- readonly なフィールドとプロパティをメソッドで初期化する
- 副作用のあるメソッド
- プロパティの指標
- 宣言的に記述する
- record の機能
- パフォーマンスティップス
- 付録
このドキュメントについて
命名規則、コーディング規則を遵守して生産性を向上させることを目的としています。
自分で書いたコードでも長い間メンテナンスしなければ他人のコードと同じです。
一定の規則に従い、読みやすく、バグの少ない、メンテナンスのしやすいコードを目指しましょう。
規約に従うことは、多くの問題を改善し、技術的負債を減らします。
本書は、以下のページを参考にしています。
Microsoft Learn / .NET / C# / コーディングスタイル / C# 識別子の名前付け規則と表記規則
Microsoft Learn / .NET / C# / コーディングスタイル / 一般的な C# のコード規則
以下のガイドラインは、過去のものなので、最新の事情を反映していませんが、大部分は適用できます。
Microsoft Learn / .NET / フレームワーク デザインのガイドライン / 名前付けのガイドライン
本書では MSDN と記述しますが、移行後の Microsoft Docs(2016), Microsoft Learn(2022) を含みます。
開発環境
開発環境は Visual Studio 2022(以下VS2022) を想定しています。
最新の環境の方が補完機能なども優れているので、生産性やコード品質が上がります。
できるだけ最新のものを使用しましょう。
言語バージョン
最近の C# では、ターゲットフレームワーク(.NET, .NET Framework, .NET Standard)によって使用できる言語バージョンが違います。
例えば、.NET 9.0 をターゲットにすると、言語バージョンは C# 13 が使用できます。
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 |
VS2022 17.12 | -- | .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/9.0 |
Windows 10 1703 | .NET Framework 4.7 | .NET Framework 4.7.1/4.8 | .NET 6.0/9.0 |
Windows 10 1709 | .NET Framework 4.7.1 | .NET Framework 4.7.2/4.8 | .NET 6.0/9.0 |
Windows 10 1803 | .NET Framework 4.7.2 | .NET Framework 4.8 | .NET 6.0/9.0 |
Windows 10 1903 | .NET Framework 4.8 | .NET Framework 4.8 | .NET 6.0/9.0 |
Windows 10 20H2 | .NET Framework 4.8 | .NET Framework 4.8.1 | .NET 6.0/9.0 |
Windows 11 | .NET Framework 4.8 | .NET Framework 4.8.1 | .NET 6.0/9.0 |
Windows 11 22H2 | .NET Framework 4.8.1 | .NET Framework 4.8.1 | .NET 6.0/9.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/9.0 |
Windows Server 1709 | .NET Framework 4.7.1 | .NET Framework 4.7.2 Only | .NET 6.0/9.0 |
Windows Server 1803/1809/2019 | .NET Framework 4.7.2 | .NET Framework 4.8 | .NET 6.0/9.0 |
Windows 2022 | .NET Framework 4.8 | .NET Framework 4.8.1 | .NET 6.0/9.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 のダウンロード
Visual Studio 2022 では .NET Framework 4.0/4.5 の TargetPack がありません。
プロジェクトの 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 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
にする場合はプロパティにできないか検討します。
internal
は private
と同じルールとしていることがありますが、クラス外に公開するため、本書では private
より public
に近い扱いとします。
✔️DO プロパティ: public string UserId { get; set; }
❌DO NOT public/internal: public string UserId;
🔧設定
VisualStudio では以下の手順でルールを登録できます。
ルールの重要度はプロジェクトに合わせて「リファクタ/提案事項/警告/エラー」を選択します。
- [ツール] > [オプション] を開きます。
- [テキスト エディター] > [C#] > [コード スタイル] > [名前指定] を開きます。
- 「Private Field」「Begins with _」というルールを追加します。
- 「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 ではデフォルトで省略するようになっていますが、以下の手順で変更できます。
- [ツール] > [オプション] を開きます。
- [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
- 「
this
の優先」をすべて「優先する」に変更します。
static なメンバーはクラス名をつけて呼び出す
自クラスの静的メンバーを呼び出す場合は ClassName.StaticMember
とする方が可読性が上がります。
this.
を付けるのと同様に読み手の理解を助けます。
static class
の場合は冗長なのでクラス名は省略します。
インスタンスメソッド、静的メソッド、ローカル関数の混同を避けることができます。
s_-
のプレフィックスがなくてもインスタンス変数/静的変数の混同を避けることができます。
// [✔️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 ではデフォルトで定義済みの型を使用するようになっていますが、以下の手順で変更できます。
- [ツール] > [オプション] を開きます。
- [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
- 「定義済みの型の設定」で「メンバーアクセス式の場合」を「フレームワークの型を優先する」に変更します。
名前空間とクラス名の重複は避ける
名前空間とクラス名が同じだと問題があります。
名前空間は表記揺れしないように省略形を避け、重複しないように複数形を用います。
✔️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
プリプロセッサディレクティブでドキュメントフォーマットを無効化します。
#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
ですが、本書では採用しません。
読みやすさを考慮して、ローカル定数より通常の変数を使用します。
ローカル定数よりもグローバル定数にできないか検討します。
✔️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
でインデントを設定できます。
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 も可視化できるのでおすすめです。
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
) とします。
charset = utf-8-bom
end_of_line = crlf
🔧設定
Git の AutoCrLf は無効にしておきます。
設定を変更した場合は再クローンが必要です。
git config --global core.autocrlf false
🔧設定
.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
の { }
(中括弧) は省略しません。
省略すると修正時やマージ時に問題となることがあります。
// [✔️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 ではデフォルトで明示的な型を使用するようになっていますが、以下の手順で変更できます。
- [ツール] > [オプション] を開きます。
- [テキスト エディター] > [C#] > [コード スタイル] > [全般] を開きます。
- 「var を優先」ですべて「var を優先してください」に変更します。
var
のツールチップヒントで型名を確認できます。
ReSharper を利用していると var に型名を表示することができます。
Dictionary<TKey, TValue>は型パラメータのコメントを残す
Dictionary<TKey, TValue>
は変数名だけだと何の値が使われているかわかりません。
宣言時にコメントを残しておくことで読み手の負担が減ります。
// [✔️DO] Dictionary の場合は、何を表すかをコメントで残します。
// { GroupName, Average }
var groupAverages = new Dictionary<string, decimal>();
// ブロックコメントを利用することもできます。
var personNameAges = new Dictionary<string /* Name */, int /* Age */>();
Visual Studio のクリーンアップ
VisualStudio では以下の手順でクリーンアップできます。
標準のクリーンアップ機能が使いにくい場合は、ReSharper の使用を推奨します。
- [分析] > [コードのクリーンアップ] > [クリーンアップ(プロファイル)の実行] を実行します。
クリーンアップの構成で適用するルールを選択できます。
以下のルールは C# ではないので除外しておきます。
- ドキュメントのフォーマット (C++)
- ファイルインクルードグラフの最適化 (C++)
- #include ディレクティブを並べ替える (C++)
- オブジェクト作成の基本設定を適用する (VB)
- IsNot の基本設定を適用する (VB)
以下のルールは適用すると意図しない変更が発生する可能性があります。
- 名前空間の基本設定を適用する
- 名前空間に一致するフォルダーの基本設定を適用する
- アクセシビリティ修飾子を追加します
- 式/ブロック本体の基本設定を適用します
- かっこの基本設定を適用する
- 単一行のコントロール ステートメントに対する必須の波かっこを追加する
- 未使用の値の基本設定を適用する:
_
(discard) の使用 -
var
の基本設定を適用する
コメント規則
コードは実装を、コメントは意図を、コミットは変更理由を、チケットには現象や問題を記載します。
正しい名前と構造を持ったコードは、コメントがなくても読みやすいですが、要約(サマリー)を読む方が、簡単で間違いがありません。
- ブロックコメント(
/* */
)は避け、行コメント(//
)を使います。 - インテリセンスに反映されるため、XMLコメントを使用します。
- コード行の末尾ではなく別の行に記述します。
- 文章となるように記述します。(句点で終わる)
- コメント記号(
//
)とコメント文字の間には空白をひとつ挿入します。(コードは除く)
複数行コメントは使わない (設定)
ブロックコメント(/* */
)は避け、行コメント(//
)を使います。
ブロックコメントを使うとコミットの変更が2行のみで Diff できません。
マージ時にもコンフリクトが起きないため問題となります。
🔧設定
VisualStudio では、Ctrl + K, C
でコードをコメントアウトできます。
Ctrl + /
のショートカットに変更しておくと便利です。
- [ツール] > [オプション] を開きます。
- [環境] > [キーボード] を開きます。
- ショートカットを設定できます。
行末コメントは使わない
行末のコメントは編集の邪魔になるため避けます。
// [✔️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 を使います。
// [✔️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)
{
...
}
}
// [❌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 式を使います。
読みやすさを重視して使用する構文を決定します。
// 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
型を使用できます。
例えば uint
と int
で比較すると long
に拡張されて判定されます。
不必要に uint
を使うと効率が悪くなる場合があります。
unsigned
型はどうしても必要な場合のみ使用します。
const と列挙体はコンパイル時定数
const
と列挙体はコンパイル時定数です。
外部のアセンブリで使用してコンパイルすると、exe と dll ができますが、それぞれで埋め込まれます。
その状態でコンパイル時定数を変更し、dll のみコンパイルしなおしても exe は古い情報のままです。
外部のアセンブリへ変更を反映するには全体の再コンパイルが必要です。
その他に再コンパイルが必要な事例がいくつかあります。
メソッドのデフォルト引数を追加した場合は、ソースコードレベルの互換性があります。
実際には呼び出し側の引数にデフォルト値が渡されるようコンパイルされます。
// 変更前
void MyMethod(string a)
{
...
}
// 変更後
void MyMethod(string a, string b = "")
{
...
}
フィールドとプロパティを切り替えた場合は、単純な Get/Set
のみソースコードレベルの互換性があります。
プロパティの実態はメソッドです。
// 変更前
public string MyMember = "";
// 変更後
public string MyMember { get; set; } = "";
Null 許容参照型
C#8.0(.NET4.8/Core3.0) からは Null 許容参照型を有効にできます。
コンパイル時に null チェックが行われるため、null チェックのガード節は不要となります。
一般に公開するライブラリの場合は参照元のアプリケーションに依存するため、Null許容参照型を有効にするかの検討が必要です。
<PropertyGroup>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
// ファイル毎にも設定できます。
// 有効にすると、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 値を取得できます。
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; }
// [❌AVOID] 早期リターンを行わない場合はインデントが深くなります。
if (x is not null)
{
...
}
...
}
public void Method(object x)
{
// C#7.0(.NET4.6/Core1.0) からは ?? や throw式 が使えます。
// [❌AVOID] 短いメソッドの場合はインライン展開の妨げになるため後述のスローヘルパーを使います。
_ = x ?? throw new ArgumentNullException(nameof(x));
// [✔️DO] .NET6.0(C#10) から使えます。
ArgumentException.ThrowIfNullOrWhiteSpace(x);
// .NET Community Toolkit で使えます。
Microsoft.Toolkit.Diagnostics.Guard.NotNull(x, nameof(x));
if (x is null)
{
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 (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
とします。
CREATE TABLE access_logs (
access_log_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
, 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 パターンを使います。
// 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
を設定しないと解放されません。
finally
{
stream?.Dispose();
stream = null;
}
ファイナライザ(デストラクタ)は使わない
ファイナライザは C#8.0 以前はデストラクタと呼ばれていましたが、C++ のデストラクタとは動作が異なるため誤解を避けるためにファイナライザと呼ばれるようになりました。
.NET5 以降はアプリケーションの終了時にファイナライザは呼ばれなくなりました。
外部リソースのソケット閉じ忘れによるコネクション数不足にならないようにきちんと Dispose()
します。
きちんと Dispose()
している場合はファイナライザは不要です。
ファイナライザがあるとファイナライズキューに追加され、GCの世代が Gen1 になるので解放が遅くなります。
以前は、高い信頼性が求められる場合、Dispose()
忘れのセーフティとしてファイナライザが使われることがありました。
ファイナライザがある場合は Dispose()
したときに、ファイナライズが不要という GC.SuppressFinalize(this)
を呼び出しておきます。
public class MyClass: IDisposable
{
~MyClass()
{
this.Dispose(disposing: false);
}
...
}
イベントハンドラメソッドから非同期処理を行う
非同期処理を行う場合は、基本的にイベントハンドラメソッドで async void
となります。
async void
のメソッドの場合は必ず try-catch
で囲みます。
非同期処理を await
できないため、GC のタイミングで未処理例外が発生します。
OnClosing, OnClosed
などのイベントハンドラメソッドでは、await
を待たずにアプリケーションを終了してしまうため、 イベントの遅延が必要です。
e.GetDeferral()
を呼び出すことで回避できます。
The perils of async void
非同期プログラミングのシナリオ
readonly なフィールドとプロパティをメソッドで初期化する
readonly
なフィールドはコンストラクタで初期化しなければなりません。
複雑な初期化の場合は Initialize()
メソッドで分離したい場合があります。
out
パラメータで渡すことにより値を保証できます。
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(), Set()
など軽くて単純な動作が期待される名前のメソッドに副作用を持たせてはいけません。
Fetch(), Find()
で結果を返すメソッド名の場合は、副作用がないようにします。
Update(), Refresh()
などの更新するメソッド名の場合は、副作用があります。
引数に対して結果を反映したい場合は、構造体と同様に 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 = "Unknown"!;
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 の機能
record
や record struct
をプライマリコンストラクタで宣言すると以下のコードが自動的に実装されます。
- 読み取り専用プロパティの実装(
record struct
だとミュータブル) - Constructor の実装(初期化用)
- Deconstruct の実装(タプルへの分解)
- Clone の実装(専用特殊メソッド、シャローコピー)
- ToString の実装(
Person { Id = ..., Name = ... }
という書式) - Equals の実装(全プロパティ値の比較)
- GetHashCode の実装(ハッシュ値)
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;
}
<PropertyGroup>
<!-- DynamicPGOを抑制 -->
<TieredPGO>false</TieredPGO>
</PropertyGroup>
最新の実行ランタイムを使用する
最新のランタイムを使用するだけでパフォーマンスが改善されます。
.NET Framework より .NET Core を使います。
.NET Core であれば、最新の .NET への追従は簡単です。
.NET 6 でかなりの高速化がされていますが、.NET 7、.NET 8 ではさらに高速化されています。
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
にしてからファイルへ書き込むと改善する可能性があります。
多量のバイト配列が繰り返し必要な場合は ArrayPool
と Stackalloc
を検討します。
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()
している場合は、ループの外に出せないか検討します。
最適化で解消されることも多いですが、明示した方が確実です。
while (true)
{
// [❌DO NOT] 無駄な new
var items = new List<string>();
items = this.CreateItems();
...
}
文字列の改善
文字列型は参照型ですが、イミュータブルです。
そのためループ内で文字列の連結をすると、そのたびにヒープの確保が発生するので 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.Enum
は enum
の基底クラスです。
メソッド引数で enum
を Enum
で受けるとボクシングが発生します。
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()
を使います。
キャストも is
か as
を優先し、括弧によるキャストはなるべく避けます。
最適化
コアライブラリを記述する場合はクリーンで効率的な方がよいですが、通常のアプリケーションは読みやすさ、書きやすさを優先します。
読みやすさを損なわない範囲であれば、積極的に適用します。
微々たる速度アップのためにメンテナンス性を損なう必要はありません。
局所的に最適化する場合はコメントを併記して意図を説明します。
メソッドのインライン化
メソッドのインライン展開を意識します。
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.Async
の SelectAwait()
が使えます。
ループ内で 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 rem = a % b;
var div = a / b;
var fast_rem = a - b * div;
var isOdd = ((x & 1) == 1);
var isEven = ((x & 1) == 0);
var mul = x * 2;
var fast_mul = x << 1;
var div = x / 2;
var fast_div = 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)
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 fast_bitCount = BitOperations.PopCount(x);
その他の最適化
static readonly
のままでも const
同様に実行時最適化の対象となります。
ループはアンローリングをすると判定回数を削減できます。
interface
型からのメソッド呼び出しだと vtable を参照するので通常のメソッド呼び出しより処理がかかります。
interface
のデフォルト実装メソッドの場合は、private
か sealed
の場合のみ直接呼出しとなります。
abstract
の実装メソッドの場合は直接呼出しです。
リフレクションは基本的に遅い処理なので、例えばメンバーの一覧を取得した場合はキャッシュできないか検討します。
特に、Type
をキーとする場合は、Static Type Caching が使えます。
Console.Write()
が遅いのはバッファがなく毎回 AutoFlush
されるからです。
AutoFlush
をオフにして、バッファを確保した StreamWriter
を使用すると改善されます。
通常の lock
を避けて、Interlocked
や volatile
やメモリバリアを使ってロックフリーにできないか検討します。
ネイティブを使う
C# は開発生産性を重視した言語です。
コンパイル時に様々な安全対策が自動的に組み込まれます。
コンパイル時に自動的に生成されるので、ネイティブな言語に比べると処理が遅くなります。
シンプルな書き心地を実現するために、ソースジェネレーターも利用されています。
極限まで速度が必要な部分はネイティブの処理を呼び出すことを検討します。
csbindgen
を使うと Rust 言語を C 形式(extern "C"
)で簡単に DllImport
できます。
csbindgen
開発環境の改善
デバッグは繰り返し行うため、少しでも早い方が効率的です。
Visual Studio やリポジトリの場所は、ウイルススキャンの除外設定をしておきます。
リポジトリが HDD にある場合は SSD にできないか検討します。
プロジェクト数が多いと起動やコンパイルに時間がかかります。
変更が少ないライブラリが多くなってきた場合は、ローカル NuGet にできないか検討します。
参照するソースコード量が減ることで、起動時の分析時間も削減できます。
不要な拡張がある場合は無効化しておきます。
開発環境が貧弱な場合は、IncrediBuild の導入を検討します。
Visual Studio 上で使わないウィンドウを開いている場合は閉じます。
.NET Framework の場合は、同一コードの場合は、同一バイナリにするオプション(決定論的ビルド)を指定します。
Directory.Build.props
ファイルを作成しておくと全プロジェクトに適用できます。
<Project>
<PropertyGroup>
<Deterministic>true</Deterministic>
</PropertyGroup>
</Project>
開発環境が仮想マシン上にある場合、CPU の割り当てを増やすときは、Visual Studio の設定も変更します。
Visual Studio はインストール時のコア数に従ってビルド時のスレッドを決定しています。
🔧設定
VisualStudio では以下の手順でビルド時のCPUの割り当てを増やせます。
- [ツール] > [オプション] を開きます。
- [プロジェクトおよびソリューション] > [ビルドして実行] を開きます。
- 「平行してビルドするプロジェクトの数」を変更します。
付録
アーキテクチャ
シンプルさと便利さは、複雑性のトレードオフです。
テストは実施したいのでMVVMは採用します。
MVVMを採用することで宣言的なUIの記述になります。
IHostを利用するならIServiceProviderによるDIの仕組みが使えます。
DIが使えるならインターセプターで開発時のログを強化できます。
どうせならDBアクセスをDIすればクリーンアーキテクチャを実現できます。
Repositoryパターンを入れればDBなしでテストするためのMockを作成できます。
コードビハインド(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: ドメイン駆動設計) で紹介され広く使われるようになりました。
DBアクセスへのMockを作成できます。
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描画がサポートされています。
高解像度へも対応し、相対配置によるレスポンシブ対応ができるようになりました。
柔軟になった分、メモリ消費が多く、パフォーマンスが悪化しやすくなります。
とくに DataGrid で項目数が多いとレンダリング、レイアウト再計算あたりに難があります。
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 をフル活用する場合はパッケージ化が必要です。
<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 の知識は必要です。
Shell を利用しなければ、Android もしくは iPhone の薄いラッパーとして C# の資産を生かせます。
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フレームワークのデザインやアニメーションなどの自由度が高いです。
プログラミング原則、格言
-
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 設定を継承する場合は、以下の行を削除します
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のサンプル(一方向)
public class SimpleModel
{
// 一方向バインド用(VM → M)
public int Id { get; set; }
}
// 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();
}
// 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 などでデコンパイルすると確認できます。
public record Person(int Id, string Name);
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
参考資料
-
Microsoft Learn / .NET / C# / コーディングスタイル / C# 識別子の名前付け規則と表記規則
-
書籍: リーダブルコード -より良いコードを書くためのシンプルで実践的なテクニック-
-
書籍: .NETのクラスライブラリ設計 改訂新版
-
書籍: Effective C# 6.0/7.0
-
書籍: More Effective C# 6.0/7.0
-
書籍: CODE COMPLETE 第2版 上・下 -完全なプログラミングを目指して-
-
書籍: ルールズ・オブ・プログラミング より良いコードを書くための21のルール
-
書籍: 達人プログラマー 第2版 熟達に向けたあなたの旅
-
書籍: プリンシプル オブ プログラミング -3年目までに身につけたい一生役立つ101の原理原則-