この記事はC# Advent Calendar 2015の13日目の記事です。
ネタ元
C# Advent Calendar 2015 5日目 - C#のジェネリクスでできないこと
ちょうど興味深い内容があったので、「そういえば、困ったことがあったなぁ。でも何とか出来たかも。」という内容でAdvent Calendarを乗り切ろうと思います。
Size<T>というジェネリック型を作ってSize<T> + Size<T>
等の演算が出来るようにします。Sizeをなぜジェネリックにするかって?浮動小数点に対応したいとか、自然数に制限したいとか、色々事情があったのです。あと、それが出来たらちょっとかっこいいかなって思ったのもちょっぴりありました。
実際に、Size<T>型のa,b,cに対してc=a+b
をやる際に、内部的には、c.Width = a.Width + b.Width
のような演算をするのですが、この時Widthプロパティがジェネリックのパラメータ型なので普通の足し算が出来ません。そこで、式木を用いるとその型の演算が取得できます。
サンプルコード
このユーティリティクラスがミソです。
/// <summary>
/// 演算子の参照を取得するクラス
/// 同じ型同士の演算
/// </summary>
/// <typeparam name="T">演算する型</typeparam>
public static class Operator<T>
{
/// <summary>
/// 加算
/// </summary>
public static readonly Func<T, T, T> Add = GetLambda(Expression.Add);
/// <summary>
/// 減算
/// </summary>
public static readonly Func<T, T, T> Subtract = GetLambda(Expression.Subtract);
/// <summary>
/// 積算
/// </summary>
public static readonly Func<T, T, T> Multiply = GetLambda(Expression.Multiply);
/// <summary>
/// 除算
/// </summary>
public static readonly Func<T, T, T> Divide = GetLambda(Expression.Divide);
/// <summary>
/// インクリメント
/// </summary>
public static readonly Func<T, T> Increment = GetLambda(Expression.Increment);
/// <summary>
/// デクリメント
/// </summary>
public static readonly Func<T, T> Decrement = GetLambda(Expression.Decrement);
/// <summary>
/// 入力1、出力1の演算子ラムダ式を取得する
/// </summary>
/// <param name="ex">演算子のExpression</param>
/// <returns>ラムダ式</returns>
private static Func<T, T> GetLambda(Func<ParameterExpression, UnaryExpression> ex)
{
var x = Expression.Parameter(type: typeof(T)); // 引数 x の式
return Expression.Lambda<Func<T, T>>
(ex(x), x)
.Compile();
}
/// <summary>
/// 入力2、出力1の演算子ラムダ式を取得する
/// </summary>
/// <param name="ex">演算子のExpression</param>
/// <returns>ラムダ式</returns>
private static Func<T, T, T> GetLambda(Func<ParameterExpression, ParameterExpression, BinaryExpression> ex)
{
var x = Expression.Parameter(type: typeof(T));
var y = Expression.Parameter(type: typeof(T));
return Expression.Lambda<Func<T, T, T>>
(ex(x, y), x, y)
.Compile();
}
}
これを使うと、たとえば以下のように演算子のオーバーロードが書けます。
struct GenericSize<T> : where T : struct
{
private T width;
/// <summary>
/// 幅
/// </summary>
public T Width
{
get { return width; }
set { width = value; }
}
private T height;
/// <summary>
/// 高さ
/// </summary>
public T Height
{
get { return height; }
set { height = value; }
}
/// <summary>
/// 各座標を加算する。
/// </summary>
/// <param name="size1">座標1</param>
/// <param name="size2">座標2</param>
/// <returns>加算した座標</returns>
public static VSize<T> operator+ (VSize<T> size1, VSize<T> size2)
{
var add = Operator<T>.Add;
return new VSize<T>(add(size1.Width, size2.Width), add(size1.Height, size2.Height));
}
}
これで、Size<int>でも、Size<double>でも、Size<uint>でも一つの構造体で表現できます。もし、Size<int>に対してのみSystem.Drawing.Sizeにキャストするような独自のメソッドを定義したい場合は、別のクラスで拡張メソッドを定義すれば良いです。ジェネリッククラスにはあくまで汎用的なものだけ入れます。
処理速度比較
class Program
{
static void Main(string[] args)
{
const int loop = 1000000;
// 普通のSize
Size sizeNorm = new Size(1, 1);
Stopwatch swNorm = new Stopwatch();
swNorm.Start();
for(int i = 0; i < loop; i++)
{
sizeNorm = sizeNorm + sizeNorm - 1;
}
swNorm.Stop();
// ジェネリック
Size<int> sizeGen = new Size<int>(1, 1);
Stopwatch swGen = new Stopwatch();
swGen.Start();
for(int i = 0; i < loop; i++)
{
sizeGen = sizeGen + sizeGen - 1;
}
swGen.Stop();
// 出力
Console.WriteLine("loop : {0}", loop);
Console.WriteLine("Normal Size : {0} msec.", swNorm.ElapsedMilliseconds);
Console.WriteLine("Generic Size<int> : {0} msec.", swGen.ElapsedMilliseconds);
Console.WriteLine("Result");
Console.WriteLine("Normal Size : {0}", sizeNorm.ToString());
Console.WriteLine("Generic Size<int> : {0}", sizeGen.ToString());
}
}
loop : 1000000
Normal Size : 179 msec.
Generic Size<int> : 184 msec.
Result
Normal Size : 1,1
Generic Size<int> : 1,1
ジェネリックの方が3%程度遅いですが、パラメータ型を色々変えたい場合はジェネリックを使うのもありだと思います。もっと複雑な演算をしたい場合には速度は更に速度が落ちるとは思います。
ちなみに、structをclassにすればクラスとしても実行できるのですが、この場合は以下のようになり、ジェネリックの方が約40%遅いという結果になりました。
loop : 1000000
Normal Size : 199 msec.
Generic Size<int> : 275 msec.
Result
Normal Size : 1,1
Generic Size<int> : 1,1
コード全体
ExpressionUtilityは上を参照。
/// <summary>
/// 2次元サイズ構造体
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
public struct Size<T> : IEquatable<Size<T>> where T : struct, IComparable
{
#region プロパティ
private T width;
/// <summary>
/// 幅
/// </summary>
public T Width
{
get { return width; }
set { width = value; }
}
private T height;
/// <summary>
/// 高さ
/// </summary>
public T Height
{
get { return height; }
set { height = value; }
}
/// <summary>
/// 空のインスタンス
/// </summary>
public readonly static Size<T> Empty = new Size<T>(default(T), default(T));
/// <summary>
/// 空かどうか判定する。
/// </summary>
/// <returns>判定</returns>
public bool IsEmpty
{
get
{
return Equals(Empty);
}
}
/// <summary>
/// 面積
/// </summary>
public T AreaSize
{
get
{
var multiply = Operator<T>.Multiply;
return multiply(Width, Height);
}
}
#endregion プロパティ
#region メソッドのオーバーライド
/// <summary>
/// 等しいかどうか判定する。
/// </summary>
/// <param name="obj">比較対象</param>
/// <returns>判定</returns>
public override bool Equals(object obj)
{
Size<T> compObj = (Size<T>)obj;
return Equals(compObj);
}
/// <summary>
/// 等しいかどうか判定する。
/// </summary>
/// <param name="obj">比較対象</param>
/// <returns>判定</returns>
public bool Equals(Size<T> obj)
{
if (obj.GetHashCode() != this.GetHashCode())
return false;
if (obj.Width.Equals(this.Width) && obj.Height.Equals(this.Height))
return true;
else
return false;
}
/// <summary>
/// Equalsがtrueを返すときに同じ値を返す。
/// </summary>
/// <returns>ハッシュコード</returns>
public override int GetHashCode()
{
return Convert.ToInt32(Width) ^ Convert.ToInt32(Height);
}
/// <summary>
/// 文字列を取得する。
/// </summary>
/// <returns>出力文字列</returns>
public override string ToString()
{
return string.Format("{0},{1}", this.Width, this.Height);
}
#endregion メソッドのオーバーライド
#region 演算子のオーバーロード
/// <summary>
/// 各座標を加算する。
/// </summary>
/// <param name="size1">座標1</param>
/// <param name="size2">座標2</param>
/// <returns>加算した座標</returns>
public static Size<T> operator+ (Size<T> size1, Size<T> size2)
{
var add = Operator<T>.Add;
return new Size<T>(add(size1.Width, size2.Width), add(size1.Height, size2.Height));
}
/// <summary>
/// 座標に一定値を加算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">加算する値</param>
/// <returns>加算した座標</returns>
public static Size<T> operator+ (Size<T> size1, T value)
{
var add = Operator<T>.Add;
return new Size<T>(add(size1.Width, value), add(size1.Height, value));
}
/// <summary>
/// 各座標を減算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="size2">減算する座標</param>
/// <returns>減算した座標</returns>
public static Size<T> operator- (Size<T> size1, Size<T> size2)
{
var subtract = Operator<T>.Subtract;
return new Size<T>(subtract(size1.Width, size2.Width), subtract(size1.Height, size2.Height));
}
/// <summary>
/// 座標に一定値を減算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">減算する値</param>
/// <returns>減算した座標</returns>
public static Size<T> operator- (Size<T> size1, T value)
{
var subtract = Operator<T>.Subtract;
return new Size<T>(subtract(size1.Width, value), subtract(size1.Height, value));
}
/// <summary>
/// 座標に一定値を乗算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">乗算する座標</param>
/// <returns>乗算した座標</returns>
public static Size<double> operator* (Size<T> size1, double value)
{
return new Size<double>(Convert.ToDouble(size1.Width) * value, Convert.ToDouble(size1.Height) * value);
}
/// <summary>
/// 座標に一定値を除算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">除算する座標</param>
/// <returns>除算する座標</returns>
public static Size<double> operator/ (Size<T> size1, double value)
{
return new Size<double>(Convert.ToDouble(size1.Width) / value, Convert.ToDouble(size1.Height) / value);
}
/// <summary>
/// 一致しているかどうか判定する。
/// </summary>
/// <param name="size1">サイズ1</param>
/// <param name="size2">サイズ2</param>
/// <returns>判定</returns>
public static bool operator ==(Size<T> size1, Size<T> size2)
{
return size1.Equals(size2);
}
/// <summary>
/// 不一致かどうかを判定する。
/// </summary>
/// <param name="size1">サイズ1</param>
/// <param name="size2">サイズ2</param>
/// <returns>判定</returns>
public static bool operator !=(Size<T> size1, Size<T> size2)
{
return !(size1.Equals(size2));
}
#endregion 演算子のオーバーロード
#region コンストラクタ
/// <summary>
/// 正方形のサイズのインスタンスを初期化する。
/// </summary>
/// <param name="length">縦横長さ</param>
public Size(T length)
: this(length, length)
{
}
/// <summary>
/// インスタンスを初期化する。
/// </summary>
/// <param name="width">幅(column方向)</param>
/// <param name="height">高さ(row方向)</param>
public Size(T width, T height)
{
this.width = width;
this.height = height;
}
#endregion コンストラクタ
}
/// <summary>
/// 2次元サイズ構造体
/// </summary>
public struct Size : IEquatable<Size>, IComparable
{
#region プロパティ
private int width;
/// <summary>
/// 幅
/// </summary>
public int Width
{
get { return width; }
set { width = value; }
}
private int height;
/// <summary>
/// 高さ
/// </summary>
public int Height
{
get { return height; }
set { height = value; }
}
/// <summary>
/// 空のインスタンス
/// </summary>
public readonly static Size Empty = new Size(0, 0);
/// <summary>
/// 空かどうか判定する。
/// </summary>
/// <returns>判定</returns>
public bool IsEmpty
{
get
{
return Equals(Empty);
}
}
/// <summary>
/// 面積
/// </summary>
public int AreaSize
{
get
{
var multiply = Operator<int>.Multiply;
return multiply(Width, Height);
}
}
#endregion プロパティ
#region メソッドのオーバーライド
/// <summary>
/// 等しいかどうか判定する。
/// </summary>
/// <param name="obj">比較対象</param>
/// <returns>判定</returns>
public override bool Equals(object obj)
{
Size compObj = (Size)obj;
return Equals(compObj);
}
/// <summary>
/// 等しいかどうか判定する。
/// </summary>
/// <param name="obj">比較対象</param>
/// <returns>判定</returns>
public bool Equals(Size obj)
{
if (obj.GetHashCode() != this.GetHashCode())
return false;
if(obj.Width == this.Width && obj.Height == this.Height)
return true;
else
return false;
}
/// <summary>
/// Equalsがtrueを返すときに同じ値を返す。
/// </summary>
/// <returns>ハッシュコード</returns>
public override int GetHashCode()
{
return Width ^ Height;
}
/// <summary>
/// 文字列を取得する。
/// </summary>
/// <returns>出力文字列</returns>
public override string ToString()
{
return string.Format("{0},{1}", this.Width, this.Height);
}
#endregion メソッドのオーバーライド
#region 演算子のオーバーロード
/// <summary>
/// 各座標を加算する。
/// </summary>
/// <param name="size1">座標1</param>
/// <param name="size2">座標2</param>
/// <returns>加算した座標</returns>
public static Size operator+ (Size size1, Size size2)
{
return new Size(size1.Width + size2.Width, size1.Height + size2.Height);
}
/// <summary>
/// 座標に一定値を加算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">加算する値</param>
/// <returns>加算した座標</returns>
public static Size operator+ (Size size1, int value)
{
return new Size(size1.Width + value, size1.Height + value);
}
/// <summary>
/// 各座標を減算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="size2">減算する座標</param>
/// <returns>減算した座標</returns>
public static Size operator- (Size size1, Size size2)
{
return new Size(size1.Width - size2.Width, size1.Height - size2.Height);
}
/// <summary>
/// 座標に一定値を減算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">減算する値</param>
/// <returns>減算した座標</returns>
public static Size operator- (Size size1, int value)
{
return new Size(size1.Width - value, size1.Height - value);
}
/// <summary>
/// 座標に一定値を乗算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">乗算する座標</param>
/// <returns>乗算した座標</returns>
public static Size operator* (Size size1, int value)
{
return new Size(size1.Width * value, size1.Height * value);
}
/// <summary>
/// 座標に一定値を除算する。
/// </summary>
/// <param name="size1">座標</param>
/// <param name="value">除算する座標</param>
/// <returns>除算する座標</returns>
public static Size operator/ (Size size1, int value)
{
return new Size((int)(size1.Width / value), (int)(size1.Height / value));
}
/// <summary>
/// 一致しているかどうか判定する。
/// </summary>
/// <param name="size1">サイズ1</param>
/// <param name="size2">サイズ2</param>
/// <returns>判定</returns>
public static bool operator ==(Size size1, Size size2)
{
return size1.Equals(size2);
}
/// <summary>
/// 不一致かどうかを判定する。
/// </summary>
/// <param name="size1">サイズ1</param>
/// <param name="size2">サイズ2</param>
/// <returns>判定</returns>
public static bool operator !=(Size size1, Size size2)
{
return !(size1.Equals(size2));
}
#endregion 演算子のオーバーロード
#region コンストラクタ
/// <summary>
/// 正方形のサイズのインスタンスを初期化する。
/// </summary>
/// <param name="length">縦横長さ</param>
public Size(int length)
: this(length, length)
{
}
/// <summary>
/// インスタンスを初期化する。
/// </summary>
/// <param name="width">幅(column方向)</param>
/// <param name="height">高さ(row方向)</param>
public Size(int width, int height)
{
this.width = width;
this.height = height;
}
#endregion コンストラクタ
}