OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

C# リフレクションによるメンバの列挙順を指定する

先日Enumeratorを使って自前オブジェクトのメンバを列挙するという話をしていたが、
この中の、リフレクションを使ってメンバを自動で列挙しようという話、
実は、列挙されるメンバの順序が不定だという問題がある。
以下はMSDNの記述。

GetProperties メソッドから返されるプロパティは、アルファベット順や宣言順などの特定の順序で返されるわけではありません。 したがって、プロパティが返される順序に依存するようなコードは避ける必要があります。

というわけで今回は、列挙するメンバの順番を指定したい場合にどうするかという話。
さほど意味のないことに無駄に深入りしているなと思われるかもしれませんが
人生に無駄なことなどないのです。

さて結論からいきますが、プロパティにOrderのカスタム属性を付けて
その値でソートしてから列挙するくらししか思いつかないかな。
とりあえずコード。Personクラスは前回と同じイメージです。

//Attributeの継承クラス  
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]  
public class OrderAttribute : Attribute {  
    //ソート番号  
    public int Value { get; set; }  
}  
  
//Personクラス(Name,Age,Addressの順で出力したい)  
public class Person : IEnumerable<KeyValuePair<string, object>> {  
    [Order(Value = 1)]  
    public int Age { get; set; }  
      
    [Order(Value = 0)]  
    public string Name { get; set; }  
  
    [Order(Value = 2)]  
    public string Address { get; set; }  
  
    public IEnumerator<KeyValuePair<string, object>> GetEnumerator() {  
        //全プロパティをOrder属性のValueの値でソート  
        var properties = this.GetType().GetProperties()  
                             .OrderBy(p => (Attribute.GetCustomAttribute(p, typeof(OrderAttribute)) as OrderAttribute).Value);  
  
        foreach (var prop in properties) {  
            yield return new KeyValuePair<string, object>(prop.Name, prop.GetValue(this));  
        }  
    }  
  
    IEnumerator IEnumerable.GetEnumerator() {  
        return GetEnumerator();  
    }  
  
    public override string ToString() {  
        string vals = string.Join(",", this.Select(p => p.Key + "=" + p.Value));  
        return "Person[" + vals + "]";  
    }  
}  

まずAttributeを継承して自前の属性クラスを作る。
今回はソート順指定に使いたいだけなので、intのプロパティ1つだけのシンプルなクラスになる。
クラス名は「ほげほげAttribute」にするのが慣習。

そしてPersonクラスのほうで全部のプロパティにOrder属性を指定。
「Order」という名前は、OrderAttributeのAttributeを除いたものになってる。

GetEnumerator()の実装は、Order属性でソートする部分以外は
前回と同じなはずです。

 
ここから先はちょっと蛇足だけども、もっとやろうとすれば
任意のクラスに対してEnumeratorを提供する汎用クラスを作ることもできるだろう。
ということで、いろいろ汎用的にしたクラスを作ってみました。
ちょっと長くなるけどそのまま掲載。

public class MemberEnumeratorProvider : IEnumerable<KeyValuePair<string, object>> {  
  
    object targetObj;  
  
    //検索条件のプロパティを公開しておく  
    public BindingFlags EnumeratorFlags { get; set; }  
  
    //コンストラクタで列挙対象のオブジェクトを渡す  
    public MemberEnumeratorProvider(object targetObj) {  
        this.targetObj = targetObj;  
  
        //検索するメンバ条件の初期値を指定  
        this.EnumeratorFlags = BindingFlags.GetProperty   
                                | BindingFlags.Public   
                                | BindingFlags.Instance;  
    }  
  
  
    public IEnumerator<KeyValuePair<string, object>> GetEnumerator() {  
        var attrType = typeof(OrderAttribute);  
  
        //Order属性のあるメンバだけを検索してソート  
        var members = targetObj.GetType().GetMembers(EnumeratorFlags).Where(m => Attribute.IsDefined(m, attrType))  
                        .OrderBy(m => (Attribute.GetCustomAttribute(m, attrType) as OrderAttribute).Value);  
                              
        foreach (var mem in members) {  
            string name = mem.Name;  
  
            //プロパティ  
            if (mem.MemberType.HasFlag(MemberTypes.Property)) {  
                var prop = mem as PropertyInfo;  
                var method = prop.GetGetMethod(true);  
                if (method == null) throw new MemberAccessException(name + "にはゲットメソッドがありません。");  
  
                object val = method.IsStatic ? prop.GetValue(null) : prop.GetValue(targetObj);  
                yield return new KeyValuePair<string, object>(name, val);  
                continue;  
            }  
  
            //フィールド  
            if (mem.MemberType.HasFlag(MemberTypes.Field)) {  
                var fld = mem as FieldInfo;  
                object val = fld.IsStatic ? fld.GetValue(null) : fld.GetValue(targetObj);  
                yield return new KeyValuePair<string, object>(name, val);  
                continue;  
            }  
  
            //メソッド  
            if (mem.MemberType.HasFlag(MemberTypes.Method)) {  
                var method = mem as MethodInfo;  
  
                //※パラメータが必要なメソッドには使用できない(throws TargetParameterCountException)  
                object val = method.IsStatic ? method.Invoke(null, null) : method.Invoke(targetObj, null);  
                yield return new KeyValuePair<string, object>(name, val);  
                continue;  
            }  
  
            //上記に当てはまらない場合に例外を投げる  
            throw new NotSupportedException(name + "は列挙できないタイプのメンバです。");  
        }  
    }  
  
    IEnumerator IEnumerable.GetEnumerator() {  
        return GetEnumerator();  
    }  
  
    //ToString()で出力する文字列を提供する  
    public string BuildToString() {  
        string vals = string.Join(",", this.Select(p => p.Key + "=" + p.Value));  
        return string.Format("{0}[{1}]", targetObj.GetType().Name, vals);  
    }  
}  

Order属性をつけたメンバだけ列挙対象にするようにしてある。
あと、あんまり使わない気もするけど、staticなメンバも対象にできるようにした。
ついでにフィールドやメソッドも列挙できるようにしたので、
OrderAttributeクラスのほうもAttributeTargets属性を以下のように変更。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false)]  
public class OrderAttribute : Attribute {  
    public int Value { get; set; }  
}  

これをPersonクラスで以下のように使います。

public class Person : IEnumerable<KeyValuePair<string, object>> {  
    [Order(Value = 1)]  
    public int Age { get; set; }  
  
    [Order(Value = 0)]  
    public string Name { get; set; }  
  
    [Order(Value = 2)]  
    public string Address { get; set; }  
  
    //staticなフィールド  
    [Order(Value = 3)]  
    private static int eternalAge = 18;  
      
    //メソッド  
    [Order(Value = 4)]  
    public int GetActualAge() {  
        return Age + 2;  
    }  
  
    //Order属性なしなので列挙されない  
    public int SecretAge { get; set; }  
  
    MemberEnumeratorProvider enumeratorProvider;  
  
    //コンストラクタ  
    public Person() {  
        enumeratorProvider = new MemberEnumeratorProvider(this);  
  
        //検索条件は外から変更できる  
        enumeratorProvider.EnumeratorFlags = BindingFlags.Instance  
                                               | BindingFlags.Static  
                                               | BindingFlags.Public  
                                               | BindingFlags.NonPublic  
                                               | BindingFlags.GetField  
                                               | BindingFlags.GetProperty  
                                               | BindingFlags.InvokeMethod;  
    }  
  
    public IEnumerator<KeyValuePair<string, object>> GetEnumerator() {  
        return enumeratorProvider.GetEnumerator();  
    }  
  
    IEnumerator IEnumerable.GetEnumerator() {  
        return GetEnumerator();  
    }  
  
    public override string ToString() {  
        return enumeratorProvider.BuildToString();  
    }  
}  
  

おおっ、これはなかなか便利かも。
Enumeratorを持たせないまでも、デバッグ用のToString()を自動生成させる機能くらいは
汎用的にしておくと楽かもしれないっすね。