裏ワザなのか基本技なのかわかりませんが、拡張メソッドを使うと「うまくやれば」既存のメソッドの動きを上書きできるかもしれん、という技です。
■Xamarin.Forms の FindByName を入れ替える
XamlProvider では、x:Name の解決のために FindByName を提供しているのですが、本当は元ネタの Xamarin.Forms の FindByName を使いたかったのです。内部実装まで真似て、Xamarin.Forms が使っている FindByName をそのまま使いたかった。が、これは Xamarin.Forms.NameScopeExtensions.FindByName 拡張メソッドで、実際に中でどうやっているかは分からないんですよね。実際、Name をキープするところは、実装依存なところがあるので、そこに手を入れらるかどうかはわかりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | namespace Xamarin.Forms { // 概要: // Extension methods for Xamarin.Forms.Element and Xamarin.Forms.INameScope // that add strongly-typed FindByName methods. // // コメント: // To be added. public static class NameScopeExtensions { // 概要: // Returns the instance of type T that has name T in the scope that includes // element. // // パラメーター: // element: // To be added. // // name: // To be added. // // 型パラメーター: // T: // To be added. // // 戻り値: // To be added. // // コメント: // To be added. public static T FindByName<T>( this Element element, string name); } } |
仕方がないので、この FindByName を入れ替えて自前の、FindByName を作ってすり替えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [<Extension>] type PageXaml() = static member LoadXaml(xaml:string) = Moonmile.XForms.ParseXaml.LoadXaml(xaml) static member LoadXaml< 'T when 'T :> Page >(xaml:string) = Moonmile.XForms.ParseXaml.LoadXaml< 'T>(xaml) static member FindByName(page:Page, name:string) = Moonmile.XForms.FindByName(name, page) /// <summary> /// Alias FindByName from Xamarin.Forms /// </summary> /// <param name= "name" ></param> [<Extension>] static member FindByName< 'T when 'T :> Element >(this, name:string) = FindByName(name, this) :?> 'T |
拡張メソッドの [<Extension>] なところは、以下なところを参考するとわかります。
C# から使いやすい F# コードの書き方 – ぐるぐる~
http://bleis-tift.hatenablog.com/entry/20121201/1354362376
で、うまくすり替えてしまったのはいいけれど、実際はどういう呼び出し方になっているのか?(どこが優先なのか)が気になっていました。
■テストコード
既定の BClass と、拡張メソッドを作った BClassExtentions, BClassOverrideExtentions を用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | class Program { static void Main( string [] args) { new Program().main(args); } public void main( string [] args) { var b = new BClass( "tomoaki" ); Console.WriteLine( "{0}" , b.GetPrint()); Console.WriteLine( "{0}" , b.GetPrintEx()); Console.WriteLine( "{0}" , (b as object ).GetPrint()); Console.WriteLine( "{0}" , (b as object ).GetPrintEx()); } } public class BClass { private string _name; public BClass( string name) { _name = name; } public string Name { get { return _name; } } public string GetPrint() { return string .Format( "BClass:{0}" , _name); } } public static class BClassExtentions { /// <summary> /// ExtentionLib による拡張 GetPrint /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrint( this BClass obj) { return string .Format( "BClassEx:{0}" , obj.Name); } /// <summary> /// ExtentionLib による拡張 GetPrintEx /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrintEx( this BClass obj) { return string .Format( "BClassEx:{0}" , obj.Name); } } public static class BClassOverrideExtentions { /// <summary> /// OverrideLib による拡張 GetPrint /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrint( this object obj) { return string .Format( "BClassOver:{0}" , (obj as BClass).Name); } /// <summary> /// OverrideLib による拡張 GetPrintEx /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrintEx( this object obj) { return string .Format( "BClassOverEx:{0}" , (obj as BClass).Name); } } |
結果は、こんな感じで、うまく BClassOverrideExtentions のメソッドが呼び出されています。
1 2 3 4 | BClass:tomoaki BClassEx:tomoaki BClassOver:tomoaki BClassOverEx:tomoaki |
まあ、よく見れば AClass#GetPrint で呼び出しているのか object#GetPrint で呼び出しているかの違いがあるので、呼び出すときのキャストで切り分けているのですが、これが、Xamarin.Forms のように BasicClass > Element > Page のような複数の階層になっている場合、Element にくっついている FindByName を Page にくっつけた FindByName で上書きができる、って話です。結構特殊ですが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class BClass { private string _name; public BClass( string name) { _name = name; } public string Name { get { return _name; } } public string GetPrint() { return string .Format( "BClass:{0}" , _name); } } public class SubClass : BClass { public SubClass( string name) : base (name) { } } public class SubSubClass : SubClass { public SubSubClass( string name) : base (name) { } } public static class SubClassExtentions { /// <summary> /// SubClassExtentions による拡張 GetPrint /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrintEx( this SubClass obj) { return string .Format( "SubClassEx:{0}" , obj.Name); } } public static class SubSubClassExtentions { /// <summary> /// SubSubClassExtentions による拡張 GetPrint /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string GetPrintEx( this SubSubClass obj) { return string .Format( "SubSubClassEx:{0}" , obj.Name); } } |
こんな風な階層構造にしておいて、最初は、SubSubClassExtentions 拡張をコメントアウトした状態で、次のコードを動かすと、当然 SubClassExtentions#GetPrintEx が動きます。
1 2 3 4 5 6 7 | var bb = new SubSubClass( "masuda" ); Console.WriteLine( "{0}" , bb.GetPrint()); Console.WriteLine( "{0}" , bb.GetPrintEx()); 結果 BClass:masuda SubClassEx:masuda |
その後に、SubSubClassExtentions を有効にして、同じコードを動かすと、結果は SubSubClassExtentions#GetPrintEx を呼び出すようになります。
1 2 | BClass:masuda SubSubClassEx:masuda |
メソッドをサブクラスから順に探索するから、当たり前といえば当たり前なんだけど、なるほど、そういう動きをしていたのか、という記録ですね。
■同じ拡張メソッドがあるとビルド時にエラー
ちなみに、同じメソッド名、同じ引数で拡張メソッドを作るとビルド時にエラーがでます。
1 | エラー 1 次のメソッドまたはプロパティ間で呼び出しが不適切です: 'OverrideLibFSharp.AClassFSharpExtentions.GetPrintEx(BaseLib.AClass)' と 'ExtentionLib.AClassExtentions.GetPrintEx(BaseLib.AClass)' C:gitSamplesOverrideExtentionsMethodOverrideExtentionsMethodProgram.cs 25 38 OverrideExtentionsMethod |
これは、F# で作った AClassFSharpExtentions.GetPrintEx メソッドと、AClassExtentions.GetPrintEx がぶつかっています、という意味です。F# のほうは PCL で作ったものを参照設定して使っています。
というわけで、結構限定的な使い方ですが、
- クラスが階層構造になっている。
- 階層構造の途中で拡張メソッドが使われている(Xamarin.Formsの場合は Element に FindByName がある)
このときは、さらにサブクラス/継承クラスに対して拡張メソッドをつけると、元の拡張メソッドを上書きできる、って話しでした。