C# の拡張メソッドってどうコンパイルされるの、っと
// ExtensionMethodTest.cs using System; namespace MyExtensions { // from chapter 13, "Programming C#" public static class ExtensionMethods { public static string Right(this string s, int n) { if (n < 0 || n > s.Length) { return s; } else { return s.Substring(s.Length - n); } } } } namespace MyApp { using MyExtensions; // これが必要 public class Tester { public static void Main() { string hello = "Hello"; Console.WriteLine("hello.Right(3) = {0}", hello.Right(2)); } } }
これをmono C# compilerでコンパイルしてみる。
$ dmcs ExtensionMethodTest.cs
すると.exeが出来上がるので逆アセンブルしてみる。
$ monodis ExtensionMethodTest.exe (snip) .namespace MyExtensions { .class public auto ansi abstract sealed beforefieldinit ExtensionMethods extends [mscorlib]System.Object { .custom instance void class [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::'.ctor'() = (01 00 00 00 ) // .... // method line 1 .method public static hidebysig default string Right (string s, int32 n) cil managed { .custom instance void class [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::'.ctor'() = (01 00 00 00 ) // .... // Method begins at RVA 0x20ec // Code size 36 (0x24) .maxstack 9 IL_0000: ldarg.1 IL_0001: ldc.i4.0 IL_0002: blt IL_0013 IL_0007: ldarg.1 IL_0008: ldarg.0 IL_0009: callvirt instance int32 string::get_Length() IL_000e: ble IL_0015 IL_0013: ldarg.0 IL_0014: ret IL_0015: ldarg.0 IL_0016: ldarg.0 IL_0017: callvirt instance int32 string::get_Length() IL_001c: ldarg.1 IL_001d: sub IL_001e: callvirt instance string string::Substring(int32) IL_0023: ret } // end of method ExtensionMethods::Right } // end of class MyExtensions.ExtensionMethods } .namespace MyApp { .class public auto ansi beforefieldinit Tester extends [mscorlib]System.Object { // method line 2 .method public hidebysig specialname rtspecialname instance default void '.ctor' () cil managed { // Method begins at RVA 0x211c // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void object::'.ctor'() IL_0006: ret } // end of method Tester::.ctor // method line 3 .method public static hidebysig default void Main () cil managed { // Method begins at RVA 0x2124 .entrypoint // Code size 24 (0x18) .maxstack 4 .locals init ( string V_0) IL_0000: ldstr "Hello" IL_0005: stloc.0 IL_0006: ldstr "hello.Right(3) = {0}" IL_000b: ldloc.0 IL_000c: ldc.i4.2 IL_000d: call string class MyExtensions.ExtensionMethods::Right(string, int32) IL_0012: call void class [mscorlib]System.Console::WriteLine(string, object) IL_0017: ret } // end of method Tester::Main } // end of class MyApp.Tester }
メソッド呼び出しだけ見ればいいのでそこを見る。まず通常のインスタンスメソッドの呼び出しはRight()の以下の部分にある。
# stackに引数を積んで callvirt IL_001e: callvirt instance string string::Substring(int32)
これに対して拡張メソッドの呼び出しは以下のとおり。
IL_000d: call string class MyExtensions.ExtensionMethods::Right(string, int32)
Right(string, int32)という静的メソッドを単にcallで呼んでいるだけだ。つまり、拡張メソッドとは単なるコンパイラへのsyntactic sugarであって、コンパイルされたCLI的にはまったくその存在はみえない。
本当にメソッドが生えたわけではないので、using MyExtensions; していない名前空間の中では当然拡張メソッドは呼べないし、この仕組を使ってstringに任意のinterfaceを実装させることもできない。そのかわり、usingを強制させるのであるメソッドが拡張メソッドかどうかは比較的わかりやすく、ソースコードの可読性はある程度保たれる。
静的型付けの言語で既存のクラスにメソッドを足す方法としては、このあたりがひとつの落とし所だと思う。