連載:C# 3.0入門

第5回 拡張メソッド

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

拡張メソッドの概要

 拡張メソッドは、すでに存在するクラスに対して、そのクラスを変更することなく、メソッドを追加できる機能である。

 まず、最もシンプルな例を見てみよう。

 下記のリスト5は、名前空間「Y」にあるクラス「A」を変更することなく、メソッド「MyMethod」を追加するものである。

using System;
using X;

namespace X
{
  static class B
  {
    // 拡張メソッド
    public static void MyMethod(this Y.A a)
    {
      Console.WriteLine("MyMethod called");
    }
  }
}

namespace Y
{
  class A
  {
  }
  class Program
  {
    static void Main(string[] args)
    {
      var a = new A();
      a.MyMethod(); // 出力:MyMethod called
    }
  }
}
リスト5 最も単純な拡張メソッドの使用例

 このサンプル・コードで、名前空間Yには何ら特別な構文は存在しない。ここで注目すべきは、名前空間X内のコードと「using X;」の2つである。

 拡張メソッドは、以下の条件を満たすメソッドを記述することで作成できる。

  • 静的なメソッド(static)である
  • 静的クラス(static)に記述されている
  • 第1パラメータの先頭にthisキーワードが前置されている
  • 第1パラメータの型が、メソッドを追加すべき型

 これで、このメソッドは、あたかも「第1パラメータの型」のメソッドであるかのように呼び出すことができる。「a.MyMethod();」というメソッド呼び出しはそれである。

 さて、「名前空間Yには何ら特別な構文は存在しない」と書いたが、それは厳密には正しくない。「a.MyMethod();」という呼び出しは、実は拡張メソッドを呼び出す専用構文なのである。一見、何ら特別な構文には見えないかもしれないが、本来このメソッドが受け取るはずの第1パラメータに引数を渡していない点に注意を払っていただきたい。つまり、本来存在する引数を自動的に補って呼び出しを行うという特別な構文なのである。

 では、第1引数はどうやって補われるのだろうか? それは使用されたインスタンスそのものである。「a.MyMethod();」であれば、「a」が第1引数に補われる。従って、リスト5においては、以下の2つのメソッド呼び出しは実質的に同じ機能性を持つ。

a.MyMethod();

B.MyMethod(a);

 さて、最後の注目すべき点は「using X;」である。拡張メソッドは、拡張メソッド自身を含む名前空間をusing文で指定しない限り有効にならない。つまり定義しただけでは使用できず、スイッチを入れねばならない。これを「拡張メソッドをインポートする」という。実際、リスト5で「using X;」をコメントアウトすると、以下のようなコンパイル・エラーが発生する。

エラー 1 'Y.A' に 'MyMethod' の定義が含まれておらず、型 'Y.A' の最初の引数を受け付ける拡張メソッドが見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。
リスト5で「using X;」をコメントアウトすると発生するコンパイル・エラー

スイッチなしで機能する例

 ここで1つだけ注意が必要である。実は、using文を用いてインポートする(スイッチを入れる)ことなく、拡張メソッドが有効になってしまうケースが存在する点である。

 リスト6は、Visual Studio 2008でコンパイルしたとき、「using Y;」抜きで拡張メソッドを呼び出すことができてしまう。

using System;

namespace X
{
  class A
  {
  }
}

namespace Y
{
  static class B
  {
    // 拡張メソッド
    public static void MyMethod(this X.A a)
    {
      Console.WriteLine("MyMethod called");
    }
  }
  class Program
  {
    static void Main(string[] args)
    {
      var a = new X.A();
      a.MyMethod();
    }
  }
}
リスト6 スイッチなしで機能する例

 拡張メソッド自身が、呼び出すコンテキスト上のデフォルト名前空間上にあるときは、特に明示的なインポートを指定しなくても呼び出せるようである。しかし、それが言語仕様上、妥当な挙動であるかまでは判断しきれなかった。このような書き方は、通常行わないと思うが、勉強のためのテスト・プログラムなどで書いてしまうことがあり得るので、特に注意喚起のために載せておく。

sealedクラスを拡張する

 「そのクラスを変更することなく」とは、ソース・コードを書き換えずに、という意味ではなく、コンパイル済みのバイナリを変更することなく追加できることを意味する。そのため、クラス・ライブラリにある基本的なクラスにメソッドを拡張することもできる。それどころか、本来拡張できないはずのシール・クラス(sealed修飾子が指定されたクラス)を拡張することもできてしまう。

 継承によって便利なメソッドを追加して使いたいと思いつつ、シール・クラスであるため機能を追加できなかったstringクラス(=System.Stringクラス)にさえ、メソッドを追加できてしまうのである。

 以下はstringクラスに、値を整数に変換して取得する「ToIntメソッド」を追加した例である。

using System;
using X;

namespace X
{
  public static class StringExtender
  {
    // 拡張メソッド
    public static int ToInt(this string s)
    {
      return Convert.ToInt32(s);
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    string s = "123";
    Console.WriteLine(s.ToInt() * 2); // 出力:246
  }
}
リスト7 拡張できないはずのstringクラスを拡張する

 ここで1つのまっとうな疑問が出てくると思う。

 拡張すると問題が起きるからシール・クラスという機能が導入されたはずなのに、それを拡張して問題はないのだろうか?

 その疑問に答えることは、継承と拡張メソッドの違いを把握することとイコールとなる。シール・クラスが制約するのは継承だが、継承と拡張メソッドはまったく違う機能性を持つからである。

 以下、それを見てみよう。

拡張メソッドはオブジェクト内部に手出しできない

 拡張メソッドの定義に立ち返って考えてみよう。リスト5に記述された拡張メソッドMyMethodは、クラスBに属している。1つのメソッドが2つのクラスに属することはないので、当然このメソッドはクラスAには属していない。ということは、メソッドMyMethodはクラスBのprivateやprotectedのメンバにアクセスすることは許されているが、クラスAのprivateやprotectedのメンバには手が出せない。

 この事実を以下のリスト8で確認してみよう。

using System;
using X;

namespace X
{
  static class B
  {
    // 拡張メソッド
    public static void MyMethod(this A a)
    {
      a.PublicMethod();

      a.protectedMethod();
      // エラー 1 'A.protectedMethod()' はアクセスできない保護レベルになっています。

      a.privateMethod();
      // エラー1 'A.protectedMethod()' はアクセスできない保護レベルになっています。
    }
  }
}

class A
{
  public void PublicMethod()
  {
    Console.WriteLine("PublicMethod called");
  }
  protected void protectedMethod()
  {
    Console.WriteLine("protectedMethod called");
  }
  private void privateMethod()
  {
    Console.WriteLine("privateMethod called");
  }
}

class Program
{
  static void Main(string[] args)
  {
    var a = new A();
    a.MyMethod();
  }
}
リスト8 拡張メソッドはオブジェクト内部には手出しできない

 この事実は、拡張メソッドは単に「あたかもそのクラスのメソッドであるかのように呼び出せるだけ」であり、実際にはクラスを拡張していないことを示す。それは、「a.MyMethod();」という呼び出しが、実際には「B.MyMethod(a);」でしかないことからも分かるだろう。

 ちなみに部分クラス(partial)も、1つのクラス定義を複数のクラス定義に分割して記述できるので、これも含めて違いを把握するとより分かりやすいだろう。

 以下に、アクセス制御に注目した場合の、継承、部分クラス、拡張メソッドの違いをまとめる。

public protected private
部分クラス
(同じ名前を持つ別のクラス定義に対して)
継承
(継承元の基本クラスに対して)
×
拡張メソッド
(第1パラメータで示した型に対して)
×
×
アクセス制御に注目した場合の継承/部分クラス/拡張メソッドの違い

拡張メソッドはオブジェクトの振る舞いを変更できない

 継承を行うと、仮想メソッドや抽象メソッドに対して、新しいメソッドを差し替えることができる。これにより、オブジェクトの振る舞いを変更できる。例えば、ToStringメソッドは通常「型名」を文字列として返すが、int型(=Systen.Int32型)では、自分が持つ整数値を文字列として返す機能性に差し替えられている。つまり振る舞いが変わっている。

 では、既存のメソッドと同じ名前のメソッドを拡張メソッドとして作成すると、メソッドを差し替えることができるだろうか?

 これはできない。既存のメソッドの方が常に優先される。それ故に、振る舞いを変更することはできない。以下、それを示すリストを掲載する。

using System;
using X;

namespace X
{
  static class B
  {
    // 拡張メソッド
    public static void MyMethod(this A a)
    {
      Console.WriteLine("B.MyMethod called");
    }
  }
}

class A
{
  public void MyMethod()
  {
    Console.WriteLine("A.MyMethod called");
  }
}

class Program
{
  static void Main(string[] args)
  {
    var a = new A();
    a.MyMethod(); // 出力: A.MyMethod called
  }
}
リスト9 同じ名前のメソッドが競合した場合

拡張メソッドが安全である理由

 シール・クラスは、なぜ継承を禁止する必要があったのだろうか。その理由はいくつもあると思うが、主要なものは以下の2つではないだろうか。

  • カプセル化によって隠された内部の挙動に依存するコードを書かれると、内部構造を変更して改良できなくなる
  • 効率などの要請のため、オブジェクトの振る舞いを変更すると、不都合を生じる実装を行うことがある

 これに対して、拡張メソッドはオブジェクト内部に手出しできず、その振る舞いも変更できない。つまり、隠された内部構造に依存するコードは書けないし、振る舞いも変更できない。それ故に、シール・クラスが懸念するような問題の発生源にはならない。


 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 記事ランキング

本日 月間