关于Excel VBA Dictionary添加方法、值/引用传递及内部变更机制的技术问询
关于VBA Dictionary的传递机制与内部运作详解
先贴出你提供的测试代码方便参考:
Sub test() Dim diz As New Dictionary Dim vett(1 To 2) As Integer Dim val As Integer Dim dvett As New Dictionary Dim a As Variant Select Case 3 '修改此值以运行不同分支 Case 1 val = 1 diz.Add "a", val Debug.Print diz("a") '1 diz("a") = 2 Debug.Print diz("a") '2 Debug.Print val '1 => 按值复制,而非按引用 val = 3 Debug.Print diz("a") '2 => 按值复制,而非按引用 Case 4 diz.Add "a", [{"a1","b1","c1"}] Debug.Print diz("a")(1) 'a1 - 正常 diz.Items(0)(1) = "2" '无错误 Debug.Print diz("a")(1) 'a1 !!!!!!!!!!!!! 无变化 diz("a")(1) = "2" Debug.Print diz("a")(1) 'a1 !!!!!!!!!!!!! 无变化 diz("a") = [{4,5,6}] Debug.Print diz("a")(1) '4 - 正常 Case 2 vett(1) = 1 diz.Add "a", vett Debug.Print diz("a")(1) '1 diz("a")(1) = 2 '无错误 Debug.Print diz("a")(1) '1 !!!!!! 无变化 Debug.Print vett(1) '1 vett(1) = 3 Debug.Print diz("a")(1) '1 diz("a") = vett Debug.Print diz("a")(1) '3 Case 3 dvett(1) = 1 diz.Add "a", dvett Debug.Print diz("a")(1) '1 diz("a")(1) = 2 Debug.Print diz("a")(1) '2 Debug.Print dvett(1) '2 此场景下为按引用复制!!!!! dvett(1) = 3 Debug.Print diz("a")(1) '3 此场景下为按引用复制!!!!! End Select End Sub
我来逐一拆解你的疑问,结合上面的测试代码解释每种场景的行为逻辑:
1. 向Dictionary添加条目时,是按值还是按引用传递?
这个问题的核心完全取决于你存入的数据类型:
- 值类型(比如Integer、String、固定大小数组等):按值传递。Dictionary会创建一份数据的完整副本存入内部哈希表,和原始变量彻底独立,两边的修改互不影响。
- 引用类型(比如Dictionary对象、自定义类实例、Excel对象如Worksheet等):按引用传递。Dictionary只存储对象的内存地址指针,你通过Dictionary访问的就是原对象本身,修改会同步到原对象。
看测试代码的典型表现:
- Case1里的
val是Integer(值类型):Add后修改val,Dictionary里的"a"值没变;修改diz("a"),val也没变化,完全符合按值传递的特征。 - Case3里的
dvett是Dictionary对象(引用类型):Add后不管改diz("a")(1)还是dvett(1),两边都会同步变化,这就是引用传递的表现。
2. Dictionary的Add方法工作原理,以及测试代码中不同行为的原因
Dictionary.Add Key, Item的核心步骤其实很直白:
- 先检查Key是否已经存在,存在就直接抛错;
- 计算Key的哈希值,在内部哈希表中找到对应的存储位置;
- 把Key和Item存入该位置——这里的Item如果是值类型就存副本,如果是引用类型就存对象的内存指针。
你测试代码里不同Case的差异,本质就是值类型和引用类型的存储逻辑不同:
- Case2的
vett是固定大小Integer数组(值类型):Add时Dictionary复制了整个数组的副本。当你写diz("a")(1) = 2时,其实是先从Dictionary里取出数组的临时副本,修改的是这个副本的元素,副本用完就被销毁了,根本碰不到Dictionary里存的那个数组,所以打印出来还是1。只有重新执行diz("a") = vett,才会把vett的新副本存入Dictionary,这时候才会看到变化。 - Case4的数组字面量
[{"a1","b1","c1"}]:本质也是值类型数组,逻辑和Case2完全一样——存入的是副本,修改临时取出的数组元素不会影响Dictionary内部的存储,只有重新赋值数组才会替换条目。 - Case3的Dictionary对象:存入的是引用指针,所以
diz("a")直接指向原dvett对象,修改它的条目自然会同步到原对象。
3. 修改Dictionary中的值时,内部是怎么处理的?
分两种情况来看:
针对值类型条目
当你执行diz(Key) = NewValue时,Dictionary会直接定位到哈希表中对应Key的位置,用新值覆盖原来存储的副本——不是删除原有元素再添加新的,就是直接更新内存里的值,效率很高。比如Case1里diz("a") = 2,就是把哈希表中"a"对应的Integer值从1改成2,没有额外的删除操作。
针对引用类型条目
- 如果是修改引用对象的内部内容(比如
diz("a")(1) = 2,这里diz("a")是Dictionary对象):Dictionary本身没有任何变化,它存储的还是原来的对象指针,你修改的是指针指向的对象的内部数据,所以原对象也会跟着变。 - 如果是给Key赋值新的对象(比如
diz("a") = New Dictionary):这时候Dictionary会替换哈希表中对应Key的指针,原来的对象如果没有其他引用指向它,就会被VBA的垃圾回收机制清理——这一步是替换条目,但也不是删除再添加,只是更新指针地址。
顺便提下你关注的执行速度:
- 值类型的添加和修改速度通常更快,因为只是复制或更新简单数据;
- 引用类型的添加只是存个指针,速度也很快,但操作对象内部数据的速度取决于对象本身的复杂度;
- 尽量避免像Case2那样频繁修改Dictionary里的值类型数组——每次取出都会生成副本,效率很低,不如先把数组取出来改完再存回去。
内容的提问来源于stack exchange,提问作者tom
相关产品推荐
相关产品推荐

