4

Why can one iterate an ArrayList using For Each but not a Hashtable?

Dim i

For Each i In CreateObject("System.Collections.ArrayList") ' no error
Next

For Each i In CreateObject("System.Collections.Hashtable") ' error
Next

Iterating the HashTable gives

Object doesn't support this property or method.

Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
  • 1
    I guess the HashTable object hasn't been built with support for enumeration. Can you use a Dictionary instead? You can iterate through a Dictionary. – 404 Sep 04 '15 at 15:54

1 Answers1

7

Scripting languages have a technical limitation, they can only use the default interface of a coclass. They have no notion of interfaces at all and no back-door to obtain another interface through IUnknown::QueryInterface(). Like you can in C# by casting to the desired interface type. The iterator for ArrayList looks like this:

private sealed class ArrayListEnumeratorSimple : IEnumerator, ICloneable {
   // etc...
}

IEnumerator is the default interface, you have no problem using it from your VBScript. However, the enumerator for Hashtable looks like this:

private class HashtableEnumerator : IDictionaryEnumerator, IEnumerable, ICloneable {
   // etc..
}

IDictionaryEnumerator is the default, not IEnumerable. So VBScript cannot find the required Current and MoveNext members. Only Entry, Key and Value, they are useless. Much the same for the Keys and Values collection:

public class KeysCollection : ICollection, IEnumerable {
   // etc..
}

Same problem, CopyTo, Count, IsSynchronized and SyncRoot are useless. Microsoft could have very easily fixed this problem by applying the [ComDefaultInterface] attribute to these classes. But they didn't.


This can be worked around. What is required is code that can QI the default interface to obtain the IEnumerable interface. You can help with a wee C# class library project:

using System;
using System.Collections;
using System.Runtime.InteropServices;

namespace VBScript
{
    [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IMapper {
        IEnumerable ToEnum(object itf);
    }

    [ComVisible(true), ProgId("VBScript.Mapper")]
    public class Mapper : IMapper {
        public IEnumerable ToEnum(object itf) {
            return (IEnumerable)itf;
        }
    }
}

Build and register the assembly with both the 32-bit and 64-bit version of Regasm. Now you can make this script work:

Set table = CreateObject("System.Collections.Hashtable")
table.Add 1, "one"
table.Add 2, "two"
Set mapper = CreateObject("VBScript.Mapper")
For Each key in mapper.ToEnum(table.Keys)
   WScript.Echo key & ": " & table(key)
Next

Output:

Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

1: one
2: two
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I am confused by your first sentence, since `DictionaryEntry` seems to be `ComVisible(true)` (according to: https://msdn.microsoft.com/de-de/library/System.Collections.DictionaryEntry%28v=vs.110%29.aspx and http://referencesource.microsoft.com/#mscorlib/system/collections/dictionaryentry.cs). – Micha Wiedenmann Sep 07 '15 at 08:31
  • Oops, wrong name. Same problem with interfaces however. I'll fix it. – Hans Passant Sep 07 '15 at 08:40