Collections

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in the .NET 10 SDK container (Linux); the output shown is real output. Examples that are meant to fail say so and show the compiler messages. The project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq, System.Threading.Tasks and others) are why List(Of T), Dictionary(Of TKey, TValue), Queue(Of T) and the LINQ methods need no Imports line. Types from other namespaces (System.Collections.ObjectModel, System.Collections.Concurrent, System.Collections.Immutable, System.Collections.Frozen, System.ComponentModel) are imported in the examples that use them.

A collection holds a variable number of elements and offers operations that arrays do not: adding, removing, searching by key, keeping order, or giving first-in-first-out access. The choice among the .NET collection types is a choice of data structure, and the rest of this page follows it: List(Of T) for an ordered, growable sequence; Dictionary(Of TKey, TValue) for lookup by key; sets for membership and uniqueness; queue, stack, linked list and priority queue for specific access orders. Fixed-size sequences are covered by Arrays; the loops that walk collections by Control Flow; the query operators that transform them by LINQ Query Expressions; writing your own sequences with Yield by Iterators and Yield; and the generic syntax (Of, constraints, variance) by Generics. The C# counterpart is Collections and Iterators (C#).

Choosing a collection

Type What it is Typical costs Use it for

List(Of T)

growable array, ordered by position

index O(1); Add amortised O(1); Insert, Remove and Contains O(n); Sort O(n log n)

the default sequence

Dictionary(Of TKey, TValue)

hash table, unique keys

lookup, add, remove O(1) on average; no defined order

lookup by ISBN, by name, by id

HashSet(Of T)

hash set, unique items

Add, Contains, Remove O(1) on average; set algebra

membership tests, removing duplicates

SortedDictionary(Of TKey, TValue)

balanced tree of key/value pairs

O(log n); iterates in key order

ordered reports keyed by title

SortedList(Of TKey, TValue)

two parallel sorted arrays

lookup O(log n); insert O(n); access by position; least memory

small, read-mostly sorted maps

SortedSet(Of T)

balanced tree of items

O(log n); Min, Max, GetViewBetween

ordered unique items, range queries

Queue(Of T)

first in, first out ring buffer

Enqueue, Dequeue, Peek O(1)

waiting lists, work items

Stack(Of T)

last in, first out

Push, Pop, Peek O(1)

undo, parsing, depth-first walks

LinkedList(Of T)

doubly linked nodes

insert or remove next to a node you hold O(1); Find O(n)

frequent splicing; rarely the right choice

PriorityQueue(Of TElement, TPriority)

binary heap, smallest priority first

Enqueue, Dequeue O(log n); Peek O(1)

schedulers, "most overdue first" (.NET 6)

ReadOnlyCollection(Of T), IReadOnlyList(Of T)

read-only view

as the wrapped list

exposing a collection without letting callers change it

ImmutableList(Of T), ImmutableArray(Of T), ImmutableDictionary(...)

persistent, never changes

changes return a new collection

shared state, thread safety by construction

FrozenDictionary(...), FrozenSet(Of T)

built once, fastest to read

slow to build

lookup tables filled at start-up (.NET 8)

ConcurrentDictionary, ConcurrentQueue, BlockingCollection and others

thread-safe

see the threads page

several threads share the collection

Collection (Microsoft.VisualBasic)

1-based, keyed, holds Object

slow, untyped

only when migrating old code

The same decision as a diagram:

flowchart TD start(["I need to hold several items"]) --> q1{"Is the size fixed
and known up front?"} q1 -- yes --> arr["Array: see the Arrays page"] q1 -- no --> q2{"Do I look items up
by a key?"} q2 -- yes --> q3{"Must iteration
be sorted by key?"} q3 -- yes --> sd["SortedDictionary
or SortedList"] q3 -- no --> d["Dictionary"] q2 -- no --> q4{"Only membership tests
and no duplicates?"} q4 -- yes --> q5{"Sorted order
or ranges needed?"} q5 -- yes --> ss["SortedSet"] q5 -- no --> hs["HashSet"] q4 -- no --> q6{"Special access order?"} q6 -- "first in, first out" --> qu["Queue"] q6 -- "last in, first out" --> st["Stack"] q6 -- "smallest priority first" --> pq["PriorityQueue"] q6 -- "splice at a held node" --> ll["LinkedList"] q6 -- "no, just a sequence" --> l["List"]

List(Of T)

List(Of T) is the workhorse. It stores its elements in an internal array that doubles when it is full, so appending is cheap on average and indexing is as fast as for an array. Count is the number of elements; Capacity is the size of the internal array. The indexer list(i) is the default property; it throws ArgumentOutOfRangeException (not IndexOutOfRangeException) when i is outside 0 to Count - 1.

Option Strict On

Module Program
    Sub Main()
        Dim titles As New List(Of String)
        titles.Add("Refactoring")
        titles.AddRange({"Clean Code", "Domain-Driven Design"})
        titles.Insert(0, "The Pragmatic Programmer")
        Console.WriteLine("Count = " & titles.Count & ", first = " & titles(0) & ", last = " & titles(titles.Count - 1))

        ' Count is the number of elements, Capacity the size of the internal array
        Dim sized As New List(Of Integer)(100)
        Console.WriteLine("new list with capacity: Count = " & sized.Count & ", Capacity = " & sized.Capacity)
        Dim growing As New List(Of Integer)
        Dim capacities As New List(Of Integer)
        For i = 1 To 20
            growing.Add(i)
            If Not capacities.Contains(growing.Capacity) Then capacities.Add(growing.Capacity)
        Next
        Console.WriteLine("capacity steps while adding 20 items: " & String.Join(" -> ", capacities))

        ' Searching
        Console.WriteLine("IndexOf Clean Code = " & titles.IndexOf("Clean Code"))
        Console.WriteLine("Contains missing    = " & titles.Contains("Missing"))
        Console.WriteLine("Find > 15 chars     = " & titles.Find(Function(t) t.Length > 15))
        Console.WriteLine("FindAll short       = " & String.Join(",", titles.FindAll(Function(t) t.Length < 12)))
        Console.WriteLine("Exists Refactoring  = " & titles.Exists(Function(t) t = "Refactoring"))

        ' Removing
        titles.Remove("Refactoring")
        titles.RemoveAt(0)
        Console.WriteLine("after removals: " & String.Join(" | ", titles))
        Console.WriteLine("Remove returns False when nothing matched: " & titles.Remove("Missing"))

        Dim numbers As New List(Of Integer) From {5, 3, 8, 1, 9, 2}
        Dim removed = numbers.RemoveAll(Function(n) n Mod 2 = 0)
        Console.WriteLine("RemoveAll removed " & removed & ", left: " & String.Join(",", numbers))

        ' Sorting, reversing, ranges, conversion
        numbers.Sort()
        Console.WriteLine("Sort:    " & String.Join(",", numbers))
        Console.WriteLine("BinarySearch(5) on the sorted list: " & numbers.BinarySearch(5) & ", for 4: " & numbers.BinarySearch(4))
        numbers.Sort(Function(left, right) right.CompareTo(left))
        Console.WriteLine("Sort desc: " & String.Join(",", numbers))
        numbers.Reverse()
        Console.WriteLine("Reverse: " & String.Join(",", numbers))
        Console.WriteLine("GetRange(1, 2): " & String.Join(",", numbers.GetRange(1, 2)))
        Dim asArray As Integer() = numbers.ToArray()
        Console.WriteLine("ToArray length: " & asArray.Length)

        ' Index errors
        Try
            Console.WriteLine(numbers(numbers.Count))
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine(ex.GetType().Name & ": " & ex.ParamName)
        End Try

        ' Modifying while iterating
        Try
            For Each n In numbers
                If n = 5 Then numbers.Remove(n)
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        titles.Clear()
        titles.TrimExcess()
        Console.WriteLine("after Clear and TrimExcess: Count = " & titles.Count & ", Capacity = " & titles.Capacity)
    End Sub
End Module
Count = 4, first = The Pragmatic Programmer, last = Domain-Driven Design
new list with capacity: Count = 0, Capacity = 100
capacity steps while adding 20 items: 4 -> 8 -> 16 -> 32
IndexOf Clean Code = 2
Contains missing    = False
Find > 15 chars     = The Pragmatic Programmer
FindAll short       = Refactoring,Clean Code
Exists Refactoring  = True
after removals: Clean Code | Domain-Driven Design
Remove returns False when nothing matched: False
RemoveAll removed 2, left: 5,3,1,9
Sort:    1,3,5,9
BinarySearch(5) on the sorted list: 2, for 4: -3
Sort desc: 9,5,3,1
Reverse: 1,3,5,9
GetRange(1, 2): 3,5
ToArray length: 4
ArgumentOutOfRangeException: index
InvalidOperationException: Collection was modified; enumeration operation may not execute.
after Clear and TrimExcess: Count = 0, Capacity = 0

Source: List<T> Class

Highlights of the output:

  • The capacity went 4 -> 8 -> 16 -> 32 while 20 items were added (the first Add allocates 4). Pass the expected size to the constructor (New List(Of Integer)(100)) when you know it, or call EnsureCapacity, to avoid the re-allocations. TrimExcess gives memory back.

  • Remove(item) returns False when nothing matched; RemoveAll(predicate) returns the number removed and is the safe way to filter a list in place (see Iterating and modifying).

  • BinarySearch needs a list that is already sorted by the same comparer; it returns the index, or the bitwise complement of the insertion point (-3 means "insert at index 2") when the item is absent.

  • Sort is unstable; use OrderBy (stable, returns a new sequence) when equal elements must keep their order.

  • Looking something up in a List(Of T) with Contains, IndexOf or Find is a linear scan. If you do that in a loop over a large list, use a HashSet or a Dictionary.

List(Of T) implements IList(Of T), IReadOnlyList(Of T) and IEnumerable(Of T). Parameters that only read should use the interface; return types should say what the caller may do with the result.

Collection initializers

A collection initializer fills a new collection with From followed by braces. The compiler translates each element into a call to an Add method of the object. The type must therefore implement IEnumerable and have an accessible Add; the extension method Add from any imported namespace counts too. For a Dictionary, each entry is its own pair of braces, because Add(key, value) takes two arguments: {"a", 1}. A braced group with several values is passed to one Add call with several arguments, which lets you pick the overload.

Option Strict On

Imports System.Runtime.CompilerServices

' A type with an instance Add method works with From
Public Class Tags
    Implements IEnumerable(Of String)

    Private ReadOnly _items As New List(Of String)

    Public Sub Add(tag As String)
        _items.Add(tag.ToLowerInvariant())
    End Sub

    Public Sub Add(first As String, second As String)       ' Add overloads are picked by the number of items in the braces
        Add(first)
        Add(second)
    End Sub

    Public Function GetEnumerator() As IEnumerator(Of String) Implements IEnumerable(Of String).GetEnumerator
        Return _items.GetEnumerator()
    End Function

    Private Function GetEnumeratorNonGeneric() As Collections.IEnumerator Implements Collections.IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function
End Class

Module QueueExtensions
    ' An extension Add makes From work for types you do not own
    <Extension>
    Public Sub Add(Of T)(queue As Queue(Of T), item As T)
        queue.Enqueue(item)
    End Sub
End Module

Public Class Rack
    Public Property Name As String = ""
    Public Property Books As New List(Of String)
End Class

Module Program
    Sub Main()
        Dim primes As New List(Of Integer) From {2, 3, 5, 7}
        Dim sameThing As List(Of Integer) = New List(Of Integer) From {2, 3, 5, 7}
        Console.WriteLine(primes.Count & " " & sameThing.Count)

        ' Dictionary: one pair of braces per entry
        Dim stock As New Dictionary(Of String, Integer) From {
            {"9780132350884", 3},
            {"9780134494166", 1}
        }
        Console.WriteLine("stock: " & stock.Count & " entries, first = " & stock("9780132350884"))

        ' A dictionary can also be built from a sequence of pairs
        Dim fromPairs = New Dictionary(Of String, Integer)(
            {New KeyValuePair(Of String, Integer)("a", 1), New KeyValuePair(Of String, Integer)("b", 2)})
        Console.WriteLine("from pairs: " & fromPairs.Count)

        ' Custom Add: the compiler calls Add once per brace group
        Dim tags As New Tags From {"Refactoring", {"Design", "TESTING"}}
        Console.WriteLine("tags: " & String.Join(",", tags))

        ' Extension Add
        Dim line As New Queue(Of String) From {"first", "second", "third"}
        Console.WriteLine("queue: " & String.Join(",", line) & ", Peek = " & line.Peek())

        ' Object initializer and collection initializer together
        Dim rack As New Rack With {
            .Name = "Software",
            .Books = New List(Of String) From {"Refactoring", "Clean Code"}
        }
        Console.WriteLine(rack.Name & ": " & String.Join(" and ", rack.Books))

        ' A property that already holds a list can be filled after construction
        Dim spare As New Rack With {.Name = "Spare"}
        spare.Books.AddRange({"X", "Y"})
        Console.WriteLine(spare.Name & " has " & spare.Books.Count & " books")

        ' Nested collections
        Dim byAuthor As New Dictionary(Of String, List(Of String)) From {
            {"Fowler", New List(Of String) From {"Refactoring"}},
            {"Martin", New List(Of String) From {"Clean Code", "Clean Architecture"}}
        }
        For Each pair In byAuthor
            Console.WriteLine(pair.Key & ": " & pair.Value.Count)
        Next
    End Sub
End Module
4 4
stock: 2 entries, first = 3
from pairs: 2
tags: refactoring,design,testing
queue: first,second,third, Peek = first
Software: Refactoring and Clean Code
Spare has 2 books
Fowler: 1
Martin: 2

Rules:

  • From is the Visual Basic 10 syntax (Visual Studio 2010). Building at LangVersion 9 gives BC36716, "Visual Basic 9.0 does not support collection initializers."

  • From may be combined with an object initializer: New Rack With {.Name = "x", .Books = New List(Of String) From {"a"}}.

  • New List(Of Integer)(100) From {1, 2} is legal: the constructor argument is the capacity (the list ends up with Count 2 and Capacity 100), not the first item.

  • The initializer calls Add; if Add throws (for example a duplicate dictionary key) the whole expression throws.

  • Visual Basic has no index initializer ({["a"] = 1} in C#) and no collection expression ([1, 2, 3] in C# 12): both are syntax errors. The old {1, 2, 3} brace list is an array literal when there is no From.

  • An existing list in a property that is not assigned can be filled after construction with AddRange or Add, as the Spare rack in the example shows.

Option Strict On

Module Program
    Public Class NoAdd
        Public Property Name As String = ""
    End Class

    Sub Main()
        Dim bad As New NoAdd From {"a", "b"}
        Dim list As New List(Of Integer) From {1, "two"}
        Dim dict As New Dictionary(Of String, Integer) From {{"a", 1}, {"b"}}
        Dim fixedArray As Integer() = New List(Of Integer) From {1}
    End Sub
End Module
Program.vb(9,30): error BC36718: Cannot initialize the type 'NoAdd' with a collection initializer because it is not a collection type.
Program.vb(10,51): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.
Program.vb(11,72): error BC30455: Argument not specified for parameter 'value' of 'Public Overloads Sub Add(key As String, value As Integer)'.
Program.vb(12,39): error BC30311: Value of type 'List(Of Integer)' cannot be converted to 'Integer()'.
Option Strict On

Module Program
    Sub Main()
        Dim d As New Dictionary(Of String, Integer) From {["a"] = 1}
    End Sub
End Module
Program.vb(5,59): error BC30201: Expression expected.
Program.vb(5,59): error BC30203: Identifier expected.
Program.vb(5,63): error BC30037: Character is not valid.
Option Strict On

Module Program
    Sub Main()
        Dim list As New List(Of Integer) From {1, 2, 3}
        Dim x = list[0]
    End Sub
End Module
Program.vb(6,21): error BC30203: Identifier expected.

Dictionary(Of TKey, TValue)

Dictionary maps unique keys to values using a hash table. Lookup, insertion and removal are O(1) on average. The class relies on two members of the key type, GetHashCode and Equals (or the IEqualityComparer(Of TKey) you pass to the constructor); a key whose hash code changes while it is in the dictionary can never be found again, so use immutable keys (strings, integers, structures with read-only fields, Isbn in the shelf scenario).

Option Strict On

Module Program
    Sub Main()
        Dim stock As New Dictionary(Of String, Integer)
        stock.Add("Refactoring", 3)
        stock("Clean Code") = 1               ' the indexer adds or replaces
        stock("Refactoring") = 4              ' replaced, no exception
        Console.WriteLine("Count = " & stock.Count & ", Refactoring = " & stock("Refactoring"))

        Try
            stock.Add("Clean Code", 9)
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Try
            Console.WriteLine(stock("Missing"))
        Catch ex As KeyNotFoundException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Dim copies As Integer
        If stock.TryGetValue("Clean Code", copies) Then
            Console.WriteLine("TryGetValue found " & copies)
        End If
        If Not stock.TryGetValue("Missing", copies) Then
            Console.WriteLine("TryGetValue left copies = " & copies & " when the key was absent")
        End If

        Console.WriteLine("GetValueOrDefault: " & stock.GetValueOrDefault("Missing", -1))
        Console.WriteLine("TryAdd existing: " & stock.TryAdd("Clean Code", 5) & ", TryAdd new: " & stock.TryAdd("Domain-Driven Design", 2))
        Console.WriteLine("ContainsKey: " & stock.ContainsKey("Clean Code") & ", ContainsValue(2): " & stock.ContainsValue(2))
        Console.WriteLine("Remove: " & stock.Remove("Clean Code") & ", again: " & stock.Remove("Clean Code"))

        ' The bang operator is shorthand for the indexer with a literal String key
        Console.WriteLine("stock!Refactoring = " & stock!Refactoring)

        ' Enumerating gives KeyValuePair(Of TKey, TValue)
        For Each entry In stock
            Console.WriteLine("  " & entry.Key & " -> " & entry.Value)
        Next
        Console.WriteLine("Keys: " & String.Join(", ", stock.Keys) & "; Values: " & String.Join(", ", stock.Values))

        ' Key comparison is part of the dictionary
        Dim loose As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
        loose("ISBN") = 1
        Console.WriteLine("case-insensitive lookup: " & loose("isbn"))
        Dim strict As New Dictionary(Of String, Integer)
        strict("ISBN") = 1
        Console.WriteLine("default comparer finds isbn: " & strict.ContainsKey("isbn"))

        ' Nothing is not a valid key
        Try
            Dim nothingKey As String = Nothing
            stock(nothingKey) = 1
        Catch ex As ArgumentNullException
            Console.WriteLine(ex.GetType().Name & ": " & ex.ParamName)
        End Try

        ' Counting words: the classic dictionary exercise
        Dim words = "the cat and the hat and the bat".Split(" "c)
        Dim counts As New Dictionary(Of String, Integer)
        For Each word In words
            Dim n As Integer
            counts.TryGetValue(word, n)
            counts(word) = n + 1
        Next
        For Each pair In counts.OrderByDescending(Function(p) p.Value).ThenBy(Function(p) p.Key)
            Console.WriteLine(pair.Key & "=" & pair.Value)
        Next
    End Sub
End Module
Count = 2, Refactoring = 4
ArgumentException: An item with the same key has already been added. Key: Clean Code
KeyNotFoundException: The given key 'Missing' was not present in the dictionary.
TryGetValue found 1
TryGetValue left copies = 0 when the key was absent
GetValueOrDefault: -1
TryAdd existing: False, TryAdd new: True
ContainsKey: True, ContainsValue(2): True
Remove: True, again: False
stock!Refactoring = 4
  Refactoring -> 4
  Domain-Driven Design -> 2
Keys: Refactoring, Domain-Driven Design; Values: 4, 2
case-insensitive lookup: 1
default comparer finds isbn: False
ArgumentNullException: key
the=3
and=2
bat=1
cat=1
hat=1
Operation Behaviour

d(key) = v

adds the pair or replaces the value; never throws for an existing key

d.Add(key, v)

throws ArgumentException for an existing key

d(key) (read)

throws KeyNotFoundException when the key is absent

d.TryGetValue(key, value)

returns Boolean; the ByRef argument is set to Default(TValue) when absent. In Visual Basic declare the variable first: Dim copies As Integer and then d.TryGetValue(key, copies); there is no out var.

d.TryAdd(key, v)

adds only if absent; returns whether it did (.NET Core 2.0 and later)

d.GetValueOrDefault(key, fallback)

extension method from System.Collections.Generic.CollectionExtensions; returns the fallback when absent

d.Remove(key)

returns True if the key was present

d.ContainsKey(key), d.ContainsValue(v)

O(1) and O(n)

Nothing as a key

ArgumentNullException for reference-type keys

For Each pair In d

gives KeyValuePair(Of TKey, TValue) with Key and Value; the iteration order is not part of the contract (in practice it follows insertion order until something is removed)

The bang operator d!key is shorthand for d("key") when the default property takes a String; it exists for VB6 compatibility and works with Dictionary(Of String, T). It is documented on Properties and Default Properties. Applied to a collection whose default property takes an Integer, such as List(Of T), it is a type error (BC30512):

Option Strict On

Module Program
    Sub Main()
        Dim list As New List(Of Integer)
        Dim x = list!first
    End Sub
End Module
Program.vb(6,22): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.

Counting words with TryGetValue and an indexer, as at the end of the example, is the standard idiom; it looks the key up twice, once to read and once to write. Where that matters, Dictionary has CollectionsMarshal.GetValueRefOrAddDefault, but its ByRef return cannot be used from Visual Basic, so use TryGetValue plus the indexer, or ConcurrentDictionary.AddOrUpdate for shared counters.

Visual Basic has no tuple deconstruction in For Each: C# foreach (var (key, value) in d) has no counterpart. Declare the pair variable and read pair.Key and pair.Value:

Option Strict On

Module Program
    Sub Main()
        Dim list As New List(Of Integer)
        Dim count = list.Length
        Dim text As List(Of String) = New List(Of Object)
        Dim d As New Dictionary(Of String, Integer)
        For Each (key, value) In d
        Next
    End Sub
End Module
Program.vb(6,21): error BC30456: 'Length' is not a member of 'List(Of Integer)'.
Program.vb(7,39): error BC30311: Value of type 'List(Of Object)' cannot be converted to 'List(Of String)'.
Program.vb(9,19): error BC30451: 'key' is not declared. It may be inaccessible due to its protection level.
Program.vb(9,24): error BC30451: 'value' is not declared. It may be inaccessible due to its protection level.

Note that KeyValuePair(Of TKey, TValue).Value is read-only (BC30526 in the next example), so updating a value during enumeration means calling the indexer, which is allowed for an existing key, as the "Iterating and modifying" section shows.

Option Strict On

Module Program
    Sub Main()
        Dim d As New Dictionary(Of String, Integer)
        For Each pair As KeyValuePair(Of String, Integer) In d
            pair.Value = 1
        Next
        Dim untyped = New List(Of Integer)
        Dim x As Integer = untyped
        Dim q As New Queue
        Dim s As Stack(Of Integer) = New Stack
    End Sub
End Module
Program.vb(7,13): error BC30526: Property 'Value' is 'ReadOnly'.
Program.vb(10,28): error BC30311: Value of type 'List(Of Integer)' cannot be converted to 'Integer'.
Program.vb(12,38): error BC30311: Value of type 'Stack' cannot be converted to 'Stack(Of Integer)'.

Sets and sorted collections

A set holds each item at most once. HashSet(Of T).Add returns False for a duplicate instead of throwing, and the set operations (UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith) change the set in place, so copy first when you need both inputs afterwards: Dim both As New HashSet(Of String)(readers). IsSubsetOf, IsSupersetOf, Overlaps and SetEquals answer questions without changing anything.

Option Strict On

Module Program
    Sub Main()
        Dim readers As New HashSet(Of String) From {"Ana", "Ben", "Chen"}
        Console.WriteLine("Add new: " & readers.Add("Dora") & ", Add duplicate: " & readers.Add("Ana") & ", Count = " & readers.Count)

        Dim borrowers As New HashSet(Of String) From {"Chen", "Dora", "Eli"}
        Dim both As New HashSet(Of String)(readers)
        both.IntersectWith(borrowers)
        Dim either As New HashSet(Of String)(readers)
        either.UnionWith(borrowers)
        Dim onlyReaders As New HashSet(Of String)(readers)
        onlyReaders.ExceptWith(borrowers)
        Dim exclusive As New HashSet(Of String)(readers)
        exclusive.SymmetricExceptWith(borrowers)
        Console.WriteLine("intersect: " & String.Join(",", both.OrderBy(Function(s) s)))
        Console.WriteLine("union:     " & String.Join(",", either.OrderBy(Function(s) s)))
        Console.WriteLine("except:    " & String.Join(",", onlyReaders.OrderBy(Function(s) s)))
        Console.WriteLine("symmetric: " & String.Join(",", exclusive.OrderBy(Function(s) s)))
        Console.WriteLine("Overlaps: " & readers.Overlaps(borrowers) & ", IsSubsetOf: " & both.IsSubsetOf(readers) & ", SetEquals: " & readers.SetEquals({"Dora", "Chen", "Ben", "Ana"}))

        ' SortedSet keeps order and offers Min, Max and views
        Dim years As New SortedSet(Of Integer) From {1999, 2008, 2002, 2018, 2002}
        Console.WriteLine("SortedSet: " & String.Join(",", years) & " (duplicate dropped), Min = " & years.Min & ", Max = " & years.Max)
        Console.WriteLine("view 2000..2010: " & String.Join(",", years.GetViewBetween(2000, 2010)))

        ' SortedDictionary iterates in key order, SortedList also supports access by position
        Dim byTitle As New SortedDictionary(Of String, Integer) From {{"Refactoring", 1999}, {"Clean Code", 2008}, {"Agile", 2001}}
        For Each pair In byTitle
            Console.Write(pair.Key & ":" & pair.Value & " ")
        Next
        Console.WriteLine()
        Dim list As New SortedList(Of String, Integer)(byTitle)
        Console.WriteLine("SortedList: Keys(0) = " & list.Keys(0) & ", Values(2) = " & list.Values(2) & ", IndexOfKey(Clean Code) = " & list.IndexOfKey("Clean Code"))

        ' A custom comparer defines both order and equality for sorted collections
        Dim byLengthThenName As New SortedSet(Of String)(Comparer(Of String).Create(
            Function(left, right)
                Dim c = left.Length.CompareTo(right.Length)
                Return If(c <> 0, c, String.CompareOrdinal(left, right))
            End Function)) From {"pear", "fig", "apple", "kiwi"}
        Console.WriteLine("by length: " & String.Join(",", byLengthThenName))
    End Sub
End Module
Add new: True, Add duplicate: False, Count = 4
intersect: Chen,Dora
union:     Ana,Ben,Chen,Dora,Eli
except:    Ana,Ben
symmetric: Ana,Ben,Eli
Overlaps: True, IsSubsetOf: True, SetEquals: True
SortedSet: 1999,2002,2008,2018 (duplicate dropped), Min = 1999, Max = 2018
view 2000..2010: 2002,2008
Agile:2001 Clean Code:2008 Refactoring:1999
SortedList: Keys(0) = Agile, Values(2) = 1999, IndexOfKey(Clean Code) = 1
by length: fig,kiwi,pear,apple

The sorted collections keep their items ordered by the default comparer of the item (or key) type, or by a comparer you supply. A comparer also decides what "equal" means in a SortedSet, so a comparer that returns 0 for two different items makes the second one a duplicate. See Sorted collection types for the guidance on picking among SortedDictionary, SortedList and SortedSet.

Queue, Stack, LinkedList and PriorityQueue

These four types restrict how items go in and come out, which makes the intent of the code obvious and keeps the operations O(1) (or O(log n) for the heap). Queue(Of T) and Stack(Of T) throw InvalidOperationException when you Dequeue, Pop or Peek while empty; the TryDequeue, TryPop and TryPeek forms return False instead and fill a variable you declared. PriorityQueue(Of TElement, TPriority) returns the smallest priority first; give it a negated priority or a comparer for "largest first". Enumerating a Stack goes from the top.

Option Strict On

Module Program
    Sub Main()
        ' Queue: first in, first out (holds on a popular title)
        Dim holds As New Queue(Of String)
        holds.Enqueue("Ana")
        holds.Enqueue("Ben")
        holds.Enqueue("Chen")
        Console.WriteLine("Peek = " & holds.Peek() & ", Count = " & holds.Count)
        Console.WriteLine("Dequeue = " & holds.Dequeue() & ", next = " & holds.Peek())
        Dim nextReader As String = Nothing
        Console.WriteLine("TryDequeue: " & holds.TryDequeue(nextReader) & " -> " & nextReader)
        holds.Clear()
        Console.WriteLine("TryDequeue on empty: " & holds.TryDequeue(nextReader))
        Try
            holds.Dequeue()
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        ' Stack: last in, first out (undo of catalogue edits)
        Dim undo As New Stack(Of String)
        undo.Push("add Refactoring")
        undo.Push("rename Clean Code")
        undo.Push("lend Clean Code")
        Console.WriteLine("Pop = " & undo.Pop() & ", Peek = " & undo.Peek())
        Console.WriteLine("enumeration order (top first): " & String.Join(" | ", undo))

        ' LinkedList: O(1) insertion next to a node you already hold
        Dim route As New LinkedList(Of String)
        Dim stopA = route.AddLast("A")
        route.AddLast("C")
        route.AddAfter(stopA, "B")
        route.AddFirst("Start")
        Console.WriteLine("route: " & String.Join(" > ", route))
        Dim found = route.Find("B")
        Console.WriteLine("before B: " & found.Previous.Value & ", after B: " & found.Next.Value)
        route.Remove(found)
        Console.WriteLine("after Remove: " & String.Join(" > ", route))

        ' PriorityQueue: the smallest priority comes out first
        Dim overdue As New PriorityQueue(Of String, Integer)
        overdue.Enqueue("Clean Code", 3)
        overdue.Enqueue("Refactoring", 12)
        overdue.Enqueue("Agile", 1)
        Dim title As String = Nothing
        Dim days As Integer
        Do While overdue.TryDequeue(title, days)
            Console.WriteLine(title & " (" & days & " days)")
        Loop
    End Sub
End Module
Peek = Ana, Count = 3
Dequeue = Ana, next = Ben
TryDequeue: True -> Ben
TryDequeue on empty: False
InvalidOperationException: Queue empty.
Pop = lend Clean Code, Peek = rename Clean Code
enumeration order (top first): rename Clean Code | add Refactoring
route: Start > A > B > C
before B: A, after B: C
after Remove: Start > A > C
Agile (1 days)
Clean Code (3 days)
Refactoring (12 days)

Source: Queue<T> Class

LinkedList(Of T) gives O(1) insertion and removal next to a LinkedListNode(Of T) you already hold (AddAfter, AddBefore, Remove(node)), but Find is a linear scan, nodes are separate heap objects, and cache behaviour is poor. In practice a List(Of T) beats it for all but very large lists with heavy splicing.

Iterating and modifying

For Each over a collection uses an enumerator that remembers the version of the collection when it started. If the collection is structurally changed during the loop (Add, Remove, Insert, Clear, or setting an item through a List(Of T) indexer), the next step throws InvalidOperationException ("Collection was modified; enumeration operation may not execute"). The first failing loop in the example below also leaves the list half-changed, because the item was already removed when the exception came.

Option Strict On

Module Program
    Sub Main()
        Dim list As New List(Of Integer) From {1, 2, 3, 4, 5, 6}

        ' Wrong: the enumerator notices the version change
        Try
            For Each n In list
                If n Mod 2 = 0 Then list.Remove(n)
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine("List: " & ex.Message)
        End Try
        Console.WriteLine("list after the failed loop: " & String.Join(",", list))

        ' Right 1: RemoveAll
        list = New List(Of Integer) From {1, 2, 3, 4, 5, 6}
        list.RemoveAll(Function(n) n Mod 2 = 0)
        Console.WriteLine("RemoveAll:      " & String.Join(",", list))

        ' Right 2: walk backwards by index
        list = New List(Of Integer) From {1, 2, 3, 4, 5, 6}
        For i = list.Count - 1 To 0 Step -1
            If list(i) Mod 2 = 0 Then list.RemoveAt(i)
        Next
        Console.WriteLine("backwards loop: " & String.Join(",", list))

        ' Right 3: iterate over a copy
        list = New List(Of Integer) From {1, 2, 3, 4, 5, 6}
        For Each n In list.ToList()
            If n Mod 2 = 0 Then list.Remove(n)
        Next
        Console.WriteLine("copy then loop: " & String.Join(",", list))

        ' Dictionary: Remove during enumeration is allowed on .NET Core 3.0 and later, but adding is not
        Dim stock As New Dictionary(Of String, Integer) From {{"a", 1}, {"b", 0}, {"c", 2}, {"d", 0}}
        For Each pair In stock
            If pair.Value = 0 Then stock.Remove(pair.Key)
        Next
        Console.WriteLine("Dictionary after Remove in loop: " & String.Join(",", stock.Keys))
        Try
            For Each pair In stock
                stock("z") = 9
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine("Dictionary add in loop: " & ex.GetType().Name)
        End Try

        ' Setting a value for an existing key during enumeration is also allowed
        For Each key In stock.Keys.ToList()
            stock(key) = stock(key) * 10
        Next
        Console.WriteLine("values multiplied: " & String.Join(",", stock.Values))

        ' HashSet.RemoveWhere
        Dim set1 As New HashSet(Of Integer)({1, 2, 3, 4})
        Console.WriteLine("RemoveWhere removed " & set1.RemoveWhere(Function(n) n > 2))

        ' Changing an element's property is not a structural change
        Dim names As New List(Of String) From {"a", "b"}
        For i = 0 To names.Count - 1
            names(i) = names(i).ToUpperInvariant()
        Next
        Console.WriteLine(String.Join(",", names))
        ' But replacing an element through the indexer inside For Each does bump the version
        Try
            For Each n In names
                names(0) = "changed"
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine("indexer set in For Each: " & ex.GetType().Name)
        End Try
    End Sub
End Module
List: Collection was modified; enumeration operation may not execute.
list after the failed loop: 1,3,4,5,6
RemoveAll:      1,3,5
backwards loop: 1,3,5
copy then loop: 1,3,5
Dictionary after Remove in loop: a,c
Dictionary add in loop: InvalidOperationException
values multiplied: 10,20,90
RemoveWhere removed 2
A,B
indexer set in For Each: InvalidOperationException
Situation Do this

remove items that match a condition

list.RemoveAll(predicate), set.RemoveWhere(predicate), or build a filtered list with LINQ and assign it

remove while walking by index

loop backwards: For i = list.Count - 1 To 0 Step -1

need to change the collection while reading it

iterate over a copy: For Each x In list.ToList()

change the value of existing dictionary entries

iterate d.Keys.ToList() and assign through the indexer

remove entries from a Dictionary while iterating

allowed on .NET Core 3.0 and later (Remove and Clear do not invalidate the enumerator); on .NET Framework it throws. Adding a key still throws on every version

change a property of the elements

fine: that is not a structural change of the collection

Some further rules for loops over collections:

  • The loop variable of For Each has the element type. For a Dictionary it is KeyValuePair(Of TKey, TValue); for the old ArrayList and the VB Collection it is Object, which under Option Strict On requires For Each item As Object In ... and an explicit conversion afterwards.

  • A Do While queue.Count > 0 loop is the natural way to drain a Queue, Stack or PriorityQueue; items may be added inside the loop because the loop does not use an enumerator.

  • Parallel.ForEach and ConcurrentDictionary are the choices when several threads are involved (see Threads, Parallelism and Synchronization).

Equality, ordering and comparers

Collections rely on three contracts. Hash-based collections (Dictionary, HashSet) need Equals and a matching GetHashCode, or an IEqualityComparer(Of T). Sorted collections and Sort need IComparable(Of T) or an IComparer(Of T). Searching methods (Contains, IndexOf, Remove) use Equals. For a structure used as a key, implement IEquatable(Of T) (avoids boxing) and override Equals(Object) and GetHashCode together; see Operator Overloading and Equality.

Option Strict On

Public Structure Isbn
    Implements IEquatable(Of Isbn), IComparable(Of Isbn)

    Public ReadOnly Property Value As String

    Public Sub New(value As String)
        Me.Value = value
    End Sub

    Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
        Return String.Equals(Value, other.Value, StringComparison.Ordinal)
    End Function

    Public Overrides Function Equals(obj As Object) As Boolean
        Return TypeOf obj Is Isbn AndAlso Equals(DirectCast(obj, Isbn))
    End Function

    Public Overrides Function GetHashCode() As Integer
        Return If(Value, "").GetHashCode()
    End Function

    Public Function CompareTo(other As Isbn) As Integer Implements IComparable(Of Isbn).CompareTo
        Return String.CompareOrdinal(Value, other.Value)
    End Function

    Public Overrides Function ToString() As String
        Return Value
    End Function
End Structure

' A comparer that treats two titles as the same when only case or surrounding blanks differ
Public Class TitleComparer
    Implements IEqualityComparer(Of String)

    Private Shared Function Normalize(text As String) As String
        Return text.Trim().ToUpperInvariant()
    End Function

    Public Overloads Function Equals(x As String, y As String) As Boolean Implements IEqualityComparer(Of String).Equals
        Return Normalize(x) = Normalize(y)
    End Function

    Public Overloads Function GetHashCode(obj As String) As Integer Implements IEqualityComparer(Of String).GetHashCode
        Return Normalize(obj).GetHashCode()
    End Function
End Class

Module Program
    Sub Main()
        ' A structure used as a dictionary key needs value equality and a matching hash code
        Dim stock As New Dictionary(Of Isbn, Integer)
        stock(New Isbn("9780134757599")) = 3
        stock(New Isbn("9780134757599")) = 4
        Console.WriteLine("one key, last write wins: " & stock.Count & " entry, value " & stock(New Isbn("9780134757599")))

        ' IComparable(Of T) gives the natural order for Sort, SortedSet and SortedDictionary
        Dim ordered As New SortedSet(Of Isbn) From {New Isbn("9780132350884"), New Isbn("9780134757599"), New Isbn("9780132350884")}
        Console.WriteLine("SortedSet: " & String.Join(",", ordered))

        Dim titles As New HashSet(Of String)(New TitleComparer)
        Console.WriteLine(titles.Add("Clean Code") & " " & titles.Add("  clean code ") & " " & titles.Count)

        Dim byTitle As New Dictionary(Of String, Integer)(New TitleComparer)
        byTitle("Refactoring") = 1
        Console.WriteLine("lookup ignoring case and blanks: " & byTitle(" REFACTORING"))

        ' Comparer(Of T).Create builds an IComparer(Of T) from a lambda
        Dim list As New List(Of String) From {"fig", "apple", "kiwi", "pear"}
        list.Sort(Comparer(Of String).Create(Function(left, right) left.Length.CompareTo(right.Length)))
        Console.WriteLine("List.Sort by length (lengths only): " & String.Join(",", list.Select(Function(s) s.Length)))

        ' OrderBy is stable: ties keep their original order
        Dim stable = New List(Of String) From {"pear", "fig", "kiwi", "plum", "apple"}.OrderBy(Function(s) s.Length)
        Console.WriteLine("OrderBy stable: " & String.Join(",", stable))
    End Sub
End Module
one key, last write wins: 1 entry, value 4
SortedSet: 9780132350884,9780134757599
True False 1
lookup ignoring case and blanks: 1
List.Sort by length (lengths only): 3,4,4,5
OrderBy stable: fig,pear,kiwi,plum,apple

In Visual Basic, the two members of an IEqualityComparer(Of String) implementation are declared Overloads: Equals and GetHashCode exist on Object too, and without the keyword the compiler warns with BC40005 ("shadows an overridable method in the base class 'Object'"), which TreatWarningsAsErrors turns into an error:

Option Strict On

Public Class TitleComparer
    Implements IEqualityComparer(Of String)

    Public Function Equals(x As String, y As String) As Boolean Implements IEqualityComparer(Of String).Equals
        Return x = y
    End Function

    Public Function GetHashCode(obj As String) As Integer Implements IEqualityComparer(Of String).GetHashCode
        Return obj.GetHashCode()
    End Function
End Class

Module Program
    Sub Main()
        Console.WriteLine(New TitleComparer().Equals("a", "a"))
    End Sub
End Module
Program.vb(6,21): error BC40005: function 'Equals' shadows an overridable method in the base class 'Object'. To override the base method, this method must be declared 'Overrides'.
Program.vb(10,21): error BC40005: function 'GetHashCode' shadows an overridable method in the base class 'Object'. To override the base method, this method must be declared 'Overrides'.

For strings, prefer the ready-made comparers (StringComparer.Ordinal, OrdinalIgnoreCase, InvariantCultureIgnoreCase) over ToLower() on every key.

Collection interfaces are annotated for variance. IEnumerable(Of Out T) and IReadOnlyList(Of Out T) are covariant, so a List(Of Book) can be passed where IEnumerable(Of CatalogItem) is expected; List(Of T) and IList(Of T) are invariant (they both read and write T), so the same assignment fails with BC36756; IComparer(Of In T) is contravariant, so a comparer of the base type can sort the derived type.

Option Strict On

Public Class CatalogItem
    Public Property Title As String = ""
End Class

Public Class Book
    Inherits CatalogItem
End Class

Module Program
    Sub PrintTitles(items As IEnumerable(Of CatalogItem))
        For Each item In items
            Console.Write(item.Title & ";")
        Next
        Console.WriteLine()
    End Sub

    Sub Main()
        Dim books As New List(Of Book) From {New Book With {.Title = "Refactoring"}}

        ' IEnumerable(Of Out T) and IReadOnlyList(Of Out T) are covariant: a list of Book is a sequence of CatalogItem
        PrintTitles(books)
        Dim readOnlyItems As IReadOnlyList(Of CatalogItem) = books
        Console.WriteLine(readOnlyItems.Count)

        ' List(Of T) is invariant because it also accepts items: this conversion is not allowed
        ' Dim items As List(Of CatalogItem) = books         ' BC30311
        Dim items As List(Of CatalogItem) = books.Cast(Of CatalogItem)().ToList()
        items.Add(New CatalogItem With {.Title = "Magazine"})
        Console.WriteLine("copy has " & items.Count & ", original still " & books.Count)

        ' IComparer(Of In T) is contravariant: a comparer of CatalogItem compares Books
        Dim byTitle As IComparer(Of CatalogItem) = Comparer(Of CatalogItem).Create(Function(left, right) String.Compare(left.Title, right.Title, StringComparison.Ordinal))
        Dim comparerOfBooks As IComparer(Of Book) = byTitle
        books.Sort(comparerOfBooks)
        Console.WriteLine("sorted with a contravariant comparer")
    End Sub
End Module
Refactoring;
1
copy has 2, original still 1
sorted with a contravariant comparer
Option Strict On

Public Class CatalogItem
End Class

Public Class Book
    Inherits CatalogItem
End Class

Module Program
    Sub Main()
        Dim books As New List(Of Book)
        Dim items As List(Of CatalogItem) = books
        Dim viaInterface As IList(Of CatalogItem) = books
    End Sub
End Module
Program.vb(13,45): error BC36756: 'List(Of Book)' cannot be converted to 'List(Of CatalogItem)'. Consider using 'IEnumerable(Of CatalogItem)' instead.
Program.vb(14,53): error BC36756: 'List(Of Book)' cannot be converted to 'IList(Of CatalogItem)'. Consider using 'IEnumerable(Of CatalogItem)' instead.

Read-only, immutable and frozen collections

There are three different things that people call "read-only", and the difference matters:

Kind What it guarantees Examples

Read-only interface

the type you hand out has no mutators, but the object behind it is still a mutable list that the owner changes; the caller can cast back

IReadOnlyList(Of T), IReadOnlyDictionary(Of K, V)

Read-only wrapper

a wrapper object without mutators, over a list that the owner can still change; the wrapper sees later changes

ReadOnlyCollection(Of T), list.AsReadOnly(), Array.AsReadOnly

Immutable collection

a new collection for every change; existing instances never change; safe to share between threads

ImmutableList, ImmutableArray, ImmutableDictionary, ImmutableHashSet in System.Collections.Immutable

Frozen collection

built once from a source and then read-only and optimised for lookup

FrozenDictionary, FrozenSet in System.Collections.Frozen (.NET 8)

Option Strict On

Imports System.Collections.Frozen
Imports System.Collections.Immutable
Imports System.Collections.ObjectModel

Public Class Reader
    Private ReadOnly _loans As New List(Of String)

    ' Expose a read-only view: callers can look but cannot change the list
    Public ReadOnly Property Loans As IReadOnlyList(Of String)
        Get
            Return _loans
        End Get
    End Property

    Public ReadOnly Property LoanSnapshot As ReadOnlyCollection(Of String)
        Get
            Return _loans.AsReadOnly()
        End Get
    End Property

    Public Sub Lend(title As String)
        _loans.Add(title)
    End Sub
End Class

Module Program
    Sub Main()
        Dim reader As New Reader
        reader.Lend("Refactoring")
        Dim view = reader.Loans
        Dim wrapper = reader.LoanSnapshot
        reader.Lend("Clean Code")
        Console.WriteLine("views follow the source: " & view.Count & " and " & wrapper.Count)

        ' IReadOnlyList is only an interface: a caller can cast back
        Dim hacked = CType(view, List(Of String))
        hacked.Add("Injected")
        Console.WriteLine("after the cast: " & reader.Loans.Count & " loans")
        ' ReadOnlyCollection is a real wrapper and has no cast back to the list
        Console.WriteLine("wrapper is a List: " & (TypeOf CObj(wrapper) Is List(Of String)))

        ' Immutable collections never change: every "change" returns a new collection
        Dim empty = ImmutableList(Of String).Empty
        Dim one = empty.Add("Agile")
        Dim two = one.Add("Refactoring")
        Console.WriteLine("empty = " & empty.Count & ", one = " & one.Count & ", two = " & two.Count)

        Dim builder = ImmutableArray.CreateBuilder(Of Integer)
        For i = 1 To 5
            builder.Add(i * i)
        Next
        Dim squares = builder.ToImmutable()
        Console.WriteLine("ImmutableArray: " & String.Join(",", squares))

        Dim index = ImmutableDictionary.CreateRange(New Dictionary(Of String, Integer) From {{"a", 1}})
        Dim bigger = index.SetItem("b", 2).SetItem("a", 10)
        Console.WriteLine("original a = " & index("a") & ", new a = " & bigger("a") & ", new count = " & bigger.Count)

        ' Frozen collections are built once and optimized for reading
        Dim titles = New Dictionary(Of String, Integer) From {{"Refactoring", 1999}, {"Clean Code", 2008}}.ToFrozenDictionary()
        Dim year As Integer
        If titles.TryGetValue("Clean Code", year) Then
            Console.WriteLine("frozen lookup: " & year)
        End If
        Dim allowed = {"admin", "reader", "guest"}.ToFrozenSet()
        Console.WriteLine("frozen set contains reader: " & allowed.Contains("reader"))
    End Sub
End Module
views follow the source: 2 and 2
after the cast: 3 loans
wrapper is a List: False
empty = 0, one = 1, two = 2
ImmutableArray: 1,4,9,16,25
original a = 1, new a = 10, new count = 2
frozen lookup: 2008
frozen set contains reader: True

Two Visual Basic specifics showed up while building this example:

  • FrozenDictionary(Of TKey, TValue) has an indexer that returns by reference (ref readonly), which Visual Basic 17.13 cannot consume: titles("a") is BC30643, "is of an unsupported type". Use TryGetValue, GetValueOrDefault or ContainsKey. The same compiler limitation affects other ref-returning indexers (see the Span rules on the Arrays page).

  • ImmutableArray(Of T) is a structure; an uninitialised one is a "default" array whose Length throws NullReferenceException.

Option Strict On

Imports System.Collections.Frozen

Module Program
    Sub Main()
        Dim titles = New Dictionary(Of String, Integer) From {{"a", 1}}.ToFrozenDictionary()
        Console.WriteLine(titles("a"))
    End Sub
End Module
Program.vb(8,27): error BC30643: Property 'System.Collections.Frozen.FrozenDictionary(Of TKey, TValue).Item(key As TKey)' is of an unsupported type.

The Visual Basic Collection object

Microsoft.VisualBasic.Collection is the VB6 Collection kept for compatibility. It differs from every other collection of this page: it is one-based, it holds Object, it can attach an optional string key to an item (compared without regard to case), and Add takes Before and After named arguments. The name Collection resolves to this class because the Microsoft.VisualBasic namespace is imported in every Visual Basic project; Collection(Of T) (with a type argument) is a different class, System.Collections.ObjectModel.Collection(Of T), zero-based and generic, and needs its own Imports. Without the namespace, Collection(Of String) is BC32045 ("has no type parameters").

Option Strict On

Module Program
    Sub Main()
        ' Microsoft.VisualBasic.Collection: 1-based, holds Object, optional string keys
        Dim catalog As New Collection
        catalog.Add("Refactoring", "9780134757599")
        catalog.Add("Clean Code", "9780132350884")
        catalog.Add("First on the list", Before:=1)
        catalog.Add(42)
        Console.WriteLine("Count = " & catalog.Count)

        Console.WriteLine("catalog(1) = " & CStr(catalog(1)))
        Console.WriteLine("by key = " & CStr(catalog("9780132350884")))
        catalog.Add("Tagged item", "Tag")
        Console.WriteLine("keys ignore case: " & CStr(catalog("TAG")) & ", Contains(""9780134757599"") = " & catalog.Contains("9780134757599"))

        For Each item As Object In catalog
            Console.WriteLine("  " & item.GetType().Name & ": " & item.ToString())
        Next

        Try
            Console.WriteLine(catalog(0))
        Catch ex As IndexOutOfRangeException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Try
            catalog.Add("duplicate", "9780132350884")
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        Try
            Console.WriteLine(catalog("missing"))
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try

        catalog.Remove("9780134757599")
        catalog.Remove(1)
        catalog.Remove("tag")
        Console.WriteLine("after Remove: " & catalog.Count)

        ' Elements are Object, so Option Strict On needs a conversion on the way out
        Dim number = CInt(catalog(catalog.Count))
        Console.WriteLine("last as Integer = " & number)

        ' The generic replacement
        Dim modern As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase) From {{"9780134757599", "Refactoring"}}
        Console.WriteLine("Dictionary: " & modern("9780134757599"))
    End Sub
End Module
Count = 4
catalog(1) = First on the list
by key = Clean Code
keys ignore case: Tagged item, Contains("9780134757599") = True
  String: First on the list
  String: Refactoring
  String: Clean Code
  Int32: 42
  String: Tagged item
IndexOutOfRangeException: Collection index must be in the range 1 to the size of the collection.
ArgumentException: Add failed. Duplicate key value supplied.
ArgumentException: Argument 'Index' is not a valid value. (Parameter 'Key')
after Remove: 2
last as Integer = 42
Dictionary: Refactoring
Option Strict On

Module Program
    Sub Main()
        Dim c As New Collection
        c.Add("x")
        Dim s As String = c(1)
        Dim n As Integer = c.Count()
        Dim typed As New Collection(Of String)
    End Sub
End Module
Program.vb(7,27): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'String'.
Program.vb(9,26): error BC32045: 'Collection' has no type parameters and so cannot have type arguments.

The class still shows up in code migrated from VB6 and VBA (where it is the usual replacement for the missing dictionary) and in COM interop. In new code use Dictionary(Of String, T) with StringComparer.OrdinalIgnoreCase (shown at the end of the example), or KeyedCollection when the key is a property of the item. Migration advice is in Migrating Legacy Visual Basic; the VB6 and VBScript Scripting.Dictionary (a COM object with Exists and Item) is treated on The VB Family: VB6, VBA and VBScript. On the other hand, the runtime library facts about Collection (members, exceptions) are on The Visual Basic Runtime Library.

Non-generic collections

System.Collections contains the .NET 1.x collections: ArrayList, Hashtable, SortedList, Queue, Stack. They store Object, so value types are boxed and every read needs a conversion; type errors appear at run time (InvalidCastException) instead of compile time. They are still in the framework but should not be used in new code. ArrayList is not marked obsolete, but the documentation recommends List(Of T). Two LINQ methods bridge old and new: Cast(Of T)() (throws on a wrong element) and OfType(Of T)() (skips wrong elements).

Option Strict On

Imports System.Collections

Module Program
    Sub Main()
        ' ArrayList stores Object: values are boxed and every read needs a conversion
        Dim old As New ArrayList
        old.Add(1)
        old.Add("two")
        old.Add(3.0)
        For Each item As Object In old
            Console.WriteLine(item.GetType().Name & ": " & item.ToString())
        Next
        Dim first As Integer = CInt(old(0))
        Console.WriteLine("first = " & first)

        Try
            Dim wrong As Integer = CInt(old(1))
        Catch ex As InvalidCastException
            Console.WriteLine(ex.GetType().Name)
        End Try

        ' Cast and OfType bridge to the generic world (LINQ is defined on IEnumerable(Of T))
        Dim numbers As New ArrayList From {1, 2, 3}
        Dim typed As List(Of Integer) = numbers.Cast(Of Integer)().ToList()
        Dim onlyInts As List(Of Integer) = old.OfType(Of Integer)().ToList()
        Console.WriteLine("Cast: " & typed.Count & ", OfType: " & onlyInts.Count)

        ' Hashtable returns Nothing for a missing key instead of throwing
        Dim table As New Hashtable
        table("isbn") = "9780134757599"
        Console.WriteLine("missing key gives Nothing: " & (table("nothing") Is Nothing))
        For Each entry As DictionaryEntry In table
            Console.WriteLine(entry.Key.ToString() & " = " & entry.Value.ToString())
        Next

        ' The generic way to say the same thing
        Dim modern As New Dictionary(Of String, String) From {{"isbn", "9780134757599"}}
        Console.WriteLine(modern.ContainsKey("nothing"))
    End Sub
End Module
Int32: 1
String: two
Double: 3
first = 1
InvalidCastException
Cast: 3, OfType: 1
missing key gives Nothing: True
isbn = 9780134757599
False

Source: ArrayList Class

A Hashtable returns Nothing for a missing key; a Dictionary throws. Code ported from one to the other must change its "missing key" logic.

Collections for UI binding and keyed lists

Three types in System.Collections.ObjectModel and System.ComponentModel exist to be bound to user interfaces or to be derived from:

  • ObservableCollection(Of T) raises CollectionChanged when items are added, removed, moved, replaced or the list is cleared. WPF data binding listens to it. It does not raise anything when a property of an item changes: items implement INotifyPropertyChanged for that.

  • BindingList(Of T) (Windows Forms binding) raises ListChanged. It reports ItemChanged for property changes only if the items implement INotifyPropertyChanged; the plain Book class in the example does not, which is why the property change shows no event.

  • KeyedCollection(Of TKey, TItem) is an abstract list that also offers lookup by a key extracted from the item, which you define by overriding GetKeyForItem. It keeps list order and rejects duplicate keys. Collection(Of T) is the base class for your own collections: override InsertItem, RemoveItem, SetItem and ClearItems to react to changes.

Option Strict On

Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Imports System.ComponentModel

Public Class Book
    Public Property Isbn As String = ""
    Public Property Title As String = ""
End Class

' KeyedCollection gives list order plus lookup by a key taken from the item
Public Class BookCollection
    Inherits KeyedCollection(Of String, Book)

    Protected Overrides Function GetKeyForItem(item As Book) As String
        Return item.Isbn
    End Function
End Class

Module Program
    Private Sub OnChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
        Console.WriteLine("  CollectionChanged: " & e.Action.ToString())
    End Sub

    Private Sub OnListChanged(sender As Object, e As ListChangedEventArgs)
        Console.WriteLine("  ListChanged: " & e.ListChangedType.ToString() & " at " & e.NewIndex)
    End Sub

    Sub Main()
        Dim observable As New ObservableCollection(Of String)
        AddHandler observable.CollectionChanged, AddressOf OnChanged
        Console.WriteLine("ObservableCollection:")
        observable.Add("Refactoring")
        observable.Add("Clean Code")
        observable.Move(0, 1)
        observable(0) = "Agile"
        observable.RemoveAt(1)
        observable.Clear()

        Dim binding As New BindingList(Of Book)
        AddHandler binding.ListChanged, AddressOf OnListChanged
        Console.WriteLine("BindingList:")
        binding.Add(New Book With {.Isbn = "1", .Title = "Refactoring"})
        binding(0).Title = "Refactoring, 2nd edition"
        binding.RemoveAt(0)

        Dim books As New BookCollection
        books.Add(New Book With {.Isbn = "9780134757599", .Title = "Refactoring"})
        books.Add(New Book With {.Isbn = "9780132350884", .Title = "Clean Code"})
        Console.WriteLine("KeyedCollection: by index " & books(0).Title & ", by key " & books("9780132350884").Title)
        Console.WriteLine("Contains key: " & books.Contains("9780132350884") & ", Count = " & books.Count)
        Try
            books.Add(New Book With {.Isbn = "9780132350884", .Title = "Duplicate"})
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
        End Try
    End Sub
End Module
ObservableCollection:
  CollectionChanged: Add
  CollectionChanged: Add
  CollectionChanged: Move
  CollectionChanged: Replace
  CollectionChanged: Remove
  CollectionChanged: Reset
BindingList:
  ListChanged: ItemAdded at 0
  ListChanged: ItemDeleted at 0
KeyedCollection: by index Refactoring, by key Clean Code
Contains key: True, Count = 2
ArgumentException: An item with the same key has already been added. Key: 9780132350884

Both event arguments print the name of the enumeration only when ToString() is called. Concatenating an enumeration value with & gives its number in Visual Basic (the first draft of this example printed CollectionChanged: 3 for Move), so call ToString() or use string interpolation when you want the name. The WPF and Windows Forms binding scenarios are on WPF Applications and Windows Forms Applications.

Thread-safe collections

The collections above are not safe for several threads where at least one writes. System.Collections.Concurrent provides ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBag and BlockingCollection(Of T) (a bounded producer-consumer queue). This page only shows the idioms; synchronisation in general (SyncLock, Interlocked, Task) is covered on Threads, Parallelism and Synchronization.

Option Strict On

Imports System.Collections.Concurrent
Imports System.Threading

Module Program
    Sub Main()
        ' A plain Dictionary is not safe for concurrent writers; ConcurrentDictionary is
        Dim counts As New ConcurrentDictionary(Of String, Integer)
        Parallel.For(0, 1000, Sub(i As Integer)
                                  counts.AddOrUpdate("loans", 1, Function(key, current) current + 1)
                              End Sub)
        Console.WriteLine("loans counted: " & counts("loans"))

        Dim cache As New ConcurrentDictionary(Of Integer, String)
        Dim created = 0
        Dim factory As Func(Of Integer, String) = Function(key As Integer)
                                                      Interlocked.Increment(created)
                                                      Return "value " & key
                                                  End Function
        Parallel.For(0, 100, Sub(i As Integer)
                                 Dim ignored = cache.GetOrAdd(i Mod 5, factory)
                             End Sub)
        Console.WriteLine("cache entries: " & cache.Count & " (the factory may run more than once per key: " & (created >= 5) & ")")

        Dim work As New ConcurrentQueue(Of Integer)
        For i = 1 To 5
            work.Enqueue(i)
        Next
        Dim item As Integer
        Dim total = 0
        Do While work.TryDequeue(item)
            total += item
        Loop
        Console.WriteLine("queue total = " & total)

        Dim bag As New ConcurrentBag(Of Integer)
        Parallel.For(0, 100, Sub(i As Integer) bag.Add(i))
        Console.WriteLine("bag count = " & bag.Count)

        ' BlockingCollection: a bounded producer-consumer queue
        Dim pipe As New BlockingCollection(Of String)(boundedCapacity:=2)
        Dim producer = Task.Run(Sub()
                                    For Each name In {"a", "b", "c", "d"}
                                        pipe.Add(name)
                                    Next
                                    pipe.CompleteAdding()
                                End Sub)
        Dim seen As New List(Of String)
        For Each name In pipe.GetConsumingEnumerable()
            seen.Add(name)
        Next
        producer.Wait()
        Console.WriteLine("consumed in order: " & String.Join(",", seen))
    End Sub
End Module
loans counted: 1000
cache entries: 5 (the factory may run more than once per key: True)
queue total = 15
bag count = 100
consumed in order: a,b,c,d

The factory passed to GetOrAdd may run more than once for the same key when threads race; only one result is stored. Do not put side effects in it, or wrap the value in Lazy(Of T). A missing Imports System.Threading for Interlocked inside a lambda that is an argument of GetOrAdd made the compiler report only the overload failure BC30518 ("Overload resolution failed because no accessible 'GetOrAdd' can be called with these arguments") while this example was written, not the undeclared name; when an overload failure appears next to a lambda, look for an error inside the lambda body first (for instance by moving the lambda to a named local, which then reports BC30451).

A shelf example

The catalogue of the reference scenario combines several collections, each doing the job it is best at: a Dictionary to find a book by ISBN, a dictionary of lists (with a case-insensitive comparer) to find the books of an author, a HashSet of ISBNs on loan, a Queue per title for readers who are waiting, and a Stack as a journal for undo.

Option Strict On

Public Class Book
    Public ReadOnly Property Isbn As String
    Public ReadOnly Property Title As String
    Public ReadOnly Property Author As String

    Public Sub New(isbn As String, title As String, author As String)
        Me.Isbn = isbn
        Me.Title = title
        Me.Author = author
    End Sub
End Class

Public Class Catalog
    Private ReadOnly _byIsbn As New Dictionary(Of String, Book)
    Private ReadOnly _byAuthor As New Dictionary(Of String, List(Of Book))(StringComparer.OrdinalIgnoreCase)
    Private ReadOnly _loans As New Dictionary(Of String, Queue(Of String))      ' ISBN -> readers waiting
    Private ReadOnly _onLoan As New HashSet(Of String)
    Private ReadOnly _journal As New Stack(Of String)

    Public Sub Add(book As Book)
        _byIsbn.Add(book.Isbn, book)                       ' throws on a duplicate ISBN
        Dim list As List(Of Book) = Nothing
        If Not _byAuthor.TryGetValue(book.Author, list) Then
            list = New List(Of Book)
            _byAuthor.Add(book.Author, list)
        End If
        list.Add(book)
        _journal.Push("add " & book.Title)
    End Sub

    Public Function ByAuthor(author As String) As IReadOnlyList(Of Book)
        Dim list As List(Of Book) = Nothing
        Return If(_byAuthor.TryGetValue(author, list), list, New List(Of Book))
    End Function

    Public Function Lend(isbn As String, reader As String) As Boolean
        Dim book = _byIsbn(isbn)
        If _onLoan.Add(isbn) Then
            _journal.Push("lend " & book.Title & " to " & reader)
            Return True
        End If
        Dim waiting As Queue(Of String) = Nothing
        If Not _loans.TryGetValue(isbn, waiting) Then
            waiting = New Queue(Of String)
            _loans.Add(isbn, waiting)
        End If
        waiting.Enqueue(reader)
        Return False                        ' queued instead
    End Function

    Public Function GiveBack(isbn As String) As String
        _onLoan.Remove(isbn)
        Dim waiting As Queue(Of String) = Nothing
        Dim nextReader As String = Nothing
        If _loans.TryGetValue(isbn, waiting) AndAlso waiting.TryDequeue(nextReader) Then
            _onLoan.Add(isbn)
            Return nextReader
        End If
        Return Nothing
    End Function

    Public Function Undo() As String
        Return If(_journal.Count > 0, _journal.Pop(), "nothing to undo")
    End Function
End Class

Module Program
    Sub Main()
        Dim catalog As New Catalog
        catalog.Add(New Book("9780134757599", "Refactoring", "Martin Fowler"))
        catalog.Add(New Book("9780132350884", "Clean Code", "Robert C. Martin"))
        catalog.Add(New Book("9780201485677", "Refactoring (1st ed.)", "martin fowler"))

        Console.WriteLine("Fowler wrote " & catalog.ByAuthor("MARTIN FOWLER").Count & " titles in the catalog")
        Console.WriteLine("unknown author: " & catalog.ByAuthor("Nobody").Count)

        Try
            catalog.Add(New Book("9780132350884", "Duplicate", "Someone"))
        Catch ex As ArgumentException
            Console.WriteLine("duplicate ISBN rejected: " & ex.GetType().Name)
        End Try

        Console.WriteLine("Ana borrows Clean Code, lent now: " & catalog.Lend("9780132350884", "Ana"))
        Console.WriteLine("Ben asks for it, lent now:        " & catalog.Lend("9780132350884", "Ben"))
        Console.WriteLine("Chen asks for it, lent now:       " & catalog.Lend("9780132350884", "Chen"))
        Console.WriteLine("returned, next reader: " & catalog.GiveBack("9780132350884"))
        Console.WriteLine("returned, next reader: " & catalog.GiveBack("9780132350884"))
        Console.WriteLine("returned, next reader: " & If(catalog.GiveBack("9780132350884"), "(nobody)"))

        Console.WriteLine("undo: " & catalog.Undo())
        Console.WriteLine("undo: " & catalog.Undo())
    End Sub
End Module
Fowler wrote 2 titles in the catalog
unknown author: 0
duplicate ISBN rejected: ArgumentException
Ana borrows Clean Code, lent now: True
Ben asks for it, lent now:        False
Chen asks for it, lent now:       False
returned, next reader: Ben
returned, next reader: Chen
returned, next reader: (nobody)
undo: lend Clean Code to Ana
undo: add Refactoring (1st ed.)

The ByAuthor method returns an IReadOnlyList(Of Book); it hands out the internal list for existing authors, which a caller could cast back to a List(Of Book). When the catalogue must be protected from that, return list.AsReadOnly() or list.ToArray() as the earlier section explained.

Version history and C# differences

Version Collection feature

Visual Basic .NET 1.0 (2002)

System.Collections (ArrayList, Hashtable, Queue, Stack) and the VB Collection class.

Visual Basic 8 / .NET 2.0 (2005)

generic collections in System.Collections.Generic.

Visual Basic 9 (2008)

LINQ over any IEnumerable(Of T), extension methods (Add extension methods came with VB 10 initializers).

Visual Basic 10 (2010)

collection initializers with From; implicit line continuation inside initializers.

.NET Core 2.0 / 3.0

TryAdd, Dictionary.Remove during enumeration, GetValueOrDefault as an extension.

.NET 6

PriorityQueue(Of TElement, TPriority), Enumerable.Chunk, EnsureCapacity.

.NET 8

FrozenDictionary and FrozenSet.

Visual Basic 17.13 / .NET 10

no new collection syntax. C# collection expressions and spreads ([..a, 3]) are C# only.

Topic Visual Basic C#

Create and fill

Dim list As New List(Of Integer) From {1, 2}

List<int> list = new() { 1, 2 }; or [1, 2] (C# 12)

Dictionary initializer

From {{"a", 1}, {"b", 2}}

new() { ["a"] = 1, ["b"] = 2 } or { { "a", 1 } }

Generic syntax

List(Of T)

List<T>

Indexer

list(i)

list[i]

Iterate a dictionary

For Each pair In d then pair.Key, pair.Value

foreach (var (key, value) in d)

TryGetValue

declare the variable first; pass it ByRef implicitly

out var value

By-ref access to values

not supported (CollectionsMarshal ref returns, FrozenDictionary indexer: BC30643)

supported

Modify a struct element of a list

list(0).V = 1 is BC30068; copy, change and assign back

same restriction (CS1612)

Enumeration value concatenated into a string

"x" & value gives the number; call ToString()

concatenation and interpolation give the name

Where older material is wrong

  • The Visual Basic textbooks behind this reference introduce Scripting.Dictionary (a COM object from scrrun.dll with Exists, Item, Keys) and the VB6 Collection as the dictionary and list of the language. In .NET use Dictionary(Of TKey, TValue) and List(Of T): they are typed, fast, work on Linux and macOS, and do not need COM. The Scripting.Dictionary is treated only as legacy code, on the VB family page.

  • Older .NET material builds lists of Object with ArrayList and casts every read; the generic collections replaced them in .NET 2.0.

  • The common "word counter" exercises build a dictionary with If d.ContainsKey(w) Then d(w) += 1 Else d.Add(w, 1): correct, but it looks the key up twice or three times; TryGetValue or GetValueOrDefault with the indexer shows the intention better.

  • Material that says "a For Each over a collection cannot be changed" or "list indexes start at 1" mixes the VB Collection object (one-based) with every other collection (zero-based).

Pitfalls checklist

  • Collection (Microsoft.VisualBasic) is one-based and untyped; Collection(Of T) is a different, zero-based class that needs Imports System.Collections.ObjectModel.

  • Never modify a list inside For Each over it; use RemoveAll, a backwards loop, or iterate over ToList().

  • List(Of T) is not thread-safe, Dictionary is not thread-safe; reading while another thread writes can corrupt the table or loop forever.

  • Mutable keys: changing the hash code of an item that is a dictionary key or set member loses it.

  • List.Sort and Array.Sort are unstable; OrderBy is stable.

  • IReadOnlyList(Of T) is a view, not a copy; a caller can still cast back to List(Of T).

  • The Dictionary indexer throws for a missing key; Hashtable and the VB Collection do not behave alike.

  • An enumeration value concatenated with & prints its number; call ToString().

  • Dim pair As KeyValuePair(Of ...): Value is read-only; write through the dictionary indexer.

  • The default string comparison of a Dictionary(Of String, T) is ordinal and case-sensitive; sorted collections of strings use the current culture. Pass a StringComparer explicitly.

  • FrozenDictionary indexer and ref-returning members cannot be used from Visual Basic 17.13; use TryGetValue.

  • A lambda parameter that has the name of a local variable of the enclosing method is BC36641; a missing Imports inside a lambda can give a misleading overload failure (BC30518).