C# のジェネリック演算

commits5 min read

ジェネリックで演算できない?!

ジェネリックメソッドを作りました。対象は数値です。二つの引数を取り、それを足して返します。(実際作ったのはもちろん違う仕事をするんですが、ここでは話を簡単にするためにこういうことにしておいてください)

static T Add<T>(T a, T b)
{
	return a + b;
}

エラー	CS0019	演算子 '+' を 'T' と 'T' 型のオペランドに適用することはできません

そりゃそうですよね。T が何かわからないんですから。文字列かもしれないしコントロールかもしれない。そんなものが足せるわけがありません。こういう時にはそう、型パラメータの制約です。

|制約|Description|
|:--|:--|
|where T: struct|型引数は、値型である必要があります。 Nullable 以外のすべての値型を指定できます。 詳細については、「Null 許容型の使用」を参照してください。|
|where T : class|型引数は、参照型である必要があります。このことは、クラス型、インターフェイス型、デリゲート型、配列型についても当てはまります。|
|where T : new()|型引数は、パラメーターなしのパブリック コンストラクターを持つ必要があります。 new() 制約を別の制約と併用する場合、この制約を最後に指定する必要があります。|
|where T : <基本クラス名>|型引数は、指定した基本クラスであるか、または指定した基本クラスから派生する必要があります。|
|where T : <インターフェイス名>|型引数は、指定したインターフェイスであるか、または指定したインターフェイスを実装する必要があります。 インターフェイス制約は複数指定できます。 制約元のインターフェイスもジェネリックにできます。|
|where T : U|T の位置にある型引数は、U の位置にある引数であるか、またはその引数から派生する必要があります。|

つまり、こう。

static T Add<T>(T a, T b) where T : int, long, double, float, byte, char
{
	return a + b;
}

エラー	CS0701	'int' は有効な制約ではありません。制約として使用された型はインターフェイス、非シール クラス、または型パラメーターでなければなりません。
エラー	CS0701	'long' は有効な制約ではありません。制約として......
エラー	CS0701	'double' は有効な制約では......
エラー	CS0701	'float' は違うつってんだろ

数値型はシールされたクラスなので使えないそうです。ならば、数値型の基底クラスに制約すればいいに違いありません。

Console.WriteLine(typeof(int).BaseType);

System.ValueType

……int の基底クラスは System.ValueType つまり構造体の基底クラスですね。これで制約しても構造体すべてに該当します。役に立ちません。

じゃあ無理やり計算しちゃおうじゃないか

まずジェネリック演算用のクラスを作りました。(あれ?)
 式木を組み立て、コンパイルしてメソッドにします。

public class GenericOperation<T>
{
	public GenericOperation()
	{
		var availableT = new Type[]
		{
			typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(byte),
			typeof(decimal), typeof(double)
		};
		if (!availableT.Contains(typeof(T)))
		{
			throw new NotSupportedException();
		}
		var p1 = Expression.Parameter(typeof(T));
		var p2 = Expression.Parameter(typeof(T));
		Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(p1, p2), p1, p2).Compile();
		Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(p1, p2), p1, p2).Compile();
		Multiply = Expression.Lambda<Func<T, T, T>>(Expression.Multiply(p1, p2), p1, p2).Compile();
		Divide = Expression.Lambda<Func<T, T, T>>(Expression.Divide(p1, p2), p1, p2).Compile();
		Modulo = Expression.Lambda<Func<T, T, T>>(Expression.Modulo(p1, p2), p1, p2).Compile();
		Equal = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(p1, p2), p1, p2).Compile();
		GreaterThan = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThan(p1, p2), p1, p2).Compile();
		GreaterThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.GreaterThanOrEqual(p1, p2), p1, p2).Compile();
		LessThan = Expression.Lambda<Func<T, T, bool>>(Expression.LessThan(p1, p2), p1, p2).Compile();
		LessThanOrEqual = Expression.Lambda<Func<T, T, bool>>(Expression.LessThanOrEqual(p1, p2), p1, p2).Compile();
	}

	public Func<T, T, T> Add { get; private set; }
	public Func<T, T, T> Subtract { get; private set; }
	public Func<T, T, T> Multiply { get; private set; }
	public Func<T, T, T> Divide { get; private set; }
	public Func<T, T, T> Modulo { get; private set; }
	public Func<T, T, bool> Equal { get; private set; }
	public Func<T, T, bool> GreaterThan { get; private set; }
	public Func<T, T, bool> GreaterThanOrEqual { get; private set; }
	public Func<T, T, bool> LessThan { get; private set; }
	public Func<T, T, bool> LessThanOrEqual { get; private set; }
}

このように使います。

public static void Main()
{
	Console.WriteLine(Add(1, 2));
}

static T Add<T>(T a, T b)
{
	var go = new GenericOperation<T>();
	return go.Add(a, b);
}


3

できました。四則演算と剰余と比較ができます。でも、Add を二つ作ったとか言わないでください。初めに書いたように、本当に作りたかったのは Add じゃないんです。

2017 年 3 月 28 日追記

dynamic でもいける」との情報をいただきました。なるほど盲点でした。これなら数値型以外でも + 演算子をオーバーロードしているクラスならすべて使えます。しかし、気になるのはパフォーマンス。そこで次のようにして計測してみました。

class Program
{
	public static void Main()
	{
		var random = new Random();
		var data = Enumerable
			.Range(0, 10000000)
			.Select(a => random.Next(100))
			.ToList();
		var stopwatch = new Stopwatch();

		Console.WriteLine("式木");
		stopwatch.Start();
		var testClass = new TestClass<int>();
		Console.WriteLine(data.Aggregate((a, b) => testClass.AddWithExpressionTree(a, b)));
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);

		stopwatch.Reset();

		Console.WriteLine("dynamic");
		stopwatch.Start();
		Console.WriteLine(data.Aggregate((a, b) => testClass.AddWithDynamic(a, b)));
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}
}

class TestClass<T>
{
	private GenericOperation<T> go = new GenericOperation<T>();

	public T AddWithDynamic(T a, T b)
	{
		return (dynamic)a + (dynamic)b;
	}

	public T AddWithExpressionTree(T a, T b)
	{
		return go.Add(a, b);
	}
}

0 から 99 までの乱数を 100 万個作り、それぞれのメソッドで合計を出してみました。結果はこちら。

式木
494830329
00:00:00.5353156
dynamic
494830329
00:00:01.9958122

式木の方が 4 倍弱速いようです。あらかじめコンパイルする分だけ差が出たのでしょう。

執筆日: 2017/03/28

GitHubで編集を提案

Discussion

ログインするとコメントできます