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

はじめに

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

たとえば、string型に「空白ではないかを判定するメソッド」を追加したり、int型に「偶数かどうかを判定するメソッド」を追加したりできます。

C#
string name = "Alice";

if (name.IsNotBlank())
{
Console.WriteLine(name);
}

IsNotBlank()string型に最初から用意されているメソッドではありません。しかし、拡張メソッドとして定義すると、通常のインスタンスメソッドのような形式で呼び出せます。

この記事では、C#のExtension Methodsの仕組み、基本的な書き方、実践的なコード例、使いどころや注意点を初心者向けに解説します。

1. C#のExtension Methods(拡張メソッド)とは

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

Extension Methodsとは、既存のクラス、構造体、インターフェースなどに、後からメソッドを追加したように扱えるC#の機能です。

たとえば、次のようなコードでstring型にIsNotBlank()という拡張メソッドを定義できます。

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

定義したメソッドは、次のように呼び出せます。

C#
string message = "Hello";

bool result = message.IsNotBlank();

Console.WriteLine(result); // True

実際にstringクラスのソースコードへIsNotBlank()が追加されるわけではありません。コンパイラが拡張メソッドを認識し、インスタンスメソッドのような記述を可能にしています。

拡張メソッドを利用すると、処理を読みやすい形でまとめられます。

C#
// 通常の静的メソッド
bool result1 = StringUtility.IsNotBlank(message);

// 拡張メソッド
bool result2 = message.IsNotBlank();

後者は処理対象であるmessageからコードを読み始められるため、何に対する処理なのかを理解しやすい点が特徴です。

1-2. 通常のインスタンスメソッド・静的メソッドとの違い

インスタンスメソッドは、クラスや構造体の内部に定義されます。

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

public void Introduce()
{
Console.WriteLine($"私は{Name}です。");
}
}

呼び出すときは、対象のインスタンスを通じて実行します。

C#
var user = new User
{
Name = "田中"
};

user.Introduce();

一方、通常の静的メソッドはクラス名を指定して呼び出します。

C#
public static class UserUtility
{
public static void Introduce(User user)
{
Console.WriteLine($"私は{user.Name}です。");
}
}
C#
UserUtility.Introduce(user);

拡張メソッドも実体は静的メソッドですが、呼び出すときはインスタンスメソッドのように記述できます。

C#
public static class UserExtensions
{
public static void Introduce(this User user)
{
Console.WriteLine($"私は{user.Name}です。");
}
}
C#
user.Introduce();

それぞれの違いを整理すると、次のようになります。

種類定義場所主な呼び出し方
インスタンスメソッド対象の型の内部user.Introduce()
静的メソッドstaticクラスなどUserUtility.Introduce(user)
拡張メソッド対象の型とは別のstaticクラスuser.Introduce()

拡張メソッドは通常のメンバーではないため、対象クラスのprivateフィールドやprivateメソッドにはアクセスできません。利用できるのは、基本的に外部からアクセス可能なメンバーです。

1-3. 継承や元のクラス変更をせずに機能を拡張できる理由

拡張メソッドは、対象のクラスを書き換えたり、対象クラスを継承したりする機能ではありません。

次の呼び出しは、

C#
message.IsNotBlank();

コンパイラによって、概念的には次の静的メソッド呼び出しとして処理されます。

C#
StringExtensions.IsNotBlank(message);

つまり、対象のインスタンスを拡張メソッドの第1引数として渡しているだけです。

この仕組みにより、次のような自分で変更できない型にも独自の処理を追加できます。

  • .NETが提供するstringint

  • 外部ライブラリが提供するクラス

  • 別のプロジェクトで管理されているクラス

  • インターフェースを実装する複数の型

ただし、拡張メソッドよりも対象型に元から定義されているインスタンスメソッドが優先されます。

たとえば、対象クラスにShow()というインスタンスメソッドが存在し、同じ形式の拡張メソッドも存在する場合、通常はインスタンスメソッドが呼び出されます。

C#
public class Sample
{
public void Show()
{
Console.WriteLine("インスタンスメソッド");
}
}

public static class SampleExtensions
{
public static void Show(this Sample sample)
{
Console.WriteLine("拡張メソッド");
}
}
C#
var sample = new Sample();
sample.Show(); // インスタンスメソッド

この性質から、既存メソッドの動作を拡張メソッドで上書きすることはできません。

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

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

次のコードでは、リストに対してWhere()OrderBy()Select()ToList()を呼び出しています。

C#
var numbers = new List<int> { 5, 2, 8, 1, 4 };

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

Console.WriteLine(string.Join(", ", results)); // 20, 40, 80

これらはList<T>クラスに直接定義されたメソッドではありません。System.Linq.Enumerableクラスなどで、主にIEnumerable<T>を対象とする拡張メソッドとして提供されています。Microsoft Learn+1

そのため、IEnumerable<T>を実装する配列やリストなどで、同じLINQメソッドを利用できます。

C#
int[] array = { 1, 2, 3, 4, 5 };
List<int> list = new() { 1, 2, 3, 4, 5 };

var arrayResult = array.Where(number => number >= 3);
var listResult = list.Where(number => number >= 3);

拡張メソッドを利用すると、LINQのように複数の処理を連結する「メソッドチェーン」も作りやすくなります。

2. C#で拡張メソッドを定義する基本構文

2-1. staticクラスとstaticメソッドを作成する

一般的な拡張メソッドは、staticクラスの中にstaticメソッドとして定義します。

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

このコードには、次の特徴があります。

C#
public static class StringExtensions

拡張メソッドを格納するクラスにはstaticを付けます。

C#
public static bool IsNotBlank(...)

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

クラス名に決まったルールはありませんが、対象型の名前にExtensionsを付ける形式が一般的です。

C#
StringExtensions
IntExtensions
DateTimeExtensions
EnumerableExtensions
UserExtensions

この命名にすると、拡張メソッドを定義するクラスであることを判別しやすくなります。

2-2. 第1引数にthisキーワードを付けて拡張対象を指定する

従来形式の拡張メソッドでは、第1引数の型名の前にthisキーワードを付けます。

C#
public static bool IsNotBlank(this string value)

この場合、拡張対象はstring型です。

C#
this string value

valueには、メソッドを呼び出した対象の値が渡されます。

C#
string message = "Hello";
bool result = message.IsNotBlank();

この呼び出しでは、messagevalueに渡されます。

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

C#
bool result = StringExtensions.IsNotBlank(message);

thisを付けられるのは第1引数だけです。次のように第2引数へ付けることはできません。

C#
// コンパイルエラー
public static bool Sample(string value, this int number)
{
return true;
}

なお、C# 14では、従来のthisパラメーター形式に加えて、extensionブロックを使う新しい拡張メンバー構文も導入されています。新構文ではメソッドだけでなく、拡張プロパティや拡張演算子なども定義できます。既存コードや幅広いC#バージョンで利用する場合は、この記事で扱うthisパラメーター形式が理解しやすいでしょう。Microsoft Learn+2Microsoft Learn+2

2-3. 拡張メソッドを呼び出す方法

拡張メソッドは、通常のインスタンスメソッドと同じ形式で呼び出せます。

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

bool result = number.IsEven();

Console.WriteLine(result); // True

引数がある場合は、第2引数以降の値を丸括弧内に指定します。

C#
public static class StringExtensions
{
public static string Repeat(this string value, int count)
{
return string.Concat(Enumerable.Repeat(value, count));
}
}
C#
string result = "C#".Repeat(3);

Console.WriteLine(result); // C#C#C#

第1引数のvalueには"C#"が渡され、第2引数のcountには3が渡されます。

2-4. 通常の静的メソッドとして呼び出す方法

拡張メソッドは静的メソッドとして定義されているため、クラス名を指定して直接呼び出すこともできます。

C#
int number = 10;

bool result1 = number.IsEven();
bool result2 = IntExtensions.IsEven(number);

result1result2は、どちらも同じ結果になります。

通常はインスタンスメソッド形式のほうが読みやすいため、次の形式を使用します。

C#
number.IsEven();

ただし、同名の拡張メソッドが複数存在して呼び出しが曖昧になる場合や、どのクラスのメソッドを使うか明示したい場合は、静的メソッド形式が役立ちます。

C#
bool result = MyProject.Extensions.IntExtensions.IsEven(number);

2-5. namespaceとusingディレクティブの関係

拡張メソッドをインスタンスメソッド形式で呼び出すには、通常、その拡張メソッドが定義された名前空間をusingディレクティブで読み込む必要があります。

たとえば、次の名前空間に拡張メソッドを定義したとします。

C#
namespace MyApp.Extensions;

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

利用側では、次のように名前空間を読み込みます。

C#
using MyApp.Extensions;

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

using MyApp.Extensions;がない場合、コンパイラはIsNotBlank()を拡張メソッドの候補として見つけられません。

C#
// using MyApp.Extensions; がないと呼び出せない
bool result = message.IsNotBlank();

ただし、完全修飾名を使った静的メソッド形式であれば、usingがなくても呼び出せます。

C#
bool result =
MyApp.Extensions.StringExtensions.IsNotBlank(message);

プロジェクト内の拡張メソッドは、用途ごとに名前空間を分けると管理しやすくなります。

MyApp.Extensions.Strings
MyApp.Extensions.Collections
MyApp.Extensions.Domain

一方で、多数の拡張メソッドを広い名前空間へまとめると、候補が増えてコード補完が分かりにくくなることがあります。必要な範囲に限定して公開することが重要です。

3. 拡張メソッドの書き方をコード例で解説

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

string型に、空文字や空白だけの文字列ではないことを判定するメソッドを追加してみましょう。

C#
namespace MyApp.Extensions;

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

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

C#
using MyApp.Extensions;

string? name1 = "Alice";
string? name2 = " ";
string? name3 = null;

Console.WriteLine(name1.IsNotBlank()); // True
Console.WriteLine(name2.IsNotBlank()); // False
Console.WriteLine(name3.IsNotBlank()); // False

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

ただし、nullに対して拡張メソッドを呼び出せるからといって、メソッド内部で安全に処理できるとは限りません。次の実装では、valuenullの場合に例外が発生します。

C#
public static int GetLength(this string? value)
{
return value.Length;
}

安全に処理するには、nullを考慮します。

C#
public static int GetLengthOrZero(this string? value)
{
return value?.Length ?? 0;
}
C#
string? message = null;

Console.WriteLine(message.GetLengthOrZero()); // 0

文字列の前後から特定の文字列を取り除く拡張メソッドも作成できます。

C#
public static string RemovePrefix(
this string value,
string prefix)
{
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(prefix);

return value.StartsWith(prefix, StringComparison.Ordinal)
? value[prefix.Length..]
: value;
}
C#
string fileName = "temp_report.csv";
string result = fileName.RemovePrefix("temp_");

Console.WriteLine(result); // report.csv

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

int型に偶数判定と範囲判定の拡張メソッドを追加します。

C#
namespace MyApp.Extensions;

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;
}

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

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

C#
using MyApp.Extensions;

int number = 7;

Console.WriteLine(number.IsEven()); // False
Console.WriteLine(number.IsOdd()); // True
Console.WriteLine(number.IsBetween(1, 10)); // True

IsBetween()では、最小値が最大値より大きい場合の扱いも決めておく必要があります。

不正な引数として例外を発生させる場合は、次のように実装できます。

C#
public static bool IsBetween(
this int value,
int minimum,
int maximum)
{
if (minimum > maximum)
{
throw new ArgumentException(
"minimumはmaximum以下である必要があります。");
}

return value >= minimum && value <= maximum;
}

拡張メソッドを作成するときは、正常な入力だけでなく、境界値や不正な引数をどのように扱うかも明確にしましょう。

3-3. 独自クラスに便利な処理を追加する例

拡張メソッドは、.NET標準の型だけでなく、独自クラスにも定義できます。

次のUserクラスを例にします。

C#
public class User
{
public string FirstName { get; init; } = string.Empty;

public string LastName { get; init; } = string.Empty;

public DateOnly Birthday { get; init; }
}

このクラスに、氏名を取得する拡張メソッドを追加します。

C#
public static class UserExtensions
{
public static string GetFullName(this User user)
{
ArgumentNullException.ThrowIfNull(user);

return $"{user.LastName} {user.FirstName}";
}
}
C#
var user = new User
{
FirstName = "太郎",
LastName = "山田",
Birthday = new DateOnly(2000, 4, 1)
};

Console.WriteLine(user.GetFullName()); // 山田 太郎

年齢を計算する拡張メソッドも追加できます。

C#
public static int GetAge(
this User user,
DateOnly referenceDate)
{
ArgumentNullException.ThrowIfNull(user);

int age = referenceDate.Year - user.Birthday.Year;

if (user.Birthday > referenceDate.AddYears(-age))
{
age--;
}

return age;
}
C#
var referenceDate = new DateOnly(2026, 6, 20);

Console.WriteLine(user.GetAge(referenceDate));

ただし、独自クラスを自分で変更できる場合は、常に拡張メソッドを使うべきとは限りません。

GetFullName()GetAge()Userの本質的な振る舞いであるなら、Userクラス自身のインスタンスメソッドやプロパティとして定義したほうが自然な場合があります。

拡張メソッドが向いているのは、たとえば次のようなケースです。

  • 元のクラスを変更できない

  • 特定の画面や機能だけで使う補助処理である

  • インターフェースを実装する複数の型に共通処理を提供したい

  • 変換処理や表示用処理を元のモデルから分離したい

3-4. 引数を受け取る拡張メソッドの作り方

拡張メソッドには、通常のメソッドと同じように複数の引数を定義できます。

次の例では、文字列を指定回数だけ繰り返します。

C#
public static class StringExtensions
{
public static string Repeat(
this string value,
int count,
string separator = "")
{
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(separator);

if (count < 0)
{
throw new ArgumentOutOfRangeException(
nameof(count),
"countは0以上である必要があります。");
}

return string.Join(
separator,
Enumerable.Repeat(value, count));
}
}

呼び出し側では、拡張対象以外の引数を丸括弧内に指定します。

C#
string result1 = "A".Repeat(3);
string result2 = "A".Repeat(3, "-");

Console.WriteLine(result1); // AAA
Console.WriteLine(result2); // A-A-A

拡張メソッドの定義上は、引数の並びが次のようになっています。

C#
this string value,
int count,
string separator = ""

呼び出し側では、thisを付けた第1引数を記述しません。

C#
"A".Repeat(3, "-");

これは次の静的メソッド呼び出しと同じ意味です。

C#
StringExtensions.Repeat("A", 3, "-");

通常のメソッドと同様に、名前付き引数も使用できます。

C#
string result = "A".Repeat(
count: 3,
separator: "-");

3-5. ジェネリック型に拡張メソッドを定義する方法

ジェネリックを利用すると、複数の型に対応する汎用的な拡張メソッドを作成できます。

次の例は、任意の値を配列へ変換する拡張メソッドです。

C#
public static class ObjectExtensions
{
public static T[] ToSingleItemArray<T>(this T value)
{
return new[] { value };
}
}
C#
int[] numbers = 10.ToSingleItemArray();
string[] names = "Alice".ToSingleItemArray();

Console.WriteLine(numbers[0]); // 10
Console.WriteLine(names[0]); // Alice

値が候補のいずれかに含まれているか判定するメソッドも作成できます。

C#
public static class ComparableExtensions
{
public static bool IsOneOf<T>(
this T value,
params T[] candidates)
{
ArgumentNullException.ThrowIfNull(candidates);

return candidates.Contains(value);
}
}
C#
string status = "Completed";

bool isFinished = status.IsOneOf(
"Completed",
"Cancelled");

Console.WriteLine(isFinished); // True

型制約を付けることもできます。

C#
public static class CollectionExtensions
{
public static bool HasItems<TCollection, TItem>(
this TCollection collection)
where TCollection : ICollection<TItem>
{
ArgumentNullException.ThrowIfNull(collection);

return collection.Count > 0;
}
}

ただし、この例は型推論が複雑になりやすいため、実際には次のようにインターフェース自体を拡張したほうが簡潔です。

C#
public static bool HasItems<T>(
this ICollection<T> collection)
{
ArgumentNullException.ThrowIfNull(collection);

return collection.Count > 0;
}
C#
var items = new List<string> { "A", "B" };

Console.WriteLine(items.HasItems()); // True

ジェネリック拡張メソッドでは、必要以上に複雑な型パラメーターを導入せず、呼び出し側が理解しやすいシグネチャにすることが重要です。

3-6. IEnumerableに対するコレクション向け拡張メソッドの例

IEnumerable<T>を対象にすると、配列、リスト、LINQの結果など、幅広いコレクションで利用できる拡張メソッドを作成できます。

次の例では、コレクションを指定した要素数ごとに分割します。

C#
public static class EnumerableExtensions
{
public static IEnumerable<IReadOnlyList<T>> ChunkBy<T>(
this IEnumerable<T> source,
int size)
{
ArgumentNullException.ThrowIfNull(source);

if (size <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(size),
"sizeは1以上である必要があります。");
}

var chunk = new List<T>(size);

foreach (T item in source)
{
chunk.Add(item);

if (chunk.Count == size)
{
yield return chunk;
chunk = new List<T>(size);
}
}

if (chunk.Count > 0)
{
yield return chunk;
}
}
}

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

C#
var numbers = new[] { 1, 2, 3, 4, 5 };

foreach (IReadOnlyList<int> chunk in numbers.ChunkBy(2))
{
Console.WriteLine(string.Join(", ", chunk));
}

実行結果は次のようになります。

1, 2
3, 4
5

条件に一致する要素が存在しない場合に、例外を発生させる拡張メソッドも作成できます。

C#
public static T SingleOrThrow<T>(
this IEnumerable<T> source,
Func<T, bool> predicate,
string message)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);

T? result = source.SingleOrDefault(predicate);

if (result is null)
{
throw new InvalidOperationException(message);
}

return result;
}

ただし、この実装は値型やnullを要素として持つコレクションでは意図どおりにならない可能性があります。より明確に判定するには、一致件数を確認します。

C#
public static T SingleOrThrow<T>(
this IEnumerable<T> source,
Func<T, bool> predicate,
string message)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);

using IEnumerator<T> enumerator =
source.Where(predicate).GetEnumerator();

if (!enumerator.MoveNext())
{
throw new InvalidOperationException(message);
}

T result = enumerator.Current;

if (enumerator.MoveNext())
{
throw new InvalidOperationException(
"条件に一致する要素が複数存在します。");
}

return result;
}
C#
var users = new[]
{
new User { FirstName = "太郎", LastName = "山田" },
new User { FirstName = "花子", LastName = "佐藤" }
};

User user = users.SingleOrThrow(
item => item.LastName == "山田",
"ユーザーが見つかりません。");

Console.WriteLine(user.GetFullName());

IEnumerable<T>向けの拡張メソッドでは、遅延実行にも注意が必要です。

yield returnやLINQを使ってIEnumerable<T>を返すメソッドは、メソッドを呼び出した時点ではなく、foreachToList()などで列挙した時点に処理が実行されることがあります。標準のLINQメソッドにも遅延実行を利用するものがあります。Microsoft Learn+1

C#
IEnumerable<int> query = numbers.Where(
number => number % 2 == 0);

// この時点で列挙される
List<int> results = query.ToList();

コレクション向け拡張メソッドを設計するときは、次の点を明確にしましょう。

  • すぐに処理するのか、遅延実行するのか

  • 元のコレクションを変更するのか

  • 新しいコレクションを返すのか

  • 複数回列挙する可能性があるのか

  • nullや空のコレクションをどう扱うのか

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

拡張メソッドは便利ですが、あらゆる処理を拡張メソッドにすればよいわけではありません。特に役立つのは、既存の型に対する汎用的で分かりやすい補助処理を追加したい場面です。

代表的な使いどころの一つが、値の判定処理です。

C#
email.IsNotBlank();
number.IsBetween(1, 100);
status.IsOneOf("Active", "Pending");

判定対象からメソッドを呼び出せるため、条件式を自然に読めます。

型変換や表示形式の統一にも利用できます。

C#
public static string ToDisplayText(
this decimal price)
{
return $"{price:N0}円";
}
C#
decimal price = 12800;

Console.WriteLine(price.ToDisplayText()); // 12,800円

ただし、通貨や日付の表示は文化圏や利用画面によって異なるため、汎用的な拡張メソッドに固定ルールを埋め込むべきかは慎重に判断する必要があります。

コレクションの共通処理にも適しています。

C#
items.HasItems();
items.ChunkBy(100);
users.SingleOrThrow(
user => user.Id == targetId,
"ユーザーが見つかりません。");

インターフェースを対象にすることで、複数の実装クラスへ同じ処理を提供することもできます。

C#
public interface IHasName
{
string Name { get; }
}

public static class HasNameExtensions
{
public static bool HasSameNameAs(
this IHasName source,
IHasName other)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(other);

return string.Equals(
source.Name,
other.Name,
StringComparison.Ordinal);
}
}

IHasNameを実装するすべての型でHasSameNameAs()を利用できます。

また、外部ライブラリの型にプロジェクト固有の補助処理を追加する場面でも役立ちます。外部ライブラリのソースコードを変更せず、継承もせずに機能を追加したように扱えるためです。Microsoft Learn+1

一方、拡張メソッドを使用するときは、次の点に注意が必要です。

まず、対象の型に元から存在するメソッドと同じ名前を安易に付けないようにします。後からライブラリ側に同名のインスタンスメソッドが追加された場合、呼び出される処理が変わる可能性があります。

C#
public static void Save(this ExternalType value)
{
// 独自処理
}

将来、ExternalType自身にSave()が追加されると、インスタンスメソッドが優先されます。

次に、何を行うか分からない名前や、副作用が予測しにくい処理は避けましょう。

C#
// 何をするのか分かりにくい
user.Process();

// データを変更することが名前から分かりにくい
users.Check();

次のように、処理内容が伝わる名前にします。

C#
user.NormalizeName();
users.RemoveInactiveUsers();

ただし、対象の状態を変更する拡張メソッドは、呼び出し側が副作用を見落としやすいため、特に慎重な設計が必要です。可能であれば、新しい値を返す形を検討します。

C#
public static string NormalizeWhitespace(this string value)
{
ArgumentNullException.ThrowIfNull(value);

return string.Join(
" ",
value.Split(
' ',
StringSplitOptions.RemoveEmptyEntries));
}

拡張メソッドの数が増えすぎると、型の本来のAPIとプロジェクト独自のAPIを区別しにくくなります。頻繁に使われる、意味が明確な処理に限定するとよいでしょう。

拡張メソッドが適しているか迷った場合は、次の基準で判断できます。

  • 複数の場所で再利用する処理か

  • 対象の型と強く関連する処理か

  • メソッド形式にすると読みやすくなるか

  • 元の型を変更できない、または変更すべきでないか

  • 内部状態へアクセスする必要がないか

  • 通常のサービスクラスやヘルパークラスより自然か

  • 既存メソッドと誤解されても問題がないほど直感的か

データベースアクセス、外部API通信、メール送信など、大きな副作用や複雑な依存関係を持つ処理は、拡張メソッドよりも専用のサービスクラスへ分離したほうが管理しやすい傾向があります。

C#
// 責務が分かりにくくなりやすい
await user.SendRegistrationEmailAsync();

// 専用サービスのほうが依存関係を明確にしやすい
await emailService.SendRegistrationEmailAsync(user);

拡張メソッドは、処理を短く書くためだけの機能ではありません。対象と処理の関係を自然に表現し、コードを読みやすくするために使うことが重要です。

まとめ

C#のExtension Methodsは、既存の型を変更したり継承したりせずに、独自のメソッドを追加したように扱える機能です。

一般的な拡張メソッドは、staticクラス内のstaticメソッドとして定義し、第1引数にthisキーワードを付けます。

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

定義したメソッドは、インスタンスメソッドと同じ形式で呼び出せます。

C#
string? message = "Hello";

bool result = message.IsNotBlank();

拡張メソッドは、文字列や数値の判定、型変換、コレクション操作、独自クラスの補助処理などに役立ちます。LINQのWhere()Select()なども、拡張メソッドを活用した代表例です。

一方で、既存のインスタンスメソッドより優先順位が低いこと、対象型のprivateメンバーにはアクセスできないこと、増やしすぎるとAPIが分かりにくくなることには注意が必要です。

対象の型と強く関連し、複数の場所で再利用できる小さな処理に絞って利用すると、C#のコードをより自然で読みやすくできます。