裏ワザなのか基本技なのかわかりませんが、拡張メソッドを使うと「うまくやれば」既存のメソッドの動きを上書きできるかもしれん、という技です。
■Xamarin.Forms の FindByName を入れ替える
XamlProvider では、x:Name の解決のために FindByName を提供しているのですが、本当は元ネタの Xamarin.Forms の FindByName を使いたかったのです。内部実装まで真似て、Xamarin.Forms が使っている FindByName をそのまま使いたかった。が、これは Xamarin.Forms.NameScopeExtensions.FindByName 拡張メソッドで、実際に中でどうやっているかは分からないんですよね。実際、Name をキープするところは、実装依存なところがあるので、そこに手を入れらるかどうかはわかりません。
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 を作ってすり替えています。
[<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 を用意します。
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 のメソッドが呼び出されています。
BClass:tomoaki BClassEx:tomoaki BClassOver:tomoaki BClassOverEx:tomoaki
まあ、よく見れば AClass#GetPrint で呼び出しているのか object#GetPrint で呼び出しているかの違いがあるので、呼び出すときのキャストで切り分けているのですが、これが、Xamarin.Forms のように BasicClass > Element > Page のような複数の階層になっている場合、Element にくっついている FindByName を Page にくっつけた FindByName で上書きができる、って話です。結構特殊ですが。
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 が動きます。
var bb = new SubSubClass("masuda"); Console.WriteLine("{0}", bb.GetPrint()); Console.WriteLine("{0}", bb.GetPrintEx()); 結果 BClass:masuda SubClassEx:masuda
その後に、SubSubClassExtentions を有効にして、同じコードを動かすと、結果は SubSubClassExtentions#GetPrintEx を呼び出すようになります。
BClass:masuda SubSubClassEx:masuda
メソッドをサブクラスから順に探索するから、当たり前といえば当たり前なんだけど、なるほど、そういう動きをしていたのか、という記録ですね。
■同じ拡張メソッドがあるとビルド時にエラー
ちなみに、同じメソッド名、同じ引数で拡張メソッドを作るとビルド時にエラーがでます。
エラー 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 がある)
このときは、さらにサブクラス/継承クラスに対して拡張メソッドをつけると、元の拡張メソッドを上書きできる、って話しでした。