辞書オブジェクト

何かとプログラミングでお世話になる辞書オブジェクトというか連想配列
もはやなくてはならないデータ型の一つですが、
Windows の COM で実装された辞書オブジェクトの動作にはまって
不可解なバグを作ってしまうという醜態をしてしまったので、
他の方も、自分と同じくはまってしまわないよう、情報展開しておこうと思う。


さてそれは何か。


Windows の COM で実装された辞書オブジェクト、いわゆる Scripting::Dictionary は
登録されていないキーを指定して、このオブジェクトにアクセスすると
素数が増えてしまうんです。


とりあえず、コードで書いた方が分かると思うので VB6 で書いてみた。

Option Explicit

Sub Main()
    
    ' 辞書オブジェクトを作成
    Dim oDict As Object
    Set oDict = CreateObject("Scripting.Dictionary") ' VBだとScripting.Dictionaryでオブジェクトを作る
    
    ' 自分で定義したPersonクラスのオブジェクトを作成
    Dim oPerson As Person
    Set oPerson = New Person
    
    ' 以下 oPerson のプロパティを設定
    With oPerson
        ' ....
    End With
    
    ' ここで辞書オブジェクトにPersonオブジェクトを登録
    Call oDict.Add("foo", oPerson)
    
    ' ここで辞書オブジェクトに登録されている要素数をCountプロパティでデバッガに出力
    '   -> Countプロパティから取得できる値は1になる
    Debug.Print "Dictionray Count (Before) : " & oDict.Count
    
    ' ここで辞書オブジェクトに登録されていないキーを指定して取得してみる
    Dim oResult As Person
    Set oResult = GetItem("bar", oDict) ' GetItemメソッド経由で取得する
    
    ' ここで辞書オブジェクトに登録されている要素数を出力
    '   -> Countプロパティから取得できる値は1のはず。。。
    Debug.Print "Dictionray Count (After) : " & oDict.Count

    ' 辞書オブジェクトに登録されていないキーを指定した場合型は何が取得できるのかな。
    Debug.Print "TypeName : " & TypeName(oDict("bar"))
    
End Sub


' アイテムを取得する
Private Function GetItem(ByVal strKey As String, ByRef objDict As Object) As Variant

On Error GoTo Catch
    
    Dim vItem As Variant
    Set vItem = objDict(strKey)
    
    Goto Finally
Catch:
    Debug.Print "Raise Error !! : " & Err.Description
    Set vItem = Nothing
    
Finally:
    
    Set GetItem = vItem
    
End Function


で書いた VB6 のコードを実行させるとこんな感じで出力される。

Dictionray Count (Before) : 1
Raise Error !! : 型が一致しません。
Dictionray Count (After) : 2
TypeName : Empty


なんと、辞書オブジェクトの Count プロパティで取得できる要素数
1 から 2 に増えているでありませんか。

これは、MS のオンラインの MSDN によると仕様のようです。
Item プロパティの解説にこんな感じで載っていた。

項目を変更するときに引数 key で指定したキーが見つからない場合、newitem で指定した項目と関連付けられた、引数 key で指定した新しいキーが作成されます。
また、既存の項目を取得するときに引数 key で指定したキーが見つからない場合は、空の項目と関連付けられた、引数 key で指定した新しいキーが作成されます。


というわけで、COM の辞書オブジェクトでを使う場合は、
Count プロパティで取得できる要素数は動的に変わってしまう可能性があるということになります。

実装する処理によっては、Exists メソッドでキーに該当するアイテムが
辞書オブジェクトに登録されているかどうかチェックしてから、
Item プロパティや a("key1") みたいにアクセスしないといけないですね。

VBAWSH 等で COM の辞書オブジェクトを使う場合は気を付けましょう。



ここで、他の場合はどういう動きをするかどうか気になったので確認してみた。


まずは、.NET Framework の Dictionary オブジェクト。
以下は、C#で書いたコード。

using System;
using System.Collections.Generic;
using System.Text;

namespace DictionaryTest
{
  class Program
  {
    static void Main(string[] args)
    {
      // 辞書オブジェクトを作成
      Dictionary<string, Person> dict = new Dictionary<string, Person>();

      // 自分で定義したPersonクラスのオブジェクトを作成
      Person person = new Person();

      // 以下Personオブジェクトのプロパティ設定
      // ...

      // ここで辞書オブジェクトにPersonオブジェクトを登録
      dict.Add("foo", person);

      // ここで辞書オブジェクトに登録されている要素数をCountプロパティでコンソールに出力
      //   -> Countプロパティから取得できる値は1になる
      Console.WriteLine("Dictionary Count (Before) : {0}", dict.Count);

      
      // ここで辞書オブジェクトに登録されていないキーを指定して取得してみる
      Person result = GetItem("bar", dict); // GetItemメソッド経由で取得する
    
      // ここで辞書オブジェクトに登録されている要素数を出力
      //   -> Countプロパティから取得できる値は1のはず。。。
      Console.WriteLine("Dictionary Count (After) : {0}", dict.Count);

    }

    static Person GetItem(string key, Dictionary<string, Person> dict)
    {
      Person person = null;

      try
      {
        person = dict["key1"];
      }
      catch(Exception e)
      {
        Console.WriteLine("Raise Exception !! : {0}", e);
      }

      return person;
    }
  }
}


実行結果は、以下のとおり。

Dictionary Count (Before) : 1
Raise Exception !! : System.Collections.Generic.KeyNotFoundException: 指定された
キーはディレクトリ内に存在しませんでした。
場所 System.ThrowHelper.ThrowKeyNotFoundException()
場所 System.Collections.Generic.Dictionary`2.get_Item(TKey key)
場所 DictionaryTest.Program.GetItem(String key, Dictionary`2 dict) 場所 C:\Us
ers\xxxx\Desktop\DictionaryTest\DictionaryTest\Program.cs:行 43
Dictionary Count (After) : 1


.NET Framework の辞書オブジェクトは、登録されていないキーでアクセスすると
例外が発生し、要素数は変化しません。


続いて、Python
こちらは、Mac OS XMacPorts でインストールした Python 2.5.5 で確認してみました。

macbook-2:Memo kazupon$ python
Python 2.5.5 (r255:77872, Jun 6 2010, 13:45:39)
[GCC 4.2.1 (Apple Inc. build 5659)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> d = { "key1":1 }
>>> len(d)
1
>>> d['key2']
Traceback (most recent call last):
File "", line 1, in
KeyError: 'key2'
>>> len(d)
1


Python の場合も、辞書オブジェクトに登録されていないキーでアクセスすると
例外が発生し、要素数は変化しないみたいです。


最後に、Ruby

macbook-2:Memo kazupon$ ruby -v
ruby 1.8.7 (2009-06-12 patchlevel 174) [i686-darwin10]
macbook-2:Memo kazupon$ irb
irb(main):001:0> h = { 'key1' => 1 }
=> {"key1"=>1}
irb(main):002:0> h.count
=> 1
irb(main):003:0> h['key2']
=> nil
irb(main):004:0> h.count
=> 1


こちらも、Mac OS XMacPorts でインストールした Ruby 1.8.7 で動作確認してみました。
Ruby の場合は、辞書オブジェクト(Ruby の場合は連想配列と呼びますが。)に登録されていないキーでアクセスすると
例外が発生せず、nil が取得できるようです。
これは、COM の辞書オブジェクトで Empty が取得できるのと似ています。
ただ、要素数は変化しないようです。


というわけで、言語によって辞書オブジェクトは
いろいろな動作をするみたいです。