概要
Ver. 2.0
C# 2.0 で、partial 修飾子を付けることで、クラスや構造体、インターフェイスを複数のファイルに分けて型を定義できるようになりました。
この partial によるファイルの分割は、
「片方のファイルを手書き、もう片方のファイルを開発ツールなどによって自動生成」みたいな状況を想定しています。
(それ以外の用途でむやみに複数のファイル分けると、どのファイルに何のメソッドがあるのか探しにくくなるので、 通常は、むしろ、クラス定義を複数のファイルに分割しない方がいいです。)
背景: ツール生成のソースコード
Visual Studio などの統合開発環境を利用していると分かると思いますが、 ソースファイルの一部分はプログラマーの手書きではなく、 開発ツールが自動的に生成してくれる部分があります。
例えば、データベースのテーブル定義から C# のクラスを生成するみたいなツールがあります。
仮に、Id と Name の2つの列がある Entity という名前のテーブルから、以下のようなクラスをツール生成したとします。
class Entity
{
public int Id { get; set; }
public string? Name { get; set; }
}
これに対して、プロパティの値の書き換え時に処理を挟みたいとします。 一応、ツール生成のソースコードを書き換えれば目的を達成することは可能ではあります。
class Entity
{
private int _id;
public int Id { get => _id; set { if (_id != value) { _id = value; _changed = true; } } }
private string? _name;
public string? Name { get => _name; set { if (_name != value) { _name = value; _changed = true; } } }
private bool _changed;
public void Flush() => _changed = false;
}
ここで、データベースのテーブルに列 X を追加したので、ツール生成の C# コードも更新したいとします。
「手書きで書き方部分は残して新たに追加したものだけをソースコード生成」なんてことは難しく、普通はすべて上書きされます。
class Entity
{
public int Id { get; set; }
public string? Name { get; set; }
public int X { get; set; }
}
ソースコードの一部分を「ここはツール生成だから書き換えないで」領域にすることもできなくはないですが、なかなかに危険です。 例えば、WinForms (C# 1.1 時代からある GUI フレームワーク)開発がまさにそういう方式で、 WinForms アプリを作ると、ソースコード中に以下のような領域ができます。
namespace WinFormsApp1;
partial class Form1
{
// 前略
// この部分は手書きで書き換える想定。
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
まさに「書き換えないで」(do not modify)と書かれていますし、 実際、書き換えても Visual Studio によって元に戻されたりします。 手で書き換える想定のコードとツール生成コードが1ファイルに混ざっていることで、 例えば、「ファイル内で一斉置換」みたいな作業をしたときにツール生成部分を壊したりといった事故もありました。
型の分割
このように、手書きとツール生成の混在は危険なので、別ファイルに分かれている方が安心です。
そこで、C# 2.0 では、クラス定義時に partial というキーワードを付けることで、
クラス定義を複数に分割することができるようになりました。
これを 部分クラス(partial class)と言います。
例えば前節のツール生成例に partial を付けてみましょう。
partial class Entity
{
public int Id { get; set; }
public string? Name { get; set; }
}
この型に手で何かコードを足したい場合、別ファイルに以下のような感じでコードを書きます。
partial class Entity
{
private bool _changed;
public void Flush() => _changed = false;
}
(プロパティ Id や Name の中の処理を変更したい場合の話は次節の「メソッドの実装の分離」で説明します。)
partial はクラスの他に、構造体、インターフェイスにもつけれます。
partial struct S { }
partial struct S { }
partial interface I { }
partial interface I { }
partial record R { }
partial record R { }
partial record class RC { }
partial record class RC { }
partial record struct RS { }
partial record struct RS { }
ただ、部分クラスにしたい場合、すべての型定義に partial 修飾子を付ける必要があります。
これは、「ファイルを分けるつもりがなかったのに、他の誰かに勝手に部分定義を足された」みたいなことを避けるためです。
// 片方に partial が付いてないとエラー。
class C { }
partial class C { }
ちなみに、partial 以外の修飾子に関しては、複数ある型定義についてる修飾子すべてを統合したものになります。
例えば、以下のように「片方が public、もう片方が static」な場合、この型は public static 扱いです。
// この型は public static class C 扱い。
public partial class C { }
static partial class C { }
ここで実例として、WPF アプリを紹介します。
WinForms とは違って、WPF (C# 3.0 世代の GUI フレームワーク)は partial を使ってツール生成のコードと手書きコードを分けています。
WPF アプリでは、手での書き換えを前提とした以下のようなコードを書く一方で、
using System.Windows;
namespace WpfApp1;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
ツール生成で以下のようなコードが作られます(実物はだいぶ長いので一部抜粋)。
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Windows;
// 中略
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector
{
private bool _contentLoaded;
public void InitializeComponent()
{
if (_contentLoaded)
{
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/WpfApp1;V1.0.0.0;component/mainwindow.xaml", System.UriKind.Relative);
#line 1 "..\..\..\MainWindow.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
}
// 中略
}
}
こちらのツール生成のコードは、通常、どこに生成されたのかすら意識せず、中身を覗くこともほとんどありません。
メソッドの実装の分離
Ver. 3.0
C# 3.0 で 部分メソッド(partial method)という機能も追加されました。
どういうものかというと、
部分クラス内限定で、
メソッドに partial を付けることでメソッドの宣言と定義を分けれるというものです。
定義の仕方と、制限事項は以下の通り。
partial修飾子を付けてメソッドを宣言する。必ず部分クラス内になければならない。
アクセシビリティの指定はできない(自動的に必ず
private扱い)。戻り値は
void以外不可。引数は自由に取れる。
ref,this,paramsも利用可能。ただし、out引数は不可。静的メソッド(
static)でもインスタンス メソッド(非static)でも OK。
例として、前節の Entity の例で出てきた
「プロパティ Id や Name の中の処理を変更したい場合の話」をしましょう。
用途的に、「プロパティの値が変わったときに何かはしたい」、 「ただ、何をするかはアプリごとに異なる」 みたいなことがあります。 そういった場合、「プロパティの値が変わった」のタイミングを拾えるよう、 以下のように、部分メソッドを含むコードをツール生成してもらっておきます。
partial class Entity
{
private int _id;
public int Id { get => _id; set { if (_id != value) { _id = value; OnIdChanged(); } } }
partial void OnIdChanged();
private string? _name;
public string? Name { get => _name; set { if (_name != value) { _name = value; OnNameChanged(); } } }
partial void OnNameChanged();
}
OnIdChanged と OnNameChanged が部分メソッドです。
このまま何も手書きコードを足さなければ、これらのメソッドは何もしません。
(空のメソッドが呼ばれるとかですらなく、メソッドを呼んだ痕跡すらも完全に消えます。)
(さらにいうと、メタデータすら残さず、完全に消えます。
Conditional 属性でも似たようなことができますが、こちらは少なくともメタデータは残ります。)
一方、手書きコードで処理を足したければ以下のような感じのコードを書きます。
partial class Entity
{
partial void OnIdChanged() => _changed = true;
partial void OnNameChanged() => _changed = true;
private bool _changed;
public void Flush() => _changed = false;
}
こうすると、追加した OnIdChanged や OnNameChanged の実装が呼び出されるようになります。
部分メソッドの引数で副作用を起こす場合
「不要な場合は完全削除」という仕様には、1つ奇妙な動作を招く点があります。 問題が起こり得るのは、部分メソッドの呼び出しの際に引数で副作用を起こす場合です。
例えば、以下のコードの実行結果はどうなるでしょう。
partial class Program
{
static void Main(string[] args)
{
int x = 1;
A(x = 2);
Console.Write("{0}\n", x);
}
static partial void A(int x);
}
A の実装がある場合には 2 に、ない場合には 1 になります。
部分クラスなので、当然、実装は別ファイルにあってもかまいません。
自分以外の誰かがどこか別のところで実装を書くかもしれませんし、
誰も書かないかもしれません。
要するに、自分の知らないところで実行結果が変えられてしまう可能性がある。
まあ、メソッド呼び出しの () 内で代入なんてするなよって話ではあるんですが。
こういう副作用があることも覚えておいてください。
(あるいは、この副作用を積極的に利用したトリッキーなコードも書けるでしょうが・・・。
個人的には非推奨。)
部分メソッドの拡張
Ver. 9
前節で説明した部分メソッドは「開発ツールが生成したコードが先にあって、そこに手書き処理を足したいときに使う物」です。
一方、C# 9.0 世代ではソースコード生成機能が入ったことでこの逆があり得ます。 すなわち、「ソースコード生成してもらう前提で、手書きでは不完全な C# コードを書きたい」という場面が出てきました。
C# 9.0 では、そのための「不完全なメソッド」を書く方法として partial キーワードを再利用することにしました。
旧来の部分メソッドとの文法上の差はアクセシビリティ修飾子(public とか private とか)を持つかどうかです。
// (1) ツールが事前に生成する想定のコード
partial class PartialClass
{
public void PreGeneratedMethod()
{
OnPreGeneratedMethod();
// ツール生成のコード
}
// ツール生成のコードの前に何か手書き処理を足したければこのメソッドの中身を書く
partial void OnPreGeneratedMethod();
}
// (2) 手書き前提のコード
partial class PartialClass
{
#if DEBUG
// ツール生成コード前に、Debug 時のみログを仕込む。
// これを書かなければ OnPreGeneratedMethod は呼ばれる痕跡すら残らない。
partial void OnPreGeneratedMethod()
{
System.Console.WriteLine(
"PreGeneratedMethod が呼ばれた直後"
+ WantSourceGenerated());
}
#endif
// 手書き C# コードが先にあって、これを元にソースコード生成してほしいメソッド。
private partial string WantSourceGenerated();
}
// (3) C# からのソースコード生成が前提のコード
partial class PartialClass
{
private partial string WantSourceGenerated() => "手書きはしづらしくて、ソースコード生成なら楽な文字列";
}
コード解析・コード生成の利用で紹介している StringLiteralGenerator はこの新しい部分メソッドを使っています。
ちなみに、コード生成と手書きの期待される順序が逆になっただけ(しかも文法上は非常に小さな差)ですが、結果的には結構できること・できないことが変わります。 以下の表に違いをまとめます。
| 旧(アクセス修飾子なし) | 新(アクセス修飾子あり) |
|---|---|
| アクセシビリティの指定は不可 | アクセシビリティの指定が必須(private 含む) |
| 戻り値は void のみ | 任意の戻り値を使える |
| ref 引数、out 引数を持てない | ref 引数、out 引数を持てる |
| 本体を持っていなくてもいい。なければ完全に消える。 | どこか1か所で本体を持たないとダメ。なければコンパイル エラー。 |
アクセシビリティ修飾子の有無だけでここまで差があることには少し抵抗があって、 文法をどうするかは C# チームも結構迷ったようです。 ただ、最終的には、下手にキーワードを追加したり全然違う文法を導入するよりはマシという判断が下りました。
シグネチャの一致
部分メソッドは、宣言側と実装側でシグネチャ(引数リスト、戻り値の型、修飾子)が一致している必要があります。
アクセシビリティ、static, readonly, ref の有無が違うとエラーになります。
partial class PartialClass
{
public static partial ref readonly int M0();
public static partial ref readonly int M1();
public static partial ref readonly int M2();
public static partial ref readonly int M3();
public static partial ref readonly int M4();
public static partial ref readonly int M5();
}
partial class PartialClass
{
// 全部一致。これは大丈夫。
public static partial ref readonly int M0() => throw new Exception();
// 戻り値の型が違うのは当然ダメ。エラー。
public static partial ref readonly byte M1() => throw new Exception();
// 以下、修飾子のどこかが違う。全部エラー。
private static partial ref readonly int M2() => throw new Exception();
public partial ref readonly int M3() => throw new Exception();
public static partial int M4() => throw new Exception();
public static partial ref int M5() => throw new Exception();
}
タプル要素名の差もエラーになります。
partial class PartialClass
{
public partial (int x, int y) M0((int x, int y) t);
public partial (int x, int y) M1((int x, int y) t);
public partial (int x, int y) M2((int x, int y) t);
}
partial class PartialClass
{
// 全部一致。これは大丈夫。
public partial (int x, int y) M0((int x, int y) t) => default;
// タプル要素名が違うとエラーに。
public partial (int x, int y) M1((int a, int b) t) => default;
public partial (int a, int b) M2((int x, int y) t) => default;
}
引数名、null 許容参照型のアノテーションの差は警告になります。
partial class PartialClass
{
public partial void M0(int x, string? y);
public partial void M1(int x, string? y);
public partial void M2(int x, string? y);
}
partial class PartialClass
{
// 全部一致。これは大丈夫。
public partial void M0(int x, string? y) { }
// 引数名が違う。警告。
public partial void M1(int a, string? y) { }
// nullability が違う。警告。
public partial void M2(int a, string y) { }
}
一方で、属性は統合されます。
partial class PartialClass
{
[A] public partial void M([A] int x);
}
partial class PartialClass
{
[B] public partial void M([B] int x) { }
}
// [A, B] public partial void M([A, B] int x) { } と書いたのと同じになる。
class A : Attribute { }
class B : Attribute { }
部分プロパティ
Ver. 13
C# 3.0 からある方の部分メソッド は「戻り値なし(void)でないとダメ」という制約があって、
メソッド以外では元々役に立ちません。
一方、C# 9.0 で拡張された方の部分メソッド は、 制約的にも用途的にも、プロパティやインデクサーでも使えるはずです。 実際、工数の問題で後回しになっていただけで、C# 13 でめでたく部分プロパティ・部分インデクサーが実装されました。
// 元コード。
partial class PartialClass
{
// 部分プロパティ。
public partial int PartialProprty { get; set; }
// 部分インデクサー。
public partial int this[int index] { get; set; }
}
// コード生成で作ってもらう前提のコード。
partial class PartialClass
{
private int _field;
public partial int PartialProprty { get => _field; set => _field = value; }
private int[] _array = new int[10];
public partial int this[int index] { get => _array[index]; set => _array[index] = value; }
}
この機能の追加で特にうれしいのは GeneratedRegex の存在でしょう。
C# 12 までは、以下のようにメソッドにする必要がありました。
using System.Text.RegularExpressions;
partial class MyPatterns
{
[GeneratedRegex(@"\d{4}")]
public static partial Regex FourDigits();
}
この属性を付けると、正規表現 \d{4} のマッチ処理をソースコード生成で作ってくれます。
(普通に new Regex(@"\d{4}") と書くよりもだいぶパフォーマンスがよくなります。)
ただ、生成されるコードはメソッドよりもプロパティっぽいコード (呼ぶたびに何か処理をするのではなく、最初の1回で作ったインスタンスをキャッシュして持っておいて、2回目からはほとんどノーコスト)になっています。 これまでは「部分プロパティがなかったからやむなくメソッドに付けていた」というだけで、 C# 13 と同世代の .NET 9 からはプロパティで同じことができるようになりました。
using System.Text.RegularExpressions;
partial class MyPatterns
{
[GeneratedRegex(@"\d{4}")]
public static partial Regex FourDigits { get; } // プロパティになった。
}
プロパティの宣言と自動実装
C# の文法上の紛らわしさなんですが、
プロパティに対して { get; set; } などと書いたときの扱いには2種類あるので注意が必要です。
普通のクラスや構造体で { get; set; } を書くとき、これは自動実装になります。
class C
{
// 自動実装の意味。
public int X { get; set; }
// コンパイラーが裏でフィールドを1個作って、
// public int X { get => field; set => field = value; }
// みたいなコードとして扱われる。
}
一方、インターフェイスや、抽象メンバーの場合、「宣言だけある」という扱いになります。
interface I
{
// 宣言(「このプロパティは get も set も持っていてほしい」という意思表示のみ)。
int X { get; set; }
}
abstract class C
{
// これも宣言のみ。
public abstract int X { get; set; }
}
部分プロパティの場合は後者の意味になります。
partial class C
{
// 宣言(「partial の片割れで get も set も実装してほしい」という意思表示)。
public partial int X { get; set; }
}
「片方が宣言、片方が自動実装」みたいなことにはならないので、 以下のコードはコンパイル エラーを起こします。
partial class C
{
// 宣言。
public partial int X { get; set; }
}
partial class C
{
// こっちも宣言。
// なので、実装がいなくてエラーになる。
public partial int X { get; set; }
}
部分イベントと部分コンストラクター
部分プロパティ (C# 13)に続いて、 C# 14 ではイベントとコンストラクターも部分定義できるようになりました。 (これも「工数の問題で後回しになっていただけ」の類です。)
// 元コード(手書き想定)。
partial class PartialClass
{
// 部分イベント。
public partial event Action<int>? PartialEvent;
// 部分コンストラクター。
public partial PartialClass();
}
// コード生成で作ってもらう前提のコード。
partial class PartialClass
{
private Action<int>? _partialEvent;
public partial event Action<int>? PartialEvent
{
add => _partialEvent += value;
remove => _partialEvent -= value;
}
public partial PartialClass() { }
}
partial キーワードの位置
部分クラス・部分メソッドの仕様は C# 2.0 から追加されたものです。
partialというキーワードも 2.0 からの後付けなわけで、完全に予約語(変数などの名前に使えない単語)にしてしまうと、1.0 時代に書かれたコードを壊す可能性がありました。
そこで、partial は文脈キーワードになっています。
partialという単語がキーワード扱いされるのは、class、struct、interface、voidの直前だけです。
(前節の拡張部分メソッドの場合は戻り値の型の直前だけ。)
その結果、以下のように、語順に制約があります。
// OK
public static partial class Ok1 { }
static public partial class Ok2 { }
// コンパイル エラー
public partial static class Ng1 { }
partial public static class Ng2 { }
static partial public class Ng3 { }
partial class X
{
// OK
static partial void Ok();
// コンパイル エラー
partial static void Ng();
}
