C#

TypeConverterの汎用化

More than 3 years have passed since last update.

設定クラスは、コンストラクタでメンバーを初期化するのではなく、
DefaultValue属性を使用して初期値を定義する場合がある。

DefaultValue属性

DefaultValue属性は、プロパティの初期値をメタデータとして定義するための属性で、
定数で値を指定する必要がある。単純な数値やenumであれば、定数としてそのまま使用できるが、
定数となり得ない型の場合、文字列で指定する必要が出てくる。

こんなふうにDefaultValue属性が使われることがある

[DefaultValue(typeof(Percent) , "85%")]
public Percent Alpha {
    get {
        return this._Alpha;
    }
    set {
        this._Alpha = value;
    }
}
private Percent _Alpha;

Percent型
Percent型というものは、.NETにない、独自に定義したパーセント値を扱う型だと思って貰えれば良い。

文字列からオブジェクトへ…

85% という文字列から それに対応するPercent型のオブジェクトを生成するには、
何らかの方法で文字列を等価なオブジェクトに変換してやる必要がある。

int.TryParse
数字から数値(int型)に変換する方法として、TryParseメソッドを用いる方法がある。
TryParseメソッドは、文字列を解析し、数値(int)に変換する。

そうとなれば、Percent.TryParseなるものを実装してやれば良い。
かなりの適当実装であるが、85%のような文字列からPercent型のインスタンスに変換するTryParseメソッドの実装となる。

public struct Percent {
    public readonly static Percent Zero = new Percent( 0.0d );
    private double source;
    public Percent( double source ){
         this.source = source;
    }
    public double Real => this.source;
       public int ToInt32 => this.source * 100;

    public static bool TryParse( string value , out Percent outvalue ) {
        if( string.IsNullOrWhiteSpace( value ) || value.IndexOf( '%' ) < 0 ) {
            outvalue = Zero;
            return false;
        } else {
            double tmp;
            if( double.TryParse( value.Replace( '%' , default(char) ) , NumberStyles.Any , CultureInfo.CurrentCulture , out tmp ) ) {
                outvalue = new Percent( tmp / 100d );
                return true;
            }
        }
        outvalue = Zero;
        return false;
    }
}

TypeConverter

ただTryParseメソッドを実装しただけでは、DefaultValue属性に 85% のような文字列を指定しても例外になるだけだ。
これを解決するには、TypeConverterを継承したクラスを定義しておく。

public sealed class PercentTypeConverter : TypeConverter {
    public override bool CanConvertFrom( ITypeDescriptorContext context , Type sourceType ) => sourceType == typeof(string) || base.CanConvertFrom( context , sourceType );
    public override object ConvertFrom( ITypeDescriptorContext context , System.Globalization.CultureInfo culture , object value ) {
        if( value is Percent )
            return value;
        Percent tmp;
        if( Percent.TryParse( value as string , out tmp ) )
            return tmp;
        return base.ConvertFrom( context , culture , value );
    }
}

そしてこのクラスを…
TypeConverter属性でPercent型に関連付けておけば、
** [DefaultValue(typeof(Percent) , "85%")] ** と指定すると、PercentTypeConverterを通じて、
Percent.TryParseが実行され、文字列からPercent型のオブジェクトに変換できるようになる。

[TypeConverter(typeof(PercentTypeConverter))]
public struct Percent {

}

○○TypeConverterとかいちいち作るのが面倒だった。

型が増える度にPercentTypeConverterみたいなやつを量産するのは、面倒だ。
やってることは、単純で、ある型のTryParseメソッドを呼び出すだけに過ぎない。

そこで、リフレクションと式木を用いて、解決する方法を示す。

using System;
using System.ComponentModel;
using System.Linq.Expressions;

namespace PITA {
    /// <summary>
    /// static bool TryParse( string , out T )を持つ型の汎用TypeConverterを実装します。
    /// </summary>
    /// <typeparam name="T">static bool TryParse( string , out T )を持つ型</typeparam>
    public sealed class TryParseTypeConverter<T> : TypeConverter {
        private delegate bool TryParseDelegate( string input , out T value );
        private static readonly TryParseDelegate _tryParse;
        static TryParseTypeConverter() {
            var tryParse = typeof(T).GetMethod( "TryParse" , new[] { typeof(string) , typeof(T).MakeByRefType() } );
            if( tryParse == null )
                throw new TypeAccessException( "TryParseメソッドが見つからないぽい?" );
            var outValue = Expression.Parameter( typeof(T).MakeByRefType() );
            var stringValue = Expression.Parameter( typeof(string) );

            _tryParse = Expression.Lambda<TryParseDelegate>(
                Expression.Call( tryParse , stringValue , outValue ) ,
                stringValue ,
                outValue
            ).Compile();
        }

        public override bool CanConvertFrom( ITypeDescriptorContext context , Type sourceType ) => sourceType == typeof(string) || base.CanConvertFrom( context , sourceType );
        public override object ConvertFrom( ITypeDescriptorContext context , System.Globalization.CultureInfo culture , object value ) {
            T tmp;
            if( _tryParse( value as string , out tmp ) )
                return tmp;
            return base.ConvertFrom( context , culture , value );
        }
    }
}

割り当て

[TypeConverter(typeof(TryParseTypeConverter<Percent>))]
public struct Percent {
    public static bool TryParse( string value , out Percent outvalue ) {
          // 省略…
    }
}