首页 > 解决方案 > 为什么将自身作为默认属性返回的对象会挂起 excel 并使调试器崩溃?

问题描述

我最近遇到了`Attribute Values.VB_UserMemId = 0'。我喜欢列表,所以我想我会构建一个定制的集合类型对象。

可以重现错误的类的最小代码是:

班级Lst

Option Explicit

Public c As New Collection

'this is the default property
Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0
    If IsMissing(index) Then
        Set item = Me
        'DoEvents
    Else
        item = c(index)
    End If
End Property

Public Property Let item(Optional index, itm)
    If IsMissing(index) Then 'assume itm is list
        If IsObject(itm) Then Set c = itm.c Else c.add itm
    Else
        c.add itm, , index
        c.Remove index + 1
    End If
End Property

本质上,lst(i)返回私有集合的第 i 个元素,Lst(i)=6设置第 i 个元素。(为清楚起见,删除了错误处理和索引检查代码)。

我注意到从默认属性返回的对象可以从变体中的函数返回(例如LstFunc=L下面),而不需要set从我的学生眼中消除复杂性......(你不能用集合对象做到这一点)

不幸的是,我遇到了两个挑战......这些的最低代码是:

问题

Function LstFunc() As Variant
    Dim L As New Lst
    L = 4 'replaces L.item=3
    LstFunc = L 'this is not normally allowed, but desirable (for me!)
End Function

Sub try()
    Dim L As New Lst
    L = LstFunc 'replaces L.item=LstFunc-->L.c: [4]
    L = 3 'L.c: [4,3]
    If L = 6 Then DoEvents
End Sub

这是发生的事情

1) 当表达式L = 6被评估时,excel 挂起。有时ESC会让您恢复正常,但我的经验是 excel 停止响应并需要重新启动。

为了评估表达式,最初调用 L.item 函数,返回 a Lst,为哪个项目被调用,等等。导致不需要的和未被检测到的无限重复(不完全递归)。取消注释DoEventsget item 属性中的语句可以让您停止而不会崩溃

2)取消注释后DoEvents,我逐步在调试器模式下运行。如果我现在将鼠标悬停在(意外..)变量 L 上,调试器会崩溃,我会得到绿色的死亡三角形,我担心这会让学生感到非常困惑: 死亡的绿色三角...

DoEvents请注意,如果再次注释掉类中的语句,则此行为是可恢复的。一个名副其实的渔获22...

这有点复杂,但任何关于我如何以低计算成本捕获(1)中不需要的重复并且不失去像变体一样传递对象的能力的任何建议都将被广泛接受。

PS 这是一段代码,提供了下面评论中讨论的不安全解决方法:

Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0
    static i
    If IsMissing(index) Then
        Set item = Me
        i=i+1:if i>1000 then item="":exit property
        'DoEvents
    Else
        item = c(index)
        i=0
    End If
End Property

标签: excelvbaclassdefault

解决方案


递归是无法避免的。

从VBA 语言规范的第5.6.2.2节:

  • 如果表达式的值类型是特定的类:
    • 如果源对象具有公共默认Property Get或公共默认函数,并且此默认成员的参数列表与包含 0 个参数的参数列表兼容,则简单数据值的值是将此默认成员评估为简单数据值的结果.

请注意,对于您的示例类,这行代码满足所有这些条件:

If L = 6 Then DoEvents

表达式的类型L = 6是布尔型,Lst左边是an Integer,右边是an。这意味着比较的类型是Integer,因此运行时检查是否存在Property Get您在此处提供的默认值:

Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0

参数列表与包含 0 个参数的参数列表兼容,因为index是可选的。因此,它评估为L.item() = 6。您在属性中进行的唯一测试是If IsMissing(index)如果它被称为默认成员,则保证为真- 请记住,它不能要求传递参数。如您所见,这会导致您...

5.6.2.3默认成员递归限制

如果返回的对象具有进一步的默认成员,则对其默认Property Get或默认函数返回另一个对象的对象的求值可能会导致递归求值过程。如果评估为简单数据值并且每个默认成员都有一个空参数列表,则通过此默认成员链的递归可能是隐式的,或者如果指定了专门参数化每个默认成员的索引表达式,则递归可能是显式的。

如何处理是特定于实现的。但是,Office VBA 实现不会限制递归深度,并且在堆栈空间用完时只会使主机崩溃。


也就是说,您的问题的其余部分只是一个xy 问题,尽管我的建议是放弃这个。使用默认成员隐藏了代码的意图,并且健壮、可维护的代码应该是可读的。


推荐阅读