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 |
|---|---|---|---|
|
growable array, ordered by position |
index O(1); |
the default sequence |
|
hash table, unique keys |
lookup, add, remove O(1) on average; no defined order |
lookup by ISBN, by name, by id |
|
hash set, unique items |
|
membership tests, removing duplicates |
|
balanced tree of key/value pairs |
O(log n); iterates in key order |
ordered reports keyed by title |
|
two parallel sorted arrays |
lookup O(log n); insert O(n); access by position; least memory |
small, read-mostly sorted maps |
|
balanced tree of items |
O(log n); |
ordered unique items, range queries |
|
first in, first out ring buffer |
|
waiting lists, work items |
|
last in, first out |
|
undo, parsing, depth-first walks |
|
doubly linked nodes |
insert or remove next to a node you hold O(1); |
frequent splicing; rarely the right choice |
|
binary heap, smallest priority first |
|
schedulers, "most overdue first" (.NET 6) |
|
read-only view |
as the wrapped list |
exposing a collection without letting callers change it |
|
persistent, never changes |
changes return a new collection |
shared state, thread safety by construction |
|
built once, fastest to read |
slow to build |
lookup tables filled at start-up (.NET 8) |
|
thread-safe |
see the threads page |
several threads share the collection |
|
1-based, keyed, holds |
slow, untyped |
only when migrating old code |
The same decision as a diagram:
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 -> 32while 20 items were added (the firstAddallocates 4). Pass the expected size to the constructor (New List(Of Integer)(100)) when you know it, or callEnsureCapacity, to avoid the re-allocations.TrimExcessgives memory back. -
Remove(item)returnsFalsewhen nothing matched;RemoveAll(predicate)returns the number removed and is the safe way to filter a list in place (see Iterating and modifying). -
BinarySearchneeds a list that is already sorted by the same comparer; it returns the index, or the bitwise complement of the insertion point (-3means "insert at index 2") when the item is absent. -
Sortis unstable; useOrderBy(stable, returns a new sequence) when equal elements must keep their order. -
Looking something up in a
List(Of T)withContains,IndexOforFindis a linear scan. If you do that in a loop over a large list, use aHashSetor aDictionary.
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
Source: Collection Initializers
Rules:
-
Fromis the Visual Basic 10 syntax (Visual Studio 2010). Building atLangVersion9 gives BC36716, "Visual Basic 9.0 does not support collection initializers." -
Frommay 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 withCount2 andCapacity100), not the first item. -
The initializer calls
Add; ifAddthrows (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 noFrom. -
An existing list in a property that is not assigned can be filled after construction with
AddRangeorAdd, as theSparerack 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
Source: Dictionary<TKey,TValue> Class
| Operation | Behaviour |
|---|---|
|
adds the pair or replaces the value; never throws for an existing key |
|
throws |
|
throws |
|
returns |
|
adds only if absent; returns whether it did (.NET Core 2.0 and later) |
|
extension method from |
|
returns |
|
O(1) and O(n) |
|
|
|
gives |
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
Source: List<T>.RemoveAll Method
| Situation | Do this |
|---|---|
remove items that match a condition |
|
remove while walking by index |
loop backwards: |
need to change the collection while reading it |
iterate over a copy: |
change the value of existing dictionary entries |
iterate |
remove entries from a |
allowed on .NET Core 3.0 and later ( |
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 Eachhas the element type. For aDictionaryit isKeyValuePair(Of TKey, TValue); for the oldArrayListand the VBCollectionit isObject, which underOption Strict OnrequiresFor Each item As Object In ...and an explicit conversion afterwards. -
A
Do While queue.Count > 0loop is the natural way to drain aQueue,StackorPriorityQueue; items may be added inside the loop because the loop does not use an enumerator. -
Parallel.ForEachandConcurrentDictionaryare 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 |
|
Read-only wrapper |
a wrapper object without mutators, over a list that the owner can still change; the wrapper sees later changes |
|
Immutable collection |
a new collection for every change; existing instances never change; safe to share between threads |
|
Frozen collection |
built once from a source and then read-only and optimised for lookup |
|
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". UseTryGetValue,GetValueOrDefaultorContainsKey. The same compiler limitation affects otherref-returning indexers (see the Span rules on the Arrays page). -
ImmutableArray(Of T)is a structure; an uninitialised one is a "default" array whoseLengththrowsNullReferenceException.
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
Source: Collection Class
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)raisesCollectionChangedwhen 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 implementINotifyPropertyChangedfor that. -
BindingList(Of T)(Windows Forms binding) raisesListChanged. It reportsItemChangedfor property changes only if the items implementINotifyPropertyChanged; the plainBookclass 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 overridingGetKeyForItem. It keeps list order and rejects duplicate keys.Collection(Of T)is the base class for your own collections: overrideInsertItem,RemoveItem,SetItemandClearItemsto 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
Source: ObservableCollection<T> Class
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
Source: Thread-safe collections
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.)
Source: Selecting a collection class
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) |
|
Visual Basic 8 / .NET 2.0 (2005) |
generic collections in |
Visual Basic 9 (2008) |
LINQ over any |
Visual Basic 10 (2010) |
collection initializers with |
.NET Core 2.0 / 3.0 |
|
.NET 6 |
|
.NET 8 |
|
Visual Basic 17.13 / .NET 10 |
no new collection syntax. C# collection expressions and spreads ( |
| Topic | Visual Basic | C# |
|---|---|---|
Create and fill |
|
|
Dictionary initializer |
|
|
Generic syntax |
|
|
Indexer |
|
|
Iterate a dictionary |
|
|
|
declare the variable first; pass it |
|
By-ref access to values |
not supported ( |
supported |
Modify a struct element of a list |
|
same restriction (CS1612) |
Enumeration value concatenated into a string |
|
concatenation and interpolation give the name |
Where older material is wrong
-
The Visual Basic textbooks behind this reference introduce
Scripting.Dictionary(a COM object fromscrrun.dllwithExists,Item,Keys) and the VB6Collectionas the dictionary and list of the language. In .NET useDictionary(Of TKey, TValue)andList(Of T): they are typed, fast, work on Linux and macOS, and do not need COM. TheScripting.Dictionaryis treated only as legacy code, on the VB family page. -
Older .NET material builds lists of
ObjectwithArrayListand 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;TryGetValueorGetValueOrDefaultwith the indexer shows the intention better. -
Material that says "a
For Eachover a collection cannot be changed" or "list indexes start at 1" mixes the VBCollectionobject (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 needsImports System.Collections.ObjectModel. -
Never modify a list inside
For Eachover it; useRemoveAll, a backwards loop, or iterate overToList(). -
List(Of T)is not thread-safe,Dictionaryis 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.SortandArray.Sortare unstable;OrderByis stable. -
IReadOnlyList(Of T)is a view, not a copy; a caller can still cast back toList(Of T). -
The
Dictionaryindexer throws for a missing key;Hashtableand the VBCollectiondo not behave alike. -
An enumeration value concatenated with
&prints its number; callToString(). -
Dim pair As KeyValuePair(Of ...):Valueis 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 aStringComparerexplicitly. -
FrozenDictionaryindexer and ref-returning members cannot be used from Visual Basic 17.13; useTryGetValue. -
A lambda parameter that has the name of a local variable of the enclosing method is BC36641; a missing
Importsinside a lambda can give a misleading overload failure (BC30518).
References
-
Collections and Data Structures, Selecting a Collection Class, Commonly Used Collection Types, When to Use Generic Collections, Comparisons and Sorts Within Collections, Hashtable and Dictionary Collection Types, Sorted Collection Types and Thread-Safe Collections
-
Collections (Visual Basic) and Collection Initializers (Visual Basic) with How to: Create an Add Extension Method and How to: Create a Collection Used by a Collection Initializer
-
List<T>, Dictionary<TKey,TValue>, HashSet<T>, SortedSet<T>, SortedDictionary<TKey,TValue>, SortedList<TKey,TValue>, Queue<T>, Stack<T>, LinkedList<T> and PriorityQueue<TElement,TPriority>
-
ObservableCollection<T>, BindingList<T>, KeyedCollection<TKey,TItem> and ReadOnlyCollection<T>
-
System.Collections.Immutable, System.Collections.Frozen and System.Collections.Concurrent
-
IReadOnlyList<T>, IEqualityComparer<T> and CollectionExtensions.GetValueOrDefault
-
Collection expressions (C# reference) for the C# comparison
-
Arrays, LINQ Query Expressions and Collections and Iterators (C#) on this site