裏ワザなのか基本技なのかわかりませんが、拡張メソッドを使うと「うまくやれば」既存のメソッドの動きを上書きできるかもしれん、という技です。
■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 がある)
このときは、さらにサブクラス/継承クラスに対して拡張メソッドをつけると、元の拡張メソッドを上書きできる、って話しでした。
