none
Type クラスを利用した動的キャストについて RRS feed

  • 質問

  • Type type = typeof(DataClass).GetProperty(property.Name).PropertyType;

    でプロパティの型情報を取得することができるのですが,
    この情報を使ってキャストする方法を探しています.

    ------
    やりたいことは以下の内容です.

    例えば次のような DataClass クラスの各プロパティの平均値を算出し,
    同じクラスのインスタンス averageData に結果を代入することを考えています.

    public class DataClass
    {
        int column1;
        double column2;
    };

    やりたいことをそのままコードにすると
    次のようになります.

    private List<DataClass> dataList;
    private DataClass averageData;
    private void Calculation()
    {
        int count = dataList.Count;
        if (count != 0)
        {
            double sum1 = 0.0;
            double sum2 = 0.0;
           for (int i = 0; i < count; i++)
            {
                sum1 += dataList[i].column1;
                sum2 += dataList[i].column2;
            }
            averageData.column1 = (int)(sum1 / count);
            averageData.column2 = sum2 / count;
        }
    }

    ところが,実際には DataClass のプロパティは多数あり,
    また,追加/変更の可能性があるため,
    コードに直接プロパティ名を書きたくありません.
    そこでプロパティ名を動的に取得して同様の処理をおこなうことを考え,
    次のようなコードを書いてみました.

    private void Calculation()
    {
        int count = dataList.Count;
        if (count == 0)
            return;

        PropertyInfo[] properties = typeof(DataClass).GetProperties();
        foreach (MemberInfo property in properties)
        {
            double sum = 0.0;
            for (int i = 0; i < count; i++)
            {
                var value = dataList[i].GetType().GetProperty(property.Name).GetValue(dataList[i], null);
                sum += value;
            }
            var average = sum / count;
            averageData.GetType().GetProperty(property.Name).SetValue(averageData, average, null);
        }
    }

    ところが,GetValue() の戻り値は object のため,

    sum += value;

    をすることができません.
    かといって,

    sum += (double)value;

    とすると,実行時に
    "指定されたキャストは有効ではありません。"
    という例外が発生してしまいます.

    Type type = typeof(DataClass).GetProperty(property.Name).PropertyType;

    でプロパティの型情報を取得することができるのですが,
    この情報を使ってキャストする方法がさっぱりわかりません.

    この疑問について皆さんのご意見をお願い致します.




    • 編集済み Yujiro15 2014年2月10日 0:24
    2014年2月9日 23:59

回答

  • ちょっと意味合いが違うけれど、今回の要件ならConvertクラスのメソッドたちで足りるのかな?

    http://msdn.microsoft.com/ja-jp/library/system.convert(v=vs.110).aspx

    • 回答としてマーク Yujiro15 2014年2月10日 1:28
    2014年2月10日 0:36
  • 実際の型がdoubleでないobjectをdoubleにはキャスト(unboxing)できません。それでエラーが起こっているのでしょう。

    質問者さんにお願いですが、使用している.NET Frameworkのバージョン及びC#言語のバージョンを明示してください。

    C# 4.0及び.NET 4以降であればdynamicが使えます。dynamicは実行時に実際のデータ型に合わせてコンパイルがなされるため必要に応じてint→double変換が行われます。ただし、現状では計算結果がdouble型で得られるため、averageDataへ代入することができません。

        private void Calculation() {
            int count = dataList.Count;
            if (count == 0)
                return;
    
            foreach (var property in typeof(DataClass).GetProperties()) {
                double sum = 0.0;
                foreach (var data in dataList)
                    sum += (dynamic)property.GetValue(data);
                property.SetValue(averageData, Convert.ChangeType(sum / count, property.PropertyType));
            }
        }
    

    計算結果を適切な型に戻すためにはかるあさんの提示されているようにConvertが必要になります。

    ここで気になるのはデータ型が何であれdouble精度で平均値を計算するのか?ということです。longやdecimalの場合、doubleに変換することで精度が落ち適切な平均が得られません。逆に項目数が多い場合、intをintのまま集計すると桁あふれし得るため、どう平均を出すのかは悩ましいところではありますが。

    • 回答としてマーク Yujiro15 2014年2月10日 1:46
    2014年2月10日 1:10

すべての返信

  • ちょっと意味合いが違うけれど、今回の要件ならConvertクラスのメソッドたちで足りるのかな?

    http://msdn.microsoft.com/ja-jp/library/system.convert(v=vs.110).aspx

    • 回答としてマーク Yujiro15 2014年2月10日 1:28
    2014年2月10日 0:36
  • valueって必ずdouble型じゃないんですか?恐らく、何か不適切なプロパティが混ざってそうな気がするので、そいつらを事前に排除してから総和をとってみるとかどうでしょう?それならdoubleにキャストしても平気そうなきがします。

    かずき Blog:http://d.hatena.ne.jp/okazuki/

    2014年2月10日 0:42
  • 実際の型がdoubleでないobjectをdoubleにはキャスト(unboxing)できません。それでエラーが起こっているのでしょう。

    質問者さんにお願いですが、使用している.NET Frameworkのバージョン及びC#言語のバージョンを明示してください。

    C# 4.0及び.NET 4以降であればdynamicが使えます。dynamicは実行時に実際のデータ型に合わせてコンパイルがなされるため必要に応じてint→double変換が行われます。ただし、現状では計算結果がdouble型で得られるため、averageDataへ代入することができません。

        private void Calculation() {
            int count = dataList.Count;
            if (count == 0)
                return;
    
            foreach (var property in typeof(DataClass).GetProperties()) {
                double sum = 0.0;
                foreach (var data in dataList)
                    sum += (dynamic)property.GetValue(data);
                property.SetValue(averageData, Convert.ChangeType(sum / count, property.PropertyType));
            }
        }
    

    計算結果を適切な型に戻すためにはかるあさんの提示されているようにConvertが必要になります。

    ここで気になるのはデータ型が何であれdouble精度で平均値を計算するのか?ということです。longやdecimalの場合、doubleに変換することで精度が落ち適切な平均が得られません。逆に項目数が多い場合、intをintのまま集計すると桁あふれし得るため、どう平均を出すのかは悩ましいところではありますが。

    • 回答としてマーク Yujiro15 2014年2月10日 1:46
    2014年2月10日 1:10
  • おお!

    sum += value;



    sum += Convert.ToDouble(value);

    にしたらできました!
    Convert クラスは見えていたのに,
    引数で型指定をするものばかりを探していて
    ToDouble() が見えていませんでした.
    さらに

    averageData.GetType().GetProperty(property.Name).SetValue(averageData, average, null);



    averageData.GetType().GetProperty(property.Name).SetValue(averageData, Convert.ChangeType(average, type), null);

    にすることで一応正常に動作するようになりました.
    ありがとうございます.
    2014年2月10日 1:28
  • 上の例でもそうなっているように,value は必ずしも double 型ではなく,
    int 型もあったり,実際にはまったく別の型である場合もあります.
    要は数値型のプロパティすべてについて処理したいので,
    そこは if( type == typeof(int)) などの if 文で分岐しています.
    2014年2月10日 1:32
  • かずきさんが書かれているように、計算対象以外のプロパティがある可能性が高いと思います。
    何らかの条件で計算対象のプロパティを絞り込む必要があると思います。

    計算対象がdouble以外もある場合はそれはそれで変換その他いろいろ考えなきゃなりませんが、おそらくすべてdoubleで、あくまで余計なプロパティが引っかかって邪魔してるんじゃないか、というのが率直な感想です。

    ※これはもちろん、質問者さんに明確にしてもらわないと確証はありませんが。

    で、余計なプロパティを外す方法ですが、いろいろ方法は考えられますが、属性を適用するのが一番おすすめではあります。

    ※標準で使えそうな属性を使うか、自分で定義するかはまた検討必要

    他の方法になると、名前付けルールを決めるとか、とにかく何らかの条件で判別できるようにする必要があります。

    ※いっそDataTableにしたらとか、ディクショナリにしたらとか思ってしまわないこともない。

    ----追記

    と思ったら解決してた…変換の問題だったということでよかったということですかね。

    • 編集済み なちゃ 2014年2月10日 1:38
    2014年2月10日 1:35
  • unboxing...?
    勉強してきます.

    > 質問者さんにお願いですが、使用している.NET Frameworkのバージョン及びC#言語のバージョンを明示してください。

    確かに抜けていました.失礼しました.
    使用しているのは .NET Framework 4.0 および C#4.0 です.

    dynamic 型なんてものがあったんですね!すごいなぁ.
    いただいたコードでばっちり動作しました.
    ありがとうございます.

    > ここで気になるのはデータ型が何であれdouble精度で平均値を計算するのか?ということです。

    そうですね.本気で精度を考えるとかなり気になるところです.
    現在製作中のものは簡素なソフトであまり精度を必要としていません.
    また,入力される数値もかなり限定されているため
    今回はこれで良しとしています.

    お気遣いありがとうございました.
    2014年2月10日 1:46
  • > で、余計なプロパティを外す方法ですが、いろいろ方法は考えられますが、属性を適用するのが一番おすすめではあります。

    属性を使ってそういうことができるんですか.
    考えもしない方法でした.
    少し勉強したいと思います.

    ありがとうございました.
    2014年2月10日 1:51
  • Type.GetTypeCode(type)とすることでTypeCodeが得られます。enum値なのでswitch文で簡単に型を分類できます。

    unboxingについては主にかずきさん向けの説明でした。具体的にはボックス化とボックス化解除で説明されています。そこでの例ですとint型のデータをobject型変数に代入した後、short型にキャスト(ボックス化解除;unboxing)しようとするとエラーになります。
    今回、これが発生していました。構文的にはキャストですが、キャストには複数の機能が含まれています。余談ですがC++言語ではキャストをそれぞれ別の構文に分け混乱を避けるよう目指しています。

    2014年2月10日 2:29