既存のクラスやインターフェースのソースコードを変更できない状況でも、拡張メソッド(Extension Methods) を使えばメソッドを追加しているかのように見せることができます。
LINQ の Where や Select も拡張メソッドとして定義されており、C# の中でも特によく使われる機能です。
この記事では次の 5 つを順に説明します。
- 拡張メソッドの定義
- 拡張メソッドの呼び出し
- 拡張メソッドのインポート
- 特定のインターフェースを実装した型への拡張
- GetEnumerator の拡張メソッドサポート(C# 9.0)
拡張メソッドの定義
拡張メソッドは、static クラスの中に static メソッドとして定義します。第 1 引数に this キーワードを付けることで、その型に対する拡張メソッドとして認識されます。
namespace MyExtensions;
public static class StringExtensions
{
// string 型に IsNullOrEmpty メソッドを追加する
public static bool IsNullOrEmpty(this string s)
=> string.IsNullOrEmpty(s);
// string 型に Reverse メソッドを追加する
public static string Reverse(this string s)
{
char[] chars = s.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
// string 型に WordCount メソッドを追加する
public static int WordCount(this string s)
=> s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
ポイントは 3 つです。
- 定義するクラスは
static classにする - メソッド自体も
staticにする - 第 1 引数を
this 対象の型 パラメーター名の形で書く
この this が付いた引数が「このメソッドを追加したい型」を指定しています。
拡張メソッドの呼び出し
定義した拡張メソッドは、インスタンスメソッドと同じ記法 で呼び出せます。
string message = "Hello World from C#";
// インスタンスメソッドのように呼び出せる
Console.WriteLine(message.IsNullOrEmpty()); // False
Console.WriteLine(message.Reverse()); // #C morf dlroW olleH
Console.WriteLine(message.WordCount()); // 4
string empty = "";
Console.WriteLine(empty.IsNullOrEmpty()); // True
通常のメソッドと同じように、静的メソッドとして呼び出すこともできます。
// 静的メソッドとしての呼び出し(同じ動作)
Console.WriteLine(StringExtensions.WordCount(message)); // 4
通常は前者のインスタンスメソッド形式で書くほうが読みやすいため、そちらが一般的です。
拡張メソッドのインポート
拡張メソッドを使うには、定義されている名前空間を using でインポートする 必要があります。
MyApp/
├── Extensions/
│ └── StringExtensions.cs (namespace MyApp.Extensions)
└── Program.cs
// StringExtensions.cs
namespace MyApp.Extensions;
public static class StringExtensions
{
public static bool IsPalindrome(this string s)
{
string reversed = new string(s.Reverse().ToArray());
return s.Equals(reversed, StringComparison.OrdinalIgnoreCase);
}
}
// Program.cs
using MyApp.Extensions; // この using がないと IsPalindrome は使えない
string word = "racecar";
Console.WriteLine(word.IsPalindrome()); // True
string other = "hello";
Console.WriteLine(other.IsPalindrome()); // False
using を書かない状態で拡張メソッドを呼び出そうとすると、コンパイラは「そのメソッドは存在しない」というエラーを出します。プロジェクト全体で使いたい場合は global using を使うと便利です。
// global_usings.cs などのファイルに記述
global using MyApp.Extensions;
特定のインターフェースを実装した型への拡張
拡張メソッドの第 1 引数にインターフェース型を指定すると、そのインターフェースを実装するすべての型に対して メソッドを追加できます。これは LINQ の設計でも使われているパターンです。
ここでは IEnumerable<T> を実装する型に対して拡張メソッドを追加します。
using System.Collections.Generic;
using System.Linq;
public static class EnumerableExtensions
{
// IEnumerable<int> を実装するすべての型に Sum2 を追加
public static int Sum2(this IEnumerable<int> source)
{
int total = 0;
foreach (var item in source)
total += item;
return total;
}
// IEnumerable<T> を実装するすべての型に IsEmpty を追加
public static bool IsEmpty<T>(this IEnumerable<T> source)
=> !source.Any();
// IEnumerable<T> を実装する型に ForEach を追加
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
action(item);
}
}
int[] numbers = { 1, 2, 3, 4, 5 };
var list = new List<string> { "Alice", "Bob", "Carol" };
var empty = Array.Empty<int>();
Console.WriteLine(numbers.Sum2()); // 15
Console.WriteLine(empty.IsEmpty()); // True
Console.WriteLine(list.IsEmpty()); // False
list.ForEach(name => Console.WriteLine($"Hello, {name}!"));
// Hello, Alice!
// Hello, Bob!
// Hello, Carol!
int[] も List<string> も IEnumerable<T> を実装しているため、どちらでも同じ拡張メソッドが使えます。独自のインターフェースを定義してそこに拡張メソッドを追加すれば、そのインターフェースを実装するすべてのクラスに振る舞いを付け加えることができます。
public interface IPrintable
{
string GetContent();
}
public static class PrintableExtensions
{
// IPrintable を実装するすべての型に PrintToConsole を追加
public static void PrintToConsole(this IPrintable source)
=> Console.WriteLine(source.GetContent());
}
public class Report : IPrintable
{
public string Title { get; set; } = string.Empty;
public string GetContent() => $"[Report] {Title}";
}
public class Invoice : IPrintable
{
public int Number { get; set; }
public string GetContent() => $"[Invoice] #{Number}";
}
var report = new Report { Title = "Q1 Summary" };
var invoice = new Invoice { Number = 42 };
report.PrintToConsole(); // [Report] Q1 Summary
invoice.PrintToConsole(); // [Invoice] #42
GetEnumerator の拡張メソッドサポート(C# 9.0)
C# 9.0 以降では、GetEnumerator を拡張メソッドとして定義することで、本来は foreach に対応していない型を foreach で列挙できるようになります。
foreach は、コンパイラが GetEnumerator() メソッドを探してループを展開します。このメソッドを拡張メソッドとして提供すれば、クラス自体を変更しなくても foreach が使えるようになります。
ここでは、通常は foreach で直接は回せない Range 型(1..5 のような範囲)を例にします。
// int の Range を foreach で使えるようにする拡張メソッド
public static class RangeExtensions
{
public static IEnumerator<int> GetEnumerator(this Range range)
{
int start = range.Start.Value;
int end = range.End.Value;
for (int i = start; i < end; i++)
yield return i;
}
}
// C# 9.0 以降: Range を foreach で列挙できる
foreach (int i in 1..6)
{
Console.Write($"{i} "); // 1 2 3 4 5
}
Console.WriteLine();
同様に、ライブラリの既存クラスが IEnumerable を実装していない場合でも、GetEnumerator 拡張メソッドを追加するだけで foreach に対応させることができます。
// サードパーティのクラス(変更不可と想定)
public class NumberRange
{
public int From { get; set; }
public int To { get; set; }
}
// 拡張メソッドで foreach サポートを追加
public static class NumberRangeExtensions
{
public static IEnumerator<int> GetEnumerator(this NumberRange range)
{
for (int i = range.From; i <= range.To; i++)
yield return i;
}
}
var range = new NumberRange { From = 3, To = 7 };
foreach (int n in range)
{
Console.Write($"{n} "); // 3 4 5 6 7
}
Console.WriteLine();
まとめ
| トピック | ポイント |
|---|---|
| 拡張メソッドの定義 | static class 内に static メソッドとして定義し、第 1 引数に this を付ける |
| 呼び出し | インスタンスメソッドと同じ記法で呼び出せる |
| インポート | 定義された名前空間を using でインポートする必要がある |
| インターフェースへの拡張 | 第 1 引数をインターフェース型にすれば、実装クラスすべてに追加できる |
| GetEnumerator サポート | GetEnumerator を拡張メソッドで定義すれば任意の型を foreach で使える |
拡張メソッドはソースコードを変更できないクラスやインターフェースに機能を追加したいときに強力です。ただし、乱用するとどこにメソッドが定義されているかが分かりにくくなるため、意味がまとまった単位で static class を分けることが大切です。