Как это исправить
A серьезно 1337 хакер по имени DEXWERX написал глубокую магию в 2017 году. Я адаптировал Код DEXWERX для этой ситуации, и приведите рабочий пример здесь.Куски:
MEnumerator
: измененная версия кода DEXWERX.Это делает IEnumVARIANT
, собирая его в памяти с нуля! IValueProvider
: Прямой интерфейс VBA, который должен реализовать ваш генератор.IEnumVARIANT
, созданный MEnumerator
, будет вызывать методы для экземпляра IValueProvider
для получения возвращаемых элементов. NumberRange
: Класс генератора, который реализует IValueProvider
.
Ниже приведен тестовый код для вставки в VBA и cls
и bas
файлы для импорта.
Тестовый код
Я поместил его в ThisDocument
.
Option Explicit
Sub testNumberRange()
Dim c As New NumberRange
c.generatorTo 10
Dim idx As Long: idx = 1
Dim val
For Each val In c
Debug.Print val
If idx > 100 Then Exit Sub ' Just in case of infinite loops
idx = idx + 1
Next val
End Sub
IValueProvider.cls
Сохраните это в файл и импортируйте его в VBA Editor.
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "IValueProvider"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
' IValueProvider: Provide values.
Option Explicit
Option Base 0
' Return True if there are more values
Public Function HasMore() As Boolean
End Function
' Return the next value
Public Function GetNext() As Variant
End Function
NumberRange.cls
Сохраните это вфайл и импортируйте его в редактор VBA.Обратите внимание, что функция NewEnum
теперь просто делегирует функции NewEnumerator
в MEnumerator
.Вместо использования коллекции это переопределяет методы IValueProvider_HasMore
и IValueProvider_GetNext
для использования MEnumerator
.
Также обратите внимание, что я сделал все на основе нуля для согласованности.
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "NumberRange"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Option Base 0
' === The values we're actually going to return ===================
Implements IValueProvider
Private Type TRange
isGenerator As Boolean
currentCount As Long
maxCount As Long
End Type
Private this As TRange
Private Function IValueProvider_GetNext() As Variant
IValueProvider_GetNext = this.currentCount 'Or try Chr(65 + this.currentCount)
this.currentCount = this.currentCount + 1
End Function
Private Function IValueProvider_HasMore() As Boolean
IValueProvider_HasMore = this.isGenerator And (this.currentCount <= this.maxCount)
End Function
' === Public interface ============================================
Public Sub generatorTo(ByVal count As Long)
this.isGenerator = True
this.currentCount = 0
this.maxCount = count - 1
End Sub
' === Enumeration support =========================================
Public Property Get NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
'Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = NewEnumerator(Me)
End Property
' === Internals ===================================================
Private Sub Class_Initialize()
' If you needed to initialize `this`, you could do so here
End Sub
MEnumerator.bas
Сохраните это в файл и импортируйте его в редактор VBA.IEnumVARIANT_Next
вызывает методы IValueProvider
и перенаправляет их в VBA.Метод NewEnumerator
создает IEnumVARIANT
.
Attribute VB_Name = "MEnumerator"
' Modified by cxw from code by http://www.vbforums.com/member.php?255623-DEXWERX
' posted at http://www.vbforums.com/showthread.php?854963-VB6-IEnumVARIANT-For-Each-support-without-a-typelib&p=5229095&viewfull=1#post5229095
' License: "Use it how you see fit." - http://www.vbforums.com/showthread.php?854963-VB6-IEnumVARIANT-For-Each-support-without-a-typelib&p=5232689&viewfull=1#post5232689
' Explanation at https://stackoverflow.com/a/52261687/2877364
'
' MEnumerator.bas
'
' Implementation of IEnumVARIANT to support For Each in VB6
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Option Explicit
Private Type TENUMERATOR
VTablePtr As Long
References As Long
Enumerable As IValueProvider
Index As Long
End Type
Private Enum API
NULL_ = 0
S_OK = 0
S_FALSE = 1
E_NOTIMPL = &H80004001
E_NOINTERFACE = &H80004002
E_POINTER = &H80004003
#If False Then
Dim NULL_, S_OK, S_FALSE, E_NOTIMPL, E_NOINTERFACE, E_POINTER
#End If
End Enum
Private Declare Function FncPtr Lib "msvbvm60" Alias "VarPtr" (ByVal Address As Long) As Long
Private Declare Function GetMem4 Lib "msvbvm60" (Src As Any, Dst As Any) As Long
Private Declare Function CopyBytesZero Lib "msvbvm60" Alias "__vbaCopyBytesZero" (ByVal Length As Long, Dst As Any, Src As Any) As Long
Private Declare Function CoTaskMemAlloc Lib "ole32" (ByVal cb As Long) As Long
Private Declare Sub CoTaskMemFree Lib "ole32" (ByVal pv As Long)
Private Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, ByVal lpiid As Long) As Long
Private Declare Function SysAllocStringByteLen Lib "oleaut32" (ByVal psz As Long, ByVal cblen As Long) As Long
Private Declare Function VariantCopyToPtr Lib "oleaut32" Alias "VariantCopy" (ByVal pvargDest As Long, ByRef pvargSrc As Variant) As Long
Private Declare Function InterlockedIncrement Lib "kernel32" (ByRef Addend As Long) As Long
Private Declare Function InterlockedDecrement Lib "kernel32" (ByRef Addend As Long) As Long
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Function NewEnumerator(ByRef Enumerable As IValueProvider) As IEnumVARIANT
' Class Factory
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Static VTable(6) As Long
If VTable(0) = NULL_ Then
' Setup the COM object's virtual table
VTable(0) = FncPtr(AddressOf IUnknown_QueryInterface)
VTable(1) = FncPtr(AddressOf IUnknown_AddRef)
VTable(2) = FncPtr(AddressOf IUnknown_Release)
VTable(3) = FncPtr(AddressOf IEnumVARIANT_Next)
VTable(4) = FncPtr(AddressOf IEnumVARIANT_Skip)
VTable(5) = FncPtr(AddressOf IEnumVARIANT_Reset)
VTable(6) = FncPtr(AddressOf IEnumVARIANT_Clone)
End If
Dim this As TENUMERATOR
With this
' Setup the COM object
.VTablePtr = VarPtr(VTable(0))
.References = 1
Set .Enumerable = Enumerable
End With
' Allocate a spot for it on the heap
Dim pThis As Long
pThis = CoTaskMemAlloc(LenB(this))
If pThis Then
' CopyBytesZero is used to zero out the original
' .Enumerable reference, so that VB doesn't mess up the
' reference count, and free our enumerator out from under us
CopyBytesZero LenB(this), ByVal pThis, this
DeRef(VarPtr(NewEnumerator)) = pThis
End If
End Function
Private Function RefToIID$(ByVal riid As Long)
' copies an IID referenced into a binary string
Const IID_CB As Long = 16& ' GUID/IID size in bytes
DeRef(VarPtr(RefToIID)) = SysAllocStringByteLen(riid, IID_CB)
End Function
Private Function StrToIID$(ByRef iid As String)
' converts a string to an IID
StrToIID = RefToIID$(NULL_)
IIDFromString StrPtr(iid), StrPtr(StrToIID)
End Function
Private Function IID_IUnknown() As String
Static iid As String
If StrPtr(iid) = NULL_ Then _
iid = StrToIID$("{00000000-0000-0000-C000-000000000046}")
IID_IUnknown = iid
End Function
Private Function IID_IEnumVARIANT() As String
Static iid As String
If StrPtr(iid) = NULL_ Then _
iid = StrToIID$("{00020404-0000-0000-C000-000000000046}")
IID_IEnumVARIANT = iid
End Function
Private Function IUnknown_QueryInterface(ByRef this As TENUMERATOR, _
ByVal riid As Long, _
ByVal ppvObject As Long _
) As Long
If ppvObject = NULL_ Then
IUnknown_QueryInterface = E_POINTER
Exit Function
End If
Select Case RefToIID$(riid)
Case IID_IUnknown, IID_IEnumVARIANT
DeRef(ppvObject) = VarPtr(this)
IUnknown_AddRef this
IUnknown_QueryInterface = S_OK
Case Else
IUnknown_QueryInterface = E_NOINTERFACE
End Select
End Function
Private Function IUnknown_AddRef(ByRef this As TENUMERATOR) As Long
IUnknown_AddRef = InterlockedIncrement(this.References)
End Function
Private Function IUnknown_Release(ByRef this As TENUMERATOR) As Long
IUnknown_Release = InterlockedDecrement(this.References)
If IUnknown_Release = 0& Then
Set this.Enumerable = Nothing
CoTaskMemFree VarPtr(this)
End If
End Function
Private Function IEnumVARIANT_Next(ByRef this As TENUMERATOR, _
ByVal celt As Long, _
ByVal rgVar As Long, _
ByRef pceltFetched As Long _
) As Long
Const VARIANT_CB As Long = 16 ' VARIANT size in bytes
If rgVar = NULL_ Then
IEnumVARIANT_Next = E_POINTER
Exit Function
End If
Dim Fetched As Long
Fetched = 0
Dim element As Variant
With this
Do While this.Enumerable.HasMore
element = .Enumerable.GetNext
VariantCopyToPtr rgVar, element
Fetched = Fetched + 1&
If Fetched = celt Then Exit Do
rgVar = PtrAdd(rgVar, VARIANT_CB)
Loop
End With
If VarPtr(pceltFetched) Then pceltFetched = Fetched
If Fetched < celt Then IEnumVARIANT_Next = S_FALSE
End Function
Private Function IEnumVARIANT_Skip(ByRef this As TENUMERATOR, ByVal celt As Long) As Long
IEnumVARIANT_Skip = E_NOTIMPL
End Function
Private Function IEnumVARIANT_Reset(ByRef this As TENUMERATOR) As Long
IEnumVARIANT_Reset = E_NOTIMPL
End Function
Private Function IEnumVARIANT_Clone(ByRef this As TENUMERATOR, ByVal ppEnum As Long) As Long
IEnumVARIANT_Clone = E_NOTIMPL
End Function
Private Function PtrAdd(ByVal Pointer As Long, ByVal Offset As Long) As Long
Const SIGN_BIT As Long = &H80000000
PtrAdd = (Pointer Xor SIGN_BIT) + Offset Xor SIGN_BIT
End Function
Private Property Let DeRef(ByVal Address As Long, ByVal Value As Long)
GetMem4 Value, ByVal Address
End Property
Оригинальный ответ: почему существующий код не работает
Я не могу сказать вам, как это исправить, но Я могу сказать вам, почему.Это слишком долго для комментария :).
Вы экспортируете перечислитель Collection
для собственного использования.Прямая Collection
версия testGenerator
имеет такое же поведение:
Option Explicit
Sub testCollection()
Dim c As New Collection
Dim idx As Long: idx = 1
Dim val
c.Add idx
For Each val In c
Debug.Print val
c.Add idx
If idx > 100 Then Exit Sub ' deadman, to break an infinite loop if it starts working!
idx = idx + 1
Next val
End Sub
Этот код печатает 1
и затем выходит из цикла For Each
.
Я полагаю updateObject
звонок не делает то, что вы ожидаете.Следующее основано на моих собственных знаниях, а также на этом форуме .Когда начинается цикл For Each
, VBA получает IUnknown
от _NewEnum
.Затем VBA вызывает QueryInterface
для IUnknown
, чтобы получить собственный указатель IEnumVARIANT
на один объект перечислителя с подсчетом ссылок.В результате у For Each
есть собственная копия перечислителя.
Затем, когда вы звоните updateObject
, он изменяет содержимое this.currentEnum
.Однако, это не то, где цикл For Each
фактически смотритВ результате replaceVal()
изменяет коллекцию, пока она повторяется.В VB.NET документах есть что сказать по этому вопросу.Я подозреваю, что поведение VB.NET было унаследовано от VBA, поскольку оно соответствует тому, что вы видите.В частности:
Объект перечислителя, возвращаемый GetEnumerator
[из System.Collections.IEnumerable
], обычно не позволяет изменять коллекцию путем добавления, удаления, замены или переупорядочения любых элементов.Если вы измените коллекцию после того, как вы запустили цикл For Each...Next
, объект перечислителя станет недействительным ...
Поэтому вам, возможно, придется свернуть собственную реализацию IEnumerator
, а не использовать ее повторно изCollection
.
Редактировать Я нашел эту ссылку , предлагая вам реализовать IEnumVARIANT
, что VBA не будет делать изначально ( edit но можно сделать, как показано выше!).Я сам не пробовал информацию по этой ссылке, но передал ее на случай, если она окажется полезной.