C#の拡張メソッド・partial・入れ子クラス:C# 14 extensionまで
C#拡張メソッドが選ばれる規則(CS0121・CS1061)、C# 14のextensionブロックと拡張プロパティを.NET 10でコンパイルした結果、partialメソッド・プロパティ・コンストラクター、入れ子クラスの使いどころ。
2026年11月改訂: 2021年に韓国語で書いた記事を、日本語向けに書き直しました。元の3つの例(拡張メソッド、partial クラス、入れ子クラス)は .NET 10.0.14(C# 14)でも修正なしで同じ結果になりました。直したのは説明のほうです。「拡張メソッドは既存クラスの機能を拡張する」という説明には、元の型は変わらず、private メンバーにも触れないという点が抜けていました。また
partialを付けられる対象は2021年以降、アクセス修飾子付きメソッド(C# 9)、プロパティ(C# 13)、コンストラクターとイベント(C# 14)へと広がっています。C# 14 の extension ブロックは実際にコンパイルし、その結果を載せています。
手を入れられない型にヘルパーを「生やしたい」人、生成コードと手書きコードを分けたい人、クラスの中にクラスを置くべきか迷っている人向けの記事です。3つとも、型のコードをどこに置くかに関する文法です。結論を先に書くと、拡張メソッドは合うインスタンスメソッドがないときだけ選ばれ、どれが選ばれるかは using の範囲で決まります。partial は主にソースジェネレーターと組み合わせて使い、入れ子クラスは外側の private に触る必要がある補助型にだけ使います。プロパティそのものは C#プロパティの基本 と 応用編 で扱っています。
拡張メソッド:2021年の例を .NET 10 で
拡張メソッドは static クラスの中の static メソッドで、第1引数に this が付いています。そのおかげで 値.メソッド() とインスタンスメソッドのように呼べます。2021年の例です(不要な using は省略)。
using Extension;
namespace Extension
{
public static class ExtensionMethod
{
public static int Multiplication(this int var, int a, int b)
{
int result = var;
for (int i = 0; i < b; i++)
result *= a;
return result;
}
}
}
namespace ConsoleApplication21
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("{0}", 5.Multiplication(2, 3));
}
}
}
40
結果は2021年と同じです。名前は Multiplication ですが、計算しているのは 5 × 2³ = 40、つまり 「var に a を b 回掛ける」です。実務なら MultiplyByPower のように動作が伝わる名前にします。2021年の出力の最後にあった「続行するには何かキーを押してください」は Visual Studio のコンソールが付けていた行で、dotnet run では出ません。
2021年版に書いていなかった大事な点が2つあります。1つ目に、拡張メソッドは元の型を変えません。コンパイラは 5.Multiplication(2, 3) を ExtensionMethod.Multiplication(5, 2, 3) という普通の静的呼び出しに書き換えるだけなので、int の private メンバーには触れません。2つ目に、同じ理由で null に対しても呼べます。
public static string Truncate(this string? s, int max) =>
s is null ? "(null)" : s.Length <= max ? s : s[..max] + "…";
Console.WriteLine("Hello, extension".Truncate(5));
string? missing = null;
Console.WriteLine(missing.Truncate(5)); // NullReferenceException にならない
Console.WriteLine(StringExtensions.Truncate("static call", 6)); // 静的メソッドとして直接呼ぶ
Hello…
(null)
static…
拡張メソッドはどう選ばれるか
拡張メソッドのトラブルは、たいてい「どのメソッドが呼ばれたのか」です。実際に確かめた規則を図にまとめました。
1. インスタンスメソッドが常に先。 string に ToUpper という拡張メソッドを作っても呼ばれることはなく、コンパイラは警告すら出しません。
public static string ToUpper(this string s) => "extension ToUpper";
Console.WriteLine("abc".ToUpper());
ABC
ライブラリを更新したらその型に同名のインスタンスメソッドが増え、自作の拡張メソッドが黙って無視されるようになった、というのも同じ仕組みです。
2. 内側のスコープが先。 同じ名前の拡張メソッドを、using Shop.Text; で取り込んだ名前空間と、呼び出し側が属する Shop.App の両方に置くと、近い Shop.App 側が選ばれました。
Shop.App (enclosing namespace)
3. 同じ段に2つあればあいまいエラー、どこにもなければ using 漏れを疑う。 using A; と using B; が同じシグネチャの Shout を持ち込み、名前空間 C は using していない状態でビルドしました。
Program.cs(4,23): error CS0121: 次のメソッドまたはプロパティ間で呼び出しが不適切です: 'A.AExt.Shout(string)' と 'B.BExt.Shout(string)'
Program.cs(5,21): error CS1061: 'int' に 'Twice' の定義が含まれておらず、型 'int' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Twice' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
CS1061 のメッセージ自体が using に触れているとおり、IntelliSense に拡張メソッドが出てこないときはまず using を確認します。CS0121 は AExt.Shout(x) のように静的呼び出しにすれば回避できます。
C# 14 の extension ブロック:拡張プロパティと静的拡張メンバー
C# 14 で extension ブロックが入りました。受け取る側(レシーバー)をブロックの頭に一度だけ書き、中にはメソッドだけでなくプロパティ、静的メンバー、演算子まで定義できます。.NET 10 SDK で実際にコンパイル・実行したコードです。
int[] scores = [72, 95, 88];
Console.WriteLine(scores.IsEmptyOrNull); // 拡張プロパティ
Console.WriteLine(scores.Average2()); // ブロック内の拡張メソッド
List<string> names = ["a", "b"];
Console.WriteLine(names.SecondOrDefault); // ジェネリックな拡張プロパティ
Console.WriteLine(TimeSpan.FromMinutesSafe(90)); // 型に付く静的拡張メソッド
Console.WriteLine(string.Banner); // 静的拡張プロパティ
Console.WriteLine(new Point(5, 5) - new Point(1, 2)); // 拡張演算子
public record Point(int X, int Y);
public static class SequenceExtensions
{
extension<T>(IReadOnlyList<T>? source)
{
public bool IsEmptyOrNull => source is null || source.Count == 0;
public T? SecondOrDefault => source is { Count: > 1 } ? source[1] : default;
}
extension(int[] values)
{
public double Average2() => values.Length == 0 ? 0 : values.Sum() / (double)values.Length;
}
extension(TimeSpan) // 名前なしのレシーバー = 静的メンバーのみ
{
public static TimeSpan FromMinutesSafe(int m) => TimeSpan.FromMinutes(Math.Max(0, m));
}
extension(string)
{
public static string Banner => "== t.laboratory ==";
}
extension(Point)
{
public static Point operator -(Point a, Point b) => new(a.X - b.X, a.Y - b.Y);
}
}
False
85
b
01:30:00
== t.laboratory ==
Point { X = 4, Y = 3 }
確認できたことをまとめます。
- 設定なしでコンパイルできる。 net10.0 の既定(C# 14)でそのまま通ります。
-p:LangVersion=13に下げると、「C# 14 が必要」という案内ではなくerror CS1513: } が必要ですという構文エラーになるので、古い SDK でこのコードを開くと原因がわかりにくいはずです。 - 従来の拡張メソッドと互換。 別に
static class Ext { extension(int[] values) { … } }を作って試すと、ブロック内のメソッドはExt.Average2(a)と静的メソッドとして呼べ(結果2)、拡張プロパティTotalのゲッターもExt.get_Total(a)という静的メソッドで呼べました(結果6)。コンパイル結果は従来の拡張メソッドと同じ形です。 - 状態は持てない。 ブロック内に自動実装プロパティやフィールドを書くとエラーです。拡張でオブジェクトにデータを「足す」ことは相変わらずできません。
Program.cs(5,20): error CS9282: このメンバーは拡張ブロックでは使用できません
Program.cs(6,21): error CS9282: このメンバーは拡張ブロックでは使用できません
partial:クラス、メソッド、プロパティ、コンストラクター
2021年の partial クラスの例もそのまま動きました(Test()、Test2()、Test3() を出力)。ただしクラス名が Nested だったので、次の節の入れ子クラスと紛らわしい例でした。partial で分けた断片はコンパイル時に1つの型にまとめられ、同じアセンブリ・同じ名前空間にある必要があります。
2026年に partial を使ういちばんの理由はソースジェネレーターです。System.Text.Json、正規表現の [GeneratedRegex]、MVVM Toolkit などが「宣言は人が、実装はジェネレーターが」書く形をとっています。その分担を模した例です。
// Order.cs(手書き側)
public partial class Order
{
public partial string Code { get; set; } // C# 13:宣言部分
partial void OnShipped(); // 任意のフック:実装なし
public partial Order(int id); // C# 14:partial コンストラクター
public void Ship()
{
OnShipped(); // 実装がなければ呼び出しごと消える
OnValidated(Code);
Console.WriteLine($"Shipped #{Id} {Code}");
}
}
// Order.Generated.cs(ジェネレーターが書いたと仮定)
public partial class Order
{
public int Id { get; }
public partial Order(int id) { Id = id; }
public partial string Code
{
get => field;
set => field = value.Trim().ToUpperInvariant();
}
private partial void OnValidated(string code);
}
public partial class Order
{
private partial void OnValidated(string code) => Console.WriteLine($"validated {code}");
}
validated AB-12
Shipped #7 AB-12
OnShipped: not in the assembly
最後の行はリフレクションで OnShipped を探した結果です。アクセス修飾子のない void の partial メソッドは、実装がなければ宣言も呼び出しも削除されます。 逆に、アクセス修飾子付きの partial メソッド(C# 9)と partial プロパティ(C# 13)は実装が必須です。
error CS8795: 部分メソッド 'P.M()' にはアクセシビリティ修飾子が指定されているため、実装部分が必要です。
error CS9248: 部分プロパティ 'Settings.Volume' には実装パーツが必要です。
partial の対象 |
バージョン | 1つ下げたときのエラー |
|---|---|---|
| クラス・構造体・インターフェイス | C# 2 | CS8022(C# 1) |
void メソッド(修飾子なし、実装は任意) |
C# 3 | CS8023(C# 2) |
| 修飾子・戻り値のあるメソッド(実装必須) | C# 9 | CS8400(C# 8) |
| プロパティ・インデクサー | C# 13 | CS8703(C# 12) |
| コンストラクター・イベント | C# 14 | CS9260 / CS8703(C# 13) |
入れ子クラス:いつ使うか
2021年の入れ子クラスの例も a : 70、a : 60 と同じ結果でした。説明も正確で、入れ子の型の既定のアクセスレベルは private、入れ子クラスは外側クラスの private メンバーにアクセスできます。コンパイラで確かめた点を2つ足します。
var x = new Outer.Helper(); // 修飾子なしの入れ子クラス
public class Outer
{
class Helper { }
public int Value = 1;
public class Inner
{
public int Read() => Value; // 外側のインスタンスは自動では付いてこない
}
}
Program.cs(1,19): error CS0122: 'Outer.Helper' はアクセスできない保護レベルになっています
Program.cs(10,30): error CS0120: 静的でないフィールド、メソッド、またはプロパティ 'Outer.Value' で、オブジェクト参照が必要です
CS0120 は Java や Kotlin から来た人がよく引っかかるところです。C# の入れ子クラスは Java の static な入れ子クラスと同じで、外側のオブジェクトは自動では付いてきません。2021年の例のように、コンストラクターで外側のオブジェクトを受け取ります。
実務で入れ子クラスが合うのは、主に次の2つです。
public sealed class HttpRequestSpec
{
private HttpRequestSpec(string url, Dictionary<string, string> h) { … }
public static Builder CreateBuilder() => new();
public sealed class Builder // 外側の private コンストラクターを呼べる
{
…
public HttpRequestSpec Build() => new(url, headers);
}
}
public sealed class TtlCache<T>(TimeSpan ttl)
{
private sealed record Entry(T Value, DateTime ExpiresAt); // 外からは見えない
private readonly Dictionary<string, Entry> map = [];
…
}
https://example.com/api (1 header)
v
ビルダーは外側の private コンストラクターを呼ぶため、Entry は実装の詳細を隠すために入れ子にしました。逆に、ほかのコードが Outer.Inner と名前を何度も書くような public な型なら、トップレベルに出したほうが読みやすくなります(コード分析ルール CA1034 が公開された入れ子型を警告するのもそのためです)。
どれをいつ使うか
| やりたいこと | 選ぶもの |
|---|---|
手を入れられない型(string、IEnumerable<T>)にヘルパーを足す |
拡張メソッド、C# 14 からは extension ブロック |
| 計算した値をプロパティの形で付ける | C# 14 の拡張プロパティ(状態は不可) |
型名に静的ヘルパーを付ける(TimeSpan.FromMinutesSafe) |
C# 14 の静的拡張メンバー |
| 生成コードと手書きコードを分ける | partial 型 + partial メソッド・プロパティ |
| 外側の private に触る補助型(ビルダー、列挙子、内部レコード) | 入れ子クラス |
| あちこちで名前を使う型 | トップレベルの型 |
LINQ の Where や Select は IEnumerable<T> に対する拡張メソッドそのものです。使い方は LINQの基本、ジェネリックな拡張メソッドの型制約は C#のジェネリック にまとめています。
確認環境:.NET SDK 10.0.403 / ランタイム 10.0.14、C# 14(net10.0 の既定値)、macOS(Apple Silicon Mac)。
よくある質問
インスタンスメソッドと同じ名前の拡張メソッドを作るとどうなる?
インスタンスメソッドが常に優先され、拡張メソッドは呼ばれません。コンパイラは警告も出しません。string に ToUpper という拡張メソッドを作って "abc".ToUpper() を呼ぶと、拡張メソッドではなく string.ToUpper() が実行されて ABC が出力されました。
C# 14 で拡張プロパティは書ける?
書けます。static クラスの中に extension(int[] values) { public int Total => values.Sum(); } のような extension ブロックを書くと、拡張プロパティ、静的な拡張メンバー、演算子まで定義でき、.NET 10(C# 14)でコンパイル・実行できました。ただし自動実装プロパティとフィールドはブロック内に置けません(error CS9282)。
null の変数に拡張メソッドを呼ぶと NullReferenceException になる?
呼び出しの時点ではなりません。拡張メソッドは静的メソッドなので、null がそのまま第1引数として渡されます。メソッドの中で null を扱わずにメンバーへアクセスしたときに例外になります。例では null の文字列に Truncate を呼んで (null) と出力されました。