Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does iterating a Hashtable with `For Each` not work in VBScript?

Tags:

.net

com

vbscript

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.

like image 421
Micha Wiedenmann Avatar asked Mar 14 '23 21:03

Micha Wiedenmann


1 Answers

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
like image 91
Hans Passant Avatar answered Mar 18 '23 07:03

Hans Passant