none
構造体のプロパティの一括操作 RRS feed

  • 質問

  • お世話になります。

    今日は「構造体」の”一括”値編集について質問します。

    なお、以前私がしております別の質問について未解決の状態で質問することをお詫び申し上げます。

    質問内容は、「構造体のプロパティを、まとめて初期化(指定の値)にする方法」です。

    たとえば以下の構造体が定義されているとします。なお、この例では2つしかプロパティがないですが、実際はたくさんあります。

    '-------------------------------------------

     Public Structure STRUCデータ項目

        Private _コード As String
        Private _名前 As String

        Property p_コード As String
          Get
            Return _コード
          End Get
          Set(ByVal value As String)
            _コード= value
          End Set
        End Property

        Property p_名前 As String
          Get
            Return _名前
          End Get
          Set(ByVal value As String)
            _名前= value
          End Set
        End Property

    End Structure

    '--------------------------------------------

    この構造体を

    dim データ項目 As STRUCデータ項目

    として宣言し、利用します。

    このとき、各プロパティを明示的にある値に、共通の値で初期化したい場合(たとえばStringであれば"a"とか)、単純には

    データ項目.コード="a"

    データ項目.名前="a"

    とすればいいわけですが、プロパティがたくさんあった場合、いちいち各プロパティについて書かなければならず、面倒なのはいいですがぬけがあったりするとまずいことになります。

    そこで、各プロパティを列挙してループか何かで一括で値を格納できればいいかなと思いました。

    ポイントは

    ①格納する値は任意

    ②型に対応(Stringのときは"a",Longのときは0など)

    です。

     

    調べた結果、Reflectionが関係してくることはわかったのですが、メンバを文字列で列挙しているものが多く、

    それをどう利用したらいいのかわからず、質問させていただきました。

    どうぞよろしくお願いします。

    (追記 いつもありがとうございます。)

     

     

     

    2010年10月13日 6:01

回答

  • p.SetValue(objForm.STRUCデータ項目, "a", Nothing)

    になります。

    がしかし、プロパティが多数あるのは設計がおかしく、配列など利用して構造そのものを見直すべきかと思います。

    2010年10月13日 7:41
  • それがその型(Structure)の1つの機能なのでしたら、
    リフレクションを使用せずにメソッドにすることが素直かなと思いました。
    たとえば以下のようなものです。
    (たとえプロパティがとっても多くても、です。)

    Public Sub SetCodeAndNameValue(ByVal value As String)
        p_コード = value
        p_名前 = value
    End Sub

    リフレクションを使用する場合、本当は以下のコードでできそうなものですが、実際にはできませんでした。

    Dim item As New STRUCデータ項目()

    Dim itemType = item.GetType()
    Dim properties = itemType.GetProperties( _
        Reflection.BindingFlags.Instance Or _
        Reflection.BindingFlags.Public)

    For Each itemProperty In properties
        If Not itemProperty.CanWrite Then Continue For

        If itemProperty.PropertyType Is GetType(String) Then
            itemProperty.SetValue(item, "a", Nothing)
        End If
    Next

    Web を検索すると、以下のことがわかりました。
    ・struct ではボクシングが働くためにそのままでは機能しない。
    ・C# では明示的にボクシング・アンボクシングすれば可能。
    ・VB では RuntimeHelpers.GetObjectValue が自動的に行われてしまうためダメ。

    VB での対策としては以下のことが書かれていました。
    ・Structure ではなく Class に変更してしまう。
    ・C# でライブラリを作成して VB から呼び出す。

    やはり、各プロパティ値をセットするメソッドを実装した方がよいと思いました。

    2010年10月13日 7:46

すべての返信

  • http://dobon.net/vb/dotnet/programing/typegetmembers.html

    上記 URL で紹介してある GetProperties メソッドと、

    http://msdn.microsoft.com/ja-jp/library/system.reflection.propertyinfo_members(v=VS.80).aspx

    上記 URL の PropertyInfo クラスのメンバ、具体的には SetValue メソッドと PropertyType プロパティあたりを使えば、実現できると思います。


    なかむら(http://d.hatena.ne.jp/griefworker)
    2010年10月13日 6:26
  • なかむら様

    ご教示ありがとうございます。いただいた情報をもとに、なんとかプロパティ名の取得までこぎつけました。

    ところが、値の格納のところ(つまりSetValue)でつまづいています。

    setValueの第一引数のは「プロパティ値が設定されるオブジェクト」とあるのですが、意味がわからず、以下pをいれても「オブジェクトがターゲットの型と一致しません」とでてしまいます。

    この部分には何をいれるのが適切なのでしょうか?

    ---

    Dim t As Type = GetType(objForm.STRUCデータ項目)

        Dim props As PropertyInfo() = t.GetProperties()

        Dim p As PropertyInfo
        For Each p In props

          MessageBox.Show(p.Name & p.PropertyType.ToString)

          If p.PropertyType.ToString = "System.String" Then
            p.SetValue(??, "a", Nothing)  '←問題の個所
          End If

        Next

    2010年10月13日 7:02
  • p.SetValue(objForm.STRUCデータ項目, "a", Nothing)

    になります。

    がしかし、プロパティが多数あるのは設計がおかしく、配列など利用して構造そのものを見直すべきかと思います。

    2010年10月13日 7:41
  • それがその型(Structure)の1つの機能なのでしたら、
    リフレクションを使用せずにメソッドにすることが素直かなと思いました。
    たとえば以下のようなものです。
    (たとえプロパティがとっても多くても、です。)

    Public Sub SetCodeAndNameValue(ByVal value As String)
        p_コード = value
        p_名前 = value
    End Sub

    リフレクションを使用する場合、本当は以下のコードでできそうなものですが、実際にはできませんでした。

    Dim item As New STRUCデータ項目()

    Dim itemType = item.GetType()
    Dim properties = itemType.GetProperties( _
        Reflection.BindingFlags.Instance Or _
        Reflection.BindingFlags.Public)

    For Each itemProperty In properties
        If Not itemProperty.CanWrite Then Continue For

        If itemProperty.PropertyType Is GetType(String) Then
            itemProperty.SetValue(item, "a", Nothing)
        End If
    Next

    Web を検索すると、以下のことがわかりました。
    ・struct ではボクシングが働くためにそのままでは機能しない。
    ・C# では明示的にボクシング・アンボクシングすれば可能。
    ・VB では RuntimeHelpers.GetObjectValue が自動的に行われてしまうためダメ。

    VB での対策としては以下のことが書かれていました。
    ・Structure ではなく Class に変更してしまう。
    ・C# でライブラリを作成して VB から呼び出す。

    やはり、各プロパティ値をセットするメソッドを実装した方がよいと思いました。

    2010年10月13日 7:46
  • 佐祐理様

    ありがとうございます。

    おっしゃるとおりやってみましたらエラーがでなくなりました。

    ただし、setValueの個所を通過しても、"a"が格納されませんでした。

    よくわかっていませんが、以下TH01さんのおっしゃっているように、「構造体」であることが原因かも、と思っています。

    (構造体でなく、Classのプロパティで試したらできました)

     

    設計の件につきましてもご助言ありがとうございます。

    検討いたします。

    ありがとうございます。

     

    2010年10月13日 8:19
  • TH01様

    いつもありがとうございます。

    おっしゃるとおり、構造体が原因でしょうか?、値が格納されませんでした。

    ご助言のとおり、メソッドを書いていくか、Classにするよう検討いたします。

    ありがとうございます。

    2010年10月13日 8:20
  • ただし、setValueの個所を通過しても、"a"が格納されませんでした。
    よくわかっていませんが、以下TH01さんのおっしゃっているように、「構造体」であることが原因かも、と思っています。
    (構造体でなく、Classのプロパティで試したらできました)

    objForm の STRUCデータ項目 は構造体を return しているのですよね?
    構造体は参照を返せませんので、必ずコピーが帰ってきます。
    コピーに対して値を設定しても、objForm にある STRUCデータ項目 には反映されません。
    class に変えるか、値を書き換えた構造体を再度 objForm に設定し直すかが必要です。

    以下、イメージ。(コンパイルが通るかどうかは見てません。

    Dim temp As /* 構造体 じゃなくて クラス */ = objForm.STRUCデータ項目
    p.SetValue(temp, "a", Nothing)
    objForm.STRUCデータ項目 = temp

    追記:構造体じゃだめですね。orz


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年10月13日 14:07
    モデレータ
  • すみません、私のコメントは間違っています。皆さんが指摘されている通り、構造体は実体がその関数やクラスインスタンスの中にあり、PropertyInfo等に渡すことができません。classですと実体がヒープ上にあるため、PropertyInfo等に渡して値を操作することができます。
    2010年10月13日 14:49
  • ちょっと違う方向性の話になりますが、

    struct A
    {
      // コンストラクタ
      public A()
      {
       member1 = "a";
       member2 = 0;
        :
        :
       this.TestInit();
      }
      // ダミーのテストメソッド(何もしない)
      [Conditional("DEBUG")]
      private void TestInit() { }
    


    と、コンストラクタの末尾に this 参照を配置しておくと、C# コンパイラは this の参照時に「すべてのメンバーが初期化済みであること」を確認します。
    このため、

    > ぬけがあったりするとまずいことになります

    を、C# コンパイラに確認させることができますので、安心できるかもしれません。だめですかね。

    2010年10月13日 23:56
  • > 構造体が原因でしょうか?

    そうです。
    それと VB のコンパイラの仕様?が影響してます。

    構造体は値型なので、SetValue の object 型の引数に渡す際に「ボクシング」が必要になります。
    この場合は SetValue の対象はボクシング結果の別インスタンス(中身の値はコピー値)になってしまい、アンボックス化による値の戻しが行われません(渡すだけなので)。

    これへの対処として、引数に渡す前に object 型変数に格納することによってボクシングを済ませておき、それを SetValue に渡すことで、ボクシング結果の中身の値が変更されるようにします。
    その後のアンボックス化(元の構造体へのキャスト)によって変更値がコピーし戻されますので、変更結果を参照することができるようになります。
    C# ではこれで解決します。

    しかし VB では、SetValue に渡す際に自動的に RuntimeHelpers.GetObjectValue が実行されるようで、その結果が SetValue の引数に渡されます。
    ボクシング済みのオブジェクトであっても、GetObjectValue が返す値は元のインスタンスとは異なりますので、SetValue はテンポラリ的な別のインスタンスに対して行われてしまい、その結果が元のボクシング済みのインスタンスに反映されることはありません。
    ということで VB の場合には、構造体に対しての SetValue は機能しないことになります。

    別の見方をすると、RuntimeHelpers.GetObjectValue に渡す値がボクシング済みのインスタンスであれば参照型と同様にそれをそのまま返す仕様だと問題はなかったはずなので、RuntimeHelpers.GetObjectValue の仕様がよくないような気もします。

    2010年10月14日 1:49
  • K.Takaoka さんの方法を試してみたところ、これは C# に限定される話になりますね。
    (構造体のコンストラクタはパラメータが必要なので、ダミーの引数も必要ですね)
    VB では自動的にパラメータの無いコンストラクタが呼び出されますので、常にすべて初期化済みになります。
    2010年10月14日 3:01
  • > 別の見方をすると、

    GetObjectValue は VB の値渡しと参照渡しの関連とか複合型関連で必要なんですかね?

    値型以外に、object 型の実体が値型であるかどうかを判定して複製してくれるみたいですね。対処としては、参照型であることを保証できる型を与えるとよいみたいです。(名前だけのインターフェースを使うなど)

    > それをそのまま返す仕様だと問題はなかったはずなので

    それだけじゃないみたいですけどね。以下のようなコードで a as Test1 : a = Init() としても、初期値がはいりません。C# ならこのコードで入ります。

      Public Shared ReadOnly Init As Func(Of Test1) = _
          Expression.Lambda(Of Func(Of Test1))( _
              Expression.MemberInit(Expression.[New](GetType(Test1)), _
            GetType(Test1).GetProperties(BindingFlags.Instance Or BindingFlags.Public).Select( _
              Function(pi) Expression.Bind(pi, Expression.Constant( _
                             GetInitValue(pi.PropertyType)))).ToArray())).Compile()
    
      Private Shared Function GetInitValue(ByVal t As Type)
        Select Case t.FullName
          Case GetType(Integer).FullName
            Return 1
          Case GetType(String).FullName
            Return "a"
          Case GetType(Long).FullName
            Return 2
          Case Else
            Return Nothing
        End Select
      End Function
    
    
    2010年10月19日 3:32
  • あの後もう少し調べていて、GetObjectValue の仕様ではどうしようもないのかなあ、と思ったことがあり、訂正しようか迷ってました。
    ツッコミしてくださって良かったです。

    object value = 1;
    MessageBox.Show(value.GetType().IsValueType.ToString());

    が true を返すので、GetObjectValue ではどうしようもなさそうに思いました。
    ボックス化って特別なんだなあ、って改めて思ってました。

    > 対処としては、参照型であることを保証できる型を与えるとよいみたいです。(名前だけのインターフェースを使うなど)

    一生懸命検索して調べてた時、インターフェイスを使った話も少し出てたように思います。
    理解できなかったので読み飛ばしていたんですが、そんなことってできます?
    ちなみに、書かれたコードは全然まだ理解できてません。。

    2010年10月19日 3:55
  • > 以下のようなコードで a as Test1 : a = Init() としても、初期値がはいりません。C# ならこのコードで入ります。

    試してみたところ、VB でも入りましたよ!
    K.Takaoka さんはもしかして構造体で Property を定義されていないとかじゃないですか?

    でも、これが何を意味するのかと、質問者さんのためになるのかなど、全然わかってません。(^^;
    理解できそうな気もしません。。

    2010年10月19日 4:35
  • > true を返すので、GetObjectValue ではどうしようもなさそうに思いました

    ちょっと違うと思います。

    GetObjectValue の呼び出しを追加するのは VB のコンパイラです。よって、実行時の型ではなくてコンパイル時の型情報を基準に判定しているはずです。つまり、コンパイラは文脈から「値型」を想定できる場合にのみ、GetObjectValue の呼び出しを挿入していると思います。

    VB の言語仕様に詳しくないのできちんと列挙はできませんが、値型と Object 型、Variant 型の場合に GetObjectValue の呼び出しが追加されるようです。Object 型が対象に含まれているのは、おそらく VB の Variant 型の変数が .NET の Object 型にコンパイルされるからでしょう。GetObjectValue や VB の複合型がどのようにデザインされているのかは詳しくありませんが、Variant として定義された Object 型変数に格納されるものが Variant のためのランタイムヘルパオブジェクトだったりする場合、適切な値を返すような機能が GetObjectValue なのではないかと想像します。

    上記の挙動/仕様から、「ボックス化したオブジェクト」を Variant 型または Object 型以外で保持することで、GetObjectValue による繰り返しの複製を避けることができるようになります。具体的にあげると、

    Public Interface IamStructureReference
    End Interface
    

    と、ボックス化したオブジェクトを保持するためだけのインターフェースを定義しておき、

    Public Structure Test1
      Implements IamStructureReference
      ' ... 任意の内容...
    End Structure
    

    と、ボックス化したい構造体に、このインターフェースを実装させます。後は、

    Dim test as Test1
    Dim obj as IamStructureReference
    
    ' 事前にボックス化する
    obj = test
    
    ' ボックス化した内容を更新する
    GetType(Test1).GetProperty("Property1").SetValue(obj, "new value")
    
    ' 変更結果をアンボックス化してコピー
    test = obj
    

    と書けるようになります。

    2010年10月19日 6:27
  • > ちょっと違うと思います。

    ですね。(^^;
    でも、何か共通するルールがありそうな気がしました。

    それと、構造体もインターフェイスを実装できたんですね。忘れてました。

    K.Takaoka さんが書かれた通り、VB でもできました!
    構造体の修正も必要になりますが、できないよりはいいですね。
    一応、全コードを下に示します(テストの為に、「コード」は Integer に変更してます)。

    Public Class Form1
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
            Dim item As New STRUCデータ項目()

            '事前にボックス化
            'コンパイラによって GetObjectValue が挿入されないように
            'インターフェイス型にする。
            Dim boxedItem As IamStructureReference = item

            Dim itemType = boxedItem.GetType()
            Dim properties = itemType.GetProperties( _
                Reflection.BindingFlags.Instance Or _
                Reflection.BindingFlags.Public)

            For Each itemProperty In properties
                If Not itemProperty.CanWrite Then Continue For

                '型ごとの既定値
                Select Case itemProperty.PropertyType.FullName
                    Case GetType(String).FullName
                        itemProperty.SetValue(boxedItem, "a", Nothing)
                    Case GetType(Integer).FullName
                        itemProperty.SetValue(boxedItem, 1, Nothing)
                    Case Else
                        itemProperty.SetValue(boxedItem, Nothing, Nothing)
                End Select
            Next

            'アンボックス
            item = DirectCast(boxedItem, STRUCデータ項目)

            '変更結果の確認
            MessageBox.Show(String.Format("{0}:{1}", item.p_コード, item.p_名前))
        End Sub
    End Class

    Public Interface IamStructureReference
        'なし
    End Interface

    Public Structure STRUCデータ項目
        Implements IamStructureReference

        Private _コード As Integer
        Private _名前 As String

        Property p_コード() As Integer
            Get
                Return _コード
            End Get
            Set(ByVal value As Integer)
                _コード = value
            End Set
        End Property

        Property p_名前() As String
            Get
                Return _名前
            End Get
            Set(ByVal value As String)
                _名前 = value
            End Set
        End Property

    End Structure

    K.Takaoka さんの2つの方法は、場合によっては有効そうですね。
    (本題の話は、リフレクションを使用しない方がいいと思っていることは変わりません。)

    そうそう、インターフェイス名、面白いですね。

    2010年10月19日 8:19