Generics
|
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 (unless it is
meant to fail, in which case the compiler messages are shown) and was run in the .NET 10 SDK container (Linux); the
output shown is real output. The root namespace of the example projects is ShelfDocs, which is why run-time type names
such as ShelfDocs.Pair with the CLR arity suffix appear in the output. The project-level imports that the SDK adds (System,
System.Collections.Generic, System.Linq, System.Threading.Tasks and others) are why List(Of T) and Func(Of T)
need no Imports.
A generic type or procedure is code that is written once and parameterised by one or more types. List(Of Book) is a list
that holds only books, Dictionary(Of String, List(Of Book)) maps names to lists of books, and a function
Largest(Of T) finds the biggest of anything that can be compared. The compiler checks every use against the type
arguments, so a wrong type is a compile-time error instead of an InvalidCastException at run time, and value types are
stored without boxing. This page covers how Visual Basic declares generics, how it infers and constrains type arguments,
the rules for Nothing, covariance and contravariance with In and Out, what generics look like at run time, the
generic collections of the base class library, the generic features that Visual Basic 17.13 can consume from C#
(unmanaged, OverloadResolutionPriority) and the ones it cannot, and the compiler errors you will meet.
The running example is the generic repository behind the ICatalogStore of the shelf library catalogue.
The shared .NET behaviour (the type system, JIT code generation, the variance annotations of the base class library) is the same as in C#, which is covered in Generics (C#); this page concentrates on the Visual Basic syntax and rules, and on the places where Visual Basic differs.
Why generics exist
Before .NET 2.0 (and Visual Basic 2005, language version 8.0) the only way to write a reusable container was to store
Object, as ArrayList and Hashtable still do. Generics replaced that design:
-
Type safety.
Dim books As New List(Of Book)refusesbooks.Add("a string")at compile time. WithArrayListthe same mistake compiles and fails later in whatever code casts the element. -
No casts, no late binding. A
List(Of Book)returnsBook; the caller never writesDirectCast(list(0), Book). -
No boxing. A
List(Of Integer)stores the integers themselves; anArrayListallocates a box for each one. The runtime creates specialised code for each value type used as a type argument. -
One implementation. The algorithm is written once and checked once, for every element type.
The Learn topic on generic types lists the same advantages (type safety, performance, code consolidation, reuse, IDE support). Generics are not a substitute for inheritance: use a generic when the code does not need to know what the type is beyond a few capabilities (comparable, creatable, a reference type), and use a base class or an interface when it needs members.
Terminology
| Term | Meaning |
|---|---|
Generic type |
A class, structure, interface or delegate declared with a type parameter list: |
Generic procedure |
A |
Type parameter |
The placeholder name in the declaration ( |
Type argument |
The actual type that replaces it: |
Constructed type |
A generic type with type arguments supplied: |
Open type |
A generic type that still has type parameters: |
Constraint |
A condition on the type argument, introduced by |
Variance |
The rule that lets |
Declaring generic types and procedures
The type parameter list follows the name, in parentheses, and starts with the Of keyword. Of is written once, at the start
of the list; further parameters are separated by commas.
Class Name(Of T1, T2 As constraint)
Structure Name(Of T)
Interface Name(Of T)
Delegate Function Name(Of T)(arg As T) As Boolean
Function Name(Of T)(arg As T) As T
-
Classes, structures, interfaces, delegates,
SubandFunctioncan be generic. -
Modules, enumerations, constructors, properties, events and operators cannot (see the error table below). A generic property is written as a generic class with a property of type
T, or as a generic method. -
A generic procedure may be placed in a non-generic class, structure or module, and a generic class may contain non-generic procedures.
-
Visual Basic is case-insensitive:
Tandtare the same name (see BC32049 and BC32089 below). Type parameters have the scope of the declaration that introduces them. -
Visual Basic has no local functions and lambda expressions cannot declare type parameters, so a helper that must be generic is a private
Functionof the enclosing type or module.
The Learn topic on the Of clause documents the clause itself; the topic on generic procedures documents the rest.
The first example declares a generic class with two type parameters, two generic procedures (one with a constraint), a
generic class with a Shared field, and then looks at the constructed types.
Option Strict On
Public Class Pair(Of TFirst, TSecond)
Public ReadOnly Property First As TFirst
Public ReadOnly Property Second As TSecond
Public Sub New(first As TFirst, second As TSecond)
Me.First = first
Me.Second = second
End Sub
Public Function Swap() As Pair(Of TSecond, TFirst)
Return New Pair(Of TSecond, TFirst)(Second, First)
End Function
Public Overrides Function ToString() As String
Return "(" & First.ToString() & ", " & Second.ToString() & ")"
End Function
End Class
Public Class Counter(Of T)
Public Shared Count As Integer
Public Sub New()
Count += 1
End Sub
End Class
Module Program
Function Largest(Of T As IComparable(Of T))(first As T, ParamArray rest As T()) As T
Dim best = first
For Each candidate In rest
If candidate.CompareTo(best) > 0 Then best = candidate
Next
Return best
End Function
Sub SwapValues(Of T)(ByRef left As T, ByRef right As T)
Dim temp = left
left = right
right = temp
End Sub
Sub Main()
Dim entry As New Pair(Of String, Integer)("Dune", 1965)
Console.WriteLine(entry.ToString() & " -> " & entry.Swap().ToString())
' Type arguments inferred from the arguments, or given explicitly.
Console.WriteLine(Largest(3, 9, 4))
Console.WriteLine(Largest("pear", "apple", "zebra"))
Console.WriteLine(Largest(Of Double)(1, 2.5))
Console.WriteLine(Largest(1, 2.5))
Console.WriteLine(TypeName(Largest(1, 2.5)))
Dim a = "left"
Dim b = "right"
SwapValues(a, b)
Console.WriteLine(a & " " & b)
' Each closed type has its own Shared fields.
Dim c1 As New Counter(Of Integer)
Dim c2 As New Counter(Of Integer)
Dim c3 As New Counter(Of String)
Console.WriteLine("Counter(Of Integer): " & Counter(Of Integer).Count)
Console.WriteLine("Counter(Of String): " & Counter(Of String).Count)
' Open and closed constructed types at run time.
Console.WriteLine(GetType(Pair(Of ,)))
Console.WriteLine(GetType(Pair(Of String, Integer)))
Console.WriteLine(GetType(Pair(Of ,)).IsGenericTypeDefinition)
Console.WriteLine(GetType(Pair(Of String, Integer)).GetGenericTypeDefinition() Is GetType(Pair(Of ,)))
Console.WriteLine(GetType(List(Of Integer)) Is GetType(List(Of Long)))
GC.KeepAlive(New Object() {c1, c2, c3})
End Sub
End Module
(Dune, 1965) -> (1965, Dune)
9
zebra
2.5
2.5
Double
right left
Counter(Of Integer): 2
Counter(Of String): 1
ShelfDocs.Pair`2[TFirst,TSecond]
ShelfDocs.Pair`2[System.String,System.Int32]
True
True
False
Source: Of Clause
What this output shows:
-
Largest(3, 9, 4)is the first inference.Tis deduced from the arguments, with noOf Integeranywhere. The constraintIComparable(Of T)is what makescandidate.CompareTo(best)legal. -
Largest(1, 2.5)mixes anIntegerand aDouble. Visual Basic infers the dominant type of the arguments,Double, andTypeNameconfirms it. The rules are in the next section. -
Counter(Of Integer).Countis2andCounter(Of String).Countis1: every constructed type has its ownSharedmembers. This is the same as in C#, and it surprises programmers who expect one counter per generic class. -
GetType(Pair(Of ,))is the open generic type definition (note the empty slot before the comma). The closed typePair(Of String, Integer)knows its definition, andList(Of Integer)andList(Of Long)are different types. The CLR name of a generic type ends with a backtick and the number of type parameters (the arity), soPairwith two type parameters is printed as ShelfDocs.Pair followed by a backtick and 2. -
ByRefparameters work as usual in generic procedures;SwapValues(Of T)swaps two strings and would swap two integers just as well.
Type inference
A call to a generic procedure may omit the type arguments. The compiler then infers them from the arguments of the call.
-
Inference uses the arguments only, never the expected result type:
Dim e = Echo()forFunction Echo(Of T)() As Tfails with BC32050 whatever variable receives the result. -
It is all or nothing. You may omit every type argument or supply them all; you cannot supply some and have the compiler infer the rest.
-
If the arguments suggest several types, the compiler uses the dominant type, which is the one every candidate converts to widening.
Largest(1, 2.5)infersDouble. When no single type works (anIntegerand aString) the call fails with BC36651 and you must name the type yourself, for examplePick(Of Object)(1, "x"). -
Inference exists only for procedures. A constructor call,
New Pair(Of String, Integer)("Dune", 1965), always spells out the type arguments, and a missing or extra one is BC32042 or BC32043. Lambda parameters are inferred from the delegate they are converted to, as inbooks.Select(Function(b) b.Title). -
Array rank mismatches also defeat inference; the Learn topic on generic procedures gives an example.
-
Option Infercontrols the type inference of local variables (Dim e = ...); it does not control the inference of type arguments, which also works withOption Infer Off(tested). See Option Statements.
Constraints
A type parameter with no constraint can be used only through the members of Object, and can be assigned Nothing.
Anything more has to be requested with an As clause. Several constraints are written in braces, and As appears once:
Class Repository(Of T As {Class, IEntity})
Function Create(Of T As {CatalogItem, New})(title As String) As T
| Constraint | What it guarantees and allows | C# spelling |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Cannot be declared in Visual Basic. Visual Basic 17.13 does enforce it when it calls a generic type or method that C# declared with it (see below). |
|
Rules enforced by the compiler (all the messages are real, from the examples below):
-
ClassandStructurecannot appear together: BC32104. -
StructureandNewcannot appear together: BC32103, becauseStructurealready implies the constructor. -
Classcannot be combined with a specific class constraint: BC32107. A second class constraint is BC32047. A constraint on aNotInheritableclass is BC32060, because only that class could ever satisfy it. -
A constraint must be a class, an interface or a type parameter:
T As Integeris BC32048.ObjectandSystem.ValueTypeare rejected with BC32061; useClassorStructureinstead. -
Listing the same interface twice is BC32071; constraining a parameter to itself is BC32113.
-
Visual Basic has no spelling for
notnull,default,allows ref structor nullable-annotated constraints: the constraint list of the Learn Type List topic is interfaces, at most one class,New, andClassorStructure.
The next program uses most of them on the catalogue classes. Everything about T that is not in its constraints is
unavailable: Oldest can read item.Year because CatalogItem has the property, and FirstOrNothing can return
Nothing because of Class.
Option Strict On
Public Interface IEntity
ReadOnly Property Id As Integer
End Interface
Public MustInherit Class CatalogItem
Implements IEntity
Public Property Id As Integer Implements IEntity.Id
Public Property Title As String
Public Property Year As Integer
Public Overrides Function ToString() As String
Return Title & " (" & Year & ")"
End Function
End Class
Public Class Book
Inherits CatalogItem
Public Sub New()
Title = "Untitled book"
Year = 2000
End Sub
End Class
Public Class Magazine
Inherits CatalogItem
Public Sub New(title As String)
Me.Title = title
End Sub
End Class
Module Program
' Base-class constraint plus New: both must hold for the type argument.
Function Create(Of T As {CatalogItem, New})(title As String) As T
Dim item As New T()
item.Title = title
Return item
End Function
' Base-class constraint: members of CatalogItem are available on T.
Function Oldest(Of T As CatalogItem)(items As IEnumerable(Of T)) As T
Dim best As T = Nothing
For Each item In items
If best Is Nothing OrElse item.Year < best.Year Then best = item
Next
Return best
End Function
' Class constraint: a reference-type argument, so Nothing and Is are available.
Function FirstOrNothing(Of T As Class)(items As IEnumerable(Of T), predicate As Func(Of T, Boolean)) As T
For Each item In items
If predicate(item) Then Return item
Next
Return Nothing
End Function
' Structure constraint: a non-nullable value type, so T? is legal.
Function OrFallback(Of T As Structure)(value As T?, fallback As T) As T
Return If(value.HasValue, value.Value, fallback)
End Function
Function ZeroOf(Of T As Structure)() As T
Return New T()
End Function
' A constraint may name another type parameter of the same procedure.
Function Upcast(Of TDerived As TBase, TBase)(item As TDerived) As TBase
Return item
End Function
' With no constraint, Nothing means "the default value of T".
Function DefaultOf(Of T)() As T
Return Nothing
End Function
' Is Nothing compiles for an unconstrained T: False for value types, True for a null reference.
Function IsNull(Of T)(value As T) As Boolean
Return value Is Nothing
End Function
Function IsDefault(Of T)(value As T) As Boolean
Return EqualityComparer(Of T).Default.Equals(value, Nothing)
End Function
Sub Main()
Dim book = Create(Of Book)("Dune")
book.Year = 1965
Dim old = New Book With {.Title = "Emma", .Year = 1815}
Console.WriteLine(book.ToString() & " / " & Oldest({book, old}).ToString())
Console.WriteLine(Oldest(New List(Of CatalogItem) From {New Magazine("Wired") With {.Year = 1993}, book}))
Console.WriteLine(FirstOrNothing({"alpha", "beta"}, Function(s) s.StartsWith("b", StringComparison.Ordinal)))
Console.WriteLine(FirstOrNothing({"alpha", "beta"}, Function(s) s.Length > 10) Is Nothing)
Console.WriteLine(OrFallback(Of Integer)(Nothing, 7) & " " & OrFallback(New Date?(#2026-10-19#), Date.MinValue).Year)
Console.WriteLine(ZeroOf(Of Integer)() & " " & ZeroOf(Of Date)().Year & " " & ZeroOf(Of Boolean)())
Dim item As CatalogItem = Upcast(Of Book, CatalogItem)(book)
Console.WriteLine(item.Title)
Console.WriteLine("default Integer: " & DefaultOf(Of Integer)())
Console.WriteLine("default String is Nothing: " & (DefaultOf(Of String)() Is Nothing))
Console.WriteLine("default Date: " & DefaultOf(Of Date)().Year)
Console.WriteLine(IsNull(0) & " " & IsNull(Of String)(Nothing) & " " & IsNull(Of Integer?)(Nothing))
Console.WriteLine(IsDefault(0) & " " & IsDefault(5) & " " & IsDefault(Of String)(Nothing) & " " & IsDefault("x"))
End Sub
End Module
Dune (1965) / Emma (1815)
Dune (1965)
beta
True
7 2026
0 1 False
Dune
default Integer: 0
default String is Nothing: True
default Date: 1
False True True
True False True False
Source: Type List
The compiler also checks every use. Each line in the next listing names a constraint that the type argument does not meet, or an inference that cannot work; the build is expected to fail, and the messages are the real compiler output (the unused-variable warning, BC42024, is suppressed in this project so that only the errors are listed).
Option Strict On
Public Interface IEntity
ReadOnly Property Id As Integer
End Interface
Public Class Widget
Implements IEntity
Public ReadOnly Property Id As Integer Implements IEntity.Id
End Class
Public Class NoDefault
Public Sub New(x As Integer)
End Sub
End Class
Public Class Repo(Of T As {Class, IEntity})
End Class
Public Class Maker(Of T As New)
End Class
Public Class Holder(Of T As Structure)
End Class
Module Program
Function Echo(Of T)() As T
Return Nothing
End Function
Function Pick(Of T)(a As T, b As T) As T
Return a
End Function
Sub Main()
Dim a As Repo(Of String)
Dim b As Repo(Of Integer)
Dim c As Maker(Of NoDefault)
Dim d As Holder(Of String)
Dim e = Echo()
Dim f = Pick(1, "x")
Dim h As List(Of Object) = New List(Of String)
Dim i As Repo = Nothing
Dim j As Repo(Of Widget, Widget)
End Sub
End Module
Program.vb(36,26): error BC32044: Type argument 'String' does not inherit from or implement the constraint type 'IEntity'.
Program.vb(37,26): error BC32044: Type argument 'Integer' does not inherit from or implement the constraint type 'IEntity'.
Program.vb(37,26): error BC32106: Type argument 'Integer' does not satisfy the 'Class' constraint for type parameter 'T'.
Program.vb(38,27): error BC32083: Type argument 'NoDefault' must have a public parameterless instance constructor to satisfy the 'New' constraint for type parameter 'T'.
Program.vb(39,28): error BC32105: Type argument 'String' does not satisfy the 'Structure' constraint for type parameter 'T'.
Program.vb(40,17): error BC32050: Type parameter 'T' for 'Public Function Echo(Of T)() As T' cannot be inferred.
Program.vb(41,17): error BC36651: Data type(s) of the type parameter(s) in method 'Public Function Pick(Of T)(a As T, b As T) As T' cannot be inferred from these arguments because more than one type is possible. Specifying the data type(s) explicitly might correct this error.
Program.vb(42,36): error BC36756: 'List(Of String)' cannot be converted to 'List(Of Object)'. Consider using 'IEnumerable(Of Object)' instead.
Program.vb(43,18): error BC32042: Too few type arguments to 'Repo(Of T As {Class, IEntity})'.
Program.vb(44,18): error BC32043: Too many type arguments to 'Repo(Of T As {Class, IEntity})'.
Source: Generic Types in Visual Basic
Notice that Repo(Of Integer) produces two errors, one for each constraint that Integer fails.
The constraint declarations themselves are checked too. This project declares one invalid type parameter after another:
Option Strict On
Public Interface IEntity
End Interface
Public Class Animal
End Class
Public Class Plant
End Class
Public NotInheritable Class Sealed1
End Class
Public Class A1(Of T As {Animal, Plant})
End Class
Public Class A2(Of T As {Structure, New})
End Class
Public Class A3(Of T As {Class, New})
End Class
Public Class A4(Of T As {IEntity, IEntity})
End Class
Public Class A5(Of T As Sealed1)
End Class
Public Class A6(Of T As {Animal, IEntity, New})
End Class
Public Class A7(Of T As Integer)
End Class
Public Class A8(Of T As T)
End Class
Public Class A9(Of T, U As T)
End Class
Public Class A10(Of T As Object)
End Class
Public Class A12(Of T As System.ValueType)
End Class
Public Class A13(Of T As {Animal, Class})
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(15,34): error BC32047: Type parameter 'T' can only have one constraint that is a class.
Program.vb(18,37): error BC32103: 'New' constraint and 'Structure' constraint cannot be combined.
Program.vb(24,35): error BC32071: Constraint type 'IEntity' already specified for this type parameter.
Program.vb(27,25): error BC32060: Type constraint cannot be a 'NotInheritable' class.
Program.vb(33,25): error BC32048: Type constraint 'Integer' must be either a class, interface or type parameter.
Program.vb(36,25): error BC32113: Type parameter 'T' cannot be constrained to itself:
Program.vb(42,26): error BC32061: 'Object' cannot be used as a type constraint.
Program.vb(45,26): error BC32061: 'ValueType' cannot be used as a type constraint.
Program.vb(48,35): error BC32107: 'Class' constraint and a specific class type constraint cannot be combined.
Source: Type List
The classes A3 (As {Class, New}), A6 (As {Animal, IEntity, New}) and A9 (U As T) compile; every other
declaration is an error. Declaration errors are reported before errors inside method bodies, so an unrelated error in a body appears only after
the declarations are fixed.
And these declarations cannot be generic at all:
Option Strict On
Public Interface IEntity
End Interface
Public Class Animal
End Class
Public Class Plant
End Class
Public NotInheritable Class Sealed1
End Class
Public Module Mod1(Of T)
End Module
Public Enum Enum1(Of T)
A
End Enum
Public Class Host
Public Sub New(Of T)(value As T)
End Sub
Public Property Prop(Of T) As Integer
Public Shared Operator +(Of T)(a As Host, b As Host) As Host
Return a
End Operator
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(15,19): error BC32073: Modules cannot be generic.
Program.vb(18,18): error BC32065: Type parameters cannot be specified on this declaration.
Program.vb(23,19): error BC32065: Type parameters cannot be specified on this declaration.
Program.vb(26,25): error BC32065: Type parameters cannot be specified on this declaration.
Program.vb(28,29): error BC32065: Type parameters cannot be specified on this declaration.
Source: Class Statement
Operators are not available on a type parameter
The Learn topic on generic procedures uses CompareTo instead of = in its example and says why: nothing guarantees that a
type argument supports the operator. Visual Basic operators are looked up on the declared type, and a type parameter declares none.
-
To compare for equality, use
EqualityComparer(Of T).Default.Equals(a, b)(the constraint-free way, which also handlesNothingandEqualsoverrides), or constrainT As IEquatable(Of T). -
To order, constrain
T As IComparable(Of T)and callCompareTo, or take aComparer(Of T)orIComparer(Of T)argument. -
To add, subtract or multiply, take a
Func(Of T, T, T)argument, or useSystem.Linq.Enumerable.Sumand its overloads when the element type is a built-in numeric type. -
The generic math interfaces such as
INumber(Of T)(see Generics (C#)) can be named in a constraint, but their members are static abstract members and Visual Basic 17.13 cannot call them:T.Zerois BC32098 andtotal += itemis BC30452. The compiler accepts the declaration and rejects the use, as the first two errors of the listing show.
Option Strict On
Imports System.Numerics
Public Class CatalogItem
End Class
Public Class Book
Inherits CatalogItem
End Class
Module Program
Function SumAll(Of T As INumber(Of T))(items As IEnumerable(Of T)) As T
Dim total As T = Nothing
For Each item In items
total += item
Next
Return total
End Function
Function ZeroOf(Of T As INumber(Of T))() As T
Return T.Zero
End Function
Function IsNullStructure(Of T As Structure)(value As T) As Boolean
Return value Is Nothing
End Function
Sub Main()
Dim numbers As IEnumerable(Of Integer) = New List(Of Integer)
Dim objects As IEnumerable(Of Object) = numbers
Dim books As IList(Of Book) = New List(Of Book)
Dim items As IList(Of CatalogItem) = books
Dim list As List(Of CatalogItem) = New List(Of Book)
End Sub
End Module
Program.vb(15,13): error BC30452: Operator '+' is not defined for types 'T' and 'T'.
Program.vb(21,16): error BC32098: Type parameters cannot be used as qualifiers.
Program.vb(25,16): error BC30020: 'Is' operator does not accept operands of type 'T'. Operands must be reference or nullable types.
Program.vb(30,49): error BC36754: 'IEnumerable(Of Integer)' cannot be converted to 'IEnumerable(Of Object)' because 'Integer' is not derived from 'Object', as required for the 'Out' generic parameter 'T' in 'Interface IEnumerable(Of Out T)'.
Program.vb(32,46): error BC36756: 'IList(Of Book)' cannot be converted to 'IList(Of CatalogItem)'. Consider using 'IEnumerable(Of CatalogItem)' instead.
Program.vb(33,44): error BC36756: 'List(Of Book)' cannot be converted to 'List(Of CatalogItem)'. Consider using 'IEnumerable(Of CatalogItem)' instead.
Source: Generic math
The same listing shows BC30020, the reason value Is Nothing is rejected under a Structure constraint, and the last three
errors (BC36754 and BC36756), which belong to the variance section below.
Default values and Nothing
Nothing in Visual Basic means "the default value of the type", not only "no object". For a type parameter it is the same
rule applied late: Dim x As T = Nothing is null when T is a reference type, 0 when it is a numeric type, False
for Boolean, the zero-initialised structure for any other structure. The constrained Class and Structure forms
narrow what Nothing can mean. C# spells the same idea default and default(T); Visual Basic has no such keyword.
The rules of Nothing itself (the comparison = Nothing, strings, nullable value types) are on
Nothing and Nullable Value Types.
-
Return Nothingfrom aFunction ... As Tis always legal and always the default ofT. -
value Is Nothingcompiles for an unconstrainedT. It isFalsefor every value type (the value is boxed for the test) andTruefor a null reference; theIsNullfunction of the constraints example printsFalse True Truefor anInteger, aStringthat isNothingand anInteger?that isNothing. With aStructureconstraint it is rejected with BC30020, because a value type can never beNothing. With aClassconstraint it is the normal null test. -
EqualityComparer(Of T).Default.Equals(value, Nothing)answers "is this the default value?" for anyT, which theIsDefaultfunction above shows: it isTruefor0and for aNothingstring. -
T?, that isNullable(Of T), requiresT As Structure.OrFallback(Of T As Structure)in the constraints example takes aT?.
The Dim best As T = Nothing line in Oldest is the usual pattern for "no result yet" in a generic search loop; the test
best Is Nothing OrElse item.Year < best.Year is legal because T is constrained to the class CatalogItem.
A generic repository for the shelf
The shelf library has a non-generic contract, ICatalogStore, that Catalog depends on. Behind it, the same bookkeeping
(auto-numbered identifiers, lookups, "list everything") is needed for books, magazines and loans. A generic repository
captures it once. The design is an IEntity interface (everything with an Id), a generic contract
IRepository(Of T As IEntity), a generic implementation Repository(Of T As {Class, IEntity}), and an adapter that
presents a Repository(Of CatalogItem) as an ICatalogStore.
The diagram shows the relationships: Repository(Of T) implements the generic IRepository(Of T) and requires its argument to
implement IEntity; BookRepository closes the type argument as Book and derives from the closed type
Repository(Of Book); RepositoryCatalogStore implements the plain ICatalogStore and delegates to an
IRepository(Of CatalogItem). In the shelf solution CatalogItem gets its Id through IEntity; the example below
is a standalone program, so it declares the minimal versions of the shelf types it needs.
Option Strict On
Public Interface IEntity
Property Id As Integer
End Interface
Public MustInherit Class CatalogItem
Implements IEntity
Public Property Id As Integer Implements IEntity.Id
Public Property Title As String
Public Property Year As Integer
Protected Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
Public MustOverride ReadOnly Property Kind As String
Public Overridable Function Describe() As String
Return Kind & ": " & Title & " (" & Year & ")"
End Function
End Class
Public Class Book
Inherits CatalogItem
Public Sub New(title As String, year As Integer)
MyBase.New(title, year)
End Sub
Public Overrides ReadOnly Property Kind As String
Get
Return "Book"
End Get
End Property
End Class
Public Class Magazine
Inherits CatalogItem
Public Sub New(title As String, year As Integer)
MyBase.New(title, year)
End Sub
Public Overrides ReadOnly Property Kind As String
Get
Return "Magazine"
End Get
End Property
End Class
' The shelf's non-generic contract, as in Shelf.Core.
Public Interface ICatalogStore
Function GetAll() As IReadOnlyList(Of CatalogItem)
Sub Add(item As CatalogItem)
Function Find(id As Integer) As CatalogItem
End Interface
' A reusable generic contract and implementation.
Public Interface IRepository(Of T As IEntity)
Function GetAll() As IReadOnlyList(Of T)
Sub Add(item As T)
Function Find(id As Integer) As T
End Interface
Public Class Repository(Of T As {Class, IEntity})
Implements IRepository(Of T)
Private ReadOnly _items As New Dictionary(Of Integer, T)
Private _nextId As Integer = 1
Public Overridable Sub Add(item As T) Implements IRepository(Of T).Add
If item Is Nothing Then Throw New ArgumentNullException(NameOf(item))
If item.Id = 0 Then
item.Id = _nextId
_nextId += 1
End If
_items.Add(item.Id, item)
End Sub
Public Function Find(id As Integer) As T Implements IRepository(Of T).Find
Dim result As T = Nothing
_items.TryGetValue(id, result)
Return result
End Function
Public Function GetAll() As IReadOnlyList(Of T) Implements IRepository(Of T).GetAll
Return _items.Values.ToList()
End Function
End Class
' A closed generic base class: Repository(Of Book) is an ordinary type.
Public Class BookRepository
Inherits Repository(Of Book)
Public Function ByTitle(text As String) As IEnumerable(Of Book)
Return GetAll().Where(Function(b) b.Title.Contains(text, StringComparison.OrdinalIgnoreCase))
End Function
End Class
' Adapter: the non-generic store contract implemented on top of the generic repository.
Public Class RepositoryCatalogStore
Implements ICatalogStore
Private ReadOnly _repository As IRepository(Of CatalogItem)
Public Sub New(repository As IRepository(Of CatalogItem))
_repository = repository
End Sub
Public Function GetAll() As IReadOnlyList(Of CatalogItem) Implements ICatalogStore.GetAll
Return _repository.GetAll()
End Function
Public Sub Add(item As CatalogItem) Implements ICatalogStore.Add
_repository.Add(item)
End Sub
Public Function Find(id As Integer) As CatalogItem Implements ICatalogStore.Find
Return _repository.Find(id)
End Function
End Class
Module Program
Sub Main()
Dim store As ICatalogStore = New RepositoryCatalogStore(New Repository(Of CatalogItem))
store.Add(New Book("Dune", 1965))
store.Add(New Magazine("Wired", 1993))
For Each item In store.GetAll()
Console.WriteLine(item.Id & " " & item.Describe())
Next
Console.WriteLine(store.Find(2).Title)
Console.WriteLine(store.Find(99) Is Nothing)
Dim books As New BookRepository()
books.Add(New Book("Dune", 1965))
books.Add(New Book("Dune Messiah", 1969))
books.Add(New Book("Emma", 1815))
For Each b In books.ByTitle("dune")
Console.WriteLine(b.Describe())
Next
' The same class, a different closed type: an interface variable of the generic contract.
Dim viaInterface As IRepository(Of Book) = books
Console.WriteLine(viaInterface.GetAll().Count & " " & TypeName(viaInterface))
Console.WriteLine(GetType(BookRepository).BaseType)
End Sub
End Module
1 Book: Dune (1965)
2 Magazine: Wired (1993)
Wired
True
Book: Dune (1965)
Book: Dune Messiah (1969)
3 BookRepository
ShelfDocs.Repository`1[ShelfDocs.Book]
Reading the example:
-
IRepository(Of T As IEntity)is covered by a constraint;Repository(Of T As {Class, IEntity})addsClassbecause the code usesitem Is Nothingand returnsNothingfromFind. WithoutClass,Findwould return the default value of a structure when nothing is found, which is indistinguishable from a real result. -
item.Id = 0anditem.Id = _nextIdwork through theIEntityconstraint, with no cast. This is whyIEntity.Idis declared read-write here: the repository assigns the next identifier when the item has none. -
Implements IRepository(Of T).Addnames the closed interface member with the class’s own type parameter. TheImplementsclause is what links a method to the interface member, so the method may have a different name (asFormatBookdoes in the collections example). -
Public Overridable Sub Addlets a derived repository override the behaviour; theOverridablemodifier is independent of generics. Interfaces are covered on Interfaces and inheritance on Inheritance and Polymorphism. -
Inherits Repository(Of Book)derives from a closed generic base class.GetType(BookRepository).BaseTypeprints the CLR name of that closed type,ShelfDocs.Repositorywith the arity suffix and[ShelfDocs.Book]. -
Dim viaInterface As IRepository(Of Book) = booksconverts to the generic interface;TypeNamestill reports the run-time class,BookRepository. -
store.Find(99) Is Nothingis the "not found" convention for aClass-constrained result. For a result that must also work for value types, use theTrypattern with aByRefparameter and aBooleanreturn, asDictionary(Of TKey, TValue).TryGetValue(used insideFind) andLoanQueue(Of T).TryDequeuebelow do.
Members of generic types
Shared members and static constructors
Each closed type is a different type, with its own Shared fields, its own Shared Sub New and its own Shared
properties. Code in a generic class cannot assume that a Shared value is shared with other constructions of the same
generic type. The run-time example below makes this visible with a static constructor that runs once for each
construction.
Nested types
A type nested in a generic type is itself generic, with the type parameters of the outer type: LoanQueue(Of T).Node in the
collections example is a different type for each T, and its members can use T without declaring it again. A nested
type that should not depend on T has to be moved out of the generic class.
Shadowing and naming
A procedure may not reuse its own type parameter’s name as the name of a parameter or local: Make(Of U)(u As U) is
BC32089, because Visual Basic ignores case and u and U are the same name. A class-level type parameter T and a parameter
named t are not in conflict, as the first method of the example shows. Redeclaring an enclosing type’s type
parameter (Sub Test(Of T) inside Holder(Of T)) is accepted, with the warning BC40048, and the inner T shadows the outer.
Two type parameters that differ only in case are BC32049.
Option Strict On
Public Class Box(Of T)
Public Function Wrap(t As T) As T
Return t
End Function
Public Function Make(Of U)(u As U) As U
Return u
End Function
Public Function Local() As T
Dim t As T = Nothing
Return t
End Function
End Class
Public Class Holder(Of T)
Public Sub Test(Of T)(value As T)
End Sub
End Class
Public Class Dup(Of T, t)
End Class
Module Program
Sub Main()
Console.WriteLine(New Box(Of Integer)().Wrap(3))
End Sub
End Module
Program.vb(8,32): error BC32089: 'u' is already declared as a type parameter of this method.
Program.vb(19,24): warning BC40048: Type parameter 'T' has the same name as a type parameter of an enclosing type. Enclosing type's type parameter will be shadowed.
Program.vb(23,24): error BC32049: Type parameter already declared with name 't'.
The same case-insensitivity explains a second trap: a local variable and a method that differ only in case collide. The
variance example below declares Dim bookFactory As Func(Of Book) = AddressOf MakeBook rather than naming the local
makeBook, because a local named makeBook hides the method MakeBook and the AddressOf operand then fails with
BC30577 and BC32000.
Overloading on the number of type parameters
Types that differ only in the number of type parameters are different types: Store, Store(Of T) and
Store(Of TKey, TValue) can coexist, as Action and Action(Of T) do in the base class library. Methods with the same
name and different numbers of type parameters are separate overloads. When only the generic form exists, the name
Repo without type arguments is BC32042, as the error listing above shows.
Option Strict On
' A generic MustOverride method: the override must restate the constraints exactly.
Public MustInherit Class Factory
Public MustOverride Function Make(Of T As {Class, New})() As T
End Class
Public Class DefaultFactory
Inherits Factory
Public Overrides Function Make(Of T As {Class, New})() As T
Return New T()
End Function
End Class
' Types and methods that differ only in the number of type parameters are different members.
Public Class Store
Public Function Describe() As String
Return "Store"
End Function
End Class
Public Class Store(Of T)
Public Function Describe() As String
Return "Store(Of " & GetType(T).Name & ")"
End Function
End Class
Public Class Store(Of TKey, TValue)
Public Function Describe() As String
Return "Store(Of " & GetType(TKey).Name & ", " & GetType(TValue).Name & ")"
End Function
End Class
Public Class Reporter
Public Function Go() As String
Return "Go()"
End Function
Public Function Go(Of T)() As String
Return "Go(Of " & GetType(T).Name & ")()"
End Function
Public Function Go(Of T, U)() As String
Return "Go(Of " & GetType(T).Name & ", " & GetType(U).Name & ")()"
End Function
End Class
Public Class Outer(Of T)
' Inner is generic over the outer T: Outer(Of Integer).Inner and Outer(Of String).Inner are different types.
Public Class Inner
Public Function Describe() As String
Return "Inner of " & GetType(T).Name
End Function
End Class
End Class
Module Program
Sub Main()
Dim factory As Factory = New DefaultFactory()
Dim list = factory.Make(Of List(Of Integer))()
list.Add(7)
Console.WriteLine("made a list with " & list.Count & " element")
Console.WriteLine(New Store().Describe())
Console.WriteLine(New Store(Of Date)().Describe())
Console.WriteLine(New Store(Of String, Integer)().Describe())
Dim reporter As New Reporter()
Console.WriteLine(reporter.Go() & " " & reporter.Go(Of Integer)() & " " & reporter.Go(Of Integer, String)())
Console.WriteLine(New Outer(Of Integer).Inner().Describe())
Console.WriteLine(New Outer(Of String).Inner().Describe())
Console.WriteLine(GetType(Outer(Of Integer).Inner) Is GetType(Outer(Of String).Inner))
End Sub
End Module
made a list with 1 element
Store
Store(Of DateTime)
Store(Of String, Int32)
Go() Go(Of Int32)() Go(Of Int32, String)()
Inner of Int32
Inner of String
False
Source: Class Statement
Outer(Of Integer).Inner and Outer(Of String).Inner are different types, as the last line shows: the nested class
carries the type parameter of its container.
Inheritance and interfaces
-
A generic class can inherit from a closed or open generic class (
Inherits Repository(Of Book),Inherits List(Of T)). -
A type may implement the same generic interface more than once with different type arguments, as
CatalogFormatterdoes below withIFormatter(Of Book)andIFormatter(Of Magazine); eachImplementsclause names the closed interface member. -
A
MustOverridemethod may be generic. Unlike in C#, where an override inherits the constraints of the base method, a Visual Basic override must restate them exactly: droppingNewfrom the override ofMake(Of T As {Class, New})is BC32077, "differ by type parameter constraints".DefaultFactoryin the example below does it correctly.
Generic structures, delegates and collections
The next program covers the remaining generic constructs. Result(Of T) is a generic structure that models "a value or an
error message"; Validator(Of T) is a user-defined generic delegate; LoanQueue(Of T) is a generic class with a nested
class; CollectionExtensions.Validate is a generic extension method (see
Extension Methods and Partial Methods);
and the Main procedure uses the common generic collections.
Option Strict On
Imports System.Runtime.CompilerServices
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public ReadOnly Property Author As String
Public Sub New(title As String, year As Integer, author As String)
Me.Title = title
Me.Year = year
Me.Author = author
End Sub
Public Overrides Function ToString() As String
Return Title
End Function
End Class
Public Class Magazine
Public ReadOnly Property Title As String
Public Sub New(title As String)
Me.Title = title
End Sub
End Class
' The same generic interface implemented twice, with different type arguments.
Public Interface IFormatter(Of In T)
Function Format(item As T) As String
End Interface
Public Class CatalogFormatter
Implements IFormatter(Of Book), IFormatter(Of Magazine)
Public Function FormatBook(item As Book) As String Implements IFormatter(Of Book).Format
Return "Book " & item.Title & " by " & item.Author
End Function
Public Function FormatMagazine(item As Magazine) As String Implements IFormatter(Of Magazine).Format
Return "Magazine " & item.Title
End Function
End Class
' A generic structure: a lightweight "value or error" result for parsing.
Public Structure Result(Of T)
Public ReadOnly Property IsSuccess As Boolean
Public ReadOnly Property Value As T
Public ReadOnly Property [Error] As String
Private Sub New(isSuccess As Boolean, value As T, [error] As String)
_IsSuccess = isSuccess
_Value = value
_Error = [error]
End Sub
Public Shared Function Ok(value As T) As Result(Of T)
Return New Result(Of T)(True, value, Nothing)
End Function
Public Shared Function Fail(message As String) As Result(Of T)
Return New Result(Of T)(False, Nothing, message)
End Function
End Structure
' A user-defined generic delegate.
Public Delegate Function Validator(Of T)(item As T) As Boolean
' A generic class with a nested class: Node belongs to each closed LoanQueue(Of T).
Public Class LoanQueue(Of T)
Private Class Node
Public ReadOnly Property Value As T
Public Property [Next] As Node
Public Sub New(value As T)
Me.Value = value
End Sub
End Class
Private _head As Node
Private _tail As Node
Public Sub Enqueue(value As T)
Dim node As New Node(value)
If _tail Is Nothing Then
_head = node
Else
_tail.Next = node
End If
_tail = node
End Sub
Public Function TryDequeue(ByRef value As T) As Boolean
If _head Is Nothing Then Return False
value = _head.Value
_head = _head.Next
If _head Is Nothing Then _tail = Nothing
Return True
End Function
End Class
Public Module CollectionExtensions
<Extension>
Public Function Validate(Of T)(items As IEnumerable(Of T), rule As Validator(Of T)) As List(Of T)
Return items.Where(Function(i) rule(i)).ToList()
End Function
End Module
Public Class TitleLengthComparer
Implements IComparer(Of Book)
Public Function Compare(x As Book, y As Book) As Integer Implements IComparer(Of Book).Compare
Return x.Title.Length.CompareTo(y.Title.Length)
End Function
End Class
Module Program
Function ParseYear(text As String) As Result(Of Integer)
Dim year As Integer
If Integer.TryParse(text, year) AndAlso year > 0 Then Return Result(Of Integer).Ok(year)
Return Result(Of Integer).Fail("'" & text & "' is not a year")
End Function
Sub Main()
Dim books As New List(Of Book) From {
New Book("Dune", 1965, "Herbert"),
New Book("Emma", 1815, "Austen"),
New Book("Neuromancer", 1984, "Gibson"),
New Book("Dune Messiah", 1969, "Herbert")}
' Dictionary(Of TKey, TValue) with a List(Of T) value, built with a lookup-or-create pattern.
Dim byAuthor As New Dictionary(Of String, List(Of Book))(StringComparer.OrdinalIgnoreCase)
For Each b In books
Dim list As List(Of Book) = Nothing
If Not byAuthor.TryGetValue(b.Author, list) Then
list = New List(Of Book)
byAuthor.Add(b.Author, list)
End If
list.Add(b)
Next
Console.WriteLine(String.Join(", ", byAuthor("HERBERT")))
Console.WriteLine(byAuthor.ContainsKey("Tolkien"))
' SortedDictionary, HashSet, Stack, Queue and KeyValuePair.
Dim years As New SortedDictionary(Of Integer, String)
For Each b In books
years.Add(b.Year, b.Title)
Next
For Each pair As KeyValuePair(Of Integer, String) In years
Console.WriteLine(pair.Key & " " & pair.Value)
Next
Dim authors As New HashSet(Of String)(books.Select(Function(b) b.Author))
Console.WriteLine(authors.Count & " authors; Herbert known: " & authors.Contains("Herbert"))
' Sorting with a generic comparer, and with a lambda wrapped by Comparer(Of T).Create.
books.Sort(New TitleLengthComparer())
Console.WriteLine(String.Join(", ", books))
books.Sort(Comparer(Of Book).Create(Function(x, y) x.Year.CompareTo(y.Year)))
Console.WriteLine(String.Join(", ", books))
' One class, two closed generic interfaces.
Dim formatter As New CatalogFormatter()
Console.WriteLine(DirectCast(formatter, IFormatter(Of Book)).Format(books(0)))
Console.WriteLine(DirectCast(formatter, IFormatter(Of Magazine)).Format(New Magazine("Wired")))
' A generic structure and a generic delegate.
For Each candidate In {"1965", "abc"}
Dim r = ParseYear(candidate)
Console.WriteLine(If(r.IsSuccess, "year " & r.Value, r.Error))
Next
Dim recent = books.Validate(Function(b) b.Year > 1960)
Console.WriteLine(recent.Count & " recent books")
' Generic class with a nested class.
Dim queue As New LoanQueue(Of String)
queue.Enqueue("first")
queue.Enqueue("second")
Dim value As String = Nothing
While queue.TryDequeue(value)
Console.WriteLine("dequeued " & value)
End While
End Sub
End Module
Dune, Dune Messiah
False
1815 Emma
1965 Dune
1969 Dune Messiah
1984 Neuromancer
3 authors; Herbert known: True
Dune, Emma, Neuromancer, Dune Messiah
Emma, Dune, Dune Messiah, Neuromancer
Book Emma by Austen
Magazine Wired
year 1965
'abc' is not a year
3 recent books
dequeued first
dequeued second
Source: Generic collections
Points worth noting:
-
Dictionary values that are lists are built with the lookup-or-create pattern (
TryGetValue, thenAdd). With aStringComparer.OrdinalIgnoreCaseargument the lookupbyAuthor("HERBERT")findsHerbert. -
SortedDictionary(Of TKey, TValue)enumerates in key order;HashSet(Of T)is the set that removes duplicates;KeyValuePair(Of TKey, TValue)is a generic structure that you can name as the loop variable type. -
List(Of T).Sortis not stable: elements that compare equal may be reordered.Comparer(Of Book).Createturns a lambda into anIComparer(Of Book). -
Result(Of T)has a private constructor and twoSharedfactory functions; withTset toIntegerthe structure holds the value with no allocation. The member namedErrorneeds brackets ([Error]), becauseErroris a keyword. -
The
Validator(Of T)delegate type is called withrule(i). The framework’sPredicate(Of T)andFunc(Of T, Boolean)are equivalent for this purpose; a user-defined generic delegate is rarely needed. -
For a loop variable that shadows an in-scope name, the pitfall is described under pitfalls below.
The generic collections live in System.Collections.Generic; the immutable and concurrent ones are in
System.Collections.Immutable and System.Collections.Concurrent. The non-generic ArrayList, Hashtable, Queue,
Stack and SortedList in System.Collections still exist for compatibility; new code should use the generic ones.
The Visual Basic Collection class from the runtime library is a separate legacy type. All of them are compared on
Collections and queried with
LINQ query expressions.
| Type | Use |
|---|---|
|
Ordered, growable array. Fast index access and append; |
|
Hash table. Keys need good |
|
Ordered by key; |
|
Unique elements, set operations ( |
|
First-in-first-out and last-in-first-out. |
|
Doubly linked list with O(1) insertion at a known node. |
|
Pair structure used by dictionary enumeration. |
|
Interfaces; accept the weakest one a method needs ( |
Covariance and contravariance
Generic type arguments are normally invariant: IList(Of Book) is not an IList(Of CatalogItem), even though Book
derives from CatalogItem, because a list can be written to and a Magazine would then end up in a list of books.
Variance recovers the safe cases. A type parameter marked Out may appear only in output positions (return types) and makes the
interface or delegate covariant: IEnumerable(Of Book) converts to IEnumerable(Of CatalogItem). A type parameter marked
In appears only in input positions (parameters) and makes it contravariant: IComparer(Of CatalogItem) converts to
IComparer(Of Book), because a comparer that can compare any two catalogue items can compare two books.
Visual Basic supports variance annotations on generic interfaces and delegates since Visual Basic 2010 (.NET Framework 4); the Learn topic on covariance and contravariance states the same version. The rules:
-
OutandInare allowed on interfaces and delegates only. A class or structure gets BC36722. -
An
Outparameter cannot be used as aByValmethod parameter type (BC36742), anInparameter cannot be used as a return type (BC36743). -
Variance applies only to reference types as type arguments.
IEnumerable(Of Integer)does not convert toIEnumerable(Of Object)(BC36754), because it would need a boxing conversion. -
Classes and the usual collections are invariant:
List(Of Book)does not convert toList(Of CatalogItem)(BC36756, which suggestsIEnumerable(Of CatalogItem)).
Option Strict On
Public MustInherit Class CatalogItem
Public Property Title As String
Protected Sub New(title As String)
Me.Title = title
End Sub
End Class
Public Class Book
Inherits CatalogItem
Public Sub New(title As String)
MyBase.New(title)
End Sub
End Class
Public Class Magazine
Inherits CatalogItem
Public Sub New(title As String)
MyBase.New(title)
End Sub
End Class
' Out: T appears only in output positions, so the interface is covariant.
Public Interface IProducer(Of Out T)
Function Produce() As T
End Interface
' In: T appears only in input positions, so the interface is contravariant.
Public Interface IConsumer(Of In T)
Sub Consume(item As T)
End Interface
Public Delegate Function Factory(Of Out T)() As T
Public Delegate Sub Handler(Of In T)(item As T)
Public Class BookProducer
Implements IProducer(Of Book)
Public Function Produce() As Book Implements IProducer(Of Book).Produce
Return New Book("Dune")
End Function
End Class
Public Class ItemPrinter
Implements IConsumer(Of CatalogItem)
Public Sub Consume(item As CatalogItem) Implements IConsumer(Of CatalogItem).Consume
Console.WriteLine("consumed " & item.Title)
End Sub
End Class
Public Class TitleComparer
Implements IComparer(Of CatalogItem)
Public Function Compare(x As CatalogItem, y As CatalogItem) As Integer Implements IComparer(Of CatalogItem).Compare
Return String.Compare(x.Title, y.Title, StringComparison.OrdinalIgnoreCase)
End Function
End Class
Module Program
Function MakeBook() As Book
Return New Book("Emma")
End Function
Sub Main()
' Covariance: a Book producer is a CatalogItem producer.
Dim producer As IProducer(Of CatalogItem) = New BookProducer()
Console.WriteLine(producer.Produce().Title)
' IEnumerable(Of T) is declared Out T; IReadOnlyList(Of T) too.
Dim books As New List(Of Book) From {New Book("Emma"), New Book("Dune")}
Dim items As IEnumerable(Of CatalogItem) = books
Dim readOnlyItems As IReadOnlyList(Of CatalogItem) = books
Console.WriteLine(items.Count() & " " & readOnlyItems(1).Title)
' Contravariance: a CatalogItem consumer can stand in for a Book consumer.
Dim consumer As IConsumer(Of Book) = New ItemPrinter()
consumer.Consume(New Book("Dune"))
' IComparer(Of T) is declared In T: sort Books with a comparer written for CatalogItem.
books.Sort(New TitleComparer())
Console.WriteLine(String.Join(", ", books.Select(Function(b) b.Title)))
' The same rules for delegates, with the Out / In modifiers on a user-defined delegate.
Dim makeItem As Factory(Of CatalogItem) = AddressOf MakeBook
Console.WriteLine(makeItem().Title)
Dim handleBook As Handler(Of Book) = New Handler(Of CatalogItem)(Sub(item) Console.WriteLine("handled " & item.Title))
handleBook(New Book("Dune"))
' Func(Of Out TResult) and Action(Of In T) are variant too.
Dim bookFactory As Func(Of Book) = AddressOf MakeBook
Dim makeAny As Func(Of CatalogItem) = bookFactory
Dim actAny As Action(Of CatalogItem) = Sub(item) Console.WriteLine("action " & item.Title)
Dim actBook As Action(Of Book) = actAny
actBook(DirectCast(makeAny(), Book))
End Sub
End Module
Dune
2 Dune
consumed Dune
Dune, Emma
Emma
handled Dune
action Emma
Source: Covariance and Contravariance
Delegates: Func(Of Book) converts to Func(Of CatalogItem) and Action(Of CatalogItem) converts to Action(Of Book). A separate
feature, delegate relaxation, lets AddressOf MakeBook initialise a Factory(Of CatalogItem) even though MakeBook
returns Book; it applies to method groups and lambdas, whereas variance applies to conversions between constructed delegate types.
Which BCL types are variant
The table below is not copied from documentation: it is produced by reflection over the open generic definitions (the
GenericParameterAttributes of each parameter), run on .NET 10.
Option Strict On
Imports System.Reflection
Module Program
Function Variance(definition As Type) As String
Return String.Join(", ", definition.GetGenericArguments().Select(
Function(p)
Dim kind = p.GenericParameterAttributes And GenericParameterAttributes.VarianceMask
Select Case kind
Case GenericParameterAttributes.Covariant
Return "Out " & p.Name
Case GenericParameterAttributes.Contravariant
Return "In " & p.Name
Case Else
Return "invariant " & p.Name
End Select
End Function))
End Function
Sub Main()
Dim definitions As Type() = {
GetType(IEnumerable(Of )), GetType(IEnumerator(Of )), GetType(IReadOnlyList(Of )),
GetType(IReadOnlyCollection(Of )), GetType(IReadOnlyDictionary(Of ,)),
GetType(IList(Of )), GetType(ICollection(Of )), GetType(List(Of )), GetType(Dictionary(Of ,)),
GetType(IComparer(Of )), GetType(IEqualityComparer(Of )), GetType(IComparable(Of )),
GetType(IEquatable(Of )), GetType(IObservable(Of )), GetType(IObserver(Of )),
GetType(Func(Of ,)), GetType(Action(Of )), GetType(Predicate(Of )), GetType(Comparison(Of )),
GetType(Task(Of )), GetType(Lazy(Of )), GetType(Nullable(Of ))}
For Each definition In definitions
Console.WriteLine(definition.Name.PadRight(24) & Variance(definition))
Next
End Sub
End Module
IEnumerable`1 Out T
IEnumerator`1 Out T
IReadOnlyList`1 Out T
IReadOnlyCollection`1 Out T
IReadOnlyDictionary`2 invariant TKey, invariant TValue
IList`1 invariant T
ICollection`1 invariant T
List`1 invariant T
Dictionary`2 invariant TKey, invariant TValue
IComparer`1 In T
IEqualityComparer`1 In T
IComparable`1 In T
IEquatable`1 invariant T
IObservable`1 Out T
IObserver`1 In T
Func`2 In T, Out TResult
Action`1 In T
Predicate`1 In T
Comparison`1 In T
Task`1 invariant TResult
Lazy`1 invariant T
Nullable`1 invariant T
Source: GenericParameterAttributes
Several of these are not what intuition suggests: IEquatable(Of T) is invariant, as is Task(Of TResult) and Lazy(Of T), and
IReadOnlyDictionary(Of TKey, TValue) is invariant in both parameters. IList(Of T) and ICollection(Of T) are
invariant because they have methods that take a T.
Variance errors
Option Strict On
Public Class CatalogItem
End Class
Public Class Book
Inherits CatalogItem
End Class
Public Interface IProducer(Of Out T)
Function Produce() As T
Sub Accept(item As T)
End Interface
Public Interface IConsumer(Of In T)
Sub Consume(item As T)
Function Current() As T
End Interface
Public Class Cell(Of Out T)
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(12,24): error BC36742: Type 'T' cannot be used as a ByVal parameter type because 'T' is an 'Out' type parameter.
Program.vb(17,27): error BC36743: Type 'T' cannot be used as a return type because 'T' is an 'In' type parameter.
Program.vb(20,22): error BC36722: Keywords 'Out' and 'In' can only be used in interface and delegate declarations.
Source: Out (Generic Modifier)
Compare with the conversions that fail in Main of the generic-math listing above:
-
IEnumerable(Of Integer)toIEnumerable(Of Object): BC36754, value types do not vary; -
IList(Of Book)toIList(Of CatalogItem): BC36756,IListis invariant; -
List(Of Book)toList(Of CatalogItem): BC36756, classes are invariant.
Array covariance
Arrays are covariant for reference element types, a legacy of the .NET 1.0 type system: Dim objects As Object() = strings
compiles, and the unsafe half of the design is checked at run time. Writing a value of the wrong type through the
widened reference throws ArrayTypeMismatchException (see the run-time example below). Generic collections do not have
this hole, which is one more reason to prefer List(Of T) and IReadOnlyList(Of T).
Generics at run time
different from Java, where the type arguments are erased (see Java Reference).
-
GetType(T)andTypeOf value Is Twork inside a generic procedure.DirectCast(value, T)compiles for an unconstrainedT; at run time a wrong type is anInvalidCastException. -
GetType(Dictionary(Of ,))is the generic type definition.MakeGenericTypecloses it at run time;Activator.CreateInstancecreates an instance;MakeGenericMethodcloses a generic method. -
Every value type used as a type argument gets its own code, specialised and unboxed. Reference types share code.
-
A
Shared Sub Newruns once per closed type.
Option Strict On
Imports System.Reflection
Public Class Registry(Of T)
Public Shared ReadOnly Created As String
' Runs once for each closed type: Registry(Of Integer), Registry(Of String), ...
Shared Sub New()
Created = "static constructor ran for " & GetType(T).Name
Console.WriteLine(Created)
End Sub
Public Function Describe(value As T) As String
If TypeOf value Is IComparable Then
Return GetType(T).Name & " is comparable"
End If
Return GetType(T).Name & " is not comparable"
End Function
End Class
Public Class Plain
End Class
Module Program
Function Cast(Of T)(value As Object) As T
Return DirectCast(value, T)
End Function
Function IsInstance(Of T)(value As Object) As Boolean
Return TypeOf value Is T
End Function
Sub Main()
' One static constructor run per closed type, on first use.
Console.WriteLine(New Registry(Of Integer)().Describe(3))
Console.WriteLine(New Registry(Of Plain)().Describe(New Plain()))
Console.WriteLine(New Registry(Of Integer)().Describe(4))
' T is a real type at run time: TypeOf ... Is T and GetType(T) work.
Console.WriteLine(IsInstance(Of Integer)(5) & " " & IsInstance(Of String)(5) & " " & IsInstance(Of IComparable)("x"))
Console.WriteLine(Cast(Of Integer)(CObj(5)) + 1)
Try
Console.WriteLine(Cast(Of String)(CObj(5)))
Catch ex As InvalidCastException
Console.WriteLine("InvalidCastException: " & ex.Message)
End Try
' Building and inspecting constructed types by reflection.
Dim open = GetType(Dictionary(Of ,))
Dim closed = open.MakeGenericType(GetType(String), GetType(List(Of Integer)))
Console.WriteLine(closed)
Console.WriteLine(String.Join(" | ", closed.GetGenericArguments().Select(Function(t) t.Name)))
Dim instance = Activator.CreateInstance(closed)
Console.WriteLine(instance.GetType().Name & " created, count " & DirectCast(instance, System.Collections.ICollection).Count)
' A generic method closed over a type chosen at run time.
Dim method = GetType(Program).GetMethod("IsInstance", BindingFlags.Static Or BindingFlags.NonPublic Or BindingFlags.Public)
Dim closedMethod = method.MakeGenericMethod(GetType(Date))
Console.WriteLine(closedMethod.Invoke(Nothing, New Object() {#2026-10-19#}))
' List(Of Integer) stores the numbers unboxed; ArrayList boxes each one.
Dim numbers As New List(Of Integer) From {1, 2, 3}
Dim boxed As New ArrayList() From {1, 2, 3}
Console.WriteLine(numbers.Sum() & " " & boxed.Cast(Of Integer)().Sum())
' Arrays are covariant for reference types; the check happens at run time.
Dim strings As String() = {"a", "b"}
Dim objects As Object() = strings
Try
objects(0) = 42
Catch ex As ArrayTypeMismatchException
Console.WriteLine("ArrayTypeMismatchException: " & ex.Message)
End Try
End Sub
End Module
static constructor ran for Int32
Int32 is comparable
static constructor ran for Plain
Plain is not comparable
Int32 is comparable
True False True
6
InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
System.Collections.Generic.Dictionary`2[System.String,System.Collections.Generic.List`1[System.Int32]]
String | List`1
Dictionary`2 created, count 0
True
6 6
ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.
Source: Type.MakeGenericType
Reflection over generic types is described further on Attributes, Caller Information and Reflection.
Generic features consumed from C#
Visual Basic 17.13 consumes two generic features that C# added after Visual Basic 16.9: the unmanaged constraint and
OverloadResolutionPriorityAttribute. You can call code that uses them; the unmanaged constraint cannot be declared in
Visual Basic, and neither feature has syntax of its own. This follows the language strategy of History, Versions and Language Strategy.
The unmanaged constraint
C# can declare where T : unmanaged: T is a value type whose fields, at any depth, are also non-nullable value types (no
references). Visual Basic 17.13 recognises the constraint on a method or type that came from C# and enforces it; it cannot
declare the constraint (As unmanaged is BC30002, "Type 'unmanaged' is not defined", as the Learn topic on generic types also
states). Under LangVersion 16.9 or lower the same call is BC36716.
The test needs two projects, because the generic method is written in C#. The library:
using System.Runtime.CompilerServices;
namespace NativeLib;
public struct Dimensions
{
public int Width;
public int Height;
}
public struct Labelled
{
public string Label;
public int Value;
}
public static class Native
{
public static int SizeOfValue<T>() where T : unmanaged => Unsafe.SizeOf<T>();
public static string Describe<T>(T value) where T : unmanaged => $"{typeof(T).Name} is unmanaged";
// Two overloads: without the attribute, List<int> would pick the more specific List<int> overload.
public static string Plain(List<int> items) => "List<int>";
public static string Plain(IEnumerable<int> items) => "IEnumerable<int>";
public static string Ranked(List<int> items) => "List<int>";
[OverloadResolutionPriority(1)]
public static string Ranked(IEnumerable<int> items) => "IEnumerable<int>, priority 1";
}
The Visual Basic project references the library:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>ShelfDocs</RootNamespace>
<OptionStrict>On</OptionStrict>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../NativeLib/NativeLib.csproj" />
</ItemGroup>
</Project>
The Visual Basic program that calls it:
Option Strict On
Imports NativeLib
Module Program
Sub Main()
Console.WriteLine(Native.SizeOfValue(Of Integer)())
Console.WriteLine(Native.SizeOfValue(Of Double)())
Console.WriteLine(Native.SizeOfValue(Of Dimensions)())
Console.WriteLine(Native.Describe(42))
Console.WriteLine(Native.Describe(New Dimensions()))
Dim numbers As New List(Of Integer) From {1, 2, 3}
Console.WriteLine(Native.Plain(numbers))
Console.WriteLine(Native.Ranked(numbers))
End Sub
End Module
4
8
8
Int32 is unmanaged
Dimensions is unmanaged
List<int>
IEnumerable<int>, priority 1
Source: Generic Types in Visual Basic
The same library with arguments that violate the constraint:
Option Strict On
Imports NativeLib
Module Program
Sub Main()
Console.WriteLine(Native.SizeOfValue(Of String)())
Console.WriteLine(Native.Describe(New Labelled()))
Console.WriteLine(Native.Describe("text"))
End Sub
End Module
Program.vb(6,34): error BC32105: Type argument 'String' does not satisfy the 'Structure' constraint for type parameter 'T'.
Program.vb(6,34): error BC37332: Type argument 'String' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as type parameter 'T'.
Program.vb(7,34): error BC37332: Type argument 'Labelled' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as type parameter 'T'.
Program.vb(8,34): error BC32105: Type argument 'String' does not satisfy the 'Structure' constraint for type parameter 'T'.
Program.vb(8,34): error BC37332: Type argument 'String' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as type parameter 'T'.
Source: Generic Types in Visual Basic
BC32105 is the old Structure complaint and BC37332 is the 17.13 message for the unmanaged constraint. A structure that
contains a String field (Labelled) fails only the second. With <LangVersion>16.9</LangVersion> in the project, each
call to SizeOfValue and Describe in the first program fails with BC36716: Visual Basic 16.9 does not support recognizing
'unmanaged' constraint.
OverloadResolutionPriority
OverloadResolutionPriorityAttribute, which a library author uses to make the compiler prefer one overload whenseveral are applicable. Visual Basic 17.13 honours the attribute when it resolves a call to such a library method. In the
program above, numbers is a List(Of Integer). Native.Plain has overloads for List(Of Integer) and
IEnumerable(Of Integer) and picks the more specific List(Of Integer); Native.Ranked has the same two, but the
IEnumerable overload carries OverloadResolutionPriority(1), and it wins. Under LangVersion 16.9, the same
Ranked(numbers) call chose the List overload, i.e. the attribute was ignored.
Library authors use this to introduce a new overload (for example one that takes a ReadOnlySpan) without breaking
callers or creating ambiguities. As a consumer you do not do anything; just be aware that the overload picked can be the
one with the highest priority, not the most specific.
Other C# generic features
| C# feature | Visual Basic 17.13 |
|---|---|
Generic math and static abstract interface members |
The interface can be named in a constraint, but its static abstract members cannot be called (BC32098, BC30452). |
|
Enforced for C# declarations; cannot be declared. |
|
No syntax; not available as constraints in Visual Basic declarations. |
Nullable reference types ( |
Not available: Visual Basic has no nullable reference type annotations or analysis. |
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Type parameters |
|
|
Open type |
|
|
Constraints |
|
|
Default value |
|
|
Variance |
|
|
Inference of constructor type arguments |
None: |
None for classes either: |
Local generic functions |
Not available (no local functions, lambdas have no type parameters) |
Local functions may be generic |
Generic math, |
Constraint accepted, static members cannot be called |
Fully supported |
|
Consumed only |
Declared and consumed |
Case |
|
Case-sensitive |
|
|
|
Null test on |
|
|
Older material you may meet
-
VB6 and VBA have no generics. The only reusable container was
Collection, which storesVariant. Ports to .NET that keepMicrosoft.VisualBasic.CollectionorArrayListpay for it in casts and boxing. See Migrating Legacy Visual Basic. -
Early Visual Basic .NET tutorials (2002 and 2003, before generics) build typed collections by inheriting from
CollectionBaseand writing one class per element type. Replace those classes withList(Of T)orCollection(Of T)fromSystem.Collections.ObjectModel. -
Tutorials from Visual Basic 2005 or 2008 have no covariance and contravariance; code that works around
IEnumerable(Of Derived)toIEnumerable(Of Base)withCast(Of T)or a copy no longer needs to. -
Books and articles that mix the .NET Framework and modern .NET often describe the same constructs as language features of a particular Visual Studio edition; the version table is on History, Versions and Language Strategy.
-
Visual Basic 17.13 is the first version that can consume
unmanagedconstraints andOverloadResolutionPriorityAttribute; materials written for 16.9 and older show errors for them.
Common errors
| Code | Message (abridged) | Cause and fix |
|---|---|---|
Type argument does not inherit from or implement the constraint type |
The argument is missing the base class or interface. Pass a type that has it. |
|
does not satisfy the |
A value type was given to |
|
does not satisfy the |
A reference type was given to |
|
must have a public parameterless instance constructor |
|
|
BC37332 |
must be a non-nullable value type, along with all fields at any level of nesting |
An |
BC36716 |
Visual Basic 16.9 does not support recognizing |
|
Type parameter cannot be inferred |
No argument mentions |
|
BC36651 |
Data types cannot be inferred from these arguments because more than one type is possible |
Pass the type argument explicitly. |
Too few / too many type arguments |
The count of type arguments does not match the arity. |
|
|
Use one of them. |
|
|
|
|
only one constraint that is a class |
Keep one class constraint; the rest interfaces. |
|
BC32048 / BC32061 |
Constraint must be a class, interface or type parameter / |
Use |
Modules cannot be generic / type parameters cannot be specified on this declaration |
Move the generic part into a class or a method. |
|
Type parameter already declared / parameter is already declared as a type parameter of this method |
Visual Basic ignores case; rename. BC40048 is the matching warning when a method type parameter shadows one of its class. |
|
BC32077 |
Override differs by type parameter constraints |
Restate the constraints of the base method exactly. |
BC30020 |
The |
|
Namespace used as an expression / overload resolution failed |
A |
|
BC36722 / BC36742 / BC36743 |
|
Move |
BC36754 / BC36756 |
Cannot convert generic type (variance) |
Variance applies to reference types in interfaces and delegates only. Use |
Operator is not defined for |
Use a comparer, an interface or a |
|
BC32098 |
Type parameters cannot be used as qualifiers |
|
BC30002 |
Type is not defined |
|
Pitfalls
-
Every closed type has its own
Sharedstate and its ownShared Sub New. -
Do not expect
List(Of Derived)to convert toList(Of Base). ExposeIEnumerable(Of Base)orIReadOnlyList(Of Base). -
A
For Eachloop variable with noAsclause whose name is already in scope binds to that name instead of declaring a new variable. The listing below usestext(which names the imported namespaceSystem.Text, because Visual Basic ignores case) andinput(which names theInputprocedure of the runtime library); the compiler reports BC30112 and BC30516 at the loop header and in the body, whileentryworks. Choose names that are not imported members, or writeFor Each entry As Book In books.
Option Strict On
Public Class Book
Public Title As String
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {New Book()}
Dim names = {"a", "b"}
For Each text In names
Console.WriteLine(text.Length)
Next
For Each input In names
Console.WriteLine(input.Length)
Next
For Each entry In names
Console.WriteLine(entry.Length)
Next
End Sub
End Module
Program.vb(12,18): error BC30112: 'System.Text' is a namespace and cannot be used as an expression.
Program.vb(13,31): error BC30456: 'Length' is not a member of 'System.Text'.
Program.vb(16,18): error BC30516: Overload resolution failed because no accessible 'Input' accepts this number of arguments.
Program.vb(17,31): error BC30516: Overload resolution failed because no accessible 'Input' accepts this number of arguments.
Source: For Each…Next Statement
-
Pick(1, "x")is BC36651 underOption Strict Onand also underOption Strict Off(tested): no single type is dominant for anIntegerand aString, and turningOption Strictoff does not change that. Name the type argument,Pick(Of Object)(1, "x"). See Option Statements. -
A type parameter has the members of
Objectonly. If you needToStringit works; if you need an arithmetic or comparison operator, use a constraint or a delegate. -
Nothingcomparisons:x Is Nothingfor aTthat is a value type is alwaysFalse. UseEqualityComparer(Of T).Defaultfor "is it the default value". -
The
Classconstraint is not "not nullable":T As Classstill allowsNothing. -
Do not expose
Dictionary(Of TKey, TValue)orList(Of T)in a public contract when callers only read; prefer the read-only interfaces. -
Generic code that boxes (
ArrayList,Objectparameters, non-genericIComparable) loses the benefit of generics. -
Variance annotations are a public contract: adding
Outto an interface and later adding a method that takesTis a breaking change.
References
-
How to: Define a Class That Can Provide Identical Functionality on Different Data Types
-
Covariance and Contravariance, Variance in Generic Interfaces, Creating Variant Generic Interfaces and Using Variance for Func and Action Generic Delegates
-
Class Statement, Structure Statement, Interface Statement and Delegate Statement
-
Generics in .NET, Generic collections, Covariance and contravariance (.NET) and Generic math
-
List<T>, Dictionary<TKey,TValue>, IEnumerable<T>, IComparer<T>, Comparer<T> and EqualityComparer<T>
-
Nullable<T>, Type.MakeGenericType, Activator.CreateInstance and ArrayTypeMismatchException
-
What’s new in Visual Basic (language versions)
-
Generics (C#) on this site