C#のpartialとは?部分クラス・部分プロパティの使いどころと書き方
C#のpartialは、1つの型やメンバーの定義を複数の宣言に分割するためのキーワードです。名前から「クラスを分割して小さくする機能」と誤解されがちですが、本来の目的は人間が書くコードと、ツールが自動生成するコードを同じ型の中で共存させることにあります。Windows フォームのデザイナー、正規表現やP/Invokeのソースジェネレーターがpartialを要求するのはこのためです。ここでは部分クラスのルールから、C# 13で追加された部分プロパティ、C# 14で追加された部分コンストラクター・部分イベントまで、公式仕様に基づいて使いどころと落とし穴を整理します。
まとめ:partialの要点
- partialは「型を分ける」機能ではなく「1つの型を複数の宣言に分けて書く」機能。コンパイル時にすべての部分が結合され、出力される型は1つです。プロパティの実装を別のクラスへ移す、といった使い方はできません。実行時の型もパフォーマンスも、分割しない場合と同一です。
- 本命の用途はソースジェネレーターとの連携。
[GeneratedRegex]や[LibraryImport]、JsonSerializerContextのように「宣言だけを人間が書き、実装はビルド時にツールが生成する」設計は、すべてpartialの上に成り立っています。「partialを付けろ」とコンパイラに言われる場面のほとんどは、この実装注入先を確保するためです。 - 使えるのは class / struct / interface / record。delegate と enum には使えません。分割したすべての宣言に
partialが必要です(欠けるとCS0260)。 - 部分メソッドは4条件を満たすときだけ実装を省略できる。アクセス修飾子なし・
void戻り値・outなし・virtual等なし。満たさない形(publicや非void)はC# 9.0以降で書けるようになりましたが、その場合は実装が必須です。 - partialにできるメンバーはバージョンで増えてきた。C# 12まではメソッドのみ、C# 13でプロパティ・インデクサー、C# 14でインスタンスコンストラクター・イベント。
- 分割の境界はアセンブリまで。すべての部分は同一アセンブリ・同一モジュール内になければならず、複数のDLLにまたがれません。
partialとは?型やメンバーの定義を複数に分割するキーワード
partialは、型宣言やメンバー宣言の先頭に付けるコンテキストキーワードです。次のコードは、1つのEmployeeクラスを2つのファイルに分けて定義した例です。
// Employee_Part1.cs
public partial class Employee
{
public void DoWork() => Console.WriteLine("Employee is working.");
}
// Employee_Part2.cs
public partial class Employee
{
public void GoToLunch() => Console.WriteLine("Employee is at lunch.");
}
// Program.cs(別ファイル。トップレベル文は型宣言より前に置く必要がある)
var emp = new Employee();
emp.DoWork(); // 呼び出し側からは最初から1つのクラスに見える
emp.GoToLunch();
ファイルは2つですが、ビルド後に生成される型はEmployeeただ1つです。partialはソースコードの書き方の都合であり、実行時の型やパフォーマンスには一切影響しません。分割したことによる呼び出しコストの増加や、メモリ上の差異は発生しません。
partialを付けられるもの・付けられないもの
型については、クラス・構造体・インターフェース・レコードに付けられます。デリゲート宣言と列挙型(enum)宣言には付けられません。メンバーについては、C#のバージョンによって対象が段階的に広がってきました。
| partialにできる対象 | 使えるようになったバージョン |
|---|---|
| クラス・構造体・インターフェース | C# 2.0 |
| レコード | C# 9.0(record の導入と同時) |
| メソッド | C# 3.0(C# 9.0でアクセス修飾子・非void戻り値・outが可能に。ただし実装は必須) |
| プロパティ・インデクサー | C# 13 |
| インスタンスコンストラクター・イベント | C# 14 |
| 静的コンストラクター/ファイナライザー/演算子 | 不可(バージョンを問わず) |
「別のクラスに分ける」ではなく「同じ型を複数の宣言に分ける」
ここが最も誤解の多い点です。partialで分けられるのは同一の型(同じ名前空間・同じ型名・同じ型パラメーター)の宣言だけであり、プロパティの実装を別のクラスに置く、といった使い方はできません。別の型へロジックを移したい場合は、継承・委譲・拡張メソッドといった通常の設計手段を使います。partialはあくまで「1つの型のソースファイル上の置き場所」を分けるものだと理解してください。オブジェクト指向の設計原則との関係はオブジェクト指向とは?カプセル化・継承・ポリモーフィズムを実装目線で解説も参考になります。
部分クラス(partial class)の書き方と守るべきルール
部分クラスは自由に分割できるわけではありません。公式ドキュメントが「制限(Restrictions)」として列挙している規則のうち、実装で実際に効いてくるのは次の4点です。
すべての部分にpartialが必要/アクセシビリティは一致させる
public partial class A { }
public class A { } // エラー CS0260: partial 修飾子が必要
public partial class B { }
internal partial class B { } // エラー CS0262: アクセシビリティが一致していない
public class partial C { } // エラー CS0267: partial は class の直前にしか置けない
1つでもpartialを書き忘れた宣言があると、同名の別の型を定義したとみなされてコンパイルエラーになります。また、public・internalなどのアクセシビリティは全部分で一致させる必要があります。partialを書く位置も決まっていて、class・struct・interfaceのキーワードの直前にしか置けません(public class partial Cのような順序は不可)。
一方で、abstractやsealedはどれか1つの部分に付いていれば型全体に適用されます。基底クラスも同様で、基底クラスを指定する部分はすべて同じ基底クラスでなければならず、省略した部分はその基底クラスを継承したものとして扱われます。
インターフェース・属性・XMLコメントは結合される
各部分が別々に指定したインターフェースと属性は、コンパイル時にすべてマージされます。
[SerializableAttribute]
partial class Earth : Planet, IRotate { }
[ObsoleteAttribute]
partial class Earth : IRevolve { }
// 上の2つは、次の宣言と等価になる
// [SerializableAttribute]
// [ObsoleteAttribute]
// class Earth : Planet, IRotate, IRevolve { }
結合されるのはインターフェース・クラス属性・ジェネリック型パラメーター属性・メンバー・XMLコメントです。属性は重複していてもそのまま結合される点に注意してください(同じ属性を両方の部分に書けば二重に付きます)。
ジェネリックと入れ子型のルール
ジェネリック型もpartialにできますが、すべての部分で型パラメーターの名前と順序が一致していなければなりません。partial class Cache<TKey, TValue>とpartial class Cache<K, V>は別物として扱われエラーになります。型パラメーター制約も、ある部分に書いた場合は他の部分でも同じものを指定する必要があります。
入れ子型については、外側の型がpartialでなくても、内側の入れ子型だけをpartialにできます。
class Container // 外側はpartialでなくてよい
{
partial class Nested
{
void Test() { }
}
partial class Nested
{
void Test2() { }
}
}
越えられない境界:同一アセンブリ・同一モジュール
すべての部分は同じアセンブリかつ同じモジュール(同一の.exeまたは.dll)内で定義しなければなりません。「共通ライブラリ側で宣言し、アプリ側のDLLで実装する」といった分割はできません。プロジェクトをまたいで機能を差し込みたい場合は、部分クラスではなくインターフェースや抽象クラス、拡張メソッドを使います。
部分メソッド(partial method):実装を省略できる条件
部分メソッドには宣言宣言(本文を持たないシグネチャ)と実装宣言(本文を持つ定義)があります。C#の部分メソッドが特徴的なのは、条件を満たす場合に実装宣言そのものを書かなくてもコンパイルが通る点です。
実装なしでもコンパイルが通る4条件
次の条件をすべて満たす部分メソッドは、実装が存在しなくてもエラーになりません。
- アクセシビリティ修飾子がない(既定の
privateである) - 戻り値の型が
voidである outパラメーターを持たないvirtual・override・sealed・new・externのいずれの修飾子も持たない
partial class MyPartialClass
{
// 宣言宣言(本文なし)
partial void OnSomethingHappened(string s);
public void Run()
{
// 実装がなくても、この呼び出しごとコンパイル時に消える
OnSomethingHappened("started");
}
}
// 別ファイル。このメソッドごとコメントアウトしてもビルドは通る
partial class MyPartialClass
{
partial void OnSomethingHappened(string s) =>
Console.WriteLine($"Something happened: {s}");
}
実装が与えられなかった場合、メソッドの宣言もすべての呼び出しもコンパイル時に削除されます。引数の評価すら実行されないため、実行時に副作用は残りません。コード生成テンプレートが「開発者が実装したければ実装できるフック」を安全に置けるのは、この仕組みのおかげです。
条件を外れると実装は必須になる(拡張部分メソッド/C# 9.0以降)
逆に、publicを付けた、void以外を返す、outを持つ、といったメソッドは実装宣言が必須です。プロパティ・インデクサー・コンストラクター・イベントも、条件を満たしようがないため常に実装が必要です。
ここで前提として押さえておきたいのが、アクセス修飾子付き・非void戻り値・out付きの部分メソッドを書けるようになったのはC# 9.0からだという点です(「拡張部分メソッド」と呼ばれます)。C# 8.0以前は、これらの形は宣言そのものがコンパイルエラーでした。ソースジェネレーターが値を返すメソッドを実装する、という後述の設計は、すべてこの拡張の上に成り立っています。
public partial class ItemStore
{
// public で string? を返すため、実装宣言が必須(C# 9.0以降の書き方)
public partial string? TryGetAt(int index);
}
public partial class ItemStore
{
private List<string> _items = ["one", "two", "three"];
public partial string? TryGetAt(int index)
=> index < _items.Count ? _items[index] : null;
}
そのほか、部分メンバーにはstatic・unsafeを付けられ、ジェネリックにもできます(型パラメーターの制約は宣言宣言と実装宣言で同一にする必要があります)。ひとつ落とし穴があり、実装のない部分メソッドからはデリゲートを作れません。実装がなければメソッド自体がコンパイル時に消えるためで、実装済みの部分メソッドであればデリゲート化できます。
本命はソースジェネレーターとの組み合わせ
実装必須の部分メソッドが真価を発揮するのは、その実装をソースジェネレーターがビルド時に書くケースです。公式が例に挙げる正規表現ソースジェネレーターでは、開発者は属性と宣言だけを書きます。
public partial class RegExSourceGenerator
{
// 実装は書かない。ビルド時にソースジェネレーターが生成する
[GeneratedRegex("cat|dog", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex CatOrDogGeneratedRegex();
private static void EvaluateText(string text)
{
if (CatOrDogGeneratedRegex().IsMatch(text))
{
// マッチしたテキストへの処理
}
}
}
実行時に正規表現を解釈するnew Regex(...)と違い、生成されたコードはコンパイル時に組み立て済みです。同じ発想は、P/Invokeの[LibraryImport]やSystem.Text.JsonのJsonSerializerContextにも使われています。「partialを付けろとコンパイラに言われる」場面のほとんどは、このソースジェネレーターが実装を注入する場所を確保するためだと考えて構いません。
部分プロパティ(C# 13)と部分コンストラクター・部分イベント(C# 14)
C# 12までは、partialにできるメンバーはメソッドだけでした。C# 13でプロパティとインデクサー、C# 14でインスタンスコンストラクターとイベントが対象に加わり、ソースジェネレーターが実装できるメンバーの幅が広がりました。部分プロパティ以外を含めたC# 13の新機能全体は言語機能の進化: C# 13とF# 9の新機能解説で、C# 15や.NET 11側の動向は.NET 11の新機能・リリース日・ロードマップ総まとめ|C# 15とRuntime Async、.NET 10との違いで扱っています。
部分プロパティの書き方:実装側で自動実装構文は使えない
部分プロパティも、宣言宣言と実装宣言の2つに分けて書きます。
// file1.cs 宣言宣言(自動実装プロパティと同じ見た目)
public partial class PartialProperties
{
public partial int Capacity { get; set; }
public partial string this[int index] { get; set; }
}
// file2.cs 実装宣言(本文を持つアクセサーが必要)
public partial class PartialProperties
{
private List<string> _items = ["one", "two", "three"];
public partial int Capacity
{
get => _items.Count;
set
{
if (value != _items.Count && value >= 0)
{
_items.Capacity = value;
}
}
}
public partial string this[int index]
{
get => _items[index];
set => _items[index] = value;
}
}
ここに部分プロパティ最大の制約があります。実装宣言に自動実装構文({ get; set; })は使えません。宣言宣言とまったく同じ見た目になり、コンパイラがどちらが宣言でどちらが実装なのか区別できなくなるためです。実装宣言には、本文を持つアクセサーが最低1つ必要になります(インデクサーとイベントも同様で、部分イベントの実装宣言ではaddとremoveの両方を定義します)。
fieldキーワード(C# 14)で実装宣言を短く書く
「実装宣言に本文が要る」と言われても、実際にはバッキングフィールドへ素通しするだけの場合が大半です。ここで効くのがC# 14で正式導入されたfieldキーワードで、コンパイラが生成するバッキングフィールドを手書きせずに参照できます(C# 13ではプレビュー機能として提供されていたため、公式ドキュメントにも「C# 13以降」と書かれたページが残っています。正式導入はC# 14です)。
// file1.cs
public partial class PropertyBag
{
public partial int MyProperty { get; set; }
}
// file2.cs fieldを使えば実装宣言が1行で済む
public partial class PropertyBag
{
public partial int MyProperty { get => field; set; }
}
fieldはget・setのどちらか片方だけでも、両方でも使えます。set側にだけ検証を差し込み、getとバッキングフィールドはコンパイラ生成のまま残す、という書き方もできます(例:set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));)。
宣言と実装のシグネチャ一致ルール:厳しい部分と緩い部分
宣言宣言と実装宣言のシグネチャは原則として一致が必要です。アクセシビリティ修飾子(アクセサー個別のものを含む)、パラメーターの型とref系修飾子、戻り値の型、タプルの要素名は完全に一致していなければコンパイルエラーになります。一方、次の3点は不一致でもエラーにはならず、警告で済む・あるいは何も起きないという緩さがあり、ここが実務での混乱の元になります。
- nullable注釈:片方が「null許容未指定」ならエラーになりません。null許容有効同士で注釈が食い違う場合は警告になります。
- 既定のパラメーター値:一致させる必要はありませんが、実装宣言側に既定値を書くと警告が出ます(既定値は宣言宣言側に書くのが正解です)。
- パラメーター名:不一致でも警告止まりです。ただし公式ドキュメントが明記するとおり出力されるILには宣言宣言側のパラメーター名が入ります。呼び出し側が名前付き引数を使う場合に見えるのもこの名前なので、実装宣言側の名前だけを直しても呼び出し側は変わりません。パラメーター名を変えるときは宣言宣言側を直してください。
ドキュメントコメント(///)については、両方に書いた場合は実装宣言側が採用されます。宣言宣言にだけ書いた場合はそれが使われます。属性は結合されますが、[CallerMemberName]などの呼び出し元情報属性だけは例外で、宣言宣言側のものだけが有効です(実装宣言側に書くと無視され、警告が出ます)。
partialの使いどころと、使わないほうがよい場面
使うべき場面:自動生成コードとの共存
partialが本領を発揮するのは、同じ型のコードを人間とツールが分担して書くときです。
- ソースジェネレーター:
[GeneratedRegex]、[LibraryImport]、JsonSerializerContextなど。宣言だけを書き、実装はビルド時に生成される。 - デザイナー生成コード:Windows フォームの
Form1.Designer.csや、WPFのXAMLから生成されるInitializeComponent。ツールが上書きするファイルと、手書きのイベントハンドラーを分離できる。 - スキャフォールディングされたコード:ORMやAPIクライアントの生成コードに手を入れず、部分メソッドのフック(
partial void OnXxxChanged())だけを実装する。 - 複数人が同じ型を同時に触る場面:ファイルが分かれるためコンフリクトが減る。
使わないほうがよい場面:肥大化クラスの分割
逆に、大きくなりすぎたクラスをpartialで複数ファイルに切って「見た目だけ小さくする」使い方は避けるべきです。分割してもクラスの責務は1つのままで、フィールドは全部分から見えたままです。1つの型の中で結合度が下がるわけではないため、責務分離にはなりません。ファイルを開いても全体像が追えなくなり、かえって保守性が下がります。ファイルが長すぎることが問題なら、それは型の責務が多すぎるサインであり、partialではなくクラスの分割(委譲や抽出)で解くべき問題です。「自動生成コードが絡まないのにpartialを使っている」型を見つけたら、設計を見直す価値があります。
よくある質問
C#のpartialとは何ですか?
1つの型やメンバーの定義を、複数の宣言に分割して書けるようにするキーワードです。分割した宣言はコンパイル時に結合され、出力される型は1つになります。実行時の型やパフォーマンスに影響はありません。クラス・構造体・インターフェース・レコードに使え、デリゲートと列挙型には使えません。
partialクラスは別のファイルに分けないといけませんか?
いいえ。同じファイル内に複数の部分宣言を書いても構いません。ただし実務では、自動生成コードと手書きコードを分けるためにファイルを分けるのが一般的です。分割は同一プロジェクト内であれば任意のファイルで行えますが、すべての部分が同一アセンブリ・同一モジュール内にある必要があり、複数のDLLにまたがることはできません。
部分プロパティはどのC#バージョンから使えますか?
C# 13(.NET 9世代)からです。C# 12以前はメソッドしかpartialにできませんでした。さらにC# 14ではインスタンスコンストラクターとイベントも部分メンバーにできるようになっています。なお静的コンストラクター、ファイナライザー、オーバーロードされた演算子はバージョンを問わずpartialにできません。
部分プロパティで「実装が自動実装プロパティになっている」とエラーが出るのはなぜですか?
部分プロパティの実装宣言に自動実装構文({ get; set; })を使うと、宣言宣言と見分けがつかなくなるためコンパイラが受け付けません。実装宣言には本文を持つアクセサーが最低1つ必要です。単に値を保持したいだけなら、C# 14のfieldキーワードを使って{ get => field; set; }と書くのが最短です。
partialメソッドを実装し忘れるとどうなりますか?
アクセス修飾子なし・void戻り値・outなし・virtual等なし、の4条件をすべて満たす部分メソッドであれば、実装がなくてもエラーにならず、宣言も呼び出しもコンパイル時に削除されます。この場合、引数の評価すら実行されません。条件を1つでも外れている部分メソッド(publicである、戻り値がある、など)や、部分プロパティ・部分コンストラクター・部分イベントは実装が必須で、書き忘れるとコンパイルエラーになります。
partialでクラスを分けるとパフォーマンスは落ちますか?
落ちません。分割はソースコード上の話で、コンパイル後は1つの型に結合されます。ILの出力も、最初から1つのファイルに書いた場合と変わりません。