I am looking up keys in a Scripting.Dictionary
to make sure I add them (and their items) only once to the dictionary:
If MyDict.Exists (Key) Then ' first internal key-value lookup
Set Entry=MyDict.Item (Key) ' Second internal key->value lookup
else
Set Entry=New ItemType
MyDict.Add Key,Entry
End If
' Now I work on Entry...
Exists
looks up the key in the dictionary, and Item ()
does it, too. So i get two key lookups for one logical lookup operation. Isn't there a better way?
The dox for the Item
property say
"If key is not found when attempting to return an existing item, a new
key is created and its corresponding item is left empty."
(MSDN)
This is true, i.e. looking up a non-existant key obviously makes this key part of the dictionary, probably with associated item = empty.
But what is that good for? How could I use this to boil it down to one lookup operation? How can I set the empty item after creating the key during the Item ()
property call?
The key problem to me is the fact that VBScript forces us to use Set
with objects, and Empty
is not an object. One trick that I've used in the past to get around this is to use the Array
function to create a temporary placeholder for the value. Then I can check the array to see if the value is an object or not. Applied to your example:
tempArr = Array(dict.Item(key))
If IsEmpty(tempArr(0)) Then
' new entry
Set entry = New MyClass
' can't use Add because the key has already been implicitly created
Set dict.Item(key) = entry
Else
' existing entry
Set entry = tempArr(0)
End If
In this case, though, you haven't eliminated the double lookup, but moved it from the "existing entry" case to the "new entry" case.
This code and output:
>> Set d = CreateObject("Scripting.Dictionary")
>> WScript.Echo 0, d.Count
>> If d.Exists("soon to come") Then : WScript.Echo 1, d.Count : End If
>> WScript.Echo 2, d.Count
>> d("soon to come") = d("soon to come") + 1
>> WScript.Echo 3, d.Count, d("soon to come")
>>
0 0
2 0
3 1 1
shows:
- Looking up a non existing key with .Exists does not add the key to the dictionary (.Count is still 0 at #2)
- Accessing a non existing key via .Item or () - as on the right hand side of the assignment in my sample code - adds a key/Empty pair to the dictionary; for some task (e.g. frequency counting) this 'works', because Empty is treated as 0 in addition or "" in string concatenation. This small scale autovivification can't be used for objects (no decent way to map Empty to whatever object the programmer thinks off and no default magic as in Python or Ruby in VBScript)
- If you have to maintain a dictionary of named objects and can access the name and its object at the same time, you can just write
Set d(name) = object
- d(name) will create the key slot "name" if necessary and the Set assignment will put the object into the corresponding value (overwriting Empty or the 'old' object ('pointer')).
If you append some details about what you really want to achieve, I'm willing to add to this answer.
Added:
If you (logically) work on a list of keys with duplicates and must add new
objects to the dictionary on the fly, you can't avoid the double lookup, because
you need to check for existence (1.0) and assign (2.0) the (perhaps newly
created and assigned(1.5)) object to your work variable (see /m:a or /m:b in my
sample code). Other languages with statements that deliver a value may allow
something like
if ! (oBJ = dicX( key )) {
oBJ = dicX( key ) = new ItemType()
}
oBJ.doSomething()
and without VBScript's Set vs. Let abomination something like
oBJ = dicX( key )
If IsEmpty( oBJ ) Then
dicX( key ) = New ItemType
oBJ = dicX( key )
End If
would do the extra work for new elements only, but all that is a pipe dream.
If those double lookups really matter (which I doubt - can you give an argument
or evidence?), then the overall design of your program does matter. For example:
If you can unique-fy your work list, everything becomes simple (see /m:c in my
sample). Admittedly, I still have no idea, whether such changes are possible
for your specific task.
Code to experiment with:
Dim dicX : Set dicX = CreateObject( "Scripting.Dictionary" )
Dim aKeys : aKeys = Split( "1 2 3 4 4 3 2 1 5" )
Dim sMode : sMode = "a"
Dim oWAN : Set OWAN = WScript.Arguments.Named
If oWAN.Exists( "m" ) Then sMode = oWAN( "m" )
Dim sKey, oBJ
Select Case sMode
Case "a"
For Each sKey In aKeys
If Not dicX.Exists( sKey ) Then
Set dicX( sKey ) = New cItemType.init( sKey )
End If
Set oBJ = dicX( sKey )
WScript.Echo oBJ.m_sInfo
Next
Case "b"
For Each sKey In aKeys
If IsEmpty( dicX( sKey ) ) Then
Set dicX( sKey ) = New cItemType.init( sKey )
End If
Set oBJ = dicX( sKey )
WScript.Echo oBJ.m_sInfo
Next
Case "c"
aKeys = uniqueList( aKeys )
For Each sKey In aKeys
Set dicX( sKey ) = New cItemType.init( sKey )
Set oBJ = dicX( sKey )
WScript.Echo oBJ.m_sInfo
Next
Case Else
WScript.Echo "Unknown /m:" & sMode & ", pick one of a, b, c."
End Select
WScript.Echo "----------"
For Each sKey In dicX.Keys
WScript.Echo dicX( sKey ).m_sInfo
Next
Dim g_ITCnt : g_ITCnt = 0
Class cItemType
Public m_sInfo
Public Function init( sKey )
Set init = Me
g_ITCnt = g_ITCnt + 1
m_sInfo = "Obj for " & sKey & " (" & g_ITCnt & ")"
End Function
End Class ' cItemType
Function uniqueList( aX )
Dim dicU : Set dicU = CreateObject( "Scripting.Dictionary" )
Dim vX
For Each vX in aX
dicU( vX ) = Empty
Next
uniqueList = dicU.Keys
End Function
sample output:
/m:a
Obj for 1 (1)
Obj for 2 (2)
Obj for 3 (3)
Obj for 4 (4)
Obj for 4 (4)
Obj for 3 (3)
Obj for 2 (2)
Obj for 1 (1)
Obj for 5 (5)
----------
Obj for 1 (1)
Obj for 2 (2)
Obj for 3 (3)
Obj for 4 (4)
Obj for 5 (5)
==================================================
xpl.vbs: Erfolgreich beendet. (0) [0.07031 secs]
/m:c
Obj for 1 (1)
Obj for 2 (2)
Obj for 3 (3)
Obj for 4 (4)
Obj for 5 (5)
----------
Obj for 1 (1)
Obj for 2 (2)
Obj for 3 (3)
Obj for 4 (4)
Obj for 5 (5)
================================================
xpl.vbs: Erfolgreich beendet. (0) [0.03906 secs]
The timing difference is probably caused by the reduced output of the /m:c
mode, but it emphasizes the importance of not doing something more often
then necessary.