動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# 拡張メソッドの使い方 — 作り方と実務3パターン(既存型を汚さず .Xxx() を足す)

バイブス父さん
現役の業務SE
2026年9月9日17 min read広告 (PR) を含む場合があります
C# 拡張メソッドの使い方 — 作り方と実務3パターン(既存型を汚さず .Xxx() を足す)

みなさんこんにちは!ヒロポンです!

業務系のコードを触ってると、こんな Utility クラス、見覚えないですか??

var name = StringUtil.TrimFull(row["name"]);
var dept = StringUtil.OrDefault(StringUtil.TrimFull(row["dept"]), "(未設定)");

自作のヘルパーメソッドを static クラスにひたすら溜めてって、呼び出しが Utility.Trim(x) StringUtil.Xxx(x) だらけ。動くんですよ。動くんですけど、正直ちょっと読みにくい。

これ、x.Trim() みたいに メソッドを型に生やす 書き方に整理できます。それが拡張メソッド。

今回は「C# 拡張メソッドの使い方」を、作り方から実務の3パターンまで、コピペで動くコードで一気にいきます。

結論: 拡張メソッドは「this を付けた static メソッド」で既存型に .Xxx() を生やす仕組み

先に答えです。拡張メソッドは、static メソッドの 第一引数の頭に this を付けるだけ で作れます。

public static class StringExtensions
{
    // this を付けた第一引数の型 (string) に、このメソッドが生える
    public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
}

これで stringIsBlank() が生えます。呼び方はこう。

string name = "  ";
Console.WriteLine(name.IsBlank());          // True
Console.WriteLine("営業部".IsBlank());       // False
// StringUtil.IsBlank(name) じゃなく name.IsBlank() で呼べる!!

実行結果:

string に生やした IsBlank 拡張メソッドが、空白は True・営業部は False を返す実行結果

Utility.IsBlank(name)name.IsBlank() になりました。たったこれだけ。既存の string 型のソースには一切手を入れてません。

ここで「なるほど、あとは応用か」と分かった人は、このタイミングでページを閉じてもらってもかまいません! 以下は3つの実務パターンと、業務SEが最初に踏むハマりポイントの話です。

そもそも拡張メソッドは何がうれしいのか

一言で言うと、自分で編集できない型に、後からメソッドを足せる のがうれしいポイントです。

stringDateTime、ADO.NET の DataRow。この辺は .NET が持ってる型。私たちがソースをいじって新しいメソッドを追加、なんてできません。

じゃあ従来どうしてたか。さっきの StringUtil.TrimFull(x) みたいな static ヘルパーですね。これも動くんですけど、呼び出しがネストすると途端に読みにくい。A(B(C(x))) みたいになるやつです。

拡張メソッドなら x.C().B().A()左から右に読める 形になる。処理の流れが素直に追えます。

ん? これ普通にインスタンスメソッドっぽく見えるけど、中身は static じゃないの? と思った人、鋭いです。実は正体は完全に static メソッドで、コンパイラが name.IsBlank()StringExtensions.IsBlank(name) に翻訳してるだけなんですよね。見た目がインスタンスメソッド、中身は static。この二重性だけ頭の隅に置いといてください。

使いどころ その1: 既存の string / DataRow に業務用の便利メソッドを足す

一番よく使うのがこれ。現場で何度も書く小さな処理を、型に生やして使い回します。

public static class BusinessStringExtensions
{
    // 半角も全角スペースもまとめてトリム (業務データあるある)
    public static string TrimFull(this string? s)
        => (s ?? "").Trim().Trim(' ');

    // 空なら既定値に差し替える (DB の未入力を表示用に整える)
    public static string OrDefault(this string? s, string fallback)
        => string.IsNullOrEmpty(s) ? fallback : s;
}

使うとこんな感じ。

string dept = " 営業部 ";               // 前後に全角スペース
Console.WriteLine(dept.TrimFull());        // 営業部
Console.WriteLine("".OrDefault("(未設定)")); // (未設定)

実行結果:

TrimFull が全角スペースまで除去して営業部を返し、空文字は OrDefault で(未設定)に変わる実行結果

TrimFull() は、半角の Trim() だけだと取り切れない全角スペースまで落とします。業務データって全角スペース混じりが本当に多い。だからこれ1個あるだけで地味に助かります。

寄せるべきは、こういう 「その型に対する横断的な処理」。特定の画面や特定の業務ロジックじゃなく、「string 全般に効く便利機能」を足すイメージですね。

使いどころ その2: LINQ 風のメソッドチェーンを自作する

普段使ってる WhereSelect、あれ全部 IEnumerable<T> に対する拡張メソッドなんですよ。LINQ の正体は拡張メソッドの塊、というわけです。

ということは、同じノリで 自分専用のチェーンメソッド も作れます。

public static class EnumerableExtensions
{
    // null を除いて返す (ゆるい業務リストをきれいにする)
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> src)
        where T : class
        => src.Where(x => x is not null)!;

    // 指定件数ずつのページに切る
    public static IEnumerable<List<T>> Paged<T>(this IEnumerable<T> src, int size)
    {
        var buf = new List<T>(size);
        foreach (var x in src)
        {
            buf.Add(x);
            if (buf.Count == size) { yield return buf; buf = new List<T>(size); }
        }
        if (buf.Count > 0) yield return buf;
    }
}

呼び出し側はこう。

var names = new string?[] { "田中", null, "佐藤", null, "鈴木" };
var clean = names.WhereNotNull().ToList();
Console.WriteLine(string.Join(",", clean));   // 田中,佐藤,鈴木

foreach (var page in Enumerable.Range(1, 5).Paged(2))
    Console.WriteLine(string.Join("-", page.Select(x => x.ToString())));
// 1-2
// 3-4
// 5

実行結果:

WhereNotNull で null を除いて田中,佐藤,鈴木を得て、Paged(2) が2件ずつ 1-2 / 3-4 / 5 に分割する実行結果

WhereNotNull() は標準の Where と自然につながります。src.WhereNotNull().Select(...) みたいに、既存 LINQ と混ぜて使えるのが良い感じ!!

Paged() のほうは、1万件を1000件ずつ処理したい、みたいなバッチでよく出るやつ。標準の Chunk(.NET 6 以降)に近いんですが、戻り値の型や挙動を現場に合わせて自作できる。ここが拡張メソッドの強みですね。

使いどころ その3: null / DBNull を安全に扱うヘルパーにする

業務SEにとって一番の鬼門、ADO.NET の DBNull。これも拡張メソッドで安全に畳めます。

using System.Data;

public static class DataRowExtensions
{
    // DBNull を空文字にして安全に取り出す
    public static string GetStr(this DataRow row, string col)
        => row[col] is DBNull ? "" : Convert.ToString(row[col]) ?? "";

    // DBNull なら null 許容 int に
    public static int? GetIntOrNull(this DataRow row, string col)
        => row[col] is DBNull ? null : Convert.ToInt32(row[col]);
}

DataTable から取り出す時、いちいち row["name"] is DBNull ? ... を書かなくて済みます。使うとこんな感じ。

var dt = new DataTable();
dt.Columns.Add("name", typeof(string));
dt.Columns.Add("age", typeof(int));
dt.Rows.Add("田中", 40);
dt.Rows.Add(DBNull.Value, DBNull.Value);   // 未入力の行

foreach (DataRow r in dt.Rows)
{
    var name = r.GetStr("name").OrDefault("(名前なし)");  // その1 の OrDefault と連結!
    var age  = r.GetIntOrNull("age")?.ToString() ?? "-";
    Console.WriteLine($"{name} / {age}");
}
// 田中 / 40
// (名前なし) / -

実行結果:

DataRow の GetStr / GetIntOrNull 拡張メソッドで DBNull を空文字と - に安全に畳み、田中/40 と (名前なし)/- を出力した実行結果

注目してほしいのが r.GetStr("name").OrDefault("(名前なし)") のとこ。その1で作った OrDefault とチェーンでつながってます。 拡張メソッド同士を数珠つなぎにできるので、DBNull の分岐を本文からきれいに追い出せる。

DBNull を三項演算子で毎回書くのが面倒な話は C#でDBNullチェック時にエラーを出さずにキャストする最適解 でも触れてますが、拡張メソッドに寄せると呼び出し側がもっとスッキリします。

いつ拡張メソッドにすべきか(判断の早見表)

「なんでもかんでも拡張メソッドにすればいいの??」というと、そうでもないです。static ヘルパーのままが正解のケースもある。ざっくり判断軸を表にしました。

拡張メソッド / static ヘルパー / インスタンスメソッド追加 を観点別に比較。既存型への追加・読みやすさ・定義の追いやすさ・null 対応・型固有の振る舞いの5軸で本命用途を示す

通して見ると分かるんですが、拡張メソッドが圧倒的に強いのは 「自分で編集できない既存型に、横断的な便利機能を足したい」 の一点だけ。逆に、定義の追いやすさでは負けます。自作クラスの本質メソッドなら、素直にクラス本体に書いた方がいい。

「既存型か、自作型か」「横断的な補助か、その型の本質か」。この2軸で選ぶと、だいたい外しません。

ハマりポイント: 知らないと定義を追えなくなる3つ

拡張メソッドは便利なんですけど、業務SEが最初に踏みがちな罠が3つあります。全部知っておけば1個も踏まずに済みます。

① 同名のインスタンスメソッドがあると、そっちが優先される

拡張メソッドとインスタンスメソッドが同じシグネチャで衝突すると、常にインスタンスメソッドが勝ちます。 拡張メソッドは「他に候補がない時だけ」呼ばれる、後出しジャンケンの負け側なんですよね。

public static class Ext
{
    // string には元から ToString() がある
    public static string ToString(this string s) => $"[{s}]";
}

// 呼ぶと…
Console.WriteLine("abc".ToString());  // abc ← Ext.ToString は呼ばれない!

実行結果:

同名の ToString 拡張メソッドを定義してもインスタンスメソッドが優先され、[abc]ではなく abc が返る実行結果

[abc] を期待して書いても、返るのは abc。標準の ToString() が優先されて、拡張メソッドは黙って無視されます。エラーも出ないから気付きにくい。これで小一時間溶かした記憶があります……。既存メソッドと名前がかぶらないように寄せるのが安全です。

② using で名前空間を通さないと、そもそも見えない

拡張メソッドは、定義した static クラスの 名前空間を using で通してないと、インテリセンスにすら出てきません。

// StringExtensions が MyApp.Extensions 名前空間にある場合
using MyApp.Extensions;   // ← これが無いと name.IsBlank() が使えない

string name = " ";
bool b = name.IsBlank();

using を書き忘れると、コンパイルエラーは CS1061: 'string' に 'IsBlank' の定義が含まれておらず... という顔で出てきます。「ちゃんと定義したのに呼べない!」の8割はこれ。メソッド名じゃなく 名前空間を疑う のが近道です。

③ 乱用すると、読み手が定義を追えなくなる

これは思想の話。拡張メソッドは定義がその型のソースに無いので、x.DoSomething() を見た人が「これどこで定義されてるの??」となりがちです。

たとえば decimal.ToTax() みたいな 業務ドメイン固有すぎる処理 を全部 string や decimal に生やす。すると標準機能なのか自作なのか区別がつかなくなります。その型の本質的な振る舞いはクラス本体に置く。拡張メソッドは「横断的な補助」に絞る。この線引きだけ守れば乱用にはなりません。

現場メモ: 共通処理を拡張メソッドに寄せると、何が変わるか

冒頭の Utility.Xxx だらけのコードを、拡張メソッドで整理してみます。before / after で並べるとこうなる。

DataRow の値取得を StringUtil の入れ子呼び出しから、DataRow / string 拡張メソッドのチェーンにリファクタする before/after

before は A(B(C(x))) の入れ子。一番内側の DBNull 判定から読み解かないと何をしてるか分からない。after は「行から文字列取って、トリムして、空なら未設定に」と 書いてある順に処理が流れます。

昔、私が入った現場で StringUtil が2000行くらいに膨れ上がってて、同じような Trim 系メソッドが3つ並んでたことがありました。呼び出し側も入れ子だらけで、読むだけで疲れるやつ。ああいうのを拡張メソッドに寄せてくと、呼び出しがフラットになって、レビューがめっちゃ楽になります。

もちろん銀の弾丸じゃないです。ハマりポイントの③で書いた通り、寄せすぎると逆に追いにくくなる。「string / DataRow みたいな共通の型に効く、横断的な処理だけ」を拡張メソッドにする。この一線を引けると、コードは確実に読みやすくなります。

まとめ

  • 拡張メソッドは static メソッドの第一引数に this を付けるだけで作れる。既存型に .Xxx() を生やせる
  • 実務の使いどころは、(1) 既存型への便利メソッド (2) LINQ 風の自作チェーン (3) null / DBNull 安全ヘルパー の3つ
  • 同名インスタンスメソッド優先・using 忘れで見えない、の2つは最初に踏みがちなので先に知っておく
  • 乱用注意。「既存型への横断的な補助」に絞れば、Utility.Xxx だらけのコードがフラットで読みやすくなる

自分で編集できない stringDataRow に、後から自然な形でメソッドを足せる。これが拡張メソッドの一番おいしいとこです。共通処理を寄せてくと、現場のコードは確実に読みやすくなりますよ。

よくある質問

Q1. 拡張メソッドと普通の static ヘルパー、結局どっちがいいんですか?

既存の stringDataRow みたいな 自分で編集できない型に、横断的な便利機能を足したい 時は拡張メソッドが向きます。x.Xxx() でチェーンして読める分、呼び出しがスッキリします。逆に、複数の型をまたぐ純粋な処理や、定義をすぐ追える方を優先したい時は、static ヘルパーのままで十分。無理に全部寄せる必要はないですね。

Q2. 拡張メソッドがインテリセンスに出てこない・呼べないんですが?

定義した static クラスの 名前空間を using で通していない のが原因の大半です。同じプロジェクト内でも、別の名前空間に置いたクラスなら using が要ります。コンパイルエラーは CS1061(定義が含まれていない)で出ます。メソッド名のタイプミスを疑う前に、まず名前空間を通してるか見てください。

Q3. null に対して拡張メソッドを呼んでも大丈夫ですか?

第一引数を this string? のように null 許容で受けていれば、null に対して呼んでも NullReferenceException にはなりません。インスタンスメソッドは null.Xxx() で即例外ですが、拡張メソッドは入り口までは通ります。ただしメソッドの中で s ?? "" のように null を安全に処理する責任はこちら側にある。そこをサボると結局中で落ちるので注意です。

Q4. 拡張メソッドは乱用するとよくないって聞きました。何がまずいんですか?

定義がその型のソースに無いので、読み手が実装を追いにくくなる のが最大の問題です。標準機能なのか自作なのか、パッと見で区別がつかなくなる。対策はシンプルで、その型固有の本質的な振る舞いはクラス本体のメソッドにして、拡張メソッドは「どの型にも効く横断的な補助」に絞ること。この線引きさえ守れば、乱用にはなりません。

次に読むべき記事

以上!

同じように Utility クラスが肥大化して困ってる人いたら、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について