連載:C# 3.0入門

第5回 拡張メソッド

株式会社ピーデー 川俣 晶
2008/08/08
Page1 Page2 Page3

メソッド呼び出しと型の関係

 拡張メソッドを呼び出す場合、注意すべき点は、異なるパラメータを持つメソッドは別のメソッドと見なされるという点である。つまり、同じ名前のメソッドがすでに存在するため拡張メソッドを呼び出せないケースでも、パラメータの型を変えると呼び出せてしまうケースが存在する。

 また、その型を包含する基本的な型を持つパラメータがあれば、異なる型でも拡張メソッドは呼び出されないかもしれない。

 それを具体的に見るために、以下のサンプル・コードを作成した。

using System;
using X;

namespace X
{
  public static class A
  {
    // 拡張メソッド
    public static void MyMethod(this object obj, int i)
    {
      Console.WriteLine("A.MyMethod〜int called");
    }

    // 拡張メソッド
    public static void MyMethod(this object obj, string s)
    {
      Console.WriteLine("A.MyMethod〜string called");
    }
  }

  class B
  {
    public void MyMethod(int i)
    {
      Console.WriteLine("B.MyMethod〜int called");
    }
  }

  class C
  {
    public void MyMethod(object obj)
    {
      Console.WriteLine("C.MyMethod〜object called");
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      var b = new B();
      var c = new C();

     b.MyMethod(1);            // 出力:B.MyMethod〜int called
     b.MyMethod(string.Empty); // 出力:A.MyMethod〜string called
     c.MyMethod(1);            // 出力:C.MyMethod〜object called
     c.MyMethod(string.Empty); // 出力:C.MyMethod〜object called
    }
  }
}
リスト10 メソッド呼び出しと型の関係
  クラスBに整数のパラメータを受け取るメソッドがあるので、それが呼び出される。
  パラメータとして文字列を受け取るメソッドは存在しないので、クラスAの拡張メソッドが呼ばれる。
  object型はint型から見て基本的な型となるので、パラメータをobject型に変換して(ボックス化して)、クラスCのメソッドを呼び出している。
  int型がstring型に変わるだけで、 の結果と同じ。

 このような特徴は、例えば引数を持つToStringメソッドが存在しないクラスに対して、書式指定の引数を持つToStringメソッドを追加する、といった使い方が可能であることを示す。

 以下は、クラスに書式指定機能付きのToStringメソッドを追加した例である。引数に「u」を付けた場合は、型名を大文字に変換して返す。「l」の場合は小文字にして返す。

using System;
using X;

namespace X
{
  public static class A
  {
    // 拡張メソッド
    public static string ToString(this Target obj, string format)
    {
      switch (format)
      {
        case "u": return obj.ToString().ToUpper();
        case "l": return obj.ToString().ToLower();
        default: return obj.ToString();
      }
    }
  }

  public class Target
  {
  }

  class Program
  {
    static void Main(string[] args)
    {
      var b = new Target();
      Console.WriteLine(b.ToString()); // 出力:X.Target
      Console.WriteLine(b.ToString("u")); // 出力:X.TARGET
      Console.WriteLine(b.ToString("l")); // 出力:x.target
    }
  }
}
リスト11 クラスに書式指定機能付きのToStringメソッドを追加

 もちろん、このような事例は、元クラスを修正してメソッドを追加するか、継承を用いてメソッドを追加するのが筋である。拡張メソッドを使わないで済めばその方がよい。

 しかし、ソースを修正できず、シール・クラスなどの理由により継承も封じられた状況で、これが可能であるのはありがたい特徴ではないだろうか。

thisの正体

 拡張メソッドを宣言する際には「thisキーワード」を使うが、これはインスタンスのメソッドが自分自身を指し示すために使うthisキーワードと同じ名前である。これは、異なる役割に同じキーワードを割り当てているのではなく、役割が同じであるから同じキーワードを割り当てているものである。

 以下は、通常のメソッドと拡張メソッドで同じ機能を記述した例である。で、thisキーワードが付加されたパラメータ(this B t)と、「this.Number」のthisがまったく同じように使われていることが分かるだろう。

using System;
using X;

namespace X
{
  public static class A
  {
    public static void AddByExtension(this B t, int n)
    {
      t.Number += n; //
    }
  }
  public class B
  {
    public int Number { get; set; }
    public void AddByClassMethod(int n)
    {
      this.Number += n; //
    }
  }
  class Program
  {
    static void Main(string[] args)
    {
      var b = new B() { Number = 0 };
      b.AddByExtension(2);
      b.AddByClassMethod(3);
      Console.WriteLine(b.Number);
    }
  }
}
リスト12 thisパラメータとインスタンス参照のthisの役割は同じ

 このような、明示的にthisを渡すスタイルは、実はコンパイラの内部実装を明示的に書き直したもの、と見なすこともできる。

 例えば、インスタンスやthisという概念がないC言語の関数と、それらの概念があるC++言語のインスタンス・メソッドのマシン語レベルにおける実装面での機能差は、主にthisという概念の有無に求められる。そして、thisとは具体的には「暗黙的に追加されるもう1つのパラメータ」として実装することができる(最近のC++コンパイラはもっと効率的な実装を行っているようであり、この説明に当てはまらない)。そのため、C++言語処理系の中には、インスタンス・メソッドを、thisポインタを引数に追加した関数として呼び出せるものがあったように記憶する(かなり強引に行う必要があり、普通に記述してもできないが)。

 このような背景から考えると、拡張メソッドとは本来「暗黙的に渡されるはずのthisを、明示的に引き渡すように宣言されたメソッド」と見ることもできる。

 ちなみに、静的なメソッドはthisを持たない。そのため、C++の静的なメソッドと、C言語の関数は(いくつかの小さな相違点を除き)同じと見なせる。それ故に、C言語の関数ポインタを渡すWindows APIには、C++のインスタンス・メソッドは渡せないが、静的なメソッドなら渡せることがある。

 このような「thisを持たない」という性質は、C#の静的クラス、静的メソッドにも共通する。thisを明示的に受け取る拡張メソッドが、静的クラスかつ静的メソッドでなければならない理由はここにある。thisを明示的に受け取る以上、暗黙的に受け取るthisがあるとトラブルのもとである。

拡張メソッドを使用すべきとき

 さて、拡張メソッドはどのような場合に使用すべきだろうか。逆に、どのような場合には使えないのだろうか。

 まず、拡張メソッドは「メソッド」しか存在しないことを強調しておこう。プロパティやインデクサを拡張することはできない。あくまで「メソッド」だけである。このことだけで、拡張メソッドに期待された役割が限定された狭い範囲でしかないことが分かるだろう。可能であれば、拡張メソッドは使わない方がよい。

 また、拡張メソッドでは記述できない処理も多いことに注意を払おう。拡張メソッドは、あくまでオブジェクト外の存在であり、オブジェクト内部に手を出すことができない(リフレクションを使えば強引に割り込めるが、あまりお勧めではない)。

 では、そもそも、なぜ拡張メソッドは必要とされたのだろうか。

 その答えは、恐らく主にLINQにある。LINQは統一されたクエリを実現するために、既存コレクション群への拡張を必要としていた。しかし、LINQはオプションであり、常に使うものではない。うかつな拡張は、既存のソース・コードとの互換性を失わせるかもしれない。そのように考えると、以下のような特徴を持つ拡張メソッドは優れた選択であることが分かる。

  • 既存のクラスを変更しない
  • インポートしない限り有効にならない
  • インポートすれば有効になるので、既存コードにすぐLINQの機能を追加できる(もし継承で実現していたら、追加機能を使うためにクラス名を書き換える必要が生じる)

 逆にいえば、普通の手順で開発されているプログラムであれば、このような特殊事情はあまり見られず、拡張メソッドを使うメリットも見えにくい。

 しかし、既存のクラス・ライブラリに対して、「このメソッドがあれば便利なのに」と思うことはあるだろう。例えば、文字列に対して、ある業務で使用される特殊なハッシュ・コードを計算する処理が多発するなら、そのような計算を行うメソッドをstringクラスに追加してしまうのは有用だろう。そのような状況に対処する方法として、拡張メソッドは優れている。

コレクションに拡張されるメソッド

 MSDNのリファレンスでは、拡張メソッドによって実現されたメソッドは、通常のメソッドとは別にまとめられている。コレクションであるList<T>クラスなどのメンバ一覧を見ると、「メソッド」の下に別途「Extension のメソッド」という項目がある(版が違うと表記が違うかもしれない)。これが拡張メソッドである。

List<T>クラスのメンバ

 ちなみに、Extensionの訳語は揺れているようで、「拡張」メソッドと表記されることもあれば、このように「Extension」のメソッドと表記されることもある。さらに、IntelliSenseを発動させると「拡張子」と表示されることもあるが、これは誤訳であろう(ちなみに、ファイルの拡張子は英語で「filename extension」と呼ぶので、文脈を考えずに単語単位で訳しての誤訳であろう)。

 さて、ここではList<T>クラスを例にして、どのようなメソッドが拡張されているか見てみよう(ほかのコレクションのクラスも同様に拡張されている)。

 まず、これらのメソッドの多くは、実はList<T>クラスに対して拡張されて“いない”ことに着目しよう。実際には、List<T>クラスが実装しているIEnumerable<T>インターフェイスに対する拡張として定義されている。拡張メソッドの実装そのものは、System.Linq.Enumerableクラスに存在する。

 この事実は、実はLINQが「コレクション」ではなく「列挙」に対して作用する機能であることを示しているが、これはLINQを解説する際にあらためて取り上げよう。

 さて、以上のような前提を踏まえて、List<T>クラスの拡張メソッドの一覧を見ていこう。

メソッド 機能
Aggregate シーケンスの値を結合していく処理を行う
All シーケンスのすべての要素が条件を満たしているかを判断する
Any 何らかの要素が存在するか、または指定条件を満たす要素が1つでもあるかを判断する
AsEnumerable 自分自身をIEnumerable<T>型として返す
AsQueryable 自分自身をIQueryable<T>型またはIQueryable型に変換する
Average シーケンスの平均値を計算する
Cast 非ジェネリックのコレクションでクエリを可能とするメソッド(ジェネリックのList<T>クラスで使用する意味があるかは不明)
Concat 2つのシーケンスを連結する
Contains 指定した要素がシーケンスに含まれているかどうかを判断する
Count シーケンス内の要素数または条件を満たす要素数を返す
DefaultIfEmpty 指定されたシーケンスの要素を返すが、シーケンスが空の場合は既定値を返す
Distinct シーケンスから一意の要素を返す
ElementAt シーケンス内の指定されたインデックス位置にある要素を返す
ElementAtOrDefault ElementAtメソッドと同じだが、インデックスが範囲外の場合は既定値を返す
Except 2つのシーケンスの差集合を生成する
First シーケンスの最初の要素または条件を満たす最初の要素を返す
FirstOrDefault Firstメソッドと同じだが、シーケンスに要素が含まれていない場合は既定値を返す
Intersect 2つのシーケンスの積集合を生成する
Last シーケンスの最後の要素または条件を満たす最後の要素を返す
LastOrDefault Lastメソッドと同じだが、シーケンスに要素が含まれていない場合は既定値を返す
LongCount Countメソッドと同じだが、Int64型で返す
Max シーケンスの最大値を返す
Min シーケンスの最小値を返す
OfType 指定された型に基づいて要素をフィルタ処理する(別の型のシーケンスに変換する)
Reverse シーケンスの要素の順序を反転させる
SequenceEqual 2つのシーケンスが等しいかどうかを判断する
Single シーケンスの唯一の要素または条件を満たす唯一の値を返す。複数ある場合は例外を投げる
SingleOrDefault Singleメソッドと同じだが、シーケンスが空の場合は既定値を返す
Skip シーケンス内の指定された数の要素をバイパスし、残りの要素を返す
SkipWhile 指定された条件が満たされる限り、シーケンスの要素をバイパスした後、残りの要素を返す
Sum シーケンスの合計を計算するAggregateメソッドと違って、加算処理そのものは自前で実行できないことに注意
Take シーケンスの先頭から、指定された数の連続する要素を返す
TakeWhile 指定された条件が満たされる限り、シーケンスから要素を返す
ToArray 同じ型、同じ要素の配列を作成する
ToList List<T>オブジェクトを作成する(これをList<T>型のコレクションから使って意味があるかは不明)
Union 2つのシーケンスの和集合を生成する
Where 述語に基づいて値のシーケンスをフィルタ処理する列挙オブジェクトを返す(指定条件を満たす要素だけを列挙する列挙オブジェクトを返す)
List<T>クラスの拡張メソッド一覧

なぜ「using System.Linq;」なのか?

 さてここまで見て、ぜひ使いたいというメソッドが見つかった読者もいると思う。しかし、LINQまでは難しくて手が出ないと思った読者もいるだろう。そういう読者は、LINQは使わないのにこれらのメソッドを使うために、「using System.Linq;」と書き込む必要があることに釈然としない感じを受けるかもしれない。しかし、これらは紛れもなくLINQを構成する一部なのである。

 LINQといえば、

from 〜 in 〜 where 〜 select 〜

といったクエリ式を連想するかもしれないが、これはLINQのクエリ構文と呼ばれるものである。しかし、LINQにはこれとは別にメソッド構文というものがあり、メソッド呼び出しの連鎖としてクエリを記述できる。その際、上記リストにあるWhereメソッドなどが「まさにそのままメソッドとして」使用される。

 つまり、これらのメソッドを使い始めたあなたにとって、LINQは縁遠いものではない。むしろ、すでにLINQの世界に片足を踏み込んでいるのである。

次回予告

 そのようなわけで、次回はLINQの世界に足を踏み入れていこう。

 筆者も日常的にLINQのクエリ式を書いてはいるが、LINQのすべての機能を使ったとは、到底いいにくい。これを機会に、筆者としてもLINQに対して全面的に取り組んで、その全貌を明らかにしてみたいと考えている。

 LINQの解説は2回にわたって行う予定である。請うご期待。End of Article

 

 INDEX
  C# 3.0入門
  第5回 拡張メソッド
    1.C# 2.0プログラマーの悲劇
    2.拡張メソッドの概要/スイッチなしで機能する例/sealedクラスを拡張する
  3.メソッド呼び出しと型の関係/thisの正体/拡張メソッドを使用すべきとき
 
インデックス・ページヘ  「C# 3.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間