问题
For larger VBA projects (40,000+ lines of code) I cannot properly use interfaces because the Application (I mainly use Excel) will crash quite often. Apparently, this is because the code cannot remain compiled (from my understanding VBA code gets compiled to P-code which is later interpreted). I mainly get this behavior when the VBA Project is password protected.
The Debug/Compile menu is almost never "greyed out" when I open the hosting document:
This article describes the same behavior. Go to section 2.3
For example:IClass
interface:
Option Explicit
Public Property Get SomeProperty() As Double
End Property
Class1
:
Option Explicit
Implements IClass
Private Property Get IClass_SomeProperty() As Double
IClass_SomeProperty = 0
End Property
Code in standard module:
Option Explicit
Sub TestInterface()
Dim obj As IClass
Set obj = New Class1
Debug.Print obj.SomeProperty 'Crashes here on large projects only
End Sub
As expected the Debug.Print obj.SomeProperty
line works fine and prints 0 in the Immediate window if the project is small. However, on a large project the application crashes when this line is called. The IClass_SomeProperty
is not reached (logging to a file clarifies this).
As in the above-mentioned article, there are ways to temporarily avoid the issue:
- Recompiling solves the issue (not always) but the crash could occur on the very next time the document is opened, or it might keep things going for a few days (presuming file is opened every day)
- When option 1 is not working, deactivating all
Implements
statements in the entire project using the Find/Replace window, then compiling and then reactivating the statements back. Again, this can work for a few hours or a few days but inevitably a crash will occur in the near future
Since the VBA Project is password protected and because many people are using these VBA-capable documents (Workbooks in my case), applying the temporary fixes does not help at all.
The only way that I've found to avoid the crashes and still get the benefits of the interfaces is to use conditional compilation. Basically, I use interfaces only for development and then switch to late-binding for production. Obviously, this comes with a lot of headaches.
The example above becomes:Class1
:
Option Explicit
#Const USE_INTERFACES = True
#If USE_INTERFACES Then
Implements IClass
#End If
Private Property Get IClass_SomeProperty() As Double
IClass_SomeProperty = Me.SomeProperty
End Property
Public Property Get SomeProperty() As Double
SomeProperty = 0
End Property
Notice that all interface methods must be duplicated and made public so that late-binding is an option.
Code in standard module:
Option Explicit
#Const USE_INTERFACES = True
Sub TestInterface()
#If USE_INTERFACES Then
Dim obj As IClass
#Else
Dim obj As Object
#End If
Set obj = New Class1
Debug.Print obj.SomeProperty
End Sub
When developing new features, I follow these steps:
- turn
#Const USE_INTERFACES = True
for all occurrences using Find/Replace - add new features
- turn
#Const USE_INTERFACES = False
so that code runs on late-binding and doesn't crash
I've been experiencing this bug for at least 3 years. I would obviously avoid the conditional compilation workaround if I could.
Is there a way to keep the VBA Project compiled (let's say running a procedure when opening the document) without access to the VBA project object model? It is not an option for me to have the Trust access to the VBA project object model turned on.
I appreciate this bug is not easy to re-create unless you happen to have a large VBA project at hand.
EDIT 1
A nice point raised by @PEH in the comments: this issue is applicable for both xlsm
and xlsb
files (Excel).
回答1:
I don't have a 40K-liner project to test it, but it is known that the VBE debugger's edit-and-continue feature can corrupt the internal storage streams, resulting in "ghost breakpoints".
The VBE itself will corrupt a VBA project, given enough time and enough debugger sessions (and the right amount of pixie dust) - with or without interfaces involved.
I've been coding against interfaces in VBA ever since I've realized that VBA had that capability, and the only problem I've found that was directly caused by Implements
statements, was that if you make a document module (e.g. Sheet1
, or ThisWorkbook
), implement an interface, then you're 100% going to corrupt your project and crash Excel. But with normal user classes? Nope, never had a problem with those.
I would definitely consider edit-and-continue the primary suspect for any stream corruption happening in a VBA project - if the internal ITypeInfo
gets corrupted when the type Implements
an interface, then that would be because of the bug with edit-and-continue, and the Implements
statement "causing it" is really just a symptom.
Project size is also an important factor: 40K LoC is indeed a very large VBA project, and large VBA projects have a tendency to more easily get corrupted too. This can usually be mitigated by regularly removing+exporting all code files and re-importing them back into the project (which forces a "clean rebuild" of the internal storage, I'm guessing) - if your very large project is under source control, then this should be happening regularly.
The VBE does not want you to write OOP. It's actively fighting it: it wants you to cram as much code as possible into as few modules as possible, and since its navigation tooling objectively sucks, you don't get a "find all implementations" command to quickly locate the concrete implementations for your interfaces, so yes the VBE's "go to definition" takes you to an empty method stub - guess what, "go to definition" in Microsoft Visual Studio 2019 does exactly the same (it does give you "go to implementation" too though).
Writing OOP in VBA without Rubberduck to assist with navigation (and everything else) isn't something I would recommend, but then irony has it that Rubberduck doesn't perform very well with very large legacy code bases with lots of late binding (implicit or not).
If project size is related to the problem, then using conditional compilation is actually making things worse, by making the project even larger, and then making Rubberduck fail to "see" pretty much half of the code (late bound member calls can't be resolved, so we lose track of what's being used where), ...which essentially cripples it.
I don't have a solution, only a hunch that edit-and-continue is very likely behind this, since it rewrites chunks of p-code on the fly, and that is already known to be causing problems. If my hunch is correct, then regularly exporting & reimporting all code files should help keep the corruption at bay (and then it makes it easy to put the project under source control, should anything ever become irreparably broken). Avoid code-behind in document modules as much as possible when you do this, and whatever you do NEVER make a worksheet/document module Implements
any interface. Rubberduck has tooling to quickly & easily export/import multiple code files at once to/from a given folder.
回答2:
I too jumped into using interfaces (years ago) because I thought they were proper COM (and they are) but I too hit problems. So, I stopped using them.
Apart from anything else, if you double click on a method with normal code you jump to the code implementation but with interfaces you jump to an empty method (which is unsatisfactory).
If you really have a class, Foo, that expresses different behaviours worthy enough to break into a separate interface IBar then why not break IBar into an actual separate class Bar and then set an instance of Bar to be a public property of Foo?
That's just a suggestion. I know of no other VBA fix and it is extremely unlikely Microsoft will fix this now.
You could always migrate your code to VisualBasic.Net if you want to preserve you class/interface design.
I'd be delighted if someone actually solves this.
来源:https://stackoverflow.com/questions/63989741/bug-when-using-interfaces-on-larger-projects