2012年10月20日土曜日

[C#]IDataReader をちょっと使いやすくする

IDataReader を使うときそのままだとこんな感じだと思うんですが、

int intValue = (int) reader["int_field"];
string strValue = (string) reader["str_field"];
 

NULL のときとかちょっと面倒です。(なんで IDataReader.IsDBnull には IsDBNull(string name) みたいなオーバーロードがないんだろう

Nullable<int> nullableValue = null;
if (!reader.IsDBNull(reader.GetOrdinal("nullable_field")))
{
    nullableValue = (int) reader["nullable_field"];
}
 

なのでこんな風にできるようにしたい。

int intValue = reader.Field<int>("int_field");
string strValue = reader.Field<string>("str_field");
Nullable<int> nullableValue = reader.Field<Nullable<int>>("nullable_field");
int ifNullValue = reader.Field<int>("if_null_field", ifNull:-1);
 

3.5以上なら拡張メソッド作ればいいんですが... 残念ながら 2.0 な環境だったのでこんなヘルパクラスを作って実装しました。

public class DataReaderHelper : IDataReader
{
public DataReaderHelper(IDataReader reader)
{
if (reader == null) throw new ArgumentNullException("reader");
_reader = reader;
}
private readonly IDataReader _reader;
public T Field<T>(string fieldName)
{
return Field<T>(_reader, fieldName, default(T), false);
}
public T Field<T>(string fieldName, T ifNull)
{
return Field<T>(_reader, fieldName, ifNull, true);
}
public bool IsDBNull(string fieldName)
{
return _reader.IsDBNull(_reader.GetOrdinal(fieldName));
}
private static T Field<T>(IDataReader reader, string fieldName, T ifNull, bool useIfNull)
{
Type type = typeof (T);
Type valueType = Nullable.GetUnderlyingType(type);
bool typeIsNullable = (valueType != null);
if (!typeIsNullable)
valueType = type;
if (Convert.IsDBNull(reader[fieldName]))
{
if (typeIsNullable || !valueType.IsValueType || useIfNull)
return ifNull;
throw new InvalidCastException();
}
return (T)reader[fieldName];
}
public void Close()
{
_reader.Close();
}
public int Depth
{
get { return _reader.Depth; }
}
public DataTable GetSchemaTable()
{
return _reader.GetSchemaTable();
}
public bool IsClosed
{
get { return _reader.IsClosed; }
}
public bool NextResult()
{
return _reader.NextResult();
}
public bool Read()
{
return _reader.Read();
}
public int RecordsAffected
{
get { return _reader.RecordsAffected; }
}
public void Dispose()
{
_reader.Dispose();
}
public int FieldCount
{
get { return _reader.FieldCount; }
}
public bool GetBoolean(int i)
{
return _reader.GetBoolean(i);
}
public byte GetByte(int i)
{
return _reader.GetByte(i);
}
public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length)
{
return _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length);
}
public char GetChar(int i)
{
return _reader.GetChar(i);
}
public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length)
{
return _reader.GetChars(i, fieldoffset, buffer, bufferoffset, length);
}
public IDataReader GetData(int i)
{
return _reader.GetData(i);
}
public string GetDataTypeName(int i)
{
return _reader.GetDataTypeName(i);
}
public DateTime GetDateTime(int i)
{
return _reader.GetDateTime(i);
}
public decimal GetDecimal(int i)
{
return _reader.GetDecimal(i);
}
public double GetDouble(int i)
{
return _reader.GetDouble(i);
}
public Type GetFieldType(int i)
{
return _reader.GetFieldType(i);
}
public float GetFloat(int i)
{
return _reader.GetFloat(i);
}
public Guid GetGuid(int i)
{
return _reader.GetGuid(i);
}
public short GetInt16(int i)
{
return _reader.GetInt16(i);
}
public int GetInt32(int i)
{
return _reader.GetInt32(i);
}
public long GetInt64(int i)
{
return _reader.GetInt64(i);
}
public string GetName(int i)
{
return _reader.GetName(i);
}
public int GetOrdinal(string name)
{
return _reader.GetOrdinal(name);
}
public string GetString(int i)
{
return _reader.GetString(i);
}
public object GetValue(int i)
{
return _reader.GetValue(i);
}
public int GetValues(object[] values)
{
return _reader.GetValues(values);
}
public bool IsDBNull(int i)
{
return _reader.IsDBNull(i);
}
public object this[string name]
{
get { return _reader[name]; }
}
public object this[int i]
{
get { return _reader[i]; }
}
}
DataReaderHelper reader = new DataReaderHelper(command.ExecuteReader());
while (reader.Read())
{
int intValue = reader.Field<int>("int_field");
string strValue = reader.Field<string>("str_field");
Nullable<int> nullableValue = reader.Field<Nullable<int>>("nullable_field");
int ifNullValue = reader.Field<int>("if_null_field", ifNull:-1);
}
view raw UseCase.cs hosted with ❤ by GitHub

まあ実際は VB.NET なんですが... これで IDataReader が少し使いやすくなりました。

2012年10月3日水曜日

[C#]式木を触ってみる

メタプログラミングには以前から興味があったので、ぜひ式木について知りたい!というわけで。
こんなケースで考えてみました。

Equals と GetHashCode の実装

Equals と GetHashCode を実装するとき、大抵はプロパティや内部フィールドの比較を繋げるだけのことが多いので、その式を作ってくれる EqualityComparer があると嬉しいなあ。
※ まあ ReSharper があれば自動で実装してくれますが…

たぶんこんなクラスがあったら、


class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }
}

こんな感じに実装すると思います。

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Version) obj);
    }

    private bool Equals(Version other)
    {
        return Major == other.Major && Minor == other.Minor;
    }

    public override int GetHashCode()
    {
        return Major.GetHashCode() ^ Minor.GetHashCode();
    }
}

これの実装を EqualityComparer に移したらこんな感じになるでしょうか。※ null判定は省略します

class VersionEqualityComparer : IEqualityComparer<Version>
{
    public bool Equals(Version x, Version y)
    {
        return x.Major == y.Major && x.Minor == y.Minor;
    }

    public int GetHashCode(Version obj)
    {
        return obj.Major.GetHashCode() ^ obj.Minor.GetHashCode();
    }
}

この EqualityComparer の Equals と GetHashCode の式を作ってくれる汎用EqualityComparer を作りたいと思います。

使い方をきめる

どんな風に使うかですが…

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    private static readonly ComplexEqualityComparer<Version> Comparer = new ComplexEqualityComparer<Version>()
        .AddMember(v => v.Major)
        .AddMember(v => v.Minor)
        .Compile();

    public override bool Equals(object obj)
    {
        return Comparer.Equals(this, obj as Version);
    }

    public override int GetHashCode()
    {
        return Comparer.GetHashCode(this);
    }
}

こんな感じで実装に利用するプロパティやフィールドをラムダ式で指定できるようにします。

Compile メソッドを呼ぶと指定されたプロパティやフィールドを用いた式がコンパイルされて、あとは使うだけになる、という感じです。

書きやすくするために安直な感じですが、thisを返してメソッドチェーンできるようにしておきます。

メンバの指定を実装する

Equals に利用する “x.PropertyName == y.PropertyName” という式と、GetHashCode に利用する “obj.GetHashCode()” という式を作ります。

それぞれあとで “&&” や “^” でつなぐので、リストに放り込んでいきます。

    private readonly ParameterExpression _paramX = Expression.Parameter(typeof(T), "x"); // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY = Expression.Parameter(typeof(T), "y"); // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj = Expression.Parameter(typeof(T), "obj"); // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList = new List<BinaryExpression>();
    private readonly IList<Expression> _getHashCodeList = new List<Expression>();

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

式を完成させる

Complile メソッドで式を完成させて、コンパイルをかけます。

メンバの指定の際にリストに放り込んでおいた式を “&&” や “^” で繋いで組み立てます。
最後に Expression.Lamda<>() でコンパイルして出来上がったデリゲートを保存します。

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

できあがり

だいたいこんな感じになりました。ほんとは null のチェックとかも必要ですが面倒なのでとりあえず。

public class ComplexEqualityComparer<T> : IEqualityComparer<T>
{
    public ComplexEqualityComparer()
    {
        _compiled = false;
        _paramX = Expression.Parameter(typeof(T), "x");
        _paramY = Expression.Parameter(typeof(T), "y");
        _paramObj = Expression.Parameter(typeof(T), "obj");
        _equalsList = new List<BinaryExpression>();
        _getHashCodeList = new List<Expression>();
    }

    private bool _compiled;

    private readonly ParameterExpression _paramX; // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY; // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj; // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList;
    private readonly IList<Expression> _getHashCodeList;

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

    private void CompileIfNotCompiled()
    {
        if (_compiled) return;
        Compile();
    }

    public bool Equals(T x, T y)
    {
        CompileIfNotCompiled();
        return _compiledEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        CompileIfNotCompiled();
        return _compiledGetHashCode(obj);
    }
}

テスト

    [Test]
    public void UseCase()
    {
        var eqComparer = new ComplexEqualityComparer<Version>()
            .AddMember(v => v.Major)
            .AddMember(v => v.Minor)
            .Compile();

        var version_1_0 = new Version(1, 0);
        var version_2_3 = new Version(2, 3);

        Assert.IsTrue(eqComparer.Equals(version_1_0, version_1_0));
        Assert.IsTrue(eqComparer.Equals(version_2_3, version_2_3));
        Assert.IsTrue(eqComparer.Equals(version_2_3, new Version(2, 3)));
        Assert.IsTrue(eqComparer.Equals(new Version(2, 3), version_2_3));

        Assert.IsFalse(eqComparer.Equals(version_1_0, version_2_3));
        Assert.IsFalse(eqComparer.Equals(version_2_3, version_1_0));
    }

テスト結果

ちゃんと動くっぽいですね。

これで Equals と GetHashCode の実装は楽できそうです。

式木おもしろい!

式木とてもおもしろいですね。メタプログラミングって面白いなあと思います。

使い方を誤ると魔物が生まれそうですが、上手に使えばすごく役立ちそうです。(ぼくの頭ではなかなかアイデアが浮かびませんが…

参考にさせていただきました

大変参考にさせていただきました。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...