【C#】【黒魔術】implicit operator: あいまいなユーザー定義の変換を回避する方法

C#
このエントリーをはてなブックマークに追加

タイトルの通り黒魔術です(多分)、実行環境ごとの動作はどうなるかわかりません。

また、C#などの仕様変更で使い物にならなくなる可能性はあります。(Support extension methods everywhereの流れ次第では仕様変更が入るかもしれない)

implicit operatorとはなんぞや

C#では比較演算子などのオーバーロードができます。その中(?)にimplicit operatorやexplicit operatorがあります。こいつらは暗黙的なキャストと明示的なキャストをオーバーロードすることができます。

たとえばこんなコード↓があったとして

    public class A1
    {
        public static implicit operator B1(A1 a)
        {
            return new B1();
        }
    }
    
    public class B1
    {
    
    }

こんなコード↓は合法(コンパイル&実行可能)です

    B1 b1 = new A1();

explicit operatorなら明示的なキャストが必要でになります。

また、implicit/explicit operatorは変換元のクラス/構造体以外に変換先のクラス/構造体にも定義することができて、以下のコードも合法です。

    public class A2
    {
    
    }

    public class B2
    {
        public static implicit operator B2(A2 a)
        {
            return new B2();
        }
    }
    B2 b2 = new A2();

現時点(C# 7.3)ではimplicit/explicit operatorが定義できる場所は2箇所ということになります。

implicit operator 2箇所で定義したらどうなる?

当然生まれる疑問です。

以下のコードに関しては合法です。

    public class A3
    {
        public static implicit operator B3(A3 a)
        {
            System.Console.WriteLine("A3 implicit operator");
            return new B3();
        }
    }
    
    public class B3
    {
        public static implicit operator B3(A3 a)
        {
            System.Console.WriteLine("B3 implicit operator");
            return new B3();
        }
    }

ただし、以下のコードに関してはコンパイルエラー

    // CS0457 'A3' から 'B3' へ変換するときの、あいまいなユーザー定義の変換 'A3.implicit operator B3(A3)' および 'B3.implicit operator B3(A3)' です
    B3 b3 = new A3();

まぁ、当たり前っちゃ当たり前です。どちらのimplicit operatorを実行すればいいかわからないのだから。

黒魔術

ちょっと必要性があって式ツリーでキャストするコードを描いていた時に気づいてしまいました。

        // implicit operatorはexplicit castもできる
        // (TSource value) => (TResult)value;
        public static Func<TSource, TResult> CreateConvertDelegate<TSource, TResult>()
        {
            ParameterExpression value = Expression.Parameter(typeof(TSource), "value");
            UnaryExpression convert = Expression.Convert(value, typeof(TResult));
            return Expression.Lambda<Func<TSource, TResult>>(convert, value).Compile();
        }

こんな感じでラムダを動的生成してやり、以下のようなコードを書いてしまうと:

    Func<A3, B3> cast = CreateConvertDelegate<A3, B3>();
    System.Console.WriteLine(cast(new A3()).GetType().Name);

コンパイル時のチェックはもちろんできないのでコンパイルできてしまいます。 また、実行してやると:

A3 implicit operator
B3

が出力されるので、見事「あいまいなユーザー定義の変換」を回避できてしまいました。explicit operatorの場合も同じ挙動です。

かなり、興味深い内容ですね。 implicit operatorは内部的にはop_Implicitという名のstatic special name メソッドになります。 リフレクションで取ってくるときはMethodInfo.IsSpecialNameがtrueになるメソッドです。 変換元のop_Implicitメソッドを優先的に叩いたという感じでしょうか?

何はともあれ、こんな非合法すれっすれのimplicit/explicit operatorがあるコードが実際に飛んでくることはないとおもうので、式ツリーでキャストするときはimplicit/explicit operatorの数なんか気にせずやればいいと思います。

重要なことですが最後に、implicit operatorは容量用法を守って適切に使いましょう。多用は厳禁です。