Scripting.Dictionary Lookup-add-if-not-present wit

2020-07-24 05:13发布

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?

2条回答
神经病院院长
2楼-- · 2020-07-24 05:23

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.

查看更多
爷、活的狠高调
3楼-- · 2020-07-24 05:39

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:

  1. Looking up a non existing key with .Exists does not add the key to the dictionary (.Count is still 0 at #2)
  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)
  3. 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.

查看更多
登录 后发表回答