後付けの拡張メソッドを使って既存の拡張メソッドをオーバーライドする

裏ワザなのか基本技なのかわかりませんが、拡張メソッドを使うと「うまくやれば」既存のメソッドの動きを上書きできるかもしれん、という技です。

■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 がある)

このときは、さらにサブクラス/継承クラスに対して拡張メソッドをつけると、元の拡張メソッドを上書きできる、って話しでした。

カテゴリー: C#, F# パーマリンク