C#コード規約の基本と実践|チーム開発で迷わない命名・書き方・設計ルール完全ガイド
C#コード規約の基本と実践|チーム開発で迷わない命名・書き方・設計ルール完全ガイド
はじめに
C#でチーム開発を進めていると、同じ処理でも開発者によって書き方が異なることがあります。
たとえば、ローカル変数でvarを使うか、型名を明示するか、privateフィールドにアンダースコアを付けるか、メソッドの中で早期returnを使うかといった判断です。どの書き方もコンパイルできる場合、明確な基準がなければレビューのたびに議論が発生します。
C#コード規約は、単に見た目をそろえるためのルールではありません。コードを読みやすくし、変更時の判断を速くし、レビューや保守にかかる負担を減らすための共通基準です。
ただし、細かいルールを大量に作ればよいわけではありません。自動化できる内容はツールに任せ、人が判断すべき設計や可読性にレビュー時間を使うことが重要です。
本記事では、C#コード規約の基本から命名規則、コードスタイル、設計ルール、チームでの運用方法、自動化の設定例までを解説します。
1. C#コード規約とは?チーム開発で必要になる理由
1-1. C#コード規約の目的は「読みやすさ」と「迷わない判断基準」をそろえること
C#コード規約とは、ソースコードの書き方や名前の付け方、ファイル構成、設計上の判断基準などをチーム内で統一するルールです。
主な目的は次の2つです。
誰が書いても同じようなコードに見える状態を作る
実装やレビューで迷ったときの判断基準をそろえる
コード規約がないチームでは、開発者ごとに書き方が変わります。
var userName = user.Name;string userName = user.Name;どちらも正しいコードですが、同じプロジェクト内で方針が統一されていなければ、レビューで本質的ではない議論が発生します。
コード規約があれば、「右辺から型が明らかな場合はvarを使う」といった共通認識に基づいて判断できます。その結果、レビューでは命名の分かりやすさ、仕様の正しさ、責務の分離といった重要な点に集中できます。
1-2. コーディング規約・命名規則・設計ルールの違い
C#コード規約は、大きく3種類に分けられます。
コーディング規約は、インデント、改行、中かっこ、空白、varの使用など、コードの見た目や構文の選び方を定めるルールです。
命名規則は、クラス、メソッド、変数、プロパティ、フィールドなどの名前をどのように付けるかを定めます。
設計ルールは、クラスの責務、レイヤー間の依存関係、例外処理、アクセス修飾子など、コードの構造を決める基準です。
たとえば、次のルールはそれぞれ種類が異なります。
インデントはスペース4文字にする:コーディング規約
publicメソッドはPascalCaseにする:命名規則
ControllerからRepositoryを直接呼び出さない:設計ルール
フォーマットは自動化しやすい一方、設計ルールは機械的に判定しにくい傾向があります。そのため、すべてを同じ方法で運用するのではなく、自動チェックとコードレビューを使い分ける必要があります。
1-3. 個人開発とチーム開発でコード規約の重要度が変わる理由
個人開発では、自分が理解できる範囲で書き方を統一していれば、大きな問題は起こりにくいでしょう。しかし、チーム開発ではコードを書く人と読む人が異なります。
さらに、次のような状況が発生します。
別の開発者が既存機能を修正する
新しいメンバーが途中から参加する
数か月後に過去の実装を見直す
レビュー担当者が複数いる
障害対応で短時間にコードを理解する必要がある
このような環境では、コードの書き方が開発者ごとに違うだけで理解コストが高くなります。
コード規約を整えることは、現在の開発者だけでなく、将来そのコードを保守する人のための投資です。小規模なチームでも、命名、フォーマット、例外処理、非同期処理など、頻繁に判断が分かれる項目は早めに決めておくと効果があります。
1-4. Microsoft公式のC#コーディング規則をベースに考える
C#コード規約をゼロから作る必要はありません。基本的には、Microsoftが公開しているC#のコーディング規則や.NETの命名ガイドラインを土台にするとよいでしょう。
一般的には、次のような方針が広く採用されています。
クラス、メソッド、プロパティはPascalCase
ローカル変数と引数はcamelCase
インターフェイス名には
Iを付ける非同期メソッド名には
Asyncを付ける中かっこを省略しない
意味の分かる名前を使う
public APIには必要に応じてXMLコメントを書く
公式の考え方をベースにすると、新しいメンバーが理解しやすくなり、Visual Studioや各種Analyzerの標準設定とも合わせやすくなります。
ただし、公式ルールをすべて無条件に採用する必要はありません。プロジェクトの種類や既存コードとの整合性を考え、必要な部分を選択することが大切です。
1-5. 厳しすぎる規約が逆効果になるケース
コード規約は多いほどよいわけではありません。細かすぎる規約は、開発速度と可読性をかえって低下させることがあります。
たとえば、次のような規約は運用負荷が高くなりがちです。
すべてのメソッドを決められた行数以内にする
すべてのクラスやprivateメソッドにXMLコメントを必須とする
変数名の文字数を一律に制限する
LINQの使用を全面的に禁止する
警告の重要度に関係なく、すべてビルドエラーにする
重要なのは、ルールを守ること自体ではなく、品質と開発効率を向上させることです。
規約を追加するときは、「このルールによって、どの問題を防げるのか」「自動化できるか」「例外を認めるべき場面はあるか」を確認しましょう。
2. C#コード規約で最初に決めるべき基本ルール
2-1. インデント・改行・空白のルール
C#では、インデントをスペース4文字にそろえる方法が一般的です。タブを使う場合は、エディターによって表示幅が変わらないようにチーム設定を統一する必要があります。
演算子の前後やカンマの後には空白を入れます。
var totalPrice = unitPrice * quantity;var users = new[] { user1, user2, user3 };次のような詰まった書き方は避けます。
var totalPrice=unitPrice*quantity;var users=new[]{user1,user2,user3};長い式は、処理のまとまりが分かる位置で改行します。
var activeUsers = users.Where(user => user.IsActive).OrderBy(user => user.Name).ToList();1行の最大文字数は、絶対的な制限にする必要はありません。100文字や120文字などを目安にしつつ、不自然な改行によって読みづらくならないように運用します。
2-2. 中かっこ・ブロックの書き方
C#では、if、for、foreach、whileなどの中かっこを省略しないルールがおすすめです。
if (user is null){return;}1行だけでも中かっこを付けておけば、後から処理を追加した際のミスを防ぎやすくなります。
if (user is null)LogWarning();return;</span></code></pre><p><span>上記はインデント上では</span><code dir="ltr"><span>return</span></code><span>も条件内に見えますが、実際には常に実行されます。中かっこを必須にすれば、このような誤解を避けられます。</span></p><p><span>名前空間については、従来のブロック形式とファイルスコープ形式のどちらを使うか決めます。</span></p><pre dir="ltr"><code dir="ltr"><span>namespace SampleApp.Services;
public sealed class UserService{}
ファイルスコープ名前空間を使うと、インデントを1段減らせます。.NETの新しいプロジェクトでは採用しやすい形式ですが、既存プロジェクトでは一括変更による差分の増加に注意しましょう。
2-3. usingディレクティブの並び順と不要なusingの扱い
usingディレクティブは、並び順を統一します。代表的なルールは次のとおりです。
Systemで始まる名前空間外部ライブラリの名前空間
自社またはプロジェクト内の名前空間
各グループ内をアルファベット順に並べる
using System.Text;using Microsoft.Extensions.Logging;
using SampleApp.Domain.Users;using SampleApp.Infrastructure;
ただし、グループ間に空行を入れるかどうかはチームで決めれば問題ありません。
不要なusingは削除します。Visual Studioのコードクリーンアップやdotnet formatを利用すれば、自動的に整理できます。
global usingを使う場合は、どの名前空間を共通化するかを慎重に選びます。頻繁に使用する標準名前空間には有効ですが、ドメイン固有の名前空間を大量にglobal usingへ追加すると、型の出所が分かりにくくなります。
2-4. 1ファイル1クラスを基本にするか
基本方針としては、1ファイルに1つの主要な型を定義し、ファイル名と型名を一致させると探しやすくなります。
UserService.cs└─ UserServiceただし、1ファイル1クラスを絶対的なルールにする必要はありません。次のような型は、関連する型と同じファイルに置く選択肢があります。
非公開の小さな補助型
特定クラスだけで使うネスト型
1つの機能を構成する短いrecord
テスト専用の小さなスタブ
複数の主要なpublicクラスを1ファイルに置くと、検索や変更履歴の確認が難しくなるため、原則として分割します。
2-5. ファイル名・フォルダ構成・名前空間のそろえ方
ファイル名は、主要な型名と一致させます。
OrderService.csOrderController.csOrderRepository.csOrderDto.csフォルダ構成と名前空間も、可能な範囲で対応させます。
SampleApp└─ Features└─ Orders├─ OrderService.cs└─ OrderDto.csnamespace SampleApp.Features.Orders;ただし、フォルダ階層をそのまますべて名前空間へ反映すると、名前空間が過度に長くなる場合があります。技術レイヤーで分けるのか、機能単位で分けるのかを先に決めましょう。
たとえば、機能単位でまとめる場合は次のような構成にできます。
Features├─ Users│ ├─ UserController.cs│ ├─ UserService.cs│ └─ UserRepository.cs└─ Orders├─ OrderController.cs├─ OrderService.cs└─ OrderRepository.csプロジェクトの規模が大きくなるほど、ファイルをどこに置くべきか迷わない構成が重要になります。
2-6. コメントを書く場所と書きすぎを防ぐ基準
コメントは、コードを日本語に言い換えるためではなく、コードだけでは伝わらない背景や理由を説明するために使います。
悪い例は、処理内容をそのまま説明するコメントです。
// ユーザーを取得するvar user = repository.GetById(userId);改善するなら、「なぜその処理が必要なのか」を書きます。
// 退会済みユーザーも監査画面では表示する必要があるため、通常の検索条件を使用しないvar user = repository.GetIncludingDeleted(userId);コメントを書く対象として適しているのは、次のような内容です。
一見すると不要に見える処理の理由
外部システムの制約
仕様上の例外
アルゴリズムの意図
将来修正する必要がある既知の課題
コードを読めば分かる内容は、コメントではなく命名やメソッド分割で表現することを優先します。
3. C#の命名規則|クラス・メソッド・変数・プロパティの決め方
3-1. PascalCase・camelCase・_camelCaseの使い分け
C#の代表的な命名形式は、PascalCase、camelCase、_camelCaseの3つです。
| 対象 | 推奨形式 | 例 |
|---|---|---|
| クラス | PascalCase | UserService |
| 構造体 | PascalCase | Money |
| 列挙型 | PascalCase | OrderStatus |
| インターフェイス | I+PascalCase | IUserRepository |
| メソッド | PascalCase | FindUser |
| プロパティ | PascalCase | DisplayName |
| publicフィールド | PascalCase | DefaultTimeout |
| 引数 | camelCase | userId |
| ローカル変数 | camelCase | activeUsers |
| privateフィールド | _camelCase | _userRepository |
privateフィールドにアンダースコアを付けるかどうかは、C#コード規約で意見が分かれる項目です。重要なのは、プロジェクト内で統一されていることです。
3-2. クラス名・構造体名・列挙型名の命名ルール
クラス名は、役割や概念を表す名詞または名詞句にします。
public sealed class UserService{}public sealed class PaymentProcessor{}
public sealed class OrderValidator{}
Manager、Helper、Utilityのような曖昧な名前は、責務が広がりやすいため注意が必要です。
public sealed class UserManager{}何を管理するクラスなのか分からない場合は、役割を具体化します。
public sealed class UserRegistrationService{}public sealed class UserAuthenticationService{}
構造体やrecordも、表現する値や概念が分かる名前にします。
public readonly record struct Money(decimal Amount, string Currency);列挙型は単数形を基本とします。
public enum OrderStatus{Pending,Paid,Shipped,Canceled}Flags属性を使用し、複数の値を組み合わせる列挙型は複数形にします。
[Flags]public enum UserPermissions{None = 0,Read = 1,Write = 2,Delete = 4}3-3. インターフェイス名はIプレフィックスを付ける
C#では、インターフェイス名の先頭にIを付けるのが一般的です。
public interface IUserRepository{User? FindById(Guid userId);}実装クラスでは、役割が一つならIを外した名前を使えます。
public sealed class UserRepository : IUserRepository{}実装方式が複数ある場合は、違いが分かる名前にします。
public sealed class SqlUserRepository : IUserRepository{}public sealed class InMemoryUserRepository : IUserRepository{}
すべてのクラスに機械的にインターフェイスを作る必要はありません。差し替え、テスト、複数実装、依存関係の分離など、抽象化する目的がある場合に導入します。
3-4. メソッド名は動詞または動詞句で表す
メソッド名は、その処理が何をするのか分かる動詞または動詞句にします。
CreateOrder()CalculateTotalPrice()FindUserByEmail()SendNotification()ValidateRequest()取得系メソッドは、失敗時の挙動が伝わる名前にすると分かりやすくなります。
GetUser()FindUser()TryGetUser()たとえば、次のように使い分けます。
GetUser:対象が存在する前提。存在しなければ例外FindUser:存在しない場合はnullTryGetUser:成功可否をboolで返し、out引数で値を受け取る
public bool TryGetUser(Guid userId, out User? user){user = _users.FirstOrDefault(x => x.Id == userId);return user is not null;}非同期メソッドには、原則としてAsyncを付けます。
public Task<User?> FindUserAsync(Guid userId,CancellationToken cancellationToken){// ...}3-5. プロパティ名・フィールド名・ローカル変数名のルール
プロパティは名詞または状態を表す名前にします。
public string DisplayName { get; init; } = string.Empty;public bool IsActive { get; private set; }
public DateTimeOffset CreatedAt { get; init; }
privateフィールドに_camelCaseを採用すると、引数やローカル変数と区別しやすくなります。
public sealed class UserService{private readonly IUserRepository _userRepository;public UserService(IUserRepository userRepository){_userRepository = userRepository;}
}
ローカル変数は、スコープと役割に応じた名前を付けます。
var activeUsers = users.Where(user => user.IsActive);data、item、value、resultなどの抽象的な名前は、短いスコープを除いて避けます。
var result = service.Execute();戻り値の意味が注文作成結果なら、次のように具体化します。
var orderCreationResult = service.CreateOrder(command);3-6. 定数・readonly・staticメンバーの命名ルール
C#では、定数もPascalCaseにする方法が一般的です。
private const int MaxRetryCount = 3;private const string DefaultCountryCode = "JP";すべて大文字とアンダースコアを使う命名は、C#よりもCやJavaなどでよく見られる形式です。
private const int MAX_RETRY_COUNT = 3;既存プロジェクトで採用されていない限り、C#ではPascalCaseにそろえると自然です。
privateのreadonlyインスタンスフィールドは、通常のprivateフィールドと同じく_camelCaseにします。
private readonly TimeProvider timeProvider;staticフィールドは、通常のprivateフィールドと同じ形式にする方法と、sを付ける方法があります。
private static readonly object s_lock = new();どちらを採用する場合も、staticであることを名前で区別する必要があるかをチームで判断します。
3-7. bool型は意味が伝わる名前にする
bool型は、trueとfalseが何を意味するか分かる名前にします。
推奨される接頭辞には、次のようなものがあります。
Is:状態を表すHas:所有や存在を表すCan:可能かどうかを表すShould:実行すべきかを表すExists:存在するかを表す
bool isActive;bool hasPermission;bool canDelete;bool shouldRetry;bool userExists;次のような名前では、条件式を読んでも意味が分かりません。
bool flag;bool check;bool status;否定形もできるだけ避けます。
if (!user.IsNotActive){}次のほうが理解しやすくなります。
if (user.IsActive){}3-8. 略語・頭字語・ID/Url/Httpなどの表記ゆれを防ぐ
略語や頭字語の表記は、プロジェクト内で統一します。
代表的な命名例は次のとおりです。
UserIdCustomerIdUrlHttpClientXmlDocumentJsonSerializerApiClient次のような表記ゆれが混在すると、検索しにくくなります。
UserIDUserIdUserIDValue既存のフレームワークやライブラリで定着している表記に合わせることも重要です。たとえば、.NETにはHttpClient、Uri、XmlDocumentなどの型があります。
外部APIのフィールド名がuser_idであっても、C#側のプロパティはUserIdとし、シリアライズ属性で対応できます。
public sealed class UserResponse{[JsonPropertyName("user_id")]public required string UserId { get; init; }}3-9. 悪い命名例と改善例
悪い命名では、コードを読む人が処理内容を推測しなければなりません。
public void Do(User u){var x = u.Items.Where(i => i.F).ToList();foreach (var y in x){Save(y);}
}
役割を具体的にすると、処理の意図が伝わります。
public void SavePurchasedItems(User user){var purchasedItems = user.Items.Where(item => item.IsPurchased).ToList();foreach (var purchasedItem in purchasedItems){SaveItem(purchasedItem);}
}
命名では、短さよりも誤解なく読めることを優先します。ただし、同じ意味を過剰に繰り返す必要はありません。
public sealed class UserService{public User GetUserByUserId(Guid userId){// ...}}クラス名や引数から明らかな部分を整理できます。
public sealed class UserService{public User GetById(Guid userId){// ...}}4. C#らしい書き方をそろえるコードスタイル規約
4-1. varを使うべき場面・型を明示すべき場面
varは型推論であり、動的型付けではありません。コンパイル時には具体的な型が決まります。
右辺から型が明らかな場合は、varを使うと重複を減らせます。
var user = new User();var users = new List<User>();var cancellationTokenSource = new CancellationTokenSource();型を明示したほうが意味を理解しやすい場合は、明示型を使います。
IUserRepository repository = CreateRepository();decimal totalPrice = CalculateTotalPrice(order);次のコードでは、戻り値の型が呼び出し側から分かりません。
var result = Process(request);ただし、問題の本質がvarではなく、メソッド名や変数名にある場合もあります。
var paymentResult = ProcessPayment(request);実務では、次のような基準を採用できます。
newの右辺など、型が明らかな場合はvarLINQの匿名型は
var戻り値の型が重要で、コード上分かりにくい場合は型を明示
varを禁止するのではなく、可読性で判断する
4-2. nullチェックと早期returnの書き方
nullチェックには、パターンマッチングのis nullとis not nullを使うと意図が明確です。
if (user is null){throw new InvalidOperationException("User was not found.");}引数のnullチェックには、ArgumentNullException.ThrowIfNullも使用できます。
public void Register(User user){ArgumentNullException.ThrowIfNull(user);// 登録処理
}
異常系や処理対象外の条件を先にreturnする早期returnは、ネストを浅くできます。
public void SendNotification(User? user){if (user is null){return;}if (!user.IsActive){return;}if (string.IsNullOrWhiteSpace(user.Email)){return;}_notificationService.Send(user.Email);
}
すべてをネストすると、主要な処理が深い位置に移動します。
if (user is not null){if (user.IsActive){if (!string.IsNullOrWhiteSpace(user.Email)){_notificationService.Send(user.Email);}}}早期returnは、ガード節を明確にし、正常系を読みやすくするために使います。
4-3. if文・switch文・switch式の使い分け
単純な条件分岐やガード節にはifが適しています。
if (order.TotalAmount <= 0){throw new InvalidOperationException("Total amount must be positive.");}1つの値に対して複数の処理を分ける場合は、switch文が分かりやすくなります。
switch (order.Status){case OrderStatus.Pending:StartPayment(order);break;case OrderStatus.Paid:StartShipping(order);break;case OrderStatus.Canceled:WriteCancelLog(order);break;default:throw new ArgumentOutOfRangeException();
}
値を返すだけなら、switch式を使うと簡潔です。
var displayName = order.Status switch{OrderStatus.Pending => "支払い待ち",OrderStatus.Paid => "支払い済み",OrderStatus.Shipped => "発送済み",OrderStatus.Canceled => "キャンセル",_ => "不明"};複雑な副作用をswitch式へ詰め込むと読みにくくなるため、処理を実行する分岐にはswitch文、値を変換する分岐にはswitch式という基準が使えます。
4-4. LINQを使う場面と使いすぎを避ける基準
LINQは、コレクションの絞り込み、変換、並び替え、集計を宣言的に書ける機能です。
var activeUserNames = users.Where(user => user.IsActive).OrderBy(user => user.Name).Select(user => user.Name).ToList();単純なデータ処理では、foreachよりも意図が伝わりやすくなります。
一方、次のようなLINQは避けたほうがよい場合があります。
1つの式に多数の処理を詰め込んでいる
ラムダ式の中で副作用を発生させている
同じコレクションを何度も列挙している
パフォーマンス上の問題が計測で確認されている
デバッグが困難になっている
特に、Select内でデータを更新するような使い方は意図が分かりにくくなります。
var users = source.Select(user =>{user.LastAccessedAt = now;repository.Save(user);return user;}).ToList();副作用を伴う処理は、foreachで明示したほうが読みやすいでしょう。
foreach (var user in users){user.UpdateLastAccessedAt(now);repository.Save(user);}4-5. 例外処理の書き方とcatchの粒度
例外は、呼び出し元で通常の分岐として処理する状態ではなく、処理を継続できない異常を表すために使います。
catchする例外は、適切に処理できるものに限定します。
try{await _paymentClient.ChargeAsync(request, cancellationToken);}catch (PaymentTimeoutException exception){_logger.LogWarning(exception,"Payment request timed out. OrderId: {OrderId}",request.OrderId);return PaymentResult.Timeout();
}
すべての例外をまとめて握りつぶすコードは避けます。
try{Execute();}catch{}ログを出して再スローする場合は、スタックトレースを維持するためにthrow;を使います。
catch (ExternalServiceException exception){_logger.LogError(exception, "External service call failed.");throw;}次のように例外変数を再スローすると、元のスタックトレースが失われるため避けます。
catch (Exception exception){throw exception;}また、同じ例外を複数のレイヤーで繰り返しログ出力すると、ログが重複します。どの境界でログを記録するかを決めておきましょう。
4-6. 非同期処理 async/await の命名と実装ルール
非同期メソッドには、原則としてAsyncを付けます。
public async Task<User?> FindUserAsync(Guid userId,CancellationToken cancellationToken){return await _repository.FindByIdAsync(userId, cancellationToken);}戻り値は通常、TaskまたはTask<T>にします。イベントハンドラーを除き、async voidは避けます。
public async void Save(){}async voidでは、呼び出し元が完了を待機できず、例外も扱いにくくなります。
public async Task SaveAsync(){await _repository.SaveAsync();}外部I/Oを行うメソッドでは、必要に応じてCancellationTokenを受け取り、下位の処理へ渡します。
public async Task<Order> CreateOrderAsync(CreateOrderCommand command,CancellationToken cancellationToken){var customer = await _customerRepository.FindAsync(command.CustomerId,cancellationToken);// ...
}
CancellationTokenは、引数の最後に配置する方法が一般的です。
また、非同期メソッドの戻り値を.Resultや.Wait()で同期的に待機すると、デッドロックやスレッドのブロックにつながる可能性があります。原則として、呼び出し元までawaitを伝播させます。
4-7. nullable reference typesを前提にした書き方
nullable reference typesを有効にすると、nullを許可する参照型と許可しない参照型を区別できます。
public sealed class User{public required string Name { get; init; }public string? Nickname { get; init; }
}
stringはnullを許可しない意図を表し、string?はnullになる可能性を表します。
メソッドの戻り値でも、存在しない可能性を型で示します。
public User? FindById(Guid userId){return _users.FirstOrDefault(user => user.Id == userId);}null許容警告を消すためだけにnull許容抑制演算子!を多用するのは避けます。
var userName = user!.Name;!を使う場合は、その時点でnullではないことをコード上またはフレームワークの保証によって説明できる必要があります。
4-8. XMLコメントを書く対象と書かない対象
XMLコメントは、public API、共有ライブラリ、外部利用される型やメソッドに有効です。
/// <summary>/// 指定したユーザーIDに対応するユーザーを取得します。/// </summary>/// <param name="userId">取得対象のユーザーID。</param>/// <returns>/// ユーザーが存在する場合はユーザー、存在しない場合は<c>null</c>。/// </returns>public User? FindById(Guid userId){// ...}一方、すべてのprivateメソッドに定型的なXMLコメントを書くと、保守対象が増えます。
/// <summary>/// ユーザーを検証します。/// </summary>private void ValidateUser(User user){}この程度であれば、分かりやすいメソッド名だけで十分です。
XMLコメントを必須にする対象は、次のように限定できます。
NuGetパッケージとして公開するAPI
複数プロジェクトから利用される共通ライブラリ
利用条件や例外がコードだけでは分かりにくいpublicメソッド
外部連携用のインターフェイス
5. 保守しやすいC#コードにする設計ルール
5-1. 1つのクラスに責務を詰め込みすぎない
クラスには、変更理由が近い処理をまとめます。ユーザー登録、メール送信、CSV出力、データベースアクセスを1つのクラスへ集めると、変更の影響範囲が広がります。
public sealed class UserService{public void RegisterUser(){}public void SendWelcomeEmail(){}public void ExportUsersToCsv(){}public void SaveToDatabase(){}
}
役割ごとに分割すると、責務と依存関係が明確になります。
public sealed class UserRegistrationService{}public sealed class WelcomeEmailSender{}
public sealed class UserCsvExporter{}
public sealed class UserRepository{}
ただし、細かく分けすぎるとファイルや依存関係が増え、処理の流れを追いにくくなります。「クラスの説明に複数の無関係な役割が含まれる」「異なる理由で頻繁に変更される」といった兆候がある場合に分割を検討します。
5-2. メソッドは短く、1つの目的に絞る
メソッドの長さを機械的な行数だけで決める必要はありません。重要なのは、1つの目的を持ち、上から順に自然に読めることです。
public async Task RegisterUserAsync(RegisterUserCommand command,CancellationToken cancellationToken){ValidateCommand(command);var user = CreateUser(command);await _userRepository.AddAsync(user, cancellationToken);await _welcomeEmailSender.SendAsync(user, cancellationToken);
}
処理を適切な名前のメソッドへ分けることで、コメントを読まなくても全体の流れが分かります。
ただし、数行の処理をすべて別メソッドへ分割すると、コードを行き来する回数が増えます。分割によって名前を付ける価値があるか、再利用や単体テストが必要かを基準に判断しましょう。
5-3. public・private・internalの使い分け
アクセス修飾子は、必要最小限にします。
public:アセンブリ外へ公開する必要があるinternal:同じアセンブリ内だけで利用するprotected:派生クラスに公開するprivate:その型の内部だけで利用する
迷った場合は、まずprivateまたはinternalから始めます。
不要なpublicメンバーは、後から変更しにくい公開APIになります。特にライブラリ開発では、一度公開した型やメソッドが利用者との契約になるため注意が必要です。
継承を想定していないクラスには、sealedを付ける方針も有効です。
public sealed class OrderService{}継承が必要になった時点で明示的に解放できるため、意図しない拡張を防げます。
5-4. DTO・Entity・Service・Repositoryなどの役割を明確にする
よく使われる型には、それぞれ役割があります。
DTOは、レイヤー間や外部システムとのデータ受け渡しに使います。
public sealed record UserDto(Guid Id,string Name,string Email);Entityは、識別子と状態を持ち、業務ルールを表現します。
public sealed class User{public Guid Id { get; }public string Email { get; private set; }public bool IsActive { get; private set; }public void Deactivate(){IsActive = false;}
}
Serviceは、複数の処理や依存先を調整し、ユースケースを実行します。
Repositoryは、Entityの保存や取得を抽象化します。
役割が曖昧になると、DTOに業務ロジックを書いたり、Controllerにデータベース処理を直接書いたりする状態になります。
名称だけで設計が正しくなるわけではありません。「この型は何を知ってよいか」「どのレイヤーへ依存してよいか」を決めることが重要です。
5-5. マジックナンバー・ハードコーディングを避ける
意味の分からない数値や文字列をコードへ直接書くと、変更箇所と意図が分かりにくくなります。
if (retryCount >= 3){throw new InvalidOperationException();}名前を付けると、数値の意味が明確になります。
private const int MaxRetryCount = 3;if (retryCount >= MaxRetryCount){throw new InvalidOperationException();}
環境によって変わる値は、定数ではなく設定ファイルや環境変数へ移します。
{"Payment": {"TimeoutSeconds": 30}}ただし、すべての数値を定数化する必要はありません。0や1のように文脈上意味が明確な値まで機械的に定数へ置き換えると、かえって読みづらくなる場合があります。
5-6. 依存関係を減らすための設計ルール
クラスが多くの具象クラスへ直接依存すると、変更やテストが難しくなります。
コンストラクターインジェクションを使うと、必要な依存関係を明示できます。
public sealed class OrderService{private readonly IOrderRepository _orderRepository;private readonly IPaymentClient _paymentClient;private readonly ILogger<OrderService> _logger;public OrderService(IOrderRepository orderRepository,IPaymentClient paymentClient,ILogger<OrderService> logger){_orderRepository = orderRepository;_paymentClient = paymentClient;_logger = logger;}
}
依存関係が多すぎる場合は、そのクラスが複数の責務を持っている可能性があります。
また、抽象化は外部サービス、時刻、ファイルシステム、データベースなど、実行環境によって変化する境界に置くと効果的です。すべての小さなクラスにインターフェイスを作る必要はありません。
5-7. テストしやすいコードを書くための規約
テストしやすいコードには、次の特徴があります。
依存関係がコンストラクターで明示されている
現在時刻を直接取得しない
グローバルな状態に依存しない
staticメソッドへ処理を集めすぎない
I/Oと業務ロジックが分離されている
メソッドの入力と出力が明確である
現在時刻を直接取得すると、テスト結果が実行時刻に依存します。
public bool IsExpired(){return ExpiresAt < DateTimeOffset.UtcNow;}時刻を外部から受け取ると、テストしやすくなります。
public bool IsExpired(DateTimeOffset now){return ExpiresAt < now;}または、TimeProviderなどを依存関係として利用できます。
テストのためだけに設計を複雑にするのではなく、不安定な外部要因を境界として分離することがポイントです。
5-8. パフォーマンスより可読性を優先する判断基準
通常の業務アプリケーションでは、最初から細かな最適化を行うより、正しく読みやすいコードを書くことを優先します。
最適化が必要なのは、計測によって問題が確認された箇所です。
たとえば、LINQをforeachへ変更する、配列を再利用する、独自キャッシュを追加するといった最適化は、コードを複雑にする可能性があります。
判断するときは、次の順序で考えます。
正しく動作するか
意図を理解しやすいか
保守しやすいか
実際に性能要件を満たしているか
計測結果に基づいて最適化できるか
データ量が多い処理、低遅延が必要な処理、ゲームループ、画像処理などでは性能を優先する場面もあります。その場合でも、最適化の理由をコメントやベンチマークで残しておくと、将来の変更で元に戻されることを防げます。
6. チーム開発でC#コード規約を運用する方法
6-1. 規約は最初から完璧にせず最小限から始める
コード規約を初めから数十ページ作成しても、すべてが実際の開発に必要とは限りません。
まずは、判断が分かれやすく、影響が大きい項目から始めます。
インデントと中かっこ
命名規則
privateフィールドの形式
varの使用方針非同期メソッドの命名
nullable reference types
例外処理
フォーマッターとAnalyzerの設定
実際のレビューで繰り返し議論になる項目が見つかったら、規約へ追加します。この方法なら、使われないルールが増えることを防げます。
6-2. チームで合意しておくべきルール一覧
最低限、次の内容を合意しておくと運用しやすくなります。
| 分類 | 合意する内容 |
| フォーマット | インデント、改行、中かっこ、using |
| 命名 | 型、メソッド、変数、フィールド、定数 |
| 型推論 | varを使う条件 |
| null | nullable reference types、nullチェック |
| 非同期 | Async、CancellationToken、async void |
| 例外 | catchする場所、ログ出力、独自例外 |
| コメント | XMLコメントの対象、TODOの書き方 |
| 設計 | レイヤー間の依存、アクセス修飾子 |
| テスト | テスト名、配置、対象範囲 |
| 自動化 | EditorConfig、Analyzer、CIの判定基準 |
すべての項目に唯一の正解があるわけではありません。チームで選択し、その理由を共有できる状態が重要です。
6-3. プルリクエストで見るべき観点
プルリクエストでは、コードの見た目よりも、機械で判断しにくい内容を確認します。
主な観点は次のとおりです。
仕様どおりに動作するか
境界値や異常系が考慮されているか
名前から役割が分かるか
クラスやメソッドの責務が適切か
不要な依存関係が増えていないか
既存機能への影響がないか
セキュリティ上の問題がないか
テストが必要なケースを網羅しているか
ログへ機密情報を出力していないか
例外が握りつぶされていないか
空白や改行、usingの順序などは、自動フォーマットへ任せるのが理想です。
6-4. レビューで指摘すべきこと・自動化すべきこと
機械的に判定できる項目は自動化します。
自動化しやすい項目
インデント
空白
改行
usingの並び順
不要なusing
命名形式
varの使用方針中かっこの有無
nullable警告
コンパイラー警告
人がレビューすべき項目
名前が業務上の意味を正しく表しているか
責務の分け方が適切か
抽象化が過剰または不足していないか
例外処理の境界が適切か
将来の変更に耐えられるか
テストケースが妥当か
実装が要件を満たしているか
自動化できる内容をレビューで繰り返し指摘すると、レビュー担当者にも実装者にも負担がかかります。
6-5. 既存コードと新規コードで規約をどう適用するか
既存プロジェクトへ新しいC#コード規約を導入するときは、全ファイルを一括整形しないほうが安全です。
大規模なフォーマット変更を行うと、次の問題が発生します。
実質的な変更が差分に埋もれる
git blameで変更履歴を追いにくくなる開発中のブランチと競合する
レビュー負荷が高くなる
基本方針としては、次のいずれかを選びます。
新規ファイルから規約を適用する
変更した箇所の周辺だけ整える
フォーマット専用のプルリクエストを分ける
機能単位やフォルダ単位で段階的に移行する
フォーマット変更と機能変更を同じコミットへ混在させないことが重要です。
6-6. 例外ルールを決めて属人化を防ぐ
現実のプロジェクトでは、すべてのコードに同じ規約を適用できない場合があります。
たとえば、次のような例外があります。
自動生成コード
外部システムの命名に合わせるDTO
Entity Frameworkのマイグレーション
ネイティブAPIとの相互運用コード
パフォーマンス上の理由で特殊な実装を行う箇所
Unityのイベントメソッド
例外を個人の判断だけで決めると、規約が形骸化します。
例外を認める場合は、対象、理由、承認方法を明確にします。Analyzerを無効化する場合も、理由をコメントで残します。
#pragma warning disable CA1822// Unityからメソッド名とインスタンスメソッドであることを要求されるため抑制するprivate void Update(){}#pragma warning restore CA18226-7. 規約をドキュメント化して更新する方法
規約は、リポジトリ内でコードと一緒に管理すると更新しやすくなります。
docs/└─ coding-guidelines.mdドキュメントには、ルールだけでなく理由と例を記載します。
## privateフィールドprivateのインスタンスフィールドには _camelCase を使用する。
理由:引数やローカル変数と区別し、this.を省略してもフィールドだと判断できるため。
例:private readonly IUserRepository _userRepository;
規約の変更は、通常のコードと同様にプルリクエストでレビューします。更新日や変更履歴も残しておくと、古い情報が放置されにくくなります。
7. C#コード規約を自動化するツールと設定
7-1. EditorConfigでコードスタイルを共有する
.editorconfigをリポジトリへ配置すると、Visual Studio、Visual Studio Code、Riderなどで書式やコードスタイルを共有できます。
基本的な設定例は次のとおりです。
root = true[*.cs]charset = utf-8end_of_line = lfinsert_final_newline = trueindent_style = spaceindent_size = 4trim_trailing_whitespace = true
dotnet_sort_system_directives_first = truedotnet_separate_import_directive_groups = false
csharp_new_line_before_open_brace = allcsharp_prefer_braces = true:warning
csharp_style_namespace_declarations = file_scoped:suggestioncsharp_style_var_for_built_in_types = false:suggestioncsharp_style_var_when_type_is_apparent = true:suggestioncsharp_style_var_elsewhere = false:suggestion
dotnet_style_readonly_field = true:warningdotnet_style_require_accessibility_modifiers = always:suggestion
重要度は、silent、suggestion、warning、errorなどで調整できます。
導入時から多くのルールをerrorにすると、既存コードで大量のエラーが発生する可能性があります。最初はsuggestionやwarningで運用し、必要な項目だけ段階的に厳しくする方法が現実的です。
7-2. Visual Studioのコードスタイル設定を活用する
Visual Studioでは、C#の書式設定、命名、コードスタイル、Code Cleanupなどを設定できます。
ただし、各開発者のローカル設定だけに依存すると、環境によって結果が変わります。チーム共通のルールは.editorconfigへ保存し、Visual Studioの設定は個人の入力支援として利用しましょう。
保存時やCode Cleanup実行時に、次の処理をまとめて実行できます。
コードのフォーマット
usingの整理
不要なusingの削除
this.の整理型名の簡略化
設定済みコードスタイルの適用
7-3. dotnet formatでフォーマットを統一する
dotnet formatを利用すると、コマンドラインからソリューションやプロジェクトへフォーマットを適用できます。
dotnet format変更が必要かどうかだけを確認する場合は、次のように実行します。
dotnet format --verify-no-changesCIでこのコマンドを実行すれば、フォーマットされていないコードがマージされることを防げます。
対象範囲や診断IDを限定して実行することもできます。既存プロジェクトへ導入するときは、最初にローカル環境で差分量を確認し、意図しない大量変更が起きないようにしましょう。
7-4. StyleCop Analyzersで命名規則や書き方をチェックする
StyleCop Analyzersは、Roslynを利用してC#コードのスタイルを検査するAnalyzerです。
プロジェクトへ追加すると、次のような項目を検出できます。
要素の並び順
コメントの形式
空白や改行
usingの配置
ファイル名と型名
XMLドキュメント
命名やレイアウト
ただし、デフォルトルールをすべて有効にすると、プロジェクトに不要な警告が大量に発生する場合があります。
特に、すべてのpublicメンバーへXMLコメントを要求するルールは、アプリケーション開発では負担になることがあります。ルールを目的に合わせて選び、不要なものは設定で無効化します。
7-5. Roslyn Analyzerで独自ルールを検出する
標準のAnalyzerで検出できない社内ルールがある場合は、独自のRoslyn Analyzerを作成できます。
たとえば、次のような独自チェックが考えられます。
Controllerから特定のRepositoryを直接参照していないか
Entityの名前に禁止された接尾辞が付いていないか
特定属性を持つメソッドがCancellationTokenを受け取っているか
ログメッセージに禁止された情報を含んでいないか
独自APIの呼び出し方が統一されているか
独自Analyzerは強力ですが、作成と保守にコストがかかります。レビューで何度も発生し、機械的に判定できる問題へ限定して導入しましょう。
7-6. ReSharper/Riderを使う場合の注意点
ReSharperやRiderには、多数のコード検査と自動修正機能があります。
便利な一方で、Visual Studio、EditorConfig、StyleCop、ReSharperの設定が競合すると、開発者ごとに異なる修正が提案される可能性があります。
運用時は、次の優先順位を決めておきます。
リポジトリ内の
.editorconfigプロジェクトで有効化したAnalyzer
IDE固有のチーム設定
個人設定
IDE固有の設定ファイルをリポジトリへ含める場合は、全員が同じIDEを利用しているか、ほかのIDEでも同等の結果になるかを確認します。
7-7. CIで規約違反を検出してレビュー負荷を下げる
CIでは、ビルド、テスト、フォーマット検証、Analyzerを実行します。
代表的なコマンドは次のとおりです。
dotnet restoredotnet build --no-restoredotnet test --no-builddotnet format --verify-no-changes警告をエラーとして扱う場合は、プロジェクト設定やコマンドオプションで制御できます。
<PropertyGroup><TreatWarningsAsErrors>true</TreatWarningsAsErrors></PropertyGroup>ただし、すべての警告を一度にエラー化すると、依存ライブラリや既存コードの警告で開発が止まる可能性があります。
次のように段階的に導入すると安全です。
警告を可視化する
新規警告を増やさない
重要な警告を選んでエラー化する
既存警告を順次解消する
8. C#コード規約の実践例|プロジェクトで使えるルール集
8-1. 命名規則のサンプル
プロジェクトで採用できる命名規則の例です。
・クラス、構造体、record、列挙型はPascalCaseとする・インターフェイスはI+PascalCaseとする・メソッドとプロパティはPascalCaseとする・引数とローカル変数はcamelCaseとする・privateインスタンスフィールドは_camelCaseとする・定数はPascalCaseとする・非同期メソッドにはAsyncを付ける・bool型にはIs、Has、Can、Shouldなど意味の分かる名前を使用する・意味が伝わらない省略名を使用しない・コレクションには複数形の名前を付ける実装例は次のとおりです。
public interface IOrderRepository{Task<Order?> FindByIdAsync(Guid orderId,CancellationToken cancellationToken);}public sealed class OrderService{private const int MaxRetryCount = 3;private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository){_orderRepository = orderRepository;}public async Task<bool> CanCancelAsync(Guid orderId,CancellationToken cancellationToken){var order = await _orderRepository.FindByIdAsync(orderId,cancellationToken);return order is not null && order.CanCancel;}
}
8-2. フォーマット規約のサンプル
・インデントはスペース4文字とする・改行コードはLFとする・ファイル末尾に改行を入れる・行末の不要な空白を削除する・if、for、foreach、whileでは中かっこを省略しない・演算子の前後とカンマの後に空白を入れる・usingはSystem、外部ライブラリ、プロジェクト内の順に並べる・不要なusingは削除する・原則として1ファイルに1つの主要な型を定義する・ファイル名と主要な型名を一致させる・フォーマットはEditorConfigとdotnet formatで自動化する8-3. コメント規約のサンプル
・コードを読めば分かる処理内容をコメントで繰り返さない・仕様上の理由、外部制約、例外的な判断をコメントに残す・コメントとコードが矛盾した場合は、同じ変更で修正する・TODOには課題番号または担当を追跡できる情報を付ける・外部公開APIには必要に応じてXMLコメントを書く・privateメソッドへの定型的なXMLコメントは必須としない改善が必要なコメントです。
// 注文を保存するawait _orderRepository.SaveAsync(order, cancellationToken);理由を説明するコメントなら価値があります。
// 決済完了通知より先に永続化し、通知再送時の二重決済を防ぐawait _orderRepository.SaveAsync(order, cancellationToken);8-4. 例外処理規約のサンプル
・処理できない例外はcatchしない・空のcatchブロックを作らない・例外を再スローするときはthrow;を使用する・例外を通常の条件分岐として使用しない・入力値の不正には適切な引数例外を使用する・ログへ個人情報、認証情報、秘密情報を出力しない・同じ例外を複数レイヤーで重複してログ出力しない・独自例外は呼び出し側が区別して処理する必要がある場合に作成するpublic async Task<PaymentResult> ChargeAsync(PaymentRequest request,CancellationToken cancellationToken){try{return await _paymentClient.ChargeAsync(request,cancellationToken);}catch (PaymentTimeoutException exception){_logger.LogWarning(exception,"Payment timed out. OrderId: {OrderId}",request.OrderId); return PaymentResult.Timeout();}
}
8-5. async/await規約のサンプル
・非同期メソッド名にはAsyncを付ける・イベントハンドラー以外でasync voidを使用しない・非同期処理をResultやWaitで同期的に待機しない・キャンセル可能なI/O処理はCancellationTokenを受け取る・CancellationTokenは原則として引数の最後に置く・受け取ったCancellationTokenを下位の非同期処理へ渡す・処理を待つ必要があるTaskは必ずawaitする・意図的に待たない場合は、その理由と例外処理方法を明確にするpublic async Task<UserDto?> GetUserAsync(Guid userId,CancellationToken cancellationToken){var user = await _userRepository.FindByIdAsync(userId,cancellationToken);return user is null? null: new UserDto(user.Id, user.Name, user.Email);
}
8-6. テストコード規約のサンプル
テスト名は、対象、条件、期待結果が分かる形式にします。
[Fact]public void CalculateTotal_WhenOrderHasTwoItems_ReturnsSumOfItemPrices(){}または、アンダースコアで区切る形式も使えます。
[Fact]public void CalculateTotal_TwoItems_ReturnsSum(){}テストコードの基本ルール例は次のとおりです。
・1つのテストでは1つの振る舞いを確認する・テスト名から条件と期待結果が分かるようにする・Arrange、Act、Assertの区切りを意識する・テスト間で状態を共有しない・現在時刻や実行順序に依存させない・単体テストで実データベースや外部APIへ接続しない・実装の内部構造ではなく、外部から確認できる振る舞いをテストする・失敗時に原因を判断できるアサーションを使用する[Fact]public void Cancel_WhenOrderIsShipped_ThrowsInvalidOperationException(){// Arrangevar order = OrderTestData.CreateShippedOrder();// Actvar action = () => order.Cancel();// AssertAssert.Throws<InvalidOperationException>(action);
}
8-7. プルリクエスト時のチェックリスト
プルリクエスト作成者が確認できるチェックリスト例です。
□ 要件を満たしている□ 不要なコードやコメントを残していない□ 命名から役割が理解できる□ クラスとメソッドの責務が大きすぎない□ null、空文字、境界値を考慮している□ 例外を握りつぶしていない□ ログに機密情報を出力していない□ asyncメソッドにAsyncを付けている□ CancellationTokenを適切に伝播している□ 必要なテストを追加または更新した□ ローカルでビルドとテストが成功する□ dotnet formatの検証が成功する□ フォーマット変更と機能変更が不必要に混在していない□ 互換性や既存機能への影響を確認した9. C#コード規約でよくある失敗と対策
9-1. 規約が細かすぎて開発速度が落ちる
細かい規約を増やしすぎると、実装者がルールを確認する時間が増え、レビューも規約違反の指摘ばかりになります。
対策は、規約を次の3種類に分けることです。
必須:不具合、保守性、セキュリティに大きく関わる
推奨:基本的には従うが、理由があれば例外を認める
参考:迷ったときの判断例
また、ルールを追加するときは、実際に発生した問題と結び付けます。好みだけを理由に必須ルールを増やさないことが重要です。
9-2. 人によってレビュー基準が違う
レビュー担当者ごとに指摘内容が違うと、実装者はどの基準に従うべきか分からなくなります。
対策として、繰り返し発生する指摘をコード規約へ追加します。
「Aさんはvarを推奨し、Bさんは明示型を求める」といった状態なら、チームで議論し、EditorConfigへ設定します。
意見が分かれる項目は、正解を探し続けるより、チームとして一つの方針を選択することが大切です。
9-3. 自動整形できる内容を手作業で指摘してしまう
空白、改行、usingの順番などをレビューで指摘すると、双方の時間を消費します。
対策は、次の仕組みを導入することです。
.editorconfigIDEのCode Cleanup
dotnet formatStyleCop Analyzers
CIでのフォーマット検証
レビューコメントで同じスタイル指摘が2回以上発生したら、自動化できないか検討するとよいでしょう。
9-4. 既存コードに一括適用して差分が大きくなる
新しい規約を既存コード全体へ一括適用すると、数千ファイルの差分が発生することがあります。
対策は、規約導入と機能変更を分離することです。
一括整形が必要な場合は、次の条件を整えます。
専用ブランチと専用プルリクエストを作る
機能変更を含めない
事前にチームへ周知する
長期間残っているブランチとの競合を確認する
フォーマット後にビルドとテストを実行する
必要に応じて履歴追跡方法を共有する
9-5. Unity・Webアプリ・業務システムでルールが合わない
同じC#でも、プロジェクトの種類によって適したコード規約は異なります。
Unityでは、MonoBehaviourのライフサイクルメソッドやシリアライズ対象フィールドなど、フレームワーク固有の制約があります。
[SerializeField]private GameObject _player;ASP.NET Coreでは、DI、非同期I/O、ControllerやEndpointの責務、設定値の扱いが重要です。
バッチ処理や業務システムでは、再実行性、トランザクション、監査ログ、例外時の継続方針が重要になることがあります。
共通規約とプロジェクト固有規約を分けて管理すると、無理に一つのルールへ統一せずに済みます。
9-6. 新メンバーが規約を把握できない
規約が社内Wikiの深い階層に置かれていたり、口頭でしか共有されていなかったりすると、新メンバーは把握できません。
対策として、リポジトリ内に次の情報を置きます。
コード規約の概要
開発環境のセットアップ手順
フォーマットコマンド
ビルドとテストの実行方法
代表的な実装例
プルリクエストのチェックリスト
例外ルール
規約を学ぶための専用資料だけでなく、既存コードに良い見本があることも重要です。新メンバーがコピーして利用できる標準的な実装例を用意しておくと、定着しやすくなります。
10. C#コード規約に関するよくある質問
10-1. C#の命名規則はMicrosoft公式に合わせるべき?
基本的には、Microsoftの命名ガイドラインをベースにするのがおすすめです。
C#の開発者にとって見慣れた形式であり、IDEやAnalyzerとも合わせやすいためです。特別な理由がなければ、型やpublicメンバーにはPascalCase、引数やローカル変数にはcamelCase、インターフェイスにはIを使用するとよいでしょう。
ただし、既存プロジェクトで異なるルールが広く使われている場合は、統一によって得られる効果と変更コストを比較する必要があります。
10-2. privateフィールドはアンダースコア付きにするべき?
どちらでもコンパイル上の問題はありませんが、チーム内で統一する必要があります。
アンダースコア付きの_camelCaseには、フィールドと引数を見分けやすい利点があります。
private readonly IUserRepository _userRepository;public UserService(IUserRepository userRepository){_userRepository = userRepository;}
アンダースコアを使わない場合は、this.を付ける方針もあります。
private readonly IUserRepository userRepository;public UserService(IUserRepository userRepository){this.userRepository = userRepository;}
現在のC#プロジェクトでは_camelCaseが広く利用されているため、新規プロジェクトでは有力な選択肢です。
10-3. varは使わないほうがいい?
varを全面的に禁止する必要はありません。
右辺から型が明らかな場合や、型名を繰り返すだけになる場合は、varによってコードを簡潔にできます。
var user = new User();一方、戻り値の型がコード上分かりにくく、その型が処理を理解するうえで重要なら、明示型が有効です。
PaymentResult result = paymentService.Execute(request);varか明示型かを好みで決めるのではなく、どちらが意図を理解しやすいかで判断します。
10-4. コメントはどこまで書くべき?
コードだけでは伝わらない理由や制約を中心に書きます。
処理内容をそのまま説明するコメントは、コード変更時に古くなりやすいため避けます。
コメントを書く前に、次の改善ができないか確認しましょう。
変数名を具体的にする
メソッド名を分かりやすくする
長い処理を分割する
条件式を名前付きの変数やメソッドへ置き換える
それでも伝わらない業務上の理由、外部制約、例外的な判断がある場合にコメントを使います。
10-5. コード規約と設計原則は分けて考えるべき?
分けて整理しつつ、両方を運用するのが適切です。
インデントや命名などのコードスタイルは、自動化しやすい規約です。一方、単一責任、依存関係、レイヤー分割といった設計原則は、文脈による判断が必要です。
同じドキュメント内に記載しても問題ありませんが、次のように分類すると分かりやすくなります。
自動チェックするコードスタイル
レビューで確認する設計ルール
プロジェクト固有のアーキテクチャルール
10-6. 小規模チームでもC#コード規約は必要?
2人から3人程度の小規模チームでも、最低限のC#コード規約は有効です。
人数が少なくても、担当変更、休暇、退職、将来のメンバー追加は発生します。また、少人数だからこそ、レビューや修正に使える時間は限られています。
大規模な規約書を作る必要はありません。EditorConfig、命名規則、非同期処理、例外処理、テストの方針だけでも、十分な効果が期待できます。
10-7. 規約違反はビルドエラーにするべき?
すべての規約違反をビルドエラーにする必要はありません。
重大度に応じて使い分けます。
エラーにしやすい項目
コンパイラーエラー
null安全性に関わる重大な警告
セキュリティ上の問題
明確な不具合につながるAnalyzer警告
チームで必須と合意したルール
警告または提案に向く項目
varか明示型か式形式メンバーの使用
改行位置
一部の命名上の好み
リファクタリング候補
既存コードへ導入する場合は、最初からエラーにせず、警告の可視化から始めます。新規警告だけを禁止し、既存警告を段階的に減らす方法も有効です。
まとめ
C#コード規約の目的は、開発者の好みを制限することではありません。誰が書いても理解しやすく、実装やレビューで迷わない状態を作ることです。
基本となるポイントは次のとおりです。
MicrosoftのC#コーディング規則を土台にする
型やpublicメンバーはPascalCase、引数や変数はcamelCaseにする
privateフィールドなど意見が分かれる項目はチームで統一する
中かっこ、空白、usingなどは自動フォーマットする
var、LINQ、早期returnは可読性を基準に使う非同期メソッドには
Asyncを付け、CancellationTokenを伝播するnullable reference typesを活用してnullの可能性を型で表す
クラスとメソッドの責務を明確にする
自動化できる指摘はEditorConfig、Analyzer、CIへ任せる
規約は最小限から始め、実際の課題に応じて更新する
優れたC#コード規約は、長く詳細な規約ではなく、チーム全員が理解し、日常的に守れる規約です。
まずは命名、フォーマット、非同期処理、例外処理といった影響の大きい項目を決め、.editorconfigとCIで自動化しましょう。そのうえで、レビューでは仕様、責務、保守性、テストといった、人が判断すべき本質的な内容に集中することが重要です。

