如何在不循环的情况下将数组(Range.Value)传递给本机 .NET 类型?

Posted

技术标签:

【中文标题】如何在不循环的情况下将数组(Range.Value)传递给本机 .NET 类型?【英文标题】:How to pass an array (Range.Value) to a native .NET type without looping? 【发布时间】:2014-07-04 05:31:04 【问题描述】:

我想要做的是在 VBA 中使用 .AddRange() 方法填充 ArrayList,使用本地 C# ArrayList 上的后期绑定,但我不知道如何传递除另一个 ArrayList 之外的对象把它作为论据......到目前为止我尝试过的任何其他事情都失败了......

所以基本上我现在在做什么(注意:list 是 C# 的 ArrayList via mscorlib.dll)

Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")

Dim i As Long
For i = 1 To 5
    list.Add Range("A" & i).Value2
Next

但是,如果 i 可以是 = 500K,这是非常低效和丑陋的。

在 VBA 中这也有效:

ArrayList1.AddRange ArrayList2

但我真正需要/想要的是传递一个数组而不是 ArrayList2

所以I heard I can pass an array to the .AddRange() parameter in .NET. 我在一个小型 C# 控制台应用程序中对其进行了测试,它似乎工作得很好。以下在纯 C# 控制台应用程序中运行良好。

ArrayList list = new ArrayList();
string[] strArr = new string[1];
strArr[0] = "hello";
list.AddRange(strArr);

所以回到我的 VBA 模块尝试做 same 它失败了..

Dim arr(1) As Variant
arr(0) = "WHY!?"

Dim arrr As Variant
arrr = Range("A1:A5").Value

list.AddRange arr                     ' Fail
list.AddRange arrr                    ' Fail
list.AddRange Range("A1:A5").Value    ' Fail

注意:我已尝试传递原生 VBA 变体数组和 Collection、范围 - 除了另一个 ArrayList 之外的所有内容都失败了。

如何将原生 VBA 数组作为参数传递给 ArrayList

或者从 Range 创建集合而不循环的任何替代方法??

额外问题:*或者也许还有另一个内置的 .Net COM Visible Collection 可以从 VBA Range 对象或数组填充而无需循环并且已经有 .Reverse

注意:我知道我可以制作一个 .dll 包装器来实现这一点,但我对原生解决方案感兴趣 - 如果有的话


更新

为了更好地说明我为什么要完全避免显式迭代 - 这是一个示例(为简单起见,它仅使用一列

Sub Main()

    ' Initialize the .NET's ArrayList
    Dim list As Object
    Set list = CreateObject("System.Collections.ArrayList")


    ' There are two ways to populate the list.
    ' I selected this one as it's more efficient than a loop on a small set
    ' For details, see: http://www.dotnetperls.com/array-optimization
    list.Add Range("A1")
    list.Add Range("A2")
    list.Add Range("A3")
    list.Add Range("A4")
    list.Add Range("A5") ' It's OK with only five values but not with 50K.

    ' Alternative way to populate the list
    ' According to the above link this method has a worse performance
    'Dim i As Long
    'Dim arr2 As Variant
    'arr2 = Range("A1:A5").Value2
    'For i = 1 To 5
    '    list.Add arr2(i, 1)
    'Next

    ' Call native ArrayList.Reverse
    ' Note: no looping required!
    list.Reverse

    ' Get an array from the list
    Dim arr As Variant
    arr = list.ToArray

    ' Print reversed to Immediate Window
    'Debug.Print Join(arr, Chr(10))

    ' Print reversed list to spreadsheet starting at B1
    Range("B1").Resize(UBound(arr) + 1, 1) = Application.Transpose(arr)

End Sub

请注意:我唯一需要循环的是填充列表 (ArrayList) 我想做的就是找到一种方法将 arr2 加载到 ArrayList 或其他 .NET 兼容类型中,而无需循环。

在这一点上,我发现没有本地/内置方法可以做到这一点,这就是为什么我认为我将尝试实现自己的方式,也许如果可行,请为互操作库提交更新。

【问题讨论】:

AddRange 的参数必须实现 ICollection。 VBA 数组没有(显然互操作层也没有模拟这一点)。 为什么不一步将数据从范围加载到数组中(就像您现在所做的那样)然后循环遍历该数组?由于它在内存中(缓慢的步骤是查询 Range.Value),它应该对性能产生相当小的影响。 我看到了关于.Reverse 的小注释——你能描述一个具体的用例吗?我可以想象一个专用的本机 VBA 数据结构可以提供令人满意的性能,假设这就是您想要使用 .NET 库的原因。 @Blackhawk 实际上忽略了 .Reverse 。我最初认为,如果我可以将Range.ValueArrayList 形式(作为单列,或作为多维数组)传递给.Net,我可以轻松使用已经实现的方法,.Reverse 只是一个例子。重点是在 VBA 中收集一个基于 Range 的数组(一次性 Range.Value 没有循环),然后将其传递给原生 .Net 类型,也没有循环 我认为即使在 .NET 中本机使用 .AddRange(ICollection) 也会导致循环在幕后复制项目。但是,ArrayList 有一个方法.Adapter(IList),它简单地包装了一个 IList 并DOESN'T 复制它,使用 IList 方法对其进行就地修改!如果您对此感兴趣,我可以尝试实现该接口。当然,它仍然需要 VBA 包装类的“兼容层”才能使其正常工作。整个 api 可以简化为 WrapperFactory(var as Variant) 函数,但您需要携带类的包袱。 【参考方案1】:
   list.AddRange Range("A1:A5").Value 

范围的值被编组为一个数组。当然,这就是您可以想象的最基本的 .NET 类型。然而,这个有钟声,它不是一个“普通”的 .NET 数组。 VBA 是一个运行时环境,它喜欢创建第一个元素从索引 1 开始的数组。这是 .NET 中的不符合数组类型,CLR 喜欢第一个元素从索引 0 开始的数组。唯一的您可以使用的 .NET 类型是 System.Array 类。

一个额外的复杂因素是该数组是一个二维数组。这会让你尝试将它们转换为 ArrayList,多维数组没有枚举器。

所以这段代码可以正常工作:

    public void AddRange(object arg) 
        var arr = (Array)arg;
        for (int ix = ar.GetLowerBound(0); ix <= arr2.GetUpperBound(0); ++ix) 
            Debug.Print(arr.GetValue(ix, 1).ToString());
         
    

你可能不太在意这些。您可以使用一个小访问器类来包装笨拙的 Array 并且 acts 就像一个向量:

class Vba1DRange : IEnumerable<double> 
    private Array arr;
    public Vba1DRange(object vba) 
        arr = (Array)vba;
    
    public double this[int index] 
        get  return Convert.ToDouble(arr.GetValue(index + 1, 1)); 
        set  arr.SetValue(value, index + 1, 1); 
    
    public int Length  get  return arr.GetUpperBound(0);  
    public IEnumerator<double> GetEnumerator() 
        int upper = Length;
        for (int index = 0; index < upper; ++index)
            yield return this[index];
    
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
        return GetEnumerator();
    

现在你可以用“自然”的方式来写它了:

    public void AddRange(object arg) 
        var arr = new Vba1DRange(arg);
        foreach (double elem in arr) 
            Debug.Print(elem.ToString());
        
        // or:
        for (int ix = 0; ix < arr.Length; ++ix) 
            Debug.Print(arr[ix].ToString());
        
        // or:
        var list = new List<double>(arr);
    

【讨论】:

【参考方案2】:

这是作为@phoog 评论扩展的概念证明。正如他所指出的,AddRange 方法采用ICollection。

实现 ICollection

在 VBA IDE 中,添加对 mscorlib.tlb 的引用:Tools--->References,然后浏览找到您的 .NET Framework mscorlib.tlb。我的位于“C:\Windows\Microsoft.NET\Framework\vX.X.XXXXX\mscorlib.tlb”。

创建一个名为“clsWrapLongArray”的新类,如下所示:

Option Compare Database
Option Explicit

Implements ICollection
Dim m_lngArr() As Long

Public Sub LoadArray(lngArr() As Long)
    m_lngArr = lngArr
End Sub

Private Sub ICollection_CopyTo(ByVal arr As mscorlib.Array, ByVal index As Long)
    Dim i As Long
    Dim j As Long
    j = LBound(m_lngArr)
    For i = index To index + (ICollection_Count - 1)
        arr.SetValue m_lngArr(j), i
        j = j + 1
    Next
End Sub

Private Property Get ICollection_Count() As Long
    ICollection_Count = UBound(m_lngArr) - LBound(m_lngArr) + 1
End Property

Private Property Get ICollection_IsSynchronized() As Boolean
    'Never called for this example, so I'm leaving it blank
End Property

Private Property Get ICollection_SyncRoot() As Variant
    'Never called for this example, so I'm leaving it blank
End Property

Here is the Array.SetValue method used.

创建一个名为“mdlMain”的新模块来运行示例:

Option Compare Database
Option Explicit

Public Sub Main()

    Dim arr(0 To 3) As Long

    arr(0) = 1
    arr(1) = 2
    arr(2) = 3
    arr(3) = 4

    Dim ArrList As ArrayList
    Set ArrList = New ArrayList

    Dim wrap As clsWrapLongArray
    Set wrap = New clsWrapLongArray
    wrap.LoadArray arr
    ArrList.AddRange wrap
End Sub

如果您在End Sub 上放置一个断点并运行Main(),您可以通过在即时窗口中检查ArrList 看到它包含从Long 数组添加的4 个值。您还可以单步执行代码,看到ArrayList 实际调用了ICollection 接口成员CountCopyTo 来实现它。

我可以看到可以扩展这个想法来创建工厂函数来包装类型,如 Excel 范围、VBA 数组、集合等。

同样的方法也适用于字符串数组! :D

我通过谷歌搜索 "System.ArrayList" "SAFEARRAY""System.Array" "SAFEARRAY" 找到了一些非常接近的答案,并使用“COM Marshaling”进行了改进,因为 VBA 数组是 COM SAFEARRAY,但没有什么能解释如何将它们转换为用于 .NET 调用。

【讨论】:

【参考方案3】:

这是低效的,因为.Value2 是一个COM 调用。调用它数千次会增加很多互操作开销。在循环中向数组添加项目应该更快:

Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")

Dim i As Long
Dim a as Variant
a = Range("A1:A5").Value2
For i = 1 To 5
    list.Add a(i,1)
Next

如何将原生 VBA 数组作为参数传递给 ArrayList?

我不认为你可以 - 原生 VBA 数组不是ICollection,因此创建的唯一方法是将值复制到一个循环。

它在 C# 控制台应用程序中工作的原因是因为 C# 中的数组 ICollections,所以 AddRange 方法可以很好地接受它们。

【讨论】:

Value2 在示例中只调用了一次。我错过了什么吗? @ja72 是:Value2 在一个循环内,因此被多次调用。 是否有未显示的外循环?以上循环不调用Value2 您问的是我的答案还是原始问题?原始问题循环调用Value2 - 我的答案没有。 我同意,所以我不明白为什么它被否决了。我认为这是要走的路。

以上是关于如何在不循环的情况下将数组(Range.Value)传递给本机 .NET 类型?的主要内容,如果未能解决你的问题,请参考以下文章

C#如何在不知道新数组长度的情况下将数组中的值保存到新数组中

如何在不使用循环的情况下将json分配给另一个[重复]

如何在不使用循环的情况下将二进制转换为十进制?

如何在不使用 for 循环的情况下将列表中的所有项目与整数进行比较

如何在不清空 HTMLCollection 的情况下将其转换为数组?

Jquery 循环 - 如何在不破坏缩略图寻呼机的情况下将图像包装在跨度标签中?