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

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

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

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

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