[[20200811090110]] 『「Dictionary」からの取出し』(T20) ページの最後に飛ぶ

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

 

『「Dictionary」からの取出し』(T20)

「Dictionary」から値をループで取出す時に、うっかり「dic.keys(i)」としてエラーになります。
「dic.keys()(i)」とするように本サイトで覚えたのですが、前に「()」を付す理由がよくわかっていません。
通常の配列では「()」の有無に関係ないようなんですが…(test_3, test_4)
両者の違いを説明願えないでしょうか?
確認遅くなりますが、よろしくお願いします。

 Sub test_1()
    Dim test As Object
    Set test = CreateObject("Scripting.Dictionary")
    test.Add 11, ""
    test.Add 12, ""
    test.Add 13, ""
    MsgBox test.keys(2)     'Error 451
 End Sub
 '--------------------------------------------------
 Sub test_2()
    Dim test As Object
    Set test = CreateObject("Scripting.Dictionary")
    test.Add 11, ""
    test.Add 12, ""
    test.Add 13, ""
    MsgBox test.keys()(2)   'OK
 End Sub
 '--------------------------------------------------
 Sub test_3()
    Dim test(3)
    test(0) = 11
    test(1) = 12
    test(2) = 13
    MsgBox test(2)  'OK
 End Sub
 '--------------------------------------------------
 Sub test_4()
    Dim test(3)
    test(0) = 11
    test(1) = 12
    test(2) = 13
    MsgBox test()(2)    'OK
 End Sub

< 使用 アプリ:、使用 OS:Windows10 >


 アーリーバインドににすれば、余分な( )要らなかったと
思いますです。参照方法にかかわらず、
わたしはよくFor Each で回すか、配列なので、約、3万件以下なら
トランスポーズ関数様のお力を借り、一括書き出しします。それ以上
なら、アレイリストか二次元配列に書き直して、一括書込みでしょうか。。。^^;
さらに詳しい事は、他の回答者様のアドバイスをお待ちくださいませ。。。m(_ _)m
(隠居じーさん) 2020/08/11(火) 09:29

 私もよくわかんなくなるので、疑問は当然だと思います!
 結論から申しますと、keysは配列を返すプロパティで、引数を受け取って値を返すプロパティではないからでしょうか。
 keys() とすることで、配列全部を呼び出して keys()(0) 0番目の配列を抜き出すという意味になると思います。
 (この辺は、クラスオブジェクト自分で作ると理解できると思います・・・)

 一般的には、Dictionaryは連想配列ですので、keyでitemを呼び出す使い方が一般的ですので、
 keyを添え字で呼び出すことは稀ではないですか?

    Sub test_5()
       Dim test(2) As Variant
       Dim k(2) As Variant
       k(0) = "a"
       k(1) = "b"
       test(0) = k
       MsgBox test(0)(1) ' b
       MsgBox test(0)(0) ' a
       MsgBox test(0)    'エラー
       MsgBox test(1)(0)    'エラー
    End Sub

    Sub test_6()
        Dim test As Object
        Dim v    As Variant
        Dim w    As Variant
        Set test = CreateObject("Scripting.Dictionary")
        test("0") = "a"
        test("1") = "b"
        v = test.keys
        w = test.items
        MsgBox v(0)
        MsgBox w(0)
        MsgBox test("0")
        MsgBox test(0)    '何も格納されてない
        Set test = Nothing
    End Sub

    Sub test_7()
        'たぶん、一般的な使い方
        Dim dic As Object
        Dim k As Variant
        Set dic = CreateObject("Scripting.Dictionary")
        dic("東京") = 1000
        dic("東京") = dic("東京") + 1000
        dic("千葉") = 500
        dic("神奈川") = 800
        For Each k In dic.keys
            MsgBox k & ":" & dic(k)
        Next k
        Set dic = Nothing
    End Sub
(稲葉) 2020/08/11(火) 09:47

(隠居じーさん)さん、(稲葉)さん
ありがとうございます。
参照方法の件は、本サイトでの別の質問者さんへの回答もあり理解できました。
(と、思います^ ^;)

 >keyを添え字で呼び出すことは稀…

他の方法を考える術がなく、単に重複を避けるためだけに「Dictionary」を使いました。(_ _)

 >keysは配列を返すプロパティで、引数を受け取って値を返すプロパティではない…

これがいわゆる通常の配列とは違う、という理解でいいのでしょうか?
プロパティやらメソッドなど基本的な理解ができておらず、的外れでしたらすみません…

提示されたコードを試行して、今までモヤっとしていたのが晴れたような気がします。
これからも、よろしくお願いします。

(T20) 2020/08/11(火) 22:50


 ちょっとしつこいですが、念のため。

 |  >keysは配列を返すプロパティで、引数を受け取って値を返すプロパティではない…
 | これがいわゆる通常の配列とは違う、という理解でいいのでしょうか?

 言葉の綾かもしれませんが、
 「dic.Keys() で返る配列は普通の配列です。キー全体を集めた配列です。」

 (従って、dic.Keys()に0から始まる整数インデクスを与えると、
   個々のキーが返りますが、普通、そういう使い方をするニーズはありません。
   つまり、dictionaryは余りそういう使い方はしないのです。)

 dic("東京")のように、文字列をキーにして、値を取得できるというのが
 本来の dictionaryの使い方、眼目です。(既に指摘がなされています。)

 (キー全体の配列という限定した話でなく)一般的な普通の配列というものは、
 整数値を与えて値を得ますね。そこがdictionaryとの大きな差異です。

(γ) 2020/08/11(火) 23:36


 蛇足の蛇足になりますが、
 >他の方法を考える術がなく、単に重複を避けるためだけに「Dictionary」を使いました。
 私もよく使いますが、すでにご指摘通り
 For Each k In dic.keys
 か
 array = Application.Transpose(dic.keys)
 をつかいます。

 隠居じーさんさんの懸念されている
 Transposeが効かない状況で考えた中で、
 For i =1 to n
     array(i,1) = dic.keys()(i)
 みたいな使い方ならありかもしれませんね。
 でも、先に想定されているなら、dictionaryに値を入れるループの中で
 exists使って配列作っちゃうかも、、、

 具体的例があれば、議論しやすいので、よろしければ件のコードご提示ください!
(稲葉) 2020/08/12(水) 07:14

ありがとうございます。
実際のコードの関係部分は以下となります。

 Sub サンプル()
 '(別の処理いろいろ)
 '対象データ(ary)から比較値(ut1, ut2)と同じでない重複しないデータを取得する
    Dim i As Long
    Dim ut1, ut2                    '比較値
    Dim dic As Object, val
    Dim ary()                       '対象データ
    Set dic = CreateObject("Scripting.Dictionary")
    ut1 = 1     '実際には特定セル値から取得
    ut2 = 2     '実際には特定セル値から取得
    ary = Array(3, 1, 1, 5, 1, 2, 3, 1, 6, 2, 2, 5)     '実際には特定セル値から取得
    For i = 0 To UBound(ary)
        If ary(i) <> ut1 And ary(i) <> ut2 Then val = ary(i)
        If val <> "" And Not dic.exists(val) Then dic.Add val, ""
    Next i
    For i = 0 To dic.Count - 1
        Cells(i + 2, "B").Value = dic.keys()(i)         '書出し
    Next i
 End Sub

もちろん“普通ではない”使い方をしている自覚はあります。_ _;
当初は関数か「ary」を使わずにセルの2回ループ回しを考えていたんですが、
別の処理との関係もありこっちのが簡単かな?、と。
書出しのところはご指摘の方法にできますが、「 exists使って配列作る」とは
どういうことでしょうか?

(T20) 2020/08/12(水) 17:48


 自分ならこうするってところたくさんありますが、一点だけ
 >If val <> "" And Not dic.exists(val) Then dic.Add val, ""
 ここで、セルに書きこみすれば、後ろのループ要らないですよね?
  これがexists使って配列作るって意味です

(稲葉) 2020/08/12(水) 18:19


 色々な書き方がありますが、
 多数のセルに結果を書き込む必要があるという設定での書き方例を示します。

 Sub サンプル()
     Dim ut1&, ut2&               '比較値
     Dim dic As Object, val
     Dim ary                      '対象データ
     Dim e

     Set dic = CreateObject("Scripting.Dictionary")
     ut1 = 1
     ut2 = 2
     ary = Array(3, 1, 1, 5, 1, 2, 3, 1, 6, 2, 2, 5)     '実際には特定セル値から取得

     For Each e In ary
         dic(e) = Empty
     Next

     If dic.exists(ut1) Then dic.Remove ut1
     If dic.exists(ut2) Then dic.Remove ut2

     Range("B2").Resize(dic.count, 1).Value = Application.Transpose(dic.keys)
 End Sub

 補足:
 (1)実務的には、Addを使わずに、
         dic(e) = Empty
    と書くことが多いと思います。回答投稿でもこちらのほうが多いと思います。
    Addを使うと、既存のキーがあるとエラーになるので、存在を逐一確認する必要がありますが、
    上記形式では、その必要がありません。簡略に書けます。
    (勿論、同一キーが複数現れて、最初のキーに相当するitemに興味がある場合は、
      既存のキーの有無を調べることになりますが、重複排除後のキー取得の目的では
      そうした配慮は不要ですよね。)

 (2) dic(e) = Empty と、Emptyを使用する理由。
    (a)dictionaryの値のデフォルト値がEmptyであること。
       なお、Emptyは文字列との演算(例えば 連結&)では ""として機能し、
       数値との演算では0として機能する keywordです。
    (b)Emptyとしておくことで、dictionaryを使う意図がitemにはないこと。
       つまり、keyにしか興味を持っていないことが、コード上で明確に示せること
    の2点でしょう。
 (3)あるキーを除外したいときは、もちろん条件判定してもよいでしょうが、
    Removeを使うとよいでしょう。

(γ) 2020/08/12(水) 20:12


稲葉さんのご指摘、ごもっともです。

γさんコード、大いに勉強になりました。
「Empty」は使ったことありませんが、「Null, Nothing, ""」など調べると奥が深そうですね。
最後の「dic.keys」は「dic.keys()」の省略形、の理解でよろしいでしょうか?

基礎ができていないので、よく「()」の有無で戸惑ってしまいます。
(T20) 2020/08/13(木) 10:32


省略形かどうか知りませんが、
キー配列の特定の要素を指定しないので、
カッコは不要です。

また、特定の要素を指定することは
ほとんどありません。
各要素を走査するときは For each. .Nextを使いますし。
そういう機会はほとんどないでしょう。
(γ) 2020/08/13(木) 12:40


 >よく「()」の有無で戸惑ってしまいます。

 横からすみません。また、直接の回答ではありません。

 私はそれで悩んだことありません。
 (絶対とは言いませんが)空カッコなんてそもそも必要ないと思っているので。

 そう言ったって、こうしないと旨く行かないじゃないか、と反論されるんでしょうが・・
         ↓
 >   = dic.keys()(i) 

 そもそもこんな書き方をする必要がありますか? 
      ↓
 >    For i = 0 To dic.Count - 1
 >        Cells(i + 2, "B").Value = dic.keys()(i) 
 >    Next i

 ループの都度、配列を作っているんですよ。
 空カッコが有ろうが無かろうが同じ様に無駄です。

 こう書けば配列の作成を1回で済ますことが出来ます。

    ary = dic.keys 
    For i = 0 To dic.Count - 1
        Cells(i + 2, "B").Value = ary(i)    
    Next i

 まさか、3行で書けるなんて反論はないですよね。

 空カッコを使ってまでやりたいのは、
 1回だけ例外的に取り出したい時になると思いますが、
 そんな機会は滅多にないと思います。

(半平太) 2020/08/13(木) 12:57


 こんばんは^^
なるほどなるほど。。。今、50000件で実験したら
かたや1分11秒、もう片方は1秒ちょっと。瞬間でした
なにげに、双方使っていましたが、違いは歴然でした。
とても勉強になりました。有難うございす。( ..)φメモメモ
横入り済みませんでした。m(_ _)m
(隠居じーさん) 2020/08/13(木) 15:16

 >ループの都度、配列を作っているんですよ。
 この視点はありませんでした。
 内部的に配列を持っていて、値渡し(どうかはわかりませんが)外から見たら、確かにkeysメソッドで都度、配列として呼び出していますね。

 ということは、
 >       For Each k In dic.keys
 >           MsgBox k & ":" & dic(k)
 >       Next k
 これも当たり前のように使っていましたけど、実は無駄で順番通り取り出すなら、
 itemもプロパティじゃなくて、配列として取り込んだ後取り出したほうがいいのか?

 ということで検証してみました。
 若干ForEachのほうが遅いですが、誤差でした。
 ご指摘の通り、keys()(i)はいちいち配列を呼び出しているようで、ものすごく遅かったです。
    Sub dic検証()
        Dim dic As Object
        Dim i As Long
        Dim k As Variant
        Dim t As Double
        Dim ary As Variant
        Dim 結果(0 To 65535) As Variant
        Set dic = CreateObject("Scripting.Dictionary")
        Randomize
        For i = 0 To 65535
            dic(i) = i
        Next i
        ary = dic.keys

        t = Timer
        For i = 0 To 65535
            結果(i) = ary(i)
        Next i
        Debug.Print "配列:" & Timer - t
        i = 0
        t = Timer
        For Each k In dic.keys
            結果(i) = dic(k)
            i = i + 1
        Next k
        Debug.Print "For each:" & Timer - t
    '    t = Timer
    '    For i = 0 To 65535
    '        結果(i) = dic.keys()(i)
    '    Next i
    '    Debug.Print "順番:" & Timer - t
    End Sub
(稲葉) 2020/08/13(木) 15:36

稲葉さんの検証コードでの結果です。
(実行中は砂時計が回りっぱなしで(コメントブロック外して)ちょっと後悔しましたが(笑))
配列:0
For each:0.03125
順番:88.0546875

きっと半平太さんご指摘のことになっているんでしょうね、いい勉強になりました。
無知って怖いですね…

みなさん、本当にありがとうございました。
これからもよろしくお願いします。
(T20) 2020/08/14(金) 10:30


 稲葉様、実測ありがとうございました。
 結果(i) = dic.keys()(i) という書き方を(少なくとも大量に)用いたことは
 今まで一度もありませんでしたが、これが相当遅くなることが実感できました。

 ところで、
         ary = dic.keys
         t = Timer
         For i = 0 To 65535
             結果(i) = ary(i)
         Next i
         Debug.Print "配列:" & Timer - t

         i = 0
         t = Timer
         For Each k In dic.keys
             結果(i) = dic(k)
             i = i + 1
         Next k
         Debug.Print "For each:" & Timer - t
 という方法で、配列処理と For Each両者の比較をされ、
 >若干ForEachのほうが遅いですが、誤差でした。
 というコメントをされています。
 私も走らせてみましたが、For Eachのほうが20倍くらい時間がかかっています。

 しかし、これは公平な比較では無いと思います。
 (1) ary = dic.keys を計測の範囲から除外してしまっている点。
 (2) For each のほうにだけ、keyに対応するItemをなぜか取得させている点。

 比較するのであれば、
     t = Timer
     ary = dic.keys
     For i = 0 To 65535
         結果(i) = ary(i)
     Next i
     Debug.Print "配列:" & Timer - t

     t = Timer
     i = 0
     For Each k In dic.keys
         結果(i) = k
         i = i + 1
     Next k
     Debug.Print "For each:" & Timer - t
 でしょうか。
 ほぼ同等か、For eachのほうが速いケースもあります。

 キャッシュが利いてしまっている可能性もあるので、
 別々のプロシージャに分けて、5回繰り返すと、こんな感じでした。
 配列:     0.0068359375 
 For each: 0.005859375 
 配列:     0.0078125 
 For each: 0.0078125 
 配列:     0.0078125 
 For each: 0.0068359375 
 配列:     0.0078125 
 For each: 0.005859375 
 配列:     0.0078125 
 For each: 0.0068359375 
 まあミリ秒単位の話なので、ほぼ誤差でした、という結論のとおりでしょう。

 =======================
 ちなみに余談ですが、
 (1)
    For Each k In dic
         結果(i) = k
         i = i + 1
    Next k
 という書き方でも同じ結果が得られます。
 ・これは、dictionaryというCollectionに対して
   For Each というiteratorを使用したときに、
   各要素として抽出されるのは、Keyだからなのでしょう。
   これは、ローカルウインドウでdictionaryで覗いてみると、
   dictionaryのkeyが表示されることと平仄があっています。
 ・ただし、コードの明瞭性という点からは、 For Each k In dic.Keysと
   書いた方が間違いはないでしょうか。
 (2)
 速度もさることながら、注意すべきは、keyの順序でしょう。
 VBAで使用してdictionaryは、作成した順番通りに出力されることが保証されています。
 (dictionaryは Hashなどとも呼ばれ、
   大抵の言語では標準装備されていることが多いですが、
   中には、For Each k In dic.Keys で表示されるキーの順序が「不定」という
   言語もあるので、注意が必要です。)
(γ) 2020/08/14(金) 11:49

 γさん
 ご指摘ありがとうございます。
 aryに配列渡す部分は確かに経過時間に含むべきでした。
 >結果(i) = dic(k)
 こちらについては、
 >itemもプロパティじゃなくて、配列として取り込んだ後取り出したほうがいいのか?
 を中途半端に検証したときの名残でした、すみません

 dic.keysについても、省略できるのは始めて知りました!
 dic.itemのような標準のpropertyとは違うのでしょうか?

(稲葉) 2020/08/14(金) 13:32


 こんにちは。

 Sub test()
     Dim dic As Object
     Dim k

     Set dic = CreateObject("Scripting.Dictionary")
     dic("a") = 1
     dic("b") = 2
     For Each k In dic
         Debug.Print k, dic(k), dic.Item(k)
     Next
 End Sub
 などと実験すると、
 a              1             1 
 b              2             2 
 が返ります。

 余談:
 むかしdictionaryの説明をしたところ、質問者から、
 「ローカルウインドウには
 Item 1:     "a"
 と表示されるけどItemの値と違うじゃないですか」との指摘。
 なるほど躓くところはいろいろあるんだなあと思いました。

 このItemは いろいろなCollection(dictionaryもその範疇でしょう。
 例えば、shapesとかCollectionオブジェクトとか)の要素を表す言葉で、
 dictionaryのItemとは無関係ですね、たぶん。

(γ) 2020/08/14(金) 14:37


コメント返信:

[ 一覧(最新更新順) ]


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