来自父级的“TypeOf...Is Child”导致 Excel 文件损坏

apl*_*lum 5 excel vba

我已经跟踪这个问题好几天了,所以我想我会在这里发布它以帮助其他人解决同样的问题,并了解更多关于原因的信息。在这篇文章的末尾,我将问题代码简化为两个类模块。

基本上,简化的场景是这样的:两个类模块,Parent 和 Child,其中 Child 实现了 Parent。Parent 中的某处是 line TypeOf Me Is Child,其中Me可以是任何对象。

根据我的理解,当该TypeOf...Is行被编译为 P 代码(调试 > 编译,或调用方法)并保存到文件(.xlsm 或 .xlsb)时,它会导致文件无法正常打开。代码将运行良好,但是当文件被保存、关闭和重新打开时,它在打开(或打开 VBE)时出现错误,说Invalid data formatError accessing file. Network connection may have been lost,并且无法再打开父模块,也不能运行任何 VBA (?1=1在立即窗口中尝试,它给出了相同的错误)。

如果使用TypeName()而不是检查类型TypeOf...Is,则不会出现此问题(这是我在项目中使用的解决方案)。

任何人都可以更清楚地了解这里到底出了什么问题,或者至少确认我在导致问题的原因(P代码)方面走在正确的轨道上?

PS 是的,我知道父母对孩子的了解是糟糕的设计,但我已接近一次性项目的尾声,不值得花时间重新设计。

有用的链接:

类模块:

家长:

Option Explicit
' Class: Parent

' The problem (so far as I can tell):
'   When the compiled version of the method below is saved to the file, the file
'   will no longer load properly. Upon saving and reopening the file, I get a
'   "Invalid data format" error, and the code for this class module can no longer be
'   accessed. Furthermore, no VBA code will run after this happens. Try typing "?1=1"
'   into the Immediate Window - you'll get another "Invalid data format" window.
'   Alternatively, the error will be "Error accessing file. Network connection may
'   have been lost." if the code is changed from using "Me" to "tmp" as noted in the
'   comments in DoSomething().

' Steps to replicate:
'   1. Debug > Compile VBAProject.
'   2. Save file.
'   3. Close Excel.
'   4. Reopen file (and may need to open VBE).

Public Sub DoSomething()
    ' The TypeOf...Is statement seems to be what causes the problem.
    ' Note that checking "Me" isn't the cause of the problem (merely makes
    '   for shorter demo code); making a "Dim tmp as Object; set tmp = new Collection"
    '   and checking "TypeOf tmp Is Child" will cause the same problem.
    ' Also note, changing this to use TypeName() resolves the issue.
    ' Another note, moving the TypeOf...Is to a "Private Sub DoSomethingElse()" has
    '   no effect on the issue. Moving it to a new, unrelated class, however, does
    '   not cause the issue to occur.
    If TypeOf Me Is Child Then
        Debug.Print "Parent"
    End If
End Sub
Run Code Online (Sandbox Code Playgroud)

孩子:

Option Explicit
' Class: Child

Implements Parent

Private Sub Parent_DoSomething()
    Debug.Print "Child"
End Sub
Run Code Online (Sandbox Code Playgroud)

小智 4

IMPLMENTS 语句导致循环依赖

问题不在于TypeOf声明本身。问题是您设置了 VBA 无法解析的循环依赖关系。正如提到的,VBA并没有真正实现多态性。您创建的循环引用是您的接口的定义 "Parent"包括(需要存在)您的对象"Child"和您的类实现的定义"Child" (需要存在)"Parent"。因此,VBA 无法在编译时正确创建接口,并且下次保存、关闭并重新打开工作簿和 VB 编辑器时,接口类会损坏且无法访问。

OP 可能会被误解为暗示该声明TypeOf .. Is应受到某种指责。然而,TypeOf 语句并不特殊。接口类中引用接口类本身的类的IMPLEMENTS任何语句都会造成循环依赖问题。例如:

人物.cs

'Class Person
Option explicit

Public Sub SaySomething()
   Dim B as Boy            '<--- here we cause the problem!
End sub 
Run Code Online (Sandbox Code Playgroud)

男孩.cs

'Class Boy
Option explicit

Implements Person

Private Sub Person_SaySomething()
   Debug.Print "Hello"
End sub
Run Code Online (Sandbox Code Playgroud)

所以我希望你能看到 Boy.cs 实现了 Person.cs,其中包含一个 Boy.cs,而 Boy.cs 实现了一个包含 Boy.cs 的 Person.cs .... VBA 在这一点上变得疯狂:)

有点不幸的是,VB 编辑器没有提供比“无效数据格式”错误或“访问文件时出错。网络连接可能已丢失”更有用的错误消息。这让用户感到困惑!

解决方案是从接口类的源代码中删除这些语句。如果这被证明很难做到,因为您实际上在接口类中编写了很多业务逻辑,那么一个有用的方法是将业务逻辑移到单独的类中。只需单独执行此操作即可解决接口的编译问题并使代码再次运行。根据我自己的经验,正是出于这个原因,我故意尝试从接口类中删除任何业务逻辑,以确保不会发生此类错误,并且接口类变得非常简单 - 只是一个方法签名列表。如果存在我不想在将实现我的接口的每个类中重复的通用业务逻辑,那么我创建一个附加类来保存此通用业务逻辑并确保该接口需要此类存在。例如:

iMusicalInstrument.cs

'iMusicalInstrument interface
Option Explicit
Property Get Common() as csMusicalInstrumentCommon
End Property
Run Code Online (Sandbox Code Playgroud)

csMusicalInstrumentCommon.cs

'MusicalInstrumentCommon class
Option Explicit
' add any methods you want to be available to all implementers of the interface.
Property Get GUID() as string        '<-- just an example, could be any method
     GUID = 'function to create a GUID
End Property
Run Code Online (Sandbox Code Playgroud)

csTrumpet.cs

' csTrumpet class
Option Explicit
Implements iMusicalInstrument
Private mCommon As csMusicalInstrumentCommon
Private Sub Class_Initialize()
    Set mCommon = New csMusicalInstrumentCommon
End Sub
Private Sub Class_Terminate()
    Set mCommon = Nothing
End Sub
Private Property Get iMusicalInstrument_Common() As csMusicalInstrumentCommon
    Set iMusicalInstrument_Common = mCommon
End Property
Run Code Online (Sandbox Code Playgroud)

用法

Public Sub Test()
    Dim Trumpet As New csTrumpet
    Dim iTrumpet As iMusicalInstrument
    Set iTrumpet = Trumpet
    Debug.Print iTrumpet.Common.GUID
End Sub
Run Code Online (Sandbox Code Playgroud)

:)