C#拡張メソッドとは?書き方・使いどころ・注意点を初心者向けに徹底解説

はじめに

C#で開発していると、「既存のstring型に便利な判定処理を追加したい」「外部ライブラリのクラスを、プロジェクト内で使いやすくしたい」と感じることがあります。しかし、.NETが提供する型や外部ライブラリのクラスは、ソースコードを直接変更できません。

このような場面で役立つのが、C#の拡張メソッドです。拡張メソッドを利用すると、既存の型を変更することなく、その型に新しいメソッドが追加されたような形で処理を呼び出せます。

string message = "Hello";bool result = message.HasText();

string型には本来HasTextというメソッドはありません。しかし、拡張メソッドとして定義すれば、上記のようにインスタンスメソッドに近い構文で呼び出せます。

この記事では、C#の拡張メソッドの仕組み、基本的な書き方、実践的なコード例、使いどころ、注意点を初心者向けに解説します。さらに、C# 14で追加された拡張ブロックについても紹介します。

1. C#の拡張メソッドとは

1-1. 既存の型にメソッドを追加したように扱える機能

拡張メソッドは、既存の型を変更せずに、その型へ新しいメソッドを追加したように扱えるC#の機能です。

たとえば、string型を対象とするHasText拡張メソッドを作成すると、次のように呼び出せます。

string name = "Alice";

if (name.HasText()){Console.WriteLine("文字列が入力されています");}

実際にstringクラスへHasTextメソッドが追加されるわけではありません。コンパイラーが拡張メソッドを見つけ、適切なstaticメソッドの呼び出しとして処理します。

元の型を継承したり、ソースコードを修正したりせずに機能を追加できる点が、拡張メソッドの大きな特徴です。

1-2. 拡張メソッドの実体はstaticメソッド

従来構文で定義する拡張メソッドの実体は、staticクラス内に定義されたstaticメソッドです。

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

呼び出し側では、次の2通りの書き方ができます。

string text = "C#";

// インスタンスメソッドのように呼び出すbool result1 = text.HasText();

// 通常のstaticメソッドとして呼び出すbool result2 = StringExtensions.HasText(text);

どちらも同じHasTextメソッドを呼び出しています。拡張メソッドは「特別なインスタンスメソッド」ではなく、インスタンスメソッド形式でも呼び出せるstaticメソッドだと理解しておきましょう。

1-3. 通常のインスタンスメソッドとの違い

通常のインスタンスメソッドは、クラスや構造体の定義内に記述します。

public class User{public string Name { get; set; } = string.Empty;

public bool HasName(){return !string.IsNullOrWhiteSpace(Name);}

}

一方、拡張メソッドは対象型の外部にあるstaticクラスへ定義します。

public static class UserExtensions{public static bool HasName(this User user){return !string.IsNullOrWhiteSpace(user.Name);}}

主な違いは次のとおりです。

  • インスタンスメソッドは対象クラスの内部に定義される

  • 拡張メソッドは別のstaticクラスに定義される

  • インスタンスメソッドはprivateメンバーへアクセスできる

  • 拡張メソッドはpublicやinternalなど、呼び出し元から見えるメンバーにしかアクセスできない

  • 同じシグネチャのインスタンスメソッドと拡張メソッドがある場合、インスタンスメソッドが優先される

対象クラスを自分で管理している場合は、まず通常のインスタンスメソッドとして実装できないか検討することが大切です。

1-4. 継承やヘルパークラスとの違い

継承は、既存クラスを基に新しい派生クラスを作成する仕組みです。

public class CustomList<T> : List<T>{public bool HasItems(){return Count > 0;}}

ただし、sealedクラスや値型は継承できません。また、派生クラスを作っても、既存コードで使用している元の型が自動的に派生型へ変わるわけではありません。

ヘルパークラスの場合は、処理対象を引数として渡します。

bool result = StringHelper.HasText(text);

拡張メソッドなら、次のように対象と処理の関係を自然に表現できます。

bool result = text.HasText();

処理が明確に特定の型へ結び付いている場合は拡張メソッドが適しています。一方、複数の型や外部サービスを横断する処理は、サービスクラスやヘルパークラスとして設計したほうが責務を理解しやすくなります。

1-5. LINQで使われている代表的な拡張メソッド

C#でよく使われる拡張メソッドの代表例がLINQです。

int[] numbers = { 5, 2, 8, 1, 4 };

var result = numbers.Where(number => number % 2 == 0).OrderBy(number => number).Select(number => number * 10);

WhereOrderBySelectは、IEnumerable<T>などを対象とする拡張メソッドです。using System.Linq;によって名前空間を読み込むと、配列やリストのインスタンスメソッドであるかのように利用できます。

LINQの主な拡張メソッドには、次のようなものがあります。

  • Where:条件に一致する要素を抽出する

  • Select:各要素を別の値へ変換する

  • OrderBy:要素を昇順に並べる

  • GroupBy:指定したキーでグループ化する

  • Any:条件に一致する要素があるか判定する

  • FirstOrDefault:先頭の要素または既定値を取得する

LINQの読みやすいメソッドチェーンは、拡張メソッドの特性を活用した代表的なAPI設計です。

2. C#で拡張メソッドを使うメリット

2-1. 元のクラスを変更せずに機能を追加できる

拡張メソッドの最大のメリットは、元のクラスを変更せずに機能を追加できることです。

public static class DateTimeExtensions{public static bool IsWeekend(this DateTime date){return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;}}

呼び出し側では次のように記述できます。

DateTime today = DateTime.Today;

if (today.IsWeekend()){Console.WriteLine("今日は休日です");}

DateTimeは.NETが提供する型であり、プロジェクト側から定義を書き換えることはできません。拡張メソッドなら、元の型を変更せずにプロジェクト固有の操作を追加できます。

2-2. 継承できない型や外部ライブラリの型も拡張できる

拡張メソッドは、stringintのような型、sealedクラス、外部ライブラリが提供する型にも定義できます。

public static class IntExtensions{public static bool IsEven(this int value){return value % 2 == 0;}}
int number = 10;

Console.WriteLine(number.IsEven()); // True

対象型を所有している必要がなく、継承可能である必要もありません。ただし、本当に型そのものが変更されるわけではなく、フィールドや内部状態を追加することはできません。

2-3. メソッドチェーンで処理を読みやすく書ける

拡張メソッドが処理後の値を返すように設計されていると、複数の処理をメソッドチェーンとして記述できます。

string keyword = "  C# EXTENSION METHOD  ";

string normalized = keyword.Trim().ToLowerInvariant().Replace(" ", "-");

独自の拡張メソッドでも、次のようなAPIを作成できます。

string normalized = keyword.NormalizeWhitespace().ToLowerInvariant().ToSlug();

データが左から右へ変化するため、ネストしたstaticメソッドよりも処理の順序を追いやすくなります。

2-4. 重複する処理を共通化できる

同じ判定や変換を複数箇所へコピーすると、修正漏れや仕様の不一致が発生しやすくなります。

bool hasText = value != null && value.Trim().Length > 0;

この処理を拡張メソッドへまとめれば、呼び出し側は次のように統一できます。

bool hasText = value.HasText();

判定ルールを変更するときも、拡張メソッドの実装だけを修正すれば済みます。

2-5. ドメイン固有の処理を自然なAPIとして表現できる

業務ルールに合った名前を付けることで、コードの意図を明確にできます。

if (order.IsEligibleForFreeShipping()){// 送料無料にする}

呼び出し側で金額や会員区分を直接判定するより、IsEligibleForFreeShippingという名前に業務ルールをまとめたほうが意図を理解しやすくなります。

ただし、その処理が対象クラスの中心的な責務であり、クラス自体を修正できる場合は、インスタンスメソッドとして実装する選択肢も検討しましょう。

3. 拡張メソッドの基本的な書き方

3-1. staticクラスを作成する

従来構文で拡張メソッドを定義するには、最初にstaticクラスを作成します。

namespace Sample.Extensions;

public static class StringExtensions{}

一般的には、対象型の名前にExtensionsを付けて、StringExtensionsDateTimeExtensionsのようなクラス名にします。

staticクラスはインスタンス化できません。

// コンパイルエラーvar extensions = new StringExtensions();

拡張メソッドは状態を持つオブジェクトではなく、型に関連する操作を提供するものだからです。

3-2. staticメソッドとして定義する

拡張メソッド自体にもstaticを付けます。

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

クラスまたはメソッドのどちらか一方でもstaticが付いていないと、従来構文の拡張メソッドとして認識されません。

3-3. 第1引数にthisキーワードと拡張対象の型を指定する

従来構文では、第1引数へthisを付け、その後に拡張する型を指定します。

public static bool HasText(this string? value)

この場合、拡張対象はstring型です。第1引数のvalueには、呼び出し元の値が渡されます。

string text = "Hello";bool result = text.HasText();

上記は、概念的には次の呼び出しと同じです。

bool result = StringExtensions.HasText(text);

thisを付けられるのは第1引数だけです。

3-4. usingで名前空間を読み込む

拡張メソッドを別の名前空間に定義した場合は、呼び出し側でその名前空間を読み込みます。

using Sample.Extensions;

たとえば、次の拡張メソッドがあるとします。

namespace Sample.Extensions;

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

呼び出し側では次のように記述します。

using Sample.Extensions;

string text = "Hello";bool result = text.HasText();

拡張メソッドは、定義を含む名前空間がスコープへ取り込まれている場合に利用できます。

3-5. インスタンスメソッドのように呼び出す

必要な名前空間を読み込むと、ドット記法で呼び出せます。

string? input = "C#";

if (input.HasText()){Console.WriteLine(input);}

第1引数は呼び出し元から渡されるため、メソッド呼び出し時には記述しません。

引数が追加されている場合は、第2引数以降を指定します。

int number = 10;bool result = number.IsBetween(1, 20);

3-6. 通常のstaticメソッドとして呼び出すこともできる

従来構文で定義した拡張メソッドは、staticメソッドとして直接呼び出すこともできます。

bool result = IntExtensions.IsBetween(10, 1, 20);

通常は、次の拡張メソッド形式のほうが読みやすいでしょう。

bool result = 10.IsBetween(1, 20);

一方、同名の拡張メソッドが競合した場合や、どのクラスのメソッドを呼んでいるか明示したい場合は、staticメソッド形式が役立ちます。

4. コード例で学ぶ拡張メソッドの作り方

4-1. string型に文字列判定メソッドを追加する例

次の例では、文字列がnull、空文字、空白だけではないことを判定します。

namespace Sample.Extensions;

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

使用例は次のとおりです。

string? name = "Alice";string? empty = "   ";string? missing = null;

Console.WriteLine(name.HasText()); // TrueConsole.WriteLine(empty.HasText()); // FalseConsole.WriteLine(missing.HasText()); // False

引数をstring?にしているため、nullも明示的に受け取れます。

4-2. int型に数値判定メソッドを追加する例

整数が偶数かどうか判定する拡張メソッドです。

public static class IntExtensions{public static bool IsEven(this int value){return value % 2 == 0;}

public static bool IsOdd(this int value){return value % 2 != 0;}

}

Console.WriteLine(10.IsEven()); // TrueConsole.WriteLine(7.IsOdd());   // True

短い処理であっても、意味のある名前を付けることで、呼び出し側の意図を明確にできます。

4-3. DateTime型に日付処理メソッドを追加する例

月初と月末を取得する拡張メソッドを作成します。

public static class DateTimeExtensions{public static DateTime StartOfMonth(this DateTime date){return new DateTime(date.Year, date.Month, 1);}

public static DateTime EndOfMonth(this DateTime date){return date.StartOfMonth().AddMonths(1).AddTicks(-1);}

}

DateTime date = new DateTime(2026, 6, 15);

Console.WriteLine(date.StartOfMonth()); // 2026/06/01 00:00:00Console.WriteLine(date.EndOfMonth()); // 2026/06/30 23:59:59.9999999

データベース検索などで日付範囲を扱う場合は、月末の最終時刻を計算するより、「翌月の月初未満」という条件を使うほうが安全なこともあります。

DateTime start = date.StartOfMonth();DateTime nextMonth = start.AddMonths(1);

// start以上、nextMonth未満として検索する

4-4. 独自クラスに拡張メソッドを追加する例

拡張対象は.NETの標準型だけではありません。プロジェクト内の独自クラスにも定義できます。

public class Order{public decimal TotalAmount { get; init; }public bool IsCanceled { get; init; }}
public static class OrderExtensions{public static bool IsHighValue(this Order order){ArgumentNullException.ThrowIfNull(order);

    return !order.IsCanceled &amp;&amp; order.TotalAmount &gt;= 100_000m;}

}

var order = new Order{TotalAmount = 150_000m,IsCanceled = false};

Console.WriteLine(order.IsHighValue()); // True

ただし、IsHighValueOrderの中心的な業務ルールであり、Orderクラスを変更できるなら、インスタンスメソッドとして実装するほうが自然な場合もあります。

4-5. 引数を複数受け取る拡張メソッドの例

第1引数以外は、通常のメソッドと同じように追加できます。

public static class IntExtensions{public static bool IsBetween(this int value,int minimum,int maximum){return value >= minimum && value <= maximum;}}
int age = 25;

Console.WriteLine(age.IsBetween(20, 29)); // True

境界値を含むかどうかが名前だけでは判断しにくい場合は、IsBetweenInclusiveのように、仕様が伝わる名前へ変更する方法もあります。

4-6. 戻り値を利用してメソッドチェーンを作る例

検索用の文字列を整形する拡張メソッドを作成します。

using System.Text.RegularExpressions;

public static class StringExtensions{public static string NormalizeWhitespace(this string value){ArgumentNullException.ThrowIfNull(value);

    return Regex.Replace(value.Trim(), @"\s+", " ");}public static string ToSearchKeyword(this string value){ArgumentNullException.ThrowIfNull(value);return value.NormalizeWhitespace().ToLowerInvariant();}

}

string input = "  C#   EXTENSION   METHOD  ";string keyword = input.ToSearchKeyword();

Console.WriteLine(keyword);// c# extension method

戻り値を次の拡張メソッドが受け取れる型にすると、処理を自然につなげられます。

5. ジェネリック型・コレクション・インターフェースを拡張する方法

5-1. ジェネリック拡張メソッドの書き方

拡張メソッドにも型パラメーターを指定できます。

public static class ObjectExtensions{public static T Tap<T>(this T value, Action<T> action){ArgumentNullException.ThrowIfNull(action);

    action(value);return value;}

}

var user = new User { Name = "Alice" }.Tap(value => Console.WriteLine(value.Name));

Tapは受け取った値へ処理を実行した後、同じ値を返します。ただし、処理内容が見えにくくなる可能性もあるため、多用は避けましょう。

5-2. IEnumerableを拡張する方法

IEnumerable<T>を対象にすると、配列やList<T>など、さまざまなコレクションで利用できます。

public static class EnumerableExtensions{public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source){return source is null || !source.Any();}}
int[] numbers = { 1, 2, 3 };List<string> names = new();

Console.WriteLine(numbers.IsNullOrEmpty()); // FalseConsole.WriteLine(names.IsNullOrEmpty()); // True

対象を具体的なList<T>ではなくIEnumerable<T>にすることで、利用できる範囲が広がります。

ただし、Anyを実行すると列挙が開始されるため、遅延実行されるデータや外部リソースを読むシーケンスでは注意が必要です。

5-3. Listなどのコレクションを拡張する方法

List<T>固有の機能を使用したい場合は、対象をList<T>に限定できます。

public static class ListExtensions{public static bool AddIfNotContains<T>(this List<T> list,T item){ArgumentNullException.ThrowIfNull(list);

    if (list.Contains(item)){return false;}list.Add(item);return true;}

}

var numbers = new List<int> { 1, 2, 3 };

numbers.AddIfNotContains(3); // 追加されないnumbers.AddIfNotContains(4); // 追加される

要素数が多く、重複判定を頻繁に行う場合は、List<T>よりHashSet<T>が適していることがあります。拡張メソッドで既存設計の問題を隠さないことも重要です。

5-4. インターフェースを対象にするメリット

インターフェースを拡張すると、そのインターフェースを実装するすべての型で利用できます。

public interface IPriceProvider{decimal Price { get; }}
public static class PriceProviderExtensions{public static decimal CalculateTaxIncludedPrice(this IPriceProvider item,decimal taxRate){ArgumentNullException.ThrowIfNull(item);

    return item.Price * (1 + taxRate);}

}

public class Product : IPriceProvider{public decimal Price { get; init; }}

var product = new Product { Price = 1_000m };

Console.WriteLine(product.CalculateTaxIncludedPrice(0.1m)); // 1100

実装クラスごとに同じメソッドを書く必要がなく、共通の契約に基づいた処理を提供できます。

5-5. 型制約を指定するジェネリック拡張メソッド

whereを使うと、利用できる型を制限できます。

public static class EntityExtensions{public static bool HasId<T>(this T entity)where T : IEntity{ArgumentNullException.ThrowIfNull(entity);

    return entity.Id &gt; 0;}

}

public interface IEntity{int Id { get; }}

型制約により、メソッド内でIEntityのメンバーを安全に利用できます。

参照型のnullを除外する拡張メソッドも定義できます。

public static class EnumerableExtensions{public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)where T : class{ArgumentNullException.ThrowIfNull(source);

    foreach (T? item in source){if (item is not null){yield return item;}}}

}

5-6. LINQと組み合わせて使うときのポイント

独自拡張メソッドからLINQを利用すると、コレクション処理を簡潔に記述できます。

public static class EnumerableExtensions{public static IEnumerable<T> DistinctByKey<T, TKey>(this IEnumerable<T> source,Func<T, TKey> keySelector){ArgumentNullException.ThrowIfNull(source);ArgumentNullException.ThrowIfNull(keySelector);

    var keys = new HashSet&lt;TKey&gt;();foreach (T item in source){if (keys.Add(keySelector(item))){yield return item;}}}

}

現在のLINQにはDistinctByが用意されているため、実際のプロジェクトでは標準APIを優先します。この例のような拡張メソッドを作る前に、同等の標準機能がないか確認しましょう。

また、IEnumerable<T>を返すメソッドでは、処理がすぐに実行されず、列挙時に実行される場合があります。遅延実行なのか即時実行なのかを、命名やドキュメントで明確にすると安全です。

6. 拡張メソッドが役立つ使いどころ

6-1. 複数箇所で使う変換・判定処理を共通化するとき

日付、文字列、数値などに対する定型処理は、拡張メソッドと相性がよい用途です。

public static class DecimalExtensions{public static string ToYenText(this decimal value){return $"{value:N0}円";}}
decimal price = 12_800m;Console.WriteLine(price.ToYenText()); // 12,800円

表示ルールを1か所へまとめることで、画面ごとの表記揺れを減らせます。ただし、多言語対応や通貨設定が必要な場合は、カルチャーや通貨情報を受け取れる設計にしましょう。

6-2. 外部ライブラリやフレームワークの型を補助するとき

外部ライブラリのクラスへ、プロジェクト固有の変換や判定を追加したい場合にも便利です。

public static class ExternalResponseExtensions{public static bool IsSuccessful(this ExternalResponse response){ArgumentNullException.ThrowIfNull(response);

    return response.StatusCode is &gt;= 200 and &lt; 300;}

}

外部ライブラリの更新によって同名メソッドが追加される可能性があります。一般的すぎる名前を避け、プロジェクト固有の意味が伝わる命名を検討しましょう。

6-3. コレクション操作をわかりやすくするとき

複雑な抽出条件を拡張メソッドへまとめると、呼び出し側を読みやすくできます。

public static class OrderEnumerableExtensions{public static IEnumerable<Order> ActiveOrders(this IEnumerable<Order> orders){ArgumentNullException.ThrowIfNull(orders);

    return orders.Where(order =&gt; !order.IsCanceled);}

}

var activeOrders = orders.ActiveOrders().OrderBy(order => order.TotalAmount);

単なるWhereの短い別名を大量に作るのではなく、業務上の意味を持つ条件に限定することがポイントです。

6-4. ASP.NET Coreのサービス登録を整理するとき

ASP.NET Coreでは、DIコンテナへのサービス登録を拡張メソッドへまとめる設計がよく使われます。

using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions{public static IServiceCollection AddApplicationServices(this IServiceCollection services,IConfiguration configuration){ArgumentNullException.ThrowIfNull(services);ArgumentNullException.ThrowIfNull(configuration);

    services.AddScoped&lt;IOrderService, OrderService&gt;();services.AddScoped&lt;IProductService, ProductService&gt;();return services;}

}

呼び出し側は次のように整理できます。

builder.Services.AddApplicationServices(builder.Configuration);

サービス登録が増えた場合は、機能やレイヤーごとに拡張メソッドを分けると管理しやすくなります。

6-5. テストコードの可読性を高めるとき

テスト対象に対する共通判定を拡張メソッドへまとめると、テストの意図を読み取りやすくできます。

public static class OrderTestExtensions{public static bool HasValidTotal(this Order order){ArgumentNullException.ThrowIfNull(order);

    return order.TotalAmount &gt;= 0;}

}

Assert.True(order.HasValidTotal());

ただし、重要な検証内容を拡張メソッドの内部へ隠しすぎると、テストが失敗した理由を把握しにくくなります。テストフレームワークが提供する詳細なアサーションも活用しましょう。

6-6. ドメイン固有の処理を簡潔に表現するとき

業務用語をメソッド名として表現すると、条件式の意味が明確になります。

public static class CustomerExtensions{public static bool IsPremiumCustomer(this Customer customer){ArgumentNullException.ThrowIfNull(customer);

    return customer.TotalPurchaseAmount &gt;= 500_000m&amp;&amp; customer.RegisteredAt &lt;= DateTime.Today.AddYears(-1);}

}

if (customer.IsPremiumCustomer()){ApplyPremiumDiscount();}

ただし、判定にデータベースや外部APIへの問い合わせが必要なら、拡張メソッドよりドメインサービスやアプリケーションサービスが適しています。

7. 拡張メソッドを使わないほうがよいケース

7-1. 元のクラスにインスタンスメソッドを追加できる場合

対象クラスを自分で管理しており、その処理がクラスの本来の責務に含まれるなら、インスタンスメソッドを優先します。

public class Order{public decimal TotalAmount { get; init; }

public bool IsHighValue(){return TotalAmount &gt;= 100_000m;}

}

このほうが、Orderが持つ機能をクラス定義だけで把握できます。拡張メソッドは、型を変更できない場合や、中心的な責務とは異なる補助操作に向いています。

7-2. 特定のクラスの内部状態に強く依存する場合

拡張メソッドはprivateメンバーへアクセスできません。内部状態へ強く依存する処理を、公開メンバーだけで無理に実装すると、クラスのカプセル化を崩す原因になります。

内部状態が必要な処理は、対象クラスのインスタンスメソッドとして実装するか、必要な操作をクラスの公開APIとして設計しましょう。

7-3. 責務が異なる処理を無理に追加する場合

次のように、文字列からメールを送る拡張メソッドを作ると、対象型と処理の責務が一致しません。

// 避けたい例emailAddress.SendEmail("件名", "本文");

メール送信には、SMTP設定、認証、再送、ログなどの責務があります。IEmailSenderのようなサービスとして実装するほうが自然です。

await emailSender.SendAsync(emailAddress,subject,body);

7-4. 拡張メソッドが増えすぎて定義場所がわかりにくくなる場合

多数の拡張メソッドを1つのクラスへ詰め込むと、目的のメソッドを探しにくくなります。

public static class Extensions{// 文字列、日付、コレクション、HTTP、DB処理などが混在}

対象型や用途ごとに分割しましょう。

Extensions/├─ StringExtensions.cs├─ DateTimeExtensions.cs├─ EnumerableExtensions.cs└─ ServiceCollectionExtensions.cs

7-5. 状態を保持する必要がある場合

拡張メソッド自体は対象インスタンスへ新しいフィールドを追加できません。

呼び出し回数や前回の実行結果など、継続的な状態を保持する必要があるなら、通常のクラスを作成します。

public class RetryManager{private int _retryCount;

public void Retry(){_retryCount++;}

}

staticフィールドへ状態を保存する設計は、並行処理やテストで問題になりやすいため避けるのが基本です。

7-6. 継承・委譲・サービスクラスのほうが適切な場合

次のような処理は、拡張メソッド以外の設計が適している可能性があります。

  • 型の振る舞いを根本的に変更するなら継承やポリモーフィズム

  • 別オブジェクトへ処理を任せるなら委譲

  • 外部サービスやデータベースへアクセスするならサービスクラス

  • 状態を持つ処理なら通常のクラス

  • 複数の型を対等に扱う処理ならヘルパーやサービス

「ドット記法で書けるから」という理由だけで拡張メソッドを選ばず、処理の責務を基準に判断しましょう。

8. 拡張メソッドを使う際の注意点

8-1. 同名のインスタンスメソッドが優先される

適用可能なインスタンスメソッドと拡張メソッドが同名の場合、インスタンスメソッドが優先されます。

public class Sample{public string GetName(){return "インスタンスメソッド";}}

public static class SampleExtensions{public static string GetName(this Sample sample){return "拡張メソッド";}}

var sample = new Sample();

Console.WriteLine(sample.GetName());// インスタンスメソッド

拡張メソッド側を明示して呼び出す場合は、static形式を使用します。

Console.WriteLine(SampleExtensions.GetName(sample));// 拡張メソッド

将来、対象型に同名のインスタンスメソッドが追加されると呼び出し先が変わる可能性があるため、衝突しにくい名前を選びましょう。

8-2. private・protectedメンバーにはアクセスできない

拡張メソッドは対象クラスの外部に定義されたstaticメソッドです。そのため、対象型のprivateメンバーや、通常は外部から利用できないprotectedメンバーへ特別にアクセスできるわけではありません。

public class User{private string Secret { get; set; } = string.Empty;}
public static class UserExtensions{public static string GetSecret(this User user){// user.Secretにはアクセスできないreturn string.Empty;}}

拡張メソッドから利用できるのは、呼び出し元のコードから参照可能なメンバーです。

8-3. 拡張メソッドはオーバーライドできない

拡張メソッドは仮想メソッドではないため、virtualoverrideを使った動的ディスパッチはできません。

どの拡張メソッドを呼び出すかは、主にコンパイル時の型とスコープによって決まります。

public interface IAnimal{}

public class Dog : IAnimal{}

public static class AnimalExtensions{public static string GetSound(this IAnimal animal){return "不明";}

public static string GetSound(this Dog dog){return "ワン";}

}

IAnimal animal = new Dog();

Console.WriteLine(animal.GetSound());// コンパイル時の型がIAnimalなので「不明」

実行時の型に応じて振る舞いを切り替えたい場合は、インターフェースのメソッドや仮想メソッドを利用しましょう。

8-4. usingする名前空間によって利用可否が変わる

拡張メソッドを定義しただけでは、すべてのファイルから自動的に利用できるわけではありません。

using Sample.Extensions;

必要なusingがないと、対象インスタンスの候補に拡張メソッドが表示されず、コンパイルエラーになります。

プロジェクト全体で共通利用する場合は、global usingを使う方法もあります。

global using Sample.Extensions;

ただし、すべての拡張メソッドをグローバルに読み込むと、候補や競合が増えることがあります。

8-5. 同名の拡張メソッドによる競合に注意する

複数の名前空間から、同じ型を対象とする同じシグネチャの拡張メソッドを読み込むと、呼び出し先を決定できないことがあります。

using LibraryA.Extensions;using LibraryB.Extensions;

string text = "C#";

// 競合する可能性があるtext.Normalize();

その場合は、staticメソッドとして完全修飾名で呼び出します。

string result =LibraryA.Extensions.StringExtensions.Normalize(text);

競合を避けるには、一般的すぎるメソッド名を避けることも有効です。

8-6. nullのインスタンスでも呼び出せるため検証が必要

拡張メソッドはstaticメソッドとして処理されるため、nullの変数に対しても呼び出し構文自体は使用できます。

string? value = null;

bool result = value.HasText();

次の実装ならnullを安全に処理できます。

public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}

一方、次の実装ではvalue.Lengthへアクセスした時点でNullReferenceExceptionが発生します。

public static bool HasTextUnsafe(this string value){return value.Length > 0;}

nullを許容するのか、ArgumentNullExceptionを送出するのかを、プロジェクト内で統一しましょう。

8-7. 拡張対象を広くしすぎると候補一覧が煩雑になる

objectを対象にすると、ほぼすべての値で拡張メソッドが表示されます。

public static class ObjectExtensions{public static void DoSomething(this object value){}}

このような拡張メソッドが増えると、IDEの入力候補が煩雑になります。

必要な型やインターフェースへ対象を絞りましょう。

public static bool HasValidPrice(this IPriceProvider value)

8-8. 既存型の仕様を誤解させる命名を避ける

拡張メソッドは、既存型の正式な機能であるかのように見えます。そのため、標準APIと誤解される名前や、実際の処理と異なる名前は避けましょう。

たとえば、内部でデータベースへアクセスする処理を単にGetNameと名付けると、軽量なプロパティ参照と誤解される可能性があります。

// 外部アクセスがあることを判断しにくいuser.GetName();

非同期処理ならAsyncを付け、外部アクセスが必要ならサービスクラスとして分離するなど、コストや副作用が伝わるAPIにします。

9. 保守しやすい拡張メソッドを作る設計ポイント

9-1. クラス名を「対象型名+Extensions」にする

対象型が明確な場合は、次の形式がわかりやすい命名です。

StringExtensionsDateTimeExtensionsEnumerableExtensionsServiceCollectionExtensions

クラス名を見るだけで、どの型に対する拡張メソッドなのか判断できます。

複数の関連インターフェースを扱う場合は、用途を表す名前にする方法もあります。

DependencyInjectionExtensionsValidationExtensionsMappingExtensions

9-2. メソッド名から処理内容が伝わるようにする

判定メソッドにはIsHasCanなどを使用すると、bool値を返すことが伝わりやすくなります。

value.HasText();number.IsEven();order.CanBeCanceled();customer.IsPremiumCustomer();

変換メソッドでは、変換後の型や形式を表します。

value.ToSlug();date.ToJapaneseDateText();entity.ToDto();

ProcessExecuteHandleのような抽象的な名前は、処理内容を把握しにくいため慎重に使用しましょう。

9-3. 副作用を抑えて予測可能な処理にする

拡張メソッドは、対象を受け取って結果を返す単純な処理にすると理解しやすくなります。

public static string ToNormalizedText(this string value){ArgumentNullException.ThrowIfNull(value);

return value.Trim().ToLowerInvariant();

}

元の値を変更せず、新しい値を返す処理は予測しやすく、テストもしやすい設計です。

コレクションへ要素を追加するなどの副作用がある場合は、AddRemoveUpdateなど、変更が発生することを名前で示しましょう。

9-4. 1つのメソッドに複数の責務を持たせない

次のように、検証、保存、メール送信を1つの拡張メソッドへまとめる設計は避けます。

// 責務が多すぎる例order.ValidateSaveAndSendEmail();

責務ごとに分割します。

order.Validate();

await orderRepository.SaveAsync(order);await emailSender.SendOrderConfirmationAsync(order);

拡張メソッドは、対象型に関連する小さく明確な操作として設計するのが基本です。

9-5. nullや例外の扱いを統一する

nullの扱いには、主に次の方針があります。

// nullを通常の値として扱うpublic static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}
// nullを不正な引数として扱うpublic static string Normalize(this string value){ArgumentNullException.ThrowIfNull(value);

return value.Trim().ToLowerInvariant();

}

同じ用途の拡張メソッドで方針がばらばらだと、利用者が毎回実装を確認しなければなりません。

例外を送出する条件、nullを返す条件、空コレクションを返す条件などを、プロジェクト内で統一しましょう。

9-6. 用途ごとに名前空間とクラスを分ける

公開範囲を制御するには、名前空間の設計も重要です。

MyApp.Extensions.TextMyApp.Extensions.CollectionsMyApp.Extensions.DependencyInjectionMyApp.Tests.Extensions

アプリケーション用とテスト用の拡張メソッドを分けることで、本番コードにテスト専用APIが表示されることを防げます。

関連する拡張メソッドだけをusingできるようにすると、競合も減らせます。

9-7. XMLドキュメントコメントと単体テストを用意する

共有ライブラリやチーム開発では、XMLドキュメントコメントを付けると、IDE上で用途や注意点を確認できます。

/// <summary>/// 指定された日付が土曜日または日曜日か判定します。/// </summary>/// <param name="date">判定する日付。</param>/// <returns>/// 土曜日または日曜日の場合はtrue、それ以外はfalse。/// </returns>public static bool IsWeekend(this DateTime date){return date.DayOfWeek isDayOfWeek.Saturday or DayOfWeek.Sunday;}

単体テストでは、通常ケースだけでなく境界値も確認します。

[Fact]public void IsBetween_境界値を含む(){Assert.True(1.IsBetween(1, 10));Assert.True(10.IsBetween(1, 10));}

null、空文字、最小値、最大値、うるう年など、対象処理に応じた境界条件をテストしましょう。

10. 拡張メソッドが使えない・呼び出せないときの原因

10-1. 定義クラスまたはメソッドにstaticが付いていない

従来構文では、定義クラスとメソッドの両方にstaticが必要です。

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

次のように通常クラスへ定義しても、拡張メソッドとして認識されません。

// 誤りpublic class StringExtensions{public static bool HasText(this string value){return value.Length > 0;}}

10-2. 第1引数にthisキーワードが付いていない

従来構文では、拡張対象となる第1引数へthisを付けます。

// 正しいpublic static bool IsEven(this int value){return value % 2 == 0;}

thisがなければ、通常のstaticメソッドです。

// 拡張メソッドではないpublic static bool IsEven(int value){return value % 2 == 0;}

この場合、number.IsEven()とは呼び出せません。

10-3. 拡張対象の型が一致していない

stringを対象にした拡張メソッドは、直接関係のないintでは呼び出せません。

public static bool HasText(this string value)
int number = 10;

// 呼び出せない// number.HasText();

また、IEnumerable<T>IQueryable<T>など、似た操作を持つ型でも、対象型や必要な名前空間が異なることがあります。

変数のコンパイル時の型を確認しましょう。

10-4. 必要な名前空間をusingしていない

拡張メソッドが見つからないときは、定義された名前空間を読み込んでいるか確認します。

using MyApp.Extensions;

IDEの自動修正候補からusingを追加できる場合もあります。

同じプロジェクト内にクラスが存在していても、名前空間がスコープへ入っていなければ、拡張メソッド形式では呼び出せません。

10-5. クラスやメソッドのアクセス修飾子が適切でない

別アセンブリから利用する場合、クラスとメソッドがpublicである必要があります。

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

internalの場合は、原則として同じアセンブリ内からしか利用できません。

また、拡張メソッドを定義するstaticクラスは、通常、名前空間の直下へ配置します。

10-6. 同名のインスタンスメソッドが呼び出されている

拡張メソッドを定義したのに想定と異なる結果になる場合は、対象型に同名のインスタンスメソッドが存在しないか確認します。

value.Execute();

適用可能なインスタンスメソッドがある場合、拡張メソッドより優先されます。

拡張メソッドを直接呼び出して挙動を確認できます。

MyExtensions.Execute(value);

10-7. 複数の拡張メソッドが競合している

同じシグネチャの拡張メソッドが複数の名前空間から読み込まれていると、呼び出しがあいまいになります。

対処方法は次のとおりです。

  • 不要なusingを削除する

  • 拡張メソッドの名前を変更する

  • 対象型を具体的にする

  • staticメソッド形式で呼び出し元を明示する

LibraryA.StringExtensions.Normalize(value);

11. C# 14の拡張ブロックと従来構文

11-1. C# 14で追加された拡張ブロックとは

C# 14では、拡張メンバーを定義する新しいextensionブロック構文が追加されました。

従来の拡張メソッドは、第1引数へthisを付けたstaticメソッドとして定義します。

public static class StringExtensions{public static bool HasText(this string value){return !string.IsNullOrWhiteSpace(value);}}

C# 14では、拡張対象をextensionブロックへまとめられます。

public static class StringExtensions{extension(string value){public bool HasText(){return !string.IsNullOrWhiteSpace(value);}}}

C# 14の拡張ブロックは、メソッドに加えてプロパティや演算子、型に対するstatic拡張メンバーも定義できます。

11-2. extensionブロックを使った拡張メソッドの書き方

基本構文は次のとおりです。

public static class StringExtensions{extension(string value){public bool HasText(){return !string.IsNullOrWhiteSpace(value);}

    public string Truncate(int maximumLength){ArgumentOutOfRangeException.ThrowIfNegative(maximumLength);return value.Length &lt;= maximumLength? value: value<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[..maximumLength]</span>;}}

}

使用方法は従来の拡張メソッドと変わりません。

string text = "C# extension members";

Console.WriteLine(text.HasText());Console.WriteLine(text.Truncate(10));

extension(string value)で指定したvalueは、ブロック内のインスタンス拡張メンバーから参照できます。

11-3. 従来のthisパラメーター構文との違い

従来構文とC# 14の構文には、次のような違いがあります。

従来構文では、各メソッドの第1引数へthisを付けます。

public static bool HasText(this string value)

C# 14では、拡張対象をブロック単位で宣言します。

extension(string value){public bool HasText()}

同じ型に複数の拡張メンバーを定義するときは、拡張ブロックによって対象型をまとめて表現できます。

従来構文と新構文で定義した同等の拡張メソッドは同じ形式のILへコンパイルされ、利用側は定義に使われた構文を意識する必要がありません。既存の拡張メソッドを、必ず新構文へ移行しなければならないわけでもありません。

11-4. 拡張プロパティなど拡張メンバーの概要

C# 14の拡張ブロックでは、インスタンス拡張プロパティを定義できます。

public static class StringExtensions{extension(string value){public bool HasText =>!string.IsNullOrWhiteSpace(value);

    public int CharacterCount =&gt; value.Length;}

}

呼び出し側では、メソッドの丸括弧を付けずに利用します。

string text = "Hello";

Console.WriteLine(text.HasText);Console.WriteLine(text.CharacterCount);

型に対するstatic拡張メンバーも定義できます。

public static class DateTimeExtensions{extension(DateTime){public static DateTime UnixEpoch =>new DateTime(1970,1,1,0,0,0,DateTimeKind.Utc);}}
DateTime epoch = DateTime.UnixEpoch;

拡張ブロックでは、条件を満たす演算子も拡張メンバーとして定義できます。従来のthisパラメーター構文が対応するのは、インスタンス拡張メソッドです。

11-5. 従来構文と新構文の使い分け

既存プロジェクトや、C# 13以前も対象にするライブラリでは、従来構文が適しています。

public static bool IsEven(this int value)

C# 14以降に限定でき、同じ型へ複数の拡張メンバーを定義する場合や、拡張プロパティを利用したい場合は、拡張ブロックが選択肢になります。

extension(int value){public bool IsEven => value % 2 == 0;public bool IsOdd => value % 2 != 0;}

新規プロジェクトでも、チーム全員が新構文に慣れているとは限りません。対応するSDK、ライブラリの利用者、コーディング規約を考慮して決めましょう。

11-6. プロジェクトのC#バージョンを確認する方法

C#の言語バージョンは、対象フレームワーク、使用する.NET SDK、プロジェクト設定によって決まります。

プロジェクトファイルでは、次のように明示できます。

<PropertyGroup><TargetFramework>net10.0</TargetFramework><LangVersion>14.0</LangVersion></PropertyGroup>

インストールされているSDKは、次のコマンドで確認できます。

dotnet --version

利用可能なSDK一覧は次のコマンドで確認できます。

dotnet --list-sdks

C# 14は.NET 10の既定言語バージョンです。通常はLangVersionを固定せず、対象フレームワークに推奨される既定値を利用する方法もあります。言語バージョンを手動指定するときは、使用中のSDKがそのバージョンへ対応しているか確認してください。

12. C#の拡張メソッドに関するよくある質問

12-1. 拡張メソッドで既存クラスそのものは変更される?

既存クラスそのものは変更されません。

拡張メソッドは別のstaticクラスへ定義され、呼び出し時に対象インスタンスが第1引数として渡されます。そのため、次のような変更はできません。

  • 対象型へフィールドを追加する

  • privateメンバーへアクセスする

  • 対象型のインターフェース実装を増やす

  • 対象型の継承関係を変更する

  • 既存メソッドをオーバーライドする

あくまで「メソッドが追加されたように呼び出せる」機能です。

12-2. 拡張メソッドはどの型にも定義できる?

クラス、構造体、インターフェース、列挙型、ジェネリック型など、多くの型を対象にできます。

public static bool IsEven(this int value)public static bool HasText(this string? value)public static bool HasItems<T>(this IEnumerable<T> source)public static bool IsValid(this IValidatable value)

ただし、対象型へ内部状態を追加できるわけではありません。また、objectのように広すぎる型へ多数の拡張メソッドを定義すると、IDEの候補が煩雑になるため注意が必要です。

12-3. 拡張メソッドをオーバーロードできる?

引数の数や型が異なれば、通常のメソッドと同様にオーバーロードできます。

public static class StringExtensions{public static string Truncate(this string value,int maximumLength){return value.Length <= maximumLength? value: value[..maximumLength];}

public static string Truncate(this string value,int maximumLength,string suffix){if (value.Length &lt;= maximumLength){return value;}return value<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[..maximumLength]</span> + suffix;}

}

text.Truncate(10);text.Truncate(10, "...");

似たオーバーロードを増やしすぎると、呼び出しの意味がわかりにくくなるため、必要に応じて別名も検討しましょう。

12-4. 拡張メソッドから別の拡張メソッドを呼び出せる?

通常のメソッドと同じように呼び出せます。

public static class StringExtensions{public static string NormalizeWhitespace(this string value){ArgumentNullException.ThrowIfNull(value);

    return string.Join(" ",value.Split(' ',StringSplitOptions.RemoveEmptyEntries));}public static string ToSearchKeyword(this string value){return value.NormalizeWhitespace().ToLowerInvariant();}

}

同じ名前空間や利用可能な名前空間にある拡張メソッドなら、メソッドチェーンとして組み合わせられます。

依存関係が複雑になる場合は、共通のprivate staticメソッドへ処理を分離する方法もあります。

12-5. nullに対して拡張メソッドを呼び出すとどうなる?

拡張メソッド形式の呼び出し自体は可能です。

string? text = null;bool result = text.HasText();

拡張メソッドの内部にはnullが第1引数として渡されます。その後の結果は実装によって異なります。

public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}

この実装はnullを安全に処理します。

一方、nullを想定していないメソッドでは、先頭で例外を送出すると意図が明確です。

public static string Normalize(this string value){ArgumentNullException.ThrowIfNull(value);

return value.Trim();

}

12-6. 拡張メソッドと通常のstaticメソッドはどう使い分ける?

処理が特定の型へ自然に結び付く場合は、拡張メソッドが適しています。

date.IsWeekend();text.HasText();orders.ActiveOrders();

複数の値が対等な関係にある場合や、対象型との結び付きが弱い場合は、通常のstaticメソッドやサービスクラスが適しています。

DateRange.Overlaps(firstRange, secondRange);priceCalculator.Calculate(product, customer);emailSender.Send(message);

「第1引数を主語として読んだときに自然か」を判断基準の一つにするとよいでしょう。

12-7. 拡張メソッドはパフォーマンスに影響する?

従来構文の拡張メソッドはstaticメソッドであり、次の2つは同じメソッドを呼び出します。

text.HasText();StringExtensions.HasText(text);

そのため、同等のstaticメソッド呼び出しと比べて、拡張メソッドであること自体が特別に大きな実行コストを生むわけではありません。C# 14の拡張ブロックで定義した同等のメソッドも、従来構文と同じ形式のILへコンパイルされます。

実際のパフォーマンスは、メソッド内部の処理に左右されます。

特に、次の処理には注意しましょう。

  • コレクションを何度も列挙する

  • 不要なToListToArrayを実行する

  • ループ内で正規表現を毎回生成する

  • 大量の文字列連結を行う

  • データベースクエリを意図せず複数回実行する

  • 大きなオブジェクトを繰り返し生成する

拡張メソッドかどうかではなく、実装内容と呼び出し回数を基準に計測することが重要です。

まとめ

C#の拡張メソッドは、既存の型を変更せずに、新しいメソッドを追加したように扱える機能です。

従来構文では、staticクラス内にstaticメソッドを定義し、第1引数へthisと拡張対象の型を指定します。

public static class StringExtensions{public static bool HasText(this string? value){return !string.IsNullOrWhiteSpace(value);}}

呼び出し側では、インスタンスメソッドのように利用できます。

string? text = "C#";

if (text.HasText()){Console.WriteLine(text);}

拡張メソッドは、文字列や日付の変換、共通の判定処理、コレクション操作、ASP.NET Coreのサービス登録、外部ライブラリの補助などに役立ちます。

一方で、次の点には注意が必要です。

  • 適用可能な同名のインスタンスメソッドが優先される

  • privateメンバーにはアクセスできない

  • オーバーライドできない

  • 必要な名前空間をusingする必要がある

  • nullが第1引数として渡されることがある

  • 同名の拡張メソッドが競合することがある

  • 増やしすぎると定義場所や責務がわかりにくくなる

C# 14では、extensionブロックによって拡張メソッド、拡張プロパティ、static拡張メンバーなどをまとめて定義できるようになりました。ただし、従来のthisパラメーター構文も引き続き利用できます。

拡張メソッドを設計するときは、単にコードを短くするのではなく、「対象型に自然に結び付く処理か」「副作用やコストが予測できるか」「インスタンスメソッドやサービスクラスのほうが適切ではないか」を検討しましょう。適切に利用すれば、重複を減らし、読みやすく保守しやすいC#コードを作成できます。