よくある質問なのかもしれませんが、タイトルの通りジェネリッククラスでの算術演算子処理について質問があります。
環境の想定は.NET6.0
、C#10.0
でもそれ以下でも構いません。(現在廃止されているようなものは避けたいですが👀)
実現したいこと
c#
1// 複素数クラス 2public struct Complex<T> { 3 public T Re { get; set; } // 実数部 4 public T Im { get; set; } // 複素数部 5 6 public Complex<T> Add(Complex<T> other) { 7 Complex<T> result; 8 result.Re = Re + other.Re; 9 result.Im = Im + other.Im; 10 return result; 11 } 12 13 //... 14}
上記はよくある複素数クラスの例ですが、ジェネリクスを用いて計算する場合の型を指定したいです。
ただ、このままだとタイプTに算術演算が実装されている保証が無いので、たとえComplex<double>
と定義する予定だったとしても、演算処理でエラーが出るのは目に見えていますよね...😑
ただ、なんとかこんなことがしたいなぁと思っている所です。(将来的にdecimal
とかでも計算させたい時が来そうで🥺)
実際には複素数とは全然別の目的ですが、かなり演算処理重視で書きたい方針です👀
昔からどうやるのか気になりながらもスルーしてきましたが、現実に作成しなければいけないときがやってきてしまいました😅
自分なりの案
私の案として思いついたのは...
(1) あきらめて、Tが取り得る全ての通りの実装をする(ComplexInt
・ComplexDouble
みたいに👀)
(2) ジェネリクスは使わず、Re
・Im
をobject
にする
(3) 算術演算可能インターフェースICalculatable
を自作し、where T: ICalculatable
を指定
(4) リフレクションを使う
(5) 式木を使う
とりあえず上記の方法は思いつきました。
考察としては...
(1)のメリット
・算術演算が遅くなる理由が無い。
(1)のデメリット
・かなり頭の悪い実装ww
・メンテナンスが大変
・(int) + (double) -> (double)
みたいな暗黙の型変換などを自分で実装しなければいけない
(2)のメリット
・is演算子で型を確認すればひとまず計算は出来る
・処理速度もほぼ落ちないと思う
(2)のデメリット
・実行時エラーがでる可能性がある(コンパイルエラーにならない)
・使う型をあらかじめ想定しておかないといけない(int, long, double対象とか)
(3)のメリット
・Complex<T>
構造体ではある程度便利に書けそう
・処理速度もそこまでは落ちないかな?
・自作クラスでさえも、ICalculatable
を持たせれば対応させられる
(3)のデメリット
・int
・double
自体にICalculatable
を持たせる事が出来ないので、他の構造体などでラップが必要
・Complex<int> + Complex<double> -> Complex<double>
みたいな実装方法が思いつかない😑
(4)のメリット
・簡単に動的に型を取得して演算可能
・自作クラスでさえも対応させられる
(4)のデメリット
・考えるだけで処理速度は大幅に落ちそう😨(場合によっては10倍じゃ済まないですよね...👀)
・実行時エラーがでる可能性がある
(5)のメリット
・動的に演算処理の式を作成可能(おそらくコンパイル時?)
・自作クラスでさえも対応させられる
(5)のデメリット
・処理速度はどうなんだろう🤔(コンパイル時に評価しているならまだまし?でも最適化は難しそう👀)
・Complex<int> + Complex<double> -> Complex<double>
みたいな実装方法が思いつかない😑
疑問点
処理速度が優先という条件下ではみなさんならどの方法で実装しますか?
他にも方法があるのならキーワードだけでも教えてほしいです。
(4)は無条件排除。
実装の幅の柔軟さなら(2)。(単体テストバリバリ書かないとなぁ...🥺)
実行時エラー排除優先なら(3)。
まぁ...ひとまず現状ではそこまで多くの型を扱うわけではないので、最悪(1)の手段も考えてはいます。
ただ、将来的なメンテが無茶苦茶怖いです...😨(ライブラリとして作っていますし👀)
(5)が一番謎...あまり経験が無いのもありますが、本当にコンパイル時に全て式が作成されるのでしょうか?
たしかにCompile()
メソッドはありますが...
あと、
C#
1public struct Complex<T> where T: struct 2{ 3 public T Re { get; set; } 4 public T Im { get; set; } 5 6 static Complex() 7 { 8 // おそらくTの型を見て例外処理が必要なのかなぁ? 9 10 var param1 = Expression.Parameter(typeof(T)); 11 var param2 = Expression.Parameter(typeof(T)); 12 13 /* 14 AddValue = (T v1, T v2) => v1 + v2; 15 コメントの下の行は上記のように「コンパイル時」に展開されるという認識で正しいでしょうか? 16 例えば、Tに足し算の機能が持っていない場合コンパイルエラーになるのかな? 17 ただ、足し算の機能があるかどうか...は「operator+」があるかどうかで判断ということでしょうか? 18 (なんだか古い書き方っぽい気がしていますが...) 19 */ 20 AddValue = Expression.Lambda<Func<T, T, T>>(Expression.Add(param1, param2), param1, param2).Compile(); 21 } 22 23 // private staticで持つぐらいならComplexの外で定義するべきですね💦 24 // あくまで例と言うことで...😅 25 private static Func<T, T, T> AddValue { get; } = null!; 26 27 public Complex(T re = default, T im = default) 28 { 29 Re = re; Im = im; 30 } 31 32 public Complex<T> Add(Complex<T> other) 33 { 34 return new Complex<T>( 35 Re = AddValue(this.Re, other.Re), 36 Im = AddValue(this.Im, other.Im) 37 ); 38 } 39} 40 41//////////////////////////////////////////////////////////////// 42// ここからは少し贅沢な話かもしれませんが...(ここからは可能ならで大丈夫です) 43#if false 44Complex<int> c1 = new(1, 2); 45Complex<double> c2 = new(0.1, 0.2); 46 47// おそらく現状のComplexではコンパイルエラー 48Complex<double> result = c1.Add(c2); 49 50// 上記を実現するためには... 51public static implicit operator Complex<double>(Complex<int> other) => new(other.Re, other.Im); 52// みたいな定義が必要なのかもしれませんが、Complex<T>の中で定義できませんよね?👀 53#endif
そもそも(5)の方法(式木)を使うべきなのか...👀
それ自体よく分からないので、なにかスマートで処理速度が落ちにくい方法があればご教授頂きたいです。
C++のテンプレートなら余裕なんですけどね😅(まぁ...C#は型制約が厳しいのが良いところでもあるしなぁ🥺)
よろしくお願いします。

回答1件
あなたの回答
tips
プレビュー