[[20220711173721]] 『配列に入れたオブジェクトをWithでまとめるとEmpt』(傘) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『配列に入れたオブジェクトをWithでまとめるとEmptyになる』(傘)

ユーザーフォーム上の複数のリストオブジェクトに同じ操作をするため
Array関数で配列にしたものをForNextでループするときに
配列にしたオブジェクトをWithでまとめようとすると次の行で配列内のリストオブジェクトがEmptyに変わってしまいます。
Withを使わずに全部書いてやると問題なく動作するんですが、これは何が悪いんでしょうか?
以下はその部分の抜粋です。

 Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D)
 Dim i as Long, j as Long
 Dim dic as Object
 Set dic = CreateObject("Scripting.Dictionary") 
 For i = 0 To Ubound(lstAry)
     With lstAry(i)
         For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える
             If .Selected(j) Then
                 dic(.List(j)) = i + 1
             End If
         Next j
     End With
 Next i

< 使用 Excel:Office365、使用 OS:Windows10 >


 >リストオブジェクト
 たぶんListBoxのことですよね?

 Dim v As Variant
 For Each v In lstAry
 なら、vは[Variant/Object/ListBox]でlstAryの各要素と同じ型を維持してる様です。
 (いずれにせよ[Variant/Object/ListBox]のまま使うのは気持ち悪いかもですね^^;)

 >何が悪いんでしょうか?
 悪いとかではないと思うんですが、
 強いて言うなら「これでもイケる」と勝手に解釈した事?

 For...Next ステートメントにせよ
 For Each...Next ステートメントにせよ
 ユーザーから見えない裏っ側でいろいろやってる部分がある筈ですし、
 それは、当てはめられたlstAry側も同じく裏っ側でいろいろある事でしょう。

 その裏っ側でざっくり言えば「要素がObjectだったらSetで返す」なんて気の利いた構文になってないんでしょうね。
 たぶんですけど...

(白茶) 2022/07/11(月) 19:01


何が悪いのか?は聞き方が変でした。
こんなことになるのは自分の書き方がどこかおかしいのか?ということを訊ねたいんです。
元々は配列をリストボックスのコントロール名だけの
 Dim lstAry: lstAry = Array("A", "B", "C", "D")
 For i = 0 To Ubound(lstAry)
     With Me.Controls("lst_" & lstAry(i)) ・・・

という書き方をしてたんですが、これならコントロール自体を配列にしても同じだなと気づいて
最初のような書き方にしたらEmptyになるようになってしまったんです。
今までオブジェクトを配列にする書き方をしたことがなく今回が初めてだったので
自分が知らないだけで、オブジェクト変数への代入はSetが必要のようなルールがあるんだったら教えてほしいです。

 For i = 0 To Ubound(lstAry)
     For j = 0 To lstAry(i).ListCount - 1 
         If lstAry(i).Selected(j) Then
             dic(lstAry(i).List(j)) = i + 1
         End If
     Next j
 Next i

なら問題ないのに

 For i = 0 To Ubound(lstAry)
     With lstAry(i)
         For j = 0 To .ListCount - 1 
             If .Selected(j) Then
                 dic(.List(j)) = i + 1
             End If
         Next j
     End With
 Next i

とWithを使ったとたんおかしくなるのがよくわからなくて…
(傘) 2022/07/11(月) 20:03


 わー、マジだった...orz

     With lstAry(i)
         For j = 0 To lstAry(i).ListCount - 1 'この行に移動したとき【には既に】配列の中身が消えてますね
                     '~~~~~~~~~あえて
             If lstAry(i).Selected(j) Then
               '~~~~~~~~~あえて
             End If
         Next j
     End With

 インテリセンスを犠牲にする様なWithの書き方ってあんまやった事ないから分かんないすけど
 Withと[Variant/Object/ListBox]の組み合わせで発生するのかな...?

 どのみち私個人の見解としては、さして変わらないです。
 他の方の書き込みでも待ってみましょう。

(白茶) 2022/07/11(月) 20:43


 ちょっと実験してみました・・

 まず、一旦
  Set ListDummy=lstAry(i)
 の形にしてから、

   With ListDummy
 でやってみた。
 ・・旨く行った。

 その後、オリジナルのコードに戻して、再実行したら問題が発生しなくなっちゃった。

 この現象が、誰でも再現するなら、
 MicroSoftの人じゃないと説明出来ないのでは?

(半平太) 2022/07/11(月) 20:56


VBではコントロール配列が使えますが、
VBAではコントロール配列は使えなかったのでは?
どうしてもということならクラスを使うとできるようです。
"VBA コントロール配列"などと検索してみてはいかがですか?

(γ) 2022/07/11(月) 21:15


 私も実験してみました。
私の場合、最初から

 With lstAry(i)

 でなんのエラーもなく実行できました。
Array関数を要素をVariant型として格納するので、何として解釈するかがその時によって変わる可能性があるのかも。
型が未確定になるVariant型はなるべく使いたくないです。

 コントロールを配列のように扱いたい場合は、自分なら、Collection を使いますね。

 Public Sub Test1()
    Dim LstCol As New Collection
    LstCol.Add Me.lst_A
    LstCol.Add Me.lst_B
    LstCol.Add Me.lst_C

    Dim dic As Object
    Set dic = CreateObject("Scripting.Dictionary")

    Dim Lst As MSForms.ListBox, j As Long
    For Each Lst In LstCol
        With Lst
            For j = 0 To .ListCount - 1
                If .Selected(j) Then
                    dic(.List(j)) = dic.Count + 1
                End If
            Next j
        End With
    Next
 End Sub

(hatena) 2022/07/11(月) 22:15


 そういうコードは自分では書かないので、ほうっなるほどって言う感じなんですが、
 理由はMSの中の人に聞かないとわかりませんが、大丈夫なコードはいろいろ出来るような気が。

 hatenaさんの指摘のとり、Variatnではなくて、型が決まってないとだめな感じはしますね

 以下3パターンはどれもOKでした

    Private Sub CommandButton1_Click()  ' ちゃんと明示的にSet する

      Dim lstAry(0 To 2) As MSForms.ListBox
      Set lstAry(0) = Me.ListBox1
      Set lstAry(1) = Me.ListBox2
      Set lstAry(2) = Me.ListBox3

      Dim i As Long, j As Long
      Dim dic As Object
      Set dic = CreateObject("Scripting.Dictionary")
      For i = 0 To UBound(lstAry)
         With lstAry(i)
             For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える
                 If .Selected(j) Then
                     dic(.List(j)) = i + 1
                 End If
             Next j
         End With
     Next

    End Sub

    Private Sub CommandButton2_Click() ' 名前で指定する
      Dim lb As MSForms.ListBox
      Dim dic As Object
      Set dic = CreateObject("Scripting.Dictionary")
      For Each l In Array(Me.ListBox1.Name, Me.ListBox2.Name, Me.ListBox3.Name)
        With Me.Controls(l)
             For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える
                 If .Selected(j) Then
                     dic(.List(j)) = i + 1
                 End If
             Next j
        End With
      Next
    End Sub

    Private Sub CommandButton3_Click()  ' コレクションで hatenaさんと同じ
      Dim controlcollection As New collection
      Dim c As MSForms.ListBox
      Dim dic As Object
      Set dic = CreateObject("Scripting.Dictionary")
      controlcollection.Add Me.ListBox1
      controlcollection.Add Me.ListBox2
      controlcollection.Add Me.ListBox3
      For Each c In controlcollection
        With c
             For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える
                 If .Selected(j) Then
                     dic(.List(j)) = i + 1
                 End If
             Next j

        End With
      Next

    End Sub
(´・ω・`) 2022/07/11(月) 23:44

 ぅへぇ〜〜、気持ち悪ぃ〜よ〜  orz

    For i = 0 To UBound(lstAry)
        Debug.Print lstAry(i).Name     'イケる
        With lstAry(i)                 'ローカルウインドウによればココの通過でEmptyに変わる
            Debug.Print .Name          'でもイケる
            Debug.Print lstAry(i).Name '実行時エラー424
            Debug.Print .Name          'まだイケる (.Parent.Nameも取れるよ)
        End With
    Next

(白茶) 2022/07/12(火) 00:03


 確かに気持ち悪いですね。
 ただ、個人的には、どちらからといえば
 Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D)
 これができちゃうほうが気持ち悪いです
 ここでエラーになってほしいです

 Set ステートメント使ってないのに、配列要素にObject型が代入されるのはなぜ?
 lstAryの型が、Variant/Object/ListBox なのはなぜ? ListBoxのデフォルトプロパティではないの?
 と
 いろいろ不思議ではあります

 どうせ考えてもわからないので、(MSの中の人ではないので)
 どうやったら意図した動作をするのかを考える方が建設的では?
(´・ω・`) 2022/07/12(火) 01:09

皆さん検証ありがとうございます。
コントロールの配列というのはあまり一般的な使い方ではないんですね…。
なにかしっかりした理屈のある挙動ではないようだし、別にコントロール配列にこだわりたい理由も無いので
今回は当初のコントロール名の配列を使った書き方に戻すことにします。
Collectionオブジェクトの使い方も調べてみようと思います。
お付き合いありがとうごさいました。
(傘) 2022/07/12(火) 09:40

> コントロールの配列というのはあまり一般的な使い方ではないんですね…。

というよりArray関数でコントロールの配列を作るというのが、あまりいい方法ではないということです。Array関数で作成される配列の要素はVariant型になります。Variant型はなんでも代入できて便利なんですが、型が不確定なので想定外の動作になる可能性があるのでできれば使わない方がいいです。

(´・ω・`)さんの回答にもあるように型指定した配列に格納したり、Collectionに格納したりして、まとめて操作できるようにするということは一般的な使い方です。
(hatena) 2022/07/12(火) 10:59


 私に勘違いがありましたので、上のレスは無視願います。m(__)m

 今回の質疑の流れが私にはしっくりこないです。

 > >何が悪いんでしょうか?
 > 悪いとかではないと思うんですが、
 > 強いて言うなら「これでもイケる」と勝手に解釈した事?

 私もイケると思っちゃいそう。
 ダメになるのでは? と嗅覚を働かせるポイントが分からないです。

 > ぅへぇ〜〜、気持ち悪ぃ〜よ〜  orz
 との事ですが、よくよく考えると、With句の中は、With句を設定した時の参照で確定するので、
 その後は変数LstAryがNotingになろうとも、それには影響されないとの解釈でいいんじゃないでしょうか?

 > Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D)
 > これができちゃうほうが気持ち悪いです

 との事ですが、私は普通に For Each r in Array(Rgane("A1"),Range("Z100")) 等とするので、
 そんなに違和感が湧かないです。むしろ、これぞVariant型の真骨頂なんて思います。

 それにしても、これには困ったもんです。Withのヘルプに何かヒントでも書いておいてくれても良さそうな気がしますが。
         ↓
 >配列にしたオブジェクトをWithでまとめようとすると次の行で配列内のリストオブジェクトがEmptyに変わってしまいます。

(半平太) 2022/07/12(火) 22:16


 >ダメになるのでは? と嗅覚を働かせる
 いやいや^^;
 私もそこまで積極的に疑う場面ではないとは思ってますよ。
 (MSさんに言わせたら「勝手に解釈した」って事なんでない? って言いたかっただけ)

 >With句を設定した時の参照で確定するので
 ByVal的アレですよね。(←なんて語彙力... orz)

 With句の持つポインタはオブジェクトのアドレスを保持したまま、
 同時にlstAry(i)のポインタからオブジェクトのアドレスが失われ...って感じですか。
 ↑これがマジ意味不明ですけど。

 なんか、冒頭の事例に反する気がして「気持ち悪い」んですよね...
 Withの方がダメだったんじゃなかったっけか? って。
 やっぱForとの絡みもあるのかなぁ...

(白茶) 2022/07/13(水) 01:53


言い出しっぺなのでわからないなりにもう少し調べてみましたが
理解は深まったけど謎も深まった…みたいな話になってしまいました。
有識者からすると「当たり前でしょそんなの」的な内容も含むかもしれませんが
自分用の整理のためにも書いてみます。
 結論から書くと配列を格納する変数をlstAryではなくlstAry()と宣言することでEmptyになる現象は起きなくなりました。

 Sub test1()
     Dim ary(): ary = Array(Range("A1"), Range("A2"), Range("A3")) 'Emptyにならない
     'Dim ary: ary = Array(Range("A1"), Range("A2"), Range("A3"))  'Emptyになる
     'Dim ary(2)                                                   'Emptyにならない
     'Set ary(0) = Range("A1")
     'Set ary(1) = Range("A2")
     'Set ary(2) = Range("A3")
     Dim i as Long
     For i = 0 To 2
         With ary(i)
             .Value = 1 'Emptyになってもシート上に値は書き込まれる
         End With
     Next i
 End Sub

 以下のページを読んでいて偶然これに気づきました。
 【VBA】Split関数とArray関数の変数宣言時の括弧
 https://teratail.com/questions/337233

 今までも配列を格納する変数にVariant型を使うときに、括弧ありで宣言するのと無しで宣言する違いが正直よくわかっていなかったのですが
 OfficeVBAリファレンスのArray関数のページにこのようにあります。
 「配列として宣言されていない Variant には、配列を含めることができます。 〜 配列が含まれている Variant は
 要素の型が Variant の配列とは概念的に異なりますが、配列要素には同じ方法でアクセスされます。」 
 https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/array-function

 つまり「Variant型の配列=Variant型 "ではない"」ということです。
 そして偶然かはわかりませんがこれはWithステートメントにも関係してきます。

 Sub test2()
     Dim ary1
     Debug.Print TypeName(ary1) 'Empty ローカルウィンドウ上ではVariant/Empty
     With ary1
     End With
     Dim ary2()
     Debug.Print TypeName(ary2) 'Variant()
     With ary2 '★
     End With
 End Sub

 test2を実行すると★でコンパイルエラーになり
 「Withの対象は、ユーザー定義型、オブジェクト型、またはバリアント型のいずれかでなければなりません。」
 と表示され、TypeNameでも両者のデータ型は違う結果になります。
 つまりary2はVariant型ではないということです。
 普段配列というのは「●●型の配列」として呼ぶでしょうが、実際には「●●配列型」というのが正確なんじゃないでしょうか。
 上で載せたSplit関数とArray関数のページにある、
   Dim a
   a = Split("a-i-u-e-o", "-")
   Dim b
   b = Array("a", "i", "u", "e", "o")
 ではエラーにならずに
   Dim c()
   c() = Split("a-i-u-e-o", "-")
   Dim d() As String
   d() = Array("a", "i", "u", "e", "o")
 だとエラーになるというのは
 Split関数が「String型の配列」ではなく「String配列型」を返し
 同じくArray関数は「Variant型の配列」ではなく「Variant配列型」を返すために
 aとbでは「Variant型の変数に●●配列型のデータを代入している」状態なのでエラーにならず
 cとdは変数と返り値のデータ型が違うためエラーになる、ということなんだと思います。
 なので
 > Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D)
 > これができちゃうほうが気持ち悪いです
 というのは
 > Set ステートメント使ってないのに、配列要素にObject型が代入される
 というより、Array関数が「引数を要素に格納したVariant配列型のデータを返している」から
 こういう書き方ができちゃうんでしょう。

 そしてWithステートメントで配列の中がEmptyになる理由ですが
 Variant型変数にRangeオブジェクトをSet無しで代入しようとしたときの挙動と似ていると思います。

 Sub test3()
     Dim var
     'Set var = Range("A1") 'SetするとRangeオブジェクトが代入される
     var = Range("A1")      '★
 End Sub

 ご存じのとおり★の行では変数varにはRangeオブジェクトは代入されません。
 では何が代入されるかというと、「その時A1に書き込まれているデータ」が代入されるようです。
 A1が1なら1が、A1が"a"なら"a"が代入され、そして空欄なら「Emptyが代入」されます。
 この「Variant変数にSet無しで空欄のRangeオブジェクトを代入しようとするとEmptyが代入される」という挙動は
 「Variant型の変数に代入したオブジェクト配列型の要素がEmptyになる」という現象と
 上で述べた「Withの対象はオブジェクト型またはバリアント型である必要がある」という制約が関係している気がします。

 ここからは全くの推測ですが、Withステートメントの処理というのは対象になるオブジェクトを
 内部的に何かの変数に保持させることでその記述を省略できるようにしていて
 どんなオブジェクトを保持するかわからないためVariant/Object型のような変数が使われていて
 基本的にそのオブジェクトの下位のプロパティを操作するためにその変数はByRefの状態で
 そこに配列型を代入したVariant型の変数を入れてやろうとすると、"ガワ"がVariant型なので
 データ型の扱いが中身のオブジェクトとしてではなく空欄の"何か"("何処か"のほうが正しいか?)が代入されて
 それがByRefで元データ側に反映されて…ということが起こっているんじゃないでしょうか。
 そして"要素の型がVariantの配列 と 配列が含まれているVariant は概念的に異なる"そうなので
 Withの対象にする変数をVariant配列型にしてやるとそういうことが起きなくなる、と。

 リストボックスのEmptyも何か空欄のままになっているプロパティの値とかを引っ張ってきてるのかもしれません。
 まあ、それならそれでtest1のEmptyになってもシート上に値は書き込まれる話とか
 白茶さんが書いてくれたWithの参照ではエラーにならないのに元オブジェクトの参照ではエラーになる話はよく分かりませんが…
 (ByRefで元オブジェクトにはEmptyが反映されるけどWithで保持している物は最初に参照された状態を保つとか?)

長々と書きまくってしまいましたが、だいたいこんなような話なんじゃないかと。
自分にはあまりにも難しい話だったので無茶苦茶な内容になっているかもしれませんが
Emptyになる件の回避策も含め、いい勉強になったと思います。
(傘) 2022/07/13(水) 20:25


 何かしっくりこないですねぇ・・

 > 「Withの対象は、ユーザー定義型、オブジェクト型、
 > またはバリアント型のいずれかでなければなりません。」

 それは、ドット演算子が使える型かどうか、と言うことなんじゃないですか?

 バリアント型が含まれているのは、ドット演算子が使えるかどうかは、
 コンパイル時点ではまだ不明なのでエラーにはしないでくれているに過ぎない。

 >     Dim ary2()
 >     Debug.Print TypeName(ary2) 'Variant()
 >     With ary2 '★
 >     End With

 つまり、上のary2は配列なので、ドット演算子が使えないのが形式的に明らかなので、
 コンパイル時点でエラーとなる。

(半平太) 2022/07/14(木) 15:12


 うーむ、必ずしもそうとも言えないなぁ。

 これだと、実行時でもWithまでは通りますねぇ・・不思議
   ↓
    Dim ary1
    '     Debug.Print TypeName(ary1) 'Empty ローカルウィンドウ上ではVariant/Empty
   ary1 = Array(Range("a1"), 2, 3)
    Debug.Print IsArray(ary1)
    With ary1

    End With

(半平太) 2022/07/14(木) 15:30


 今回、問題になっているところの「With」に指定するのは配列の要素の方なので、
 それに限定したテストコードを書いてみましたが、
 Variant型が何かの形で影響を及ぼしているようには見えません。

 途中経過に差異が見当たらないです。(ForNextも関係なく現象が起きます)

 Sub test()
     Dim ary1, ary2(), o1, o2

     Range("A1").Value = "TEST"

     ary1 = Array(Range("A1"), 2, 3)
     ary2() = Array(Range("A1"), 2, 3)

     Debug.Print TypeName(ary1(0))   'Range
     Debug.Print TypeName(ary2(0))   'Range

     Set o1 = ary1(0) 'Variant/Object/Range
     Set o2 = ary2(0) 'Variant/Object/Range

     With ary1(0)
         Debug.Print .Value 'TEST (ary1(0)はEmptyになる)
     End With

     With ary2(0)
         Debug.Print .Value 'TEST (ary2(0)はEmptyにならない)
     End With
 End Sub

(半平太) 2022/07/14(木) 17:18


 あれぃ?
 ひょっとして冒頭のコード、

 >    With lstAry(i)
 >        For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える
 >            If .Selected(j) Then

 別に↑ここで「エラーで止まった」とは傘さん仰ってないのか。
 単に配列の中身が消える現象を確認しただけに過ぎない。

 であれば、私の
 >> 冒頭の事例に反する気がして「気持ち悪い」
 は無かった事になりますね。 

 何か勝手に勘違いしてたな...
 さすがにそんな一貫性のない現象起こらないか。
 (結局は↓この現象で一貫してるのね。我ながら気付くの遅せwww)

    Sub test()
        Dim v, o
        v = Array(UserForm1.ListBox1)
        Set o = v(0)
        PrintPtr v(0)
        PrintPtr o
        With v(0)
            Debug.Print .Name
        End With
        PrintPtr v(0)
        PrintPtr o
    End Sub
    Private Sub PrintPtr(a)
        Dim p As LongPtr
        If IsObject(a) Then p = ObjPtr(a)
        Debug.Print VarType(a), VarPtr(a), p
    End Sub

 ↓実行結果
  8             200621032     199583144 
  8             1570100       199583144 
 ListBox1
  0             200621032     0 
  8             1570100       199583144 

 これはまた失礼しました...^^;

(白茶) 2022/07/15(金) 19:59


コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.