演算子オーバーロードを活用してジェネリックオペレータを作る。それは即席自作演算子的な何か。
C#に限った話ではなく、その他のプログラミング言語(C++やD言語など)でも、
演算子オーバーロードを利用するようなケースは非常に限られていると思う。
というのも、適切に利用せずに無闇に使うとメリットよりもデメリットの方が大きくなる場合が多いからだ。
演算子オーバーロードを行うことで、同型または異なる型のオブジェクトを演算することを可能にし、
簡素な記述で効率的且つ自然なコーディングができるようになる。それは一般的に可読性が向上するといわれている。
確かに一時的にはそうなるだろう。でもそれは、あくまでそのオブジェクト同士の演算子オーバーロードについて
完全に把握している場合に限られる。理解度の低い人がメンテナンスをする場合や、
日をあけた未来の自分自身が再び見た場合、その可読性は明らかに低下するだろう。
そんなわけで、敬遠されがちであまり多用されることのない演算子オーバーロード。
そのメリットよりもデメリットの方が多いとされる上に、実際に実装するのも意外と面倒だったりもする。
しかし、趣味のプログラミングくらいでしか活躍の場がないというのも何か勿体無い。
なので、演算子オーバーロードを活用して、ジェネリックオペレータを作ってみた。
ジェネリックオペレータを作る。それは即席自作演算子的な何か。
using System; namespace ConsoleApplication1 { /// <summary> /// 即席単項演算子的な何か /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="TResult"></typeparam> public class InstantOperator<T, TResult> { private Func<T, TResult> _operate; public InstantOperator(Func<T, TResult> ope) { _operate = ope; } public static TResult operator %(T left, InstantOperator<T, TResult> ope) { return ope._operate(left); } } /// <summary> /// 即席2項演算子的な何か /// </summary> /// <typeparam name="T1"></typeparam> /// <typeparam name="T2"></typeparam> /// <typeparam name="TResult"></typeparam> public class InstantOperator<T1, T2, TResult> { private Func<T1, T2, TResult> _operate; public InstantOperator(Func<T1, T2, TResult> ope) { _operate = ope; } public static Relay operator %(T1 left, InstantOperator<T1, T2, TResult> ope) { return new Relay(ope._operate, left); } public class Relay { private Relay() { } private T1 _left; private Func<T1, T2, TResult> _operate; internal Relay(Func<T1, T2, TResult> ope, T1 left) { _operate = ope; _left = left; } public static TResult operator %(Relay r, T2 right) { return r._operate(r._left, right); } } } }
これだけじゃー、ぱっと見なんのこっちゃーわからんですね。
ということで、さっそく利用例。
using System; using System.Diagnostics; namespace ConsoleApplication1 { public class Program { private static void Main() { Example1(); Example2(); Example3(); Example4(); Console.ReadKey(); } [Conditional("DEBUG")] private static void Example1() { Console.WriteLine("((2^2)*3)^2 * 2 = {0}", Math.Pow((Math.Pow(2, 2) * 3), 2) * 2); var pow = new InstantOperator<double, double, double>(Math.Pow); Console.WriteLine("((2^2)*3)^2 * 2 = {0}", 2 % pow % 2 * 3 % pow % 2 * 2); Console.WriteLine(); } [Conditional("DEBUG")] private static void Example2() { Console.WriteLine("x".Times(5)); var times = new InstantOperator<string, int, string>(StringExtensions.Times); Console.WriteLine("x" % times % 5); foreach (var item in "abc".ToCharArray()) Console.Write(item.ToString() % times % 3); Console.WriteLine(); Console.WriteLine(); } [Conditional("DEBUG")] private static void Example3() { var h1 = new Hoge() { Name = "hoge" }; var h2 = new Fuga() { Name = "fuga" }; var union = new InstantOperator<Nanika, Nanika, String>(UnionName); Console.WriteLine(h1 % union % h2); Console.WriteLine(h2 % union % h1); } [Conditional("DEBUG")] private static void Example4() { Console.WriteLine(); var notNull = new InstantOperator<object, bool>(IsNotNull); string s1 = null; string s2 = string.Empty; Console.WriteLine(s1 % notNull); Console.WriteLine(s2 % notNull); } private static string UnionName(Nanika h1, Nanika h2) { return h1.Name + h2.Name; } private static bool IsNotNull(object obj) { if (obj != null) return true; return false; } } public abstract class Nanika { public string Name { get; set; } } public class Hoge : Nanika{ } public class Fuga : Nanika { } public static class StringExtensions { public static string Times(this string s, int i) { string ret = string.Empty; for (int x = 0; x < i; x++) ret += s; return ret; } } }
実行結果
((2^2)*3)^2 * 2 = 288 ((2^2)*3)^2 * 2 = 288 xxxxx xxxxx aaabbbccc hogefuga fugahoge False True
べき乗の例については、Math.Pow関数での入れ子な記述をしなくて済むので、
ある程度可読性が向上しているように見える。けど、そのほかの例については微妙な感じですね。
でも、いちいちオブジェクトごとに演算子のオーバーロードを記述しなくても、
それっぽいものが簡単に利用できるってのは嬉しいかもしれません。
まぁ、何事もご利用は計画的にってことで。