Tuples and Anonymous Types
|
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. The examples that are meant to fail say so, and show the compiler messages that
the SDK printed (with the file path removed). The project-level imports that the SDK adds (System, System.Collections.Generic,
System.Linq and others) are why Console, List, Dictionary and the LINQ operators need no Imports. The shelf
library catalogue provides the data: books with a title, a year and a kind. To keep each program self-contained, the examples declare
a small Book class instead of referencing the full Book of the shelf domain model.
Both features answer the same need, a small group of related values without the ceremony of a named type, and they answer it in
opposite ways. A tuple is a value type that can cross method boundaries: it is the right answer to "this function returns two
things". An anonymous type is a compiler-generated class that has no name you can write: it is the right answer to "this query
produces rows of a shape that only exists here". Knowing which one to pick, and where each one stops working, is most of what there
is to learn. Visual Basic differs from C# in several places that matter for both: there is no deconstruction syntax, there are
no = and <> operators for tuples, and anonymous types have value equality only for their Key properties.
Tuples
Why tuples exist
A method returns one value. Before Visual Basic 15 (Visual Studio 2017) the options for returning several were a ByRef parameter
(the Try pattern), a purpose-built Structure or Class, or the System.Tuple classes, whose elements are called Item1,
Item2 and so on, are read-only and are heap-allocated. Visual Basic 15 added tuple syntax over the System.ValueTuple structures,
the same types that C# 7 uses, so a tuple declared in one language is an ordinary, fully typed value in the other. Visual Basic 15.3
added inferred element names, the counterpart of C# 7.1.
-
A tuple is an instance of
System.ValueTuple(Of T1, T2, ...): a mutable structure with public fields. There is no allocation for the tuple itself. -
Tuple support needs the
ValueTupletype. It is part of the base class library on .NET (Core), .NET 5 and later, and .NET Framework 4.7 and later; for older targets add theSystem.ValueTupleNuGet package, otherwise the compiler reports that the predefined typeValueTuple(Of,,,)is not defined or imported. -
The language version needs to be 15 or higher, and 15.3 for name inference. The default
LangVersionof an SDK-style project on .NET 10 is 17.13, so nothing needs to be configured. -
A tuple has between two and (in practice) seven elements. Longer tuples are supported through nesting, see Value semantics and mutability. There is no one-element tuple (
(1)is a parenthesized expression) and an empty()is a syntax error (BC30201, "Expression expected").
Declaring tuples and naming their elements
A tuple literal is a comma-separated list of expressions in parentheses. Its type is inferred from the expressions; an
element name is given either in the literal, with Name:=value, or in the declared type, with (Name As Type, ...), and
Visual Basic 15.3 infers a name from a variable or member access when the literal does not give one.
Option Strict On
Public Class Book
Public Property Title As String = ""
Public Property Year As Integer
End Class
Module Program
Sub Main()
' 1. Positional tuple: the elements are called Item1, Item2.
Dim pair = (1, "Dune")
Console.WriteLine(pair.Item1 & " " & pair.Item2)
' 2. Names given in the literal.
Dim named = (Id:=2, Title:="Emma")
Console.WriteLine(named.Id & " " & named.Title)
' 3. Names given in the declared type; the literal can be positional.
Dim typed As (Id As Integer, Title As String) = (3, "Ulysses")
Console.WriteLine(typed.Id & " " & typed.Title)
' 4. Names inferred from variables and from member accesses (Visual Basic 15.3).
Dim id = 4
Dim title = "Ivanhoe"
Dim book As New Book() With {.Title = "Persuasion", .Year = 1817}
Dim fromVariables = (id, title)
Dim fromMembers = (book.Title, book.Year)
Console.WriteLine(fromVariables.id & " " & fromVariables.title)
Console.WriteLine(fromMembers.Title & " " & fromMembers.Year)
' The positional names always work, whatever the declared names are.
Console.WriteLine(typed.Item2 & " " & fromMembers.Item2)
' A tuple is a ValueTuple, whose ToString lists the values.
Console.WriteLine(named.ToString())
Console.WriteLine(typed.GetType().Name & " is a value type: " & typed.GetType().IsValueType)
Console.WriteLine(fromMembers)
' Elements are read-write fields.
typed.Title = "Ulysses (1922)"
Console.WriteLine(typed.Title)
' A nested tuple, and an array of tuples.
Dim nested = (Outer:=1, Inner:=(P:=2, Q:=3))
Console.WriteLine(nested.Inner.Q & " " & nested.Item2.Item1)
Dim rows = {(1, "a"), (2, "b")}
Console.WriteLine(rows.GetType().Name & " " & rows(1).Item2)
End Sub
End Module
1 Dune
2 Emma
3 Ulysses
4 Ivanhoe
Persuasion 1817
Ulysses 1817
(2, Emma)
ValueTuple`2 is a value type: True
(Persuasion, 1817)
Ulysses (1922)
3 2
ValueTuple`2[] b
Source: Tuples (Visual Basic)
What the output shows:
-
pairhas only the positional namesItem1andItem2. Every tuple has them, whatever other names it carries, sotyped.Item2andtyped.Titleare the same field. -
Dim fromMembers = (book.Title, book.Year)produced elements calledTitleandYear: the inferred name is the last identifier of the expression. This is convenient for projections, but a refactoring that renames the property silently changes the tuple’s element names. -
The same literal can be typed as
(Id As Integer, Title As String)on the left ofAs; the literal then needs no names, and the declared names win. -
ToStringprints the values in parentheses and never the names:(2, Emma). -
The array
rowshas typeValueTuple(Of Integer, String)(): the element names are not part of the runtime type (see Conversions, names at run time and nullable tuples).
The ways of naming the elements:
| Form | Example | Names of the elements |
|---|---|---|
Positional literal |
|
|
Named literal |
|
|
Declared type |
|
|
Inferred from names |
|
|
Inferred from members |
|
|
Return type |
|
|
Generic argument |
|
|
Name comparisons are case-insensitive, as everywhere in Visual Basic: typed.title finds Title. Names are metadata and
convenience only; they never take part in type identity, equality or conversion.
Rules and diagnostics for element names
The compiler checks names where they are written.
Option Strict On
Module Program
Sub Main()
Dim twice = (A:=1, A:=2)
Dim wrongPosition = (Item2:=1, Item1:=2)
Dim retyped As (A As Integer, B As Integer) = (X:=1, Y:=2)
Console.WriteLine(twice.ToString() & wrongPosition.ToString() & retyped.A)
End Sub
End Module
The SDK reports:
Program.vb(5,28): error BC37262: Tuple element names must be unique.
Program.vb(6,30): error BC37261: Tuple element name 'Item2' is only allowed at position 2.
Program.vb(6,40): error BC37261: Tuple element name 'Item1' is only allowed at position 1.
Program.vb(7,56): error BC41009: The tuple element name 'X' is ignored because a different name or no name is specified by the target type '(A As Integer, B As Integer)'.
Program.vb(7,62): error BC41009: The tuple element name 'Y' is ignored because a different name or no name is specified by the target type '(A As Integer, B As Integer)'.
Source: Tuples (Visual Basic)
-
BC37262, "Tuple element names must be unique": two elements cannot have the same name.
-
BC37261, "Tuple element name 'Item2' is only allowed at position 2": the names
ItemNare reserved for the element at position N, so a literal cannot call the first elementItem2. -
BC41009, "The tuple element name 'X' is ignored": a warning that appears when a literal assigned to a declared tuple type names an element differently. The declared names win and the literal’s names are dropped, which usually means a typing mistake. The examples of this section treat warnings as errors, which is why the listing says
error; in a default project it is a warning.
Name inference is deliberately lenient: when the compiler cannot infer a name it does not report anything, it just leaves the
element with its positional name. Inference does not happen when the candidate name is the same as a member of the tuple type
(Item3, Rest, ToString) or when the same candidate appears twice in the literal. The symptom is a later BC30456
("'dup' is not a member of '(Integer, Integer)'") at the use site, not at the literal.
Tuples as return values
The most common use of a tuple is the return value of a function. The element names are part of the declaration of the function and travel with it, so callers get named, IntelliSense-completed members without a named type.
Option Strict On
Imports System.Globalization
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
End Class
Module Program
' A named tuple as the return type.
Function Summarize(books As IEnumerable(Of Book)) As (Count As Integer, Oldest As Integer, Newest As Integer)
Dim years = books.Select(Function(b) b.Year).ToList()
Return (years.Count, years.Min(), years.Max())
End Function
' The Try pattern with a tuple instead of a ByRef parameter.
Function ParseYear(text As String) As (Ok As Boolean, Year As Integer)
Dim value As Integer
Dim ok = Integer.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, value)
Return (ok, value)
End Function
' A tuple-returning iterator.
Iterator Function Numbered(books As IEnumerable(Of Book)) As IEnumerable(Of (Position As Integer, Title As String))
Dim position = 0
For Each b In books
position += 1
Yield (position, b.Title)
Next
End Function
Sub Main()
Dim books = {New Book("Dune", 1965), New Book("Emma", 1815), New Book("Ulysses", 1922)}
Dim s = Summarize(books)
Console.WriteLine($"{s.Count} books from {s.Oldest} to {s.Newest}")
For Each entry In {"1922", "nineteen"}
Dim r = ParseYear(entry)
Console.WriteLine($"{entry}: ok={r.Ok}, year={r.Year}")
Next
For Each n In Numbered(books)
Console.WriteLine($"{n.Position}. {n.Title}")
Next
' A lambda can return a tuple too; the element names come from the literal.
Dim lengthOf = Function(b As Book) (b.Title, Length:=b.Title.Length)
Console.WriteLine(lengthOf(books(2)))
Console.WriteLine(lengthOf(books(2)).Length)
End Sub
End Module
3 books from 1815 to 1965
1922: ok=True, year=1922
nineteen: ok=False, year=0
1. Dune
2. Emma
3. Ulysses
(Ulysses, 7)
7
Source: Tuples (Visual Basic)
-
Summarizereturns three values with no helper type. TheReturnstatement uses a plain tuple literal, converted to the declared type by position. -
ParseYearis theTrypattern without theByRefparameter. The advantage is that the caller declares nothing in advance and cannot forget to look at the result; the cost is that a failed parse still carries a meaningless0. -
An
Iterator FunctioncanYieldtuples (see Iterators and Yield). The element names come from the declared element type(Position As Integer, Title As String). -
A lambda returns whatever its body returns, so
Function(b As Book) (b.Title, Length:=b.Title.Length)is a function that returns(Title As String, Length As Integer), with the first name inferred and the second given. Lambdas are covered in Delegates and Lambda Expressions and procedures in Procedures: Sub and Function. -
A public API that returns a tuple promises the element types and order to every caller. If the result is likely to grow members, behaviour or validation, return a named
StructureorClassinstead; a tuple cannot have methods or properties of its own.
No deconstruction syntax
C# can unpack a tuple in one statement, var (id, title) = book;, and in an assignment (id, title) = book;. Visual Basic 17.13
has neither form, and neither a For Each (key, value) In ... loop nor discards. Both attempts below are syntax errors.
Option Strict On
Module Program
Sub Main()
Dim book = (Id:=1, Title:="Dune")
Dim (id, title) = book
Console.WriteLine(id & title)
End Sub
End Module
The SDK reports:
Program.vb(6,13): error BC30203: Identifier expected.
Option Strict On
Module Program
Sub Main()
Dim book = (Id:=1, Title:="Dune")
Dim id As Integer
Dim title As String
(id, title) = book
Console.WriteLine(id & title)
End Sub
End Module
The SDK reports:
Program.vb(8,9): error BC30035: Syntax error.
Source: Tuples (Visual Basic)
BC30203 and BC30035 are the generic "Identifier expected" and "Syntax error" messages: the parser does not know the construct at
all. The same is true of the Deconstruct methods that C# records, KeyValuePair and many library types provide:
Visual Basic does not recognise them as a protocol. They remain ordinary methods, and the workarounds are all explicit.
Option Strict On
Module Program
Sub Main()
Dim book = (Id:=1, Title:="Dune")
' No deconstruction syntax: read the elements one by one.
Dim id = book.Id
Dim title = book.Title
Console.WriteLine(id & " " & title)
' A With block avoids repeating the variable name.
With book
Console.WriteLine(.Id & " " & .Title)
End With
' Library Deconstruct methods are ordinary methods in Visual Basic: call them yourself.
Dim pair As New KeyValuePair(Of String, Integer)("Dune", 1965)
Dim key As String = ""
Dim value As Integer
pair.Deconstruct(key, value)
Console.WriteLine(key & " " & value)
' Iterating a dictionary gives KeyValuePair objects, not tuples.
Dim years As New Dictionary(Of String, Integer) From {{"Dune", 1965}, {"Emma", 1815}}
For Each entry In years
Console.WriteLine(entry.Key & " " & entry.Value)
Next
' Converting to tuples is a projection.
For Each t In years.Select(Function(e) (Title:=e.Key, Year:=e.Value))
Console.WriteLine(t.Title & " " & t.Year)
Next
End Sub
End Module
1 Dune
1 Dune
Dune 1965
Dune 1965
Emma 1815
Dune 1965
Emma 1815
-
Read each element into its own variable (
Dim id = book.Id). With named elements this is no more verbose than deconstruction and it documents what each variable means. -
A
Withblock (Control Flow) lets you use.Idand.Titlewithout repeating the tuple’s name. -
pair.Deconstruct(key, value)is a real method call withByRefparameters, so the variables must be declared first. -
A
Dictionaryiterates asKeyValuePairstructures, which have.Keyand.Value. Project to tuples withSelectonly when you need the tuples (to sort them or to hand them to an API that wants tuples). -
There is no pattern matching in Visual Basic, so tuple patterns such as
(0, _)do not exist either. UseIfandSelect Caseon the individual elements.
Equality, ordering and hashing
The ValueTuple types implement IEquatable(Of T), IComparable(Of T), IStructuralEquatable and IStructuralComparable, and
Equals, CompareTo and GetHashCode work element by element. What Visual Basic lacks is the operators: in C# 7.3 and later
a == b compares tuples; in Visual Basic the compiler has no = or <> for tuple operands.
Option Strict On
Module Program
Sub Main()
Dim a = (1, "Dune")
Dim b = (1, "Dune")
Console.WriteLine(a = b)
Console.WriteLine(a <> b)
End Sub
End Module
The SDK reports:
Program.vb(7,27): error BC30452: Operator '=' is not defined for types '(Integer, String)' and '(Integer, String)'.
Program.vb(8,27): error BC30452: Operator '<>' is not defined for types '(Integer, String)' and '(Integer, String)'.
Source: ValueTuple Struct
BC30452, "Operator '=' is not defined for types …", is the usual message for an operator with no operand support. Use Equals,
or write the comparison out when you also need to ignore some elements.
Option Strict On
Module Program
Sub Main()
Dim a = (Id:=1, Title:="Dune")
Dim b = (Id:=1, Title:="Dune")
Dim c = (Id:=1, Title:="Emma")
' Equals compares element by element with each element's own Equals.
Console.WriteLine(a.Equals(b) & " " & a.Equals(c))
Console.WriteLine(a.GetHashCode() = b.GetHashCode())
' The names play no part: only position and types count.
Dim other = (Key:=1, Name:="Dune")
Console.WriteLine(a.Equals(other))
' The spelled-out comparison that the = operator would have been.
Console.WriteLine((a.Id = c.Id) AndAlso (a.Title = c.Title))
' Equals(Object) needs identical element types: Integer is not Long.
Console.WriteLine(a.Equals((CLng(1), "Dune")))
' IComparable: elements are compared left to right.
Dim ordered = {(2, "Emma"), (1, "Ulysses"), (1, "Dune")}.OrderBy(Function(t) t).ToList()
Console.WriteLine(String.Join(", ", ordered))
Console.WriteLine(a.CompareTo(c))
' Tuples work as dictionary and HashSet keys.
Dim copies As New Dictionary(Of (Isbn As String, Branch As String), Integer)
copies((Isbn:="9780441172719", Branch:="North")) = 3
copies((Isbn:="9780441172719", Branch:="South")) = 1
Console.WriteLine(copies((Isbn:="9780441172719", Branch:="North")))
Console.WriteLine(copies.ContainsKey(("9780441172719", "East")))
Dim seen As New HashSet(Of (Integer, Integer)) From {(1, 2)}
Console.WriteLine(seen.Contains((1, 2)) & " " & seen.Contains((2, 1)))
End Sub
End Module
True False
True
True
False
False
(1, Dune), (1, Ulysses), (2, Emma)
-1
3
False
True False
Source: ValueTuple<T1,T2> Struct
-
a.Equals(b)compares the values, anda.Equals(other)is alsoTruealthough the names differ (Id/TitleagainstKey/Name): names are not part of equality. -
Equals(Object)requires the same tuple type.(CLng(1), "Dune")is aValueTuple(Of Long, String), so it is not equal to an(Integer, String)tuple that holds the same number. Convert the element, or compare the elements with=. -
OrderBy(Function(t) t)andCompareTouse the default comparer of each element, from left to right. Strings use the culture’s comparison there; pass a comparer toOrderBywhen the order must be ordinal. -
Tuples are good composite keys for
DictionaryandHashSet: they are structures, so there is no allocation per lookup, and the hash code combines the elements. The mutability caveat below applies: do not modify a tuple that is stored as a key.
Value semantics and mutability
A tuple is a structure with public mutable fields. Assignment copies it, ByVal passing copies it, and a copy never changes the
original. That is what you want for a result value and a surprise for anyone who treats it like an object.
Option Strict On
Module Program
Sub Rename(ByRef pair As (Id As Integer, Title As String))
pair.Title = pair.Title.ToUpperInvariant()
End Sub
Sub Main()
Dim original = (Id:=1, Title:="Dune")
Dim copy = original
copy.Title = "Emma"
Console.WriteLine(original.Title & " " & copy.Title)
' ByRef changes the caller's tuple, ByVal does not.
Rename(original)
Console.WriteLine(original.Title)
' A list element is a value: copy it, change it, store it back.
Dim list As New List(Of (Id As Integer, Title As String)) From {(1, "A"), (2, "B")}
Dim item = list(0)
item.Title = "Z"
list(0) = item
Console.WriteLine(list(0))
' An array element is a variable, so it can be changed in place.
Dim array = {(Id:=1, Title:="A")}
array(0).Title = "Y"
Console.WriteLine(array(0))
' More than seven elements: the tenth is still Item10, stored in nested Rest tuples.
Dim wide = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Console.WriteLine(wide.Item10 & " " & wide.GetType().Name & " " & wide.Rest.GetType().Name)
End Sub
End Module
Dune Emma
DUNE
(1, Z)
(1, Y)
10 ValueTuple`8 ValueTuple`3
Source: ValueTuple Struct
-
copy.Title = "Emma"did not touchoriginal.Renamechanged it only because the parameter isByRef. -
list(0).Title = "Z"does not compile, becauselist(0)is a value returned by the indexer, a copy, and changing a copy would be silently lost; the error is shown in the next section’s listing (BC30068). Copy the element, change it, and store it back. An array element, in contrast, is a variable, soarray(0).Title = "Y"works. -
The tenth element is still called
Item10. The compiler stores elements beyond the seventh in a nestedRestfield of typeValueTuple(Of ...), which is why the printed type is theValueTuplewith arity 8: seven elements plusRest. Avoid tuples that long; a named type is clearer from about four elements. -
Read-only is not available. If the value must not change, use a
StructurewithReadOnlyproperties (see Structures and Value Types).
Conversions, names at run time and nullable tuples
Assignment between tuples is allowed when they have the same number of elements and each source element converts to the target
element type. With Option Strict On (see Option Statements) that
means a widening conversion, or an explicit CType for a narrowing one. Names are not part of the check.
Option Strict On
Imports System.Reflection
Module Program
Function Range() As (Min As Integer, Max As Integer)
Return (1, 9)
End Function
Sub Main()
Dim named = (Answer:=42, Message:="The meaning of life")
Dim differentNamed = (SecretConstant:=1, Label:="x")
' Same arity and convertible types: values are copied by position, names stay with the variable.
named = differentNamed
Console.WriteLine(named.Answer & " " & named.Message)
' Widening element conversion: (Integer, String) to (Long, String).
Dim wider As (Long, String) = named
Console.WriteLine(wider.Item1.GetType().Name)
' Narrowing needs an explicit conversion, and checks the range at run time.
Dim big As (Long, String) = (3000000000L, "big")
Try
Dim narrow = CType(big, (Integer, String))
Console.WriteLine(narrow)
Catch ex As OverflowException
Console.WriteLine("OverflowException")
End Try
' Through Object the names are gone; DirectCast needs the exact element types.
Dim boxed As Object = named
Dim back = DirectCast(boxed, (Integer, String))
Console.WriteLine(back.Item2)
Console.WriteLine(TypeOf boxed Is ValueTuple(Of Integer, String))
' The names live in an attribute on the member that declares them.
Dim method = GetType(Program).GetMethod("Range", BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic)
Dim data = method.ReturnParameter.GetCustomAttributesData().Single()
Dim names = CType(data.ConstructorArguments(0).Value, IEnumerable(Of CustomAttributeTypedArgument))
Console.WriteLine(data.AttributeType.Name & ": " & String.Join(", ", names.Select(Function(n) n.Value)))
' Nullable tuple.
Dim maybe As (Min As Integer, Max As Integer)? = Nothing
Console.WriteLine(maybe.HasValue)
maybe = Range()
Console.WriteLine(maybe.Value.Max)
End Sub
End Module
1 x
Int64
OverflowException
x
True
TupleElementNamesAttribute: Min, Max
False
9
Source: TupleElementNamesAttribute Class
-
named = differentNamedcopies the values by position.namedkeeps its own names, which is whynamed.Answerprinted1. -
(Integer, String)converts to(Long, String)implicitly; the other direction needsCTypeand checks the value at run time (theOverflowException). -
The runtime type of a tuple is just
ValueTuple(Of ...). The names are stored in aTupleElementNamesAttributeon the member that declares them: here the return parameter ofRange. ThroughObject,Dim boxed As Object = named, they are gone, and a cast back to a tuple type names the elements again. -
Reflection and serializers see
Item1,Item2; only the compiler and tools that read the attribute know the names. -
(Min As Integer, Max As Integer)?is aNullable(Of ValueTuple(Of ...)); read it with.Valueor.HasValue(see Nothing and Nullable Value Types).
Conversions that are rejected:
Option Strict On
Module Program
Sub Main()
Dim pair = (1, "Dune")
Dim narrower As (Integer, Integer) = pair
Dim list As New List(Of (Id As Integer, Title As String)) From {(1, "A")}
list(0).Title = "Z"
Dim triple = (1, 2, 3)
pair = triple
End Sub
End Module
The SDK reports:
Program.vb(12,16): error BC30311: Value of type '(Integer, Integer, Integer)' cannot be converted to '(Integer, String)'.
Program.vb(6,46): error BC30512: Option Strict On disallows implicit conversions from '(Integer, String)' to '(Integer, Integer)'.
Program.vb(9,9): error BC30068: Expression is a value and therefore cannot be the target of an assignment.
Source: Tuples (Visual Basic)
BC30311 reports tuples of a different arity, BC30512 a narrowing conversion under Option Strict On, and BC30068 the attempt to
modify a copy.
Tuples compared with the alternatives
| Feature | Tuple (ValueTuple) |
System.Tuple |
Structure |
Class |
Anonymous type |
|---|---|---|---|---|---|
Kind |
structure |
class |
structure |
class |
class (generated) |
Allocation |
none for the tuple |
heap |
none |
heap |
heap |
Mutable |
yes, always |
no |
your choice |
your choice |
only non- |
Names usable in a signature |
yes, |
no, |
yes |
yes |
no |
Own members |
no |
no |
yes |
yes |
no (properties only) |
|
not defined |
not defined |
if you overload them |
if you overload them |
not defined |
Value equality |
yes, |
yes, |
you implement it |
you implement it |
only with |
Deconstruction |
no |
no |
no |
no |
no |
Introduced in VB |
15 (2017) |
.NET Framework 4.0 |
all versions |
all versions |
9.0 (2008) |
Choose a tuple for a short-lived result of two to four values whose meaning is clear from the names. Choose a named type when the
data has behaviour, invariants or a life beyond the call. System.Tuple survives in older APIs; there is no reason to write it in
new code, and Tuple.Create(1, "x") is a reference type that cannot be mutated.
Anonymous types
Creating an anonymous type
An anonymous type is declared by an object initializer that has no type name: New With { ... }. The compiler generates a class
with the properties you list, infers each property’s type from its expression, and gives the class a name you cannot write. They
were introduced in Visual Basic 9.0 (Visual Studio 2008) together with LINQ, for which they were designed.
Option Strict On
Public Class Book
Public Property Title As String = ""
Public Property Year As Integer
End Class
Module Program
Sub Main()
Dim book As New Book() With {.Title = "Dune", .Year = 1965}
' Explicit names, with and without Key.
Dim plain = New With {.Id = 1, .Title = "Dune"}
Dim keyed = New With {Key .Id = 1, Key .Title = "Dune"}
Console.WriteLine(plain)
Console.WriteLine(keyed)
' Names and types inferred from the expressions (Visual Basic 9.0).
Dim inferred = New With {book.Title, book.Year, Key .Decade = (book.Year \ 10) * 10}
Console.WriteLine(inferred)
Console.WriteLine(inferred.Title & " " & inferred.Decade)
' Non-Key properties can be assigned; Key properties are read-only.
plain.Title = "Dune Messiah"
Console.WriteLine(plain.Title)
' Anonymous types can nest and can hold arrays and lambdas.
Dim richer = New With {.Author = New With {.Name = "Frank Herbert"}, .Years = {1965, 1969}, .Greet = Function(s As String) "Hello " & s}
Console.WriteLine(richer.Author.Name & " " & richer.Years.Length & " " & richer.Greet("reader"))
' The type is generated: internal, sealed, generic and without a usable name.
Dim t = keyed.GetType()
Console.WriteLine(t.Name.StartsWith("VB$AnonymousType_") & " " & t.IsGenericType & " " & t.IsSealed & " " & t.IsPublic)
Console.WriteLine(t.BaseType.Name)
End Sub
End Module
{ Id = 1, Title = Dune }
{ Id = 1, Title = Dune }
{ Title = Dune, Year = 1965, Decade = 1960 }
Dune 1960
Dune Messiah
Frank Herbert 2 Hello reader
True True True False
Object
Source: Anonymous Types (Visual Basic)
-
Each member is
.Name = expression. The name may be omitted when the expression is a simple name or a member access, and then the last identifier becomes the property name (book.TitlegivesTitle). Where inference cannot produce a name, you must give one:New With {1 + 2}is BC36556 ("can be inferred only from a simple or qualified name with no arguments"). Names must be unique without regard to case, and may not beEquals,GetHashCodeorToString(all BC36547), because the generated type has those methods. -
The declaration uses local type inference: the variable has no
Asclause, soOption Infer On(the default in a new project) is required. WithOption Infer OffandOption Strict On,Dim a = New With {.X = 1}is BC30209, "requires all variable declarations to have an 'As' clause", and the only way to hold the value is anObjectvariable. See Option Statements. -
The properties can be of any type: other anonymous types, arrays, lambdas, collections.
-
The generated class derives directly from
Object, issealed, is generic (its type parameters are the property types, which is whyIsGenericTypeisTrue) and is not public. Its name starts withVB$AnonymousType_followed by a number and the arity, and it depends on the compilation. Do not use it, parse it, or store it: Learn says the name may change whenever the project is recompiled. -
The properties are read-write unless marked
Key.
Key properties, equality and hashing
Marking a property with Key does three things: the property becomes read-only, it takes part in Equals and GetHashCode, and
(if at least one property is a key) the compiler also implements IEquatable(Of T).
Option Strict On
Module Program
Sub Main()
Dim keyed = New With {Key .Id = 1, Key .Title = "Dune"}
keyed.Id = 2
End Sub
End Module
The SDK reports:
Program.vb(6,9): error BC30526: Property 'Id' is 'ReadOnly'.
Source: Key (Visual Basic)
Option Strict On
Module Program
Sub Main()
Dim a1 = New With {Key .Id = 1, Key .Title = "Dune"}
Dim a2 = New With {Key .Id = 1, Key .Title = "Dune"}
Dim a3 = New With {Key .Id = 1, Key .Title = "Emma"}
Console.WriteLine("Key: " & a1.Equals(a2) & " " & a1.Equals(a3) & " hash equal: " & (a1.GetHashCode() = a2.GetHashCode()))
Dim p1 = New With {.Id = 1, .Title = "Dune"}
Dim p2 = New With {.Id = 1, .Title = "Dune"}
Console.WriteLine("no Key: " & p1.Equals(p2) & " " & p1.Equals(p1))
' Only the Key properties take part in equality.
Dim m1 = New With {Key .Id = 1, .Note = "first"}
Dim m2 = New With {Key .Id = 1, .Note = "second"}
Console.WriteLine("mixed: " & m1.Equals(m2))
' What the compiler generated.
Console.WriteLine("Key interfaces: " & String.Join(",", a1.GetType().GetInterfaces().Select(Function(i) i.Name)))
Console.WriteLine("no-Key interfaces: " & p1.GetType().GetInterfaces().Length)
Console.WriteLine(a1.GetType().GetMethod("Equals", {GetType(Object)}).DeclaringType Is a1.GetType())
Console.WriteLine(p1.GetType().GetMethod("Equals", {GetType(Object)}).DeclaringType Is p1.GetType())
' Type identity: names (case-insensitive), order, types and Key must all match.
Dim same = New With {Key .title = "x", Key .ID = 2}
Dim sameShape = New With {Key .Title = "y", Key .Id = 3}
Dim reordered = New With {Key .Id = 3, Key .Title = "y"}
Dim differentKey = New With {Key .Title = "y", .Id = 3}
Dim differentType = New With {Key .Title = "y", Key .Id = 3L}
Console.WriteLine(same.GetType() Is sameShape.GetType())
Console.WriteLine(sameShape.GetType() Is reordered.GetType())
Console.WriteLine(sameShape.GetType() Is differentKey.GetType())
Console.WriteLine(sameShape.GetType() Is differentType.GetType())
' Equals(Object) between different anonymous types is False, never an error.
Console.WriteLine(sameShape.Equals(reordered))
' Use as keys of hashed collections.
Dim set1 As New HashSet(Of Object) From {a1, a2, a3}
Console.WriteLine(set1.Count)
End Sub
End Module
Key: True False hash equal: True
no Key: False True
mixed: True
Key interfaces: IEquatable`1
no-Key interfaces: 0
True
False
True
False
False
False
False
2
Source: Anonymous Type Definition
| The anonymous type has | Equals and GetHashCode |
Generated members |
|---|---|---|
No |
inherited from |
|
One or more |
element-wise over the key properties; non-key properties are ignored |
|
-
a1.Equals(a2)isTrue: same type, equal keys. The two hash codes are equal, as theEqualscontract requires, so the instances behave as one value in aHashSet(set1.Countis2:a1anda2are one value,a3another). -
Without
Key,p1.Equals(p2)isFalsealthough every value is the same. Onlyp1.Equals(p1)isTrue. This is the opposite of the C# behaviour, where every anonymous type has value equality; a Visual Basic program that relies on value equality must writeKey. -
Equals(Object)between instances of different anonymous types returnsFalseand never throws. There is no=operator for anonymous types either (BC30452). -
ToStringprints{ Id = 1, Title = Dune }for every anonymous type, with or withoutKey. It is convenient for logging and must not be parsed. -
A key property whose value is mutable (an array, a list) still compares by that object’s own
Equals, which is reference equality for most collections.
When two anonymous types are the same type
The compiler reuses one generated class for every declaration of the same shape within an assembly. Same shape means: the same
property names (compared without regard to case), in the same order, with the same inferred types, and with the same properties
marked Key.
The program above compared four pairs of declarations with GetType; the result is in the table.
| Declarations | Same type? |
|---|---|
|
yes |
|
no |
|
no |
|
no |
The same shape declared in two assemblies |
no: each assembly has its own generated class (Learn, not tested here) |
Type identity matters in three places: Equals, assignment (a variable of one anonymous type cannot hold another: BC30311),
and generic inference, for instance when you Add to a List of an anonymous type. A one-letter slip in a property name
quietly produces a new type; a named Class or Structure would have been rejected by the compiler.
Anonymous types in query expressions
Anonymous types were built for LINQ. A Select that names several expressions creates one, and in a query expression the
properties of the generated type are Key properties automatically, so projection results already have value equality.
Query syntax is described in LINQ Query Expressions.
Option Strict On
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public ReadOnly Property Kind As String
Public Sub New(title As String, year As Integer, kind As String)
Me.Title = title
Me.Year = year
Me.Kind = kind
End Sub
End Class
Public Class Loan
Public ReadOnly Property Title As String
Public ReadOnly Property Borrower As String
Public Sub New(title As String, borrower As String)
Me.Title = title
Me.Borrower = borrower
End Sub
End Class
Module Program
Sub Main()
Dim items = {
New Book("Dune", 1965, "Novel"),
New Book("Emma", 1815, "Novel"),
New Book("Nature", 1965, "Magazine"),
New Book("Ulysses", 1922, "Novel"),
New Book("Wired", 1965, "Magazine")}
Dim loans = {New Loan("Dune", "Ana"), New Loan("Wired", "Luis"), New Loan("Dune", "Marta")}
' 1. Projection: with several expressions, Select builds an anonymous type with Key properties.
Dim projection = From b In items
Where b.Year > 1900
Order By b.Title
Select b.Title, b.Year
For Each p In projection
Console.WriteLine($"{p.Title} ({p.Year})")
Next
' 2. Renaming and computing members.
Dim labels = From b In items
Select Label = b.Kind & ": " & b.Title, Age = 2026 - b.Year
Console.WriteLine(labels.First())
' 3. Composite grouping key: it must be a Key anonymous type (or a tuple).
Dim groups = From b In items
Group By k = New With {Key b.Year, Key b.Kind} Into Count()
Order By k.Year, k.Kind
For Each g In groups
Console.WriteLine($"{g.k.Year} {g.k.Kind}: {g.Count}")
Next
Dim brokenGroups = From b In items
Group By k = New With {b.Year, b.Kind} Into Count()
Console.WriteLine("without Key: " & brokenGroups.Count() & " groups instead of " & groups.Count())
' 4. Join on a composite key.
Dim borrowed = From b In items
Join l In loans On New With {Key b.Title} Equals New With {Key l.Title}
Select b.Title, l.Borrower
Console.WriteLine(String.Join("; ", borrowed.Select(Function(r) r.Title & "->" & r.Borrower)))
Dim unmatched = From b In items
Join l In loans On New With {b.Title} Equals New With {l.Title}
Select b.Title, l.Borrower
Console.WriteLine("without Key: " & unmatched.Count() & " matches")
' 5. Distinct only collapses anonymous values when they have Key properties.
Console.WriteLine(items.Select(Function(b) New With {Key b.Kind}).Distinct().Count())
Console.WriteLine(items.Select(Function(b) New With {b.Kind}).Distinct().Count())
' 6. Method syntax, and a List of the generated type.
Dim list = items.Select(Function(b) New With {Key b.Title, .Initial = b.Title(0)}).ToList()
list.Add(New With {Key .Title = "Extra", .Initial = "E"c})
Console.WriteLine(list.Count & " " & list(5).ToString())
End Sub
End Module
Dune (1965)
Nature (1965)
Ulysses (1922)
Wired (1965)
{ Label = Novel: Dune, Age = 61 }
1815 Novel: 1
1922 Novel: 1
1965 Magazine: 2
1965 Novel: 1
without Key: 5 groups instead of 4
Dune->Ana; Dune->Marta; Wired->Luis
without Key: 0 matches
2
5
6 { Title = Extra, Initial = E }
Source: Select Clause
-
Select b.Title, b.Yearprojects an anonymous type whose properties are calledTitleandYear;Select Label = ..., Age = ...names computed members. When a query selects one expression or the whole range variable it does not need an anonymous type. -
Group By k = New With {Key b.Year, Key b.Kind}groups by a composite key. The grouping compares keys withEquals, so the key must have value equality:Keyproperties (or a tuple,Group By k = (b.Year, b.Kind), which also works). WithoutKey, every book became its own group:5 groups instead of 4, and the error is silent. -
The same rule applies to
Join ... On New With {...} Equals New With {...}: both sides must be the same anonymous type, so the property names must match (Titleon both sides) and beKey. WithoutKey, the join matched nothing (0 matches) and no diagnostic was produced. Single-expression joins,On b.Title Equals l.Title, need none of this; see LINQ Query Expressions. -
Distinctcollapses anonymous values only if they haveKeyproperties: 2 kinds withKey, 5 books without. -
Select(Function(b) New With {Key b.Title, .Initial = b.Title(0)})is the method-syntax form.ToListgives aListof the generated class and the compiler lets youAddanother instance of the same shape.
Query results of anonymous types are read-only from outside the query: a projection is a view, not a copy of the source rows. Writing
back to the source takes the original object (Select b or Select Item = b, ...).
Limits and workarounds
An anonymous type has no name, so there is no place in the language where you can write its type: not a field, a property, a parameter or a return type.
Option Strict On
Module Program
Private ReadOnly Origin = New With {.X = 0, .Y = 0}
Sub Main()
Console.WriteLine(Origin)
End Sub
End Module
The SDK reports:
Program.vb(4,22): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Option Strict On
Module Program
Function MakeItem() As Object
Return New With {.Title = "Dune", .Year = 1965}
End Function
Sub Main()
Dim item = MakeItem()
Console.WriteLine(item.Title)
End Sub
End Module
The SDK reports:
Program.vb(10,27): error BC30574: Option Strict On disallows late binding.
Source: Anonymous Types (Visual Basic)
-
A field needs an
Asclause underOption Strict On(BC30209), and no clause can name the type. -
Returning the value as
Objectworks, butitem.Titleis then late binding (BC30574 underOption Strict On). WithoutOption Strict Onthe same code compiles and fails withMissingMemberExceptionat run time when a name is wrong. See Early and Late Binding. -
So anonymous types live inside one method. When the shape must leave it, change to a tuple (pass the shape across methods) or a named type. When the data stays inside the method but is processed by a helper, a generic method can accept it:
Option Strict On
Module Program
' Generic inference lets a helper accept an anonymous type without naming it.
Function Describe(Of T)(value As T) As String
Return "anonymous=" & GetType(T).Name.StartsWith("VB$AnonymousType_") & " -> " & value.ToString()
End Function
' A lambda keeps the anonymous type in its return type, inside one method.
Function Project(Of T, TResult)(source As IEnumerable(Of T), selector As Func(Of T, TResult)) As List(Of TResult)
Return source.Select(selector).ToList()
End Function
' The "cast by example" trick: recover the type from a sample of the same shape.
Function CastByExample(Of T)(value As Object, example As T) As T
Return DirectCast(value, T)
End Function
Function Boxed() As Object
Return New With {Key .Id = 7, Key .Title = "Dune"}
End Function
Sub Main()
Console.WriteLine(Describe(New With {Key .Id = 1}))
Dim rows = Project({"Dune", "Emma"}, Function(s) New With {Key .Title = s, .Length = s.Length})
Console.WriteLine(rows(1).Title & " " & rows(1).Length)
Dim sample = New With {Key .Id = 0, Key .Title = ""}
Dim back = CastByExample(Boxed(), sample)
Console.WriteLine(back.Id & " " & back.Title)
End Sub
End Module
anonymous=True -> { Id = 1 }
Emma 4
7 Dune
Source: Anonymous Types (Visual Basic)
-
Generic type inference (
Describe(Of T),Project(Of T, TResult)) lets the compiler pass the unnameable type around without you naming it, and the lambda’s return type becomesTResult. This is how LINQ itself works. -
CastByExamplerecovers the static type from a sample value of the same shape (sample), because the sample and the boxed value are instances of one generated class. It is a well-known trick and a fragile one: it compiles whatever the shapes are, and a mismatch throwsInvalidCastExceptionat run time. Prefer a tuple or a named type to this.
Choosing between tuples, anonymous types and named types
| Situation | Choice |
|---|---|
A function returns two to four related values |
named tuple |
A |
tuple |
LINQ projection used in the same method |
anonymous type (automatically |
Composite key for |
anonymous type with |
Composite key of a |
tuple; an anonymous type cannot be named in |
Result stored in a field or passed between classes |
tuple, or better a |
Needs methods, validation, immutability or serialization shape |
|
Value object with equality semantics for a domain concept |
named |
Serialization, reflection and interoperability
Option Strict On
Imports System.Text.Json
Module Program
Sub Main()
Dim anonymous = New With {.Title = "Dune", .Year = 1965}
Console.WriteLine(JsonSerializer.Serialize(anonymous))
Dim pair = (Title:="Dune", Year:=1965)
Console.WriteLine(JsonSerializer.Serialize(pair))
Dim options As New JsonSerializerOptions() With {.IncludeFields = True}
Console.WriteLine(JsonSerializer.Serialize(pair, options))
' Deserializing needs a type name: anonymous types have none, tuples ignore names.
Dim back = JsonSerializer.Deserialize(Of (Title As String, Year As Integer))("{""Item1"":""Dune"",""Item2"":1965}", options)
Console.WriteLine(back)
End Sub
End Module
{"Title":"Dune","Year":1965}
{}
{"Item1":"Dune","Item2":1965}
(Dune, 1965)
-
System.Text.Jsonserializes the public properties of an anonymous type, so a projection can be written straight to a response body. It cannot deserialize into one, since no type name can be passed. -
Tuple elements are public fields, and the serializer ignores fields by default, so a tuple serializes as the empty object
{}. SetIncludeFields = Trueand the elements appear asItem1andItem2; the element names are not available at run time (they live in the attribute), so they never appear in the JSON. For wire formats use aClassorStructurewith properties (see Serialization: JSON and XML). -
A source-generated serializer context declares its types by name, so it cannot list an anonymous type; with trimming or native AOT prefer named types for anything that is serialized.
-
Another .NET language sees the same types: a Visual Basic function returning
(Min As Integer, Max As Integer)isValueTuple<int, int>for C# and F#, and the C# compiler readsTupleElementNamesAttributeto show the names again. An anonymous type is internal to its assembly and has no meaning outside it. See Interop with C# and Other .NET Languages. -
On .NET Framework before 4.7 the
System.ValueTuplepackage must be referenced by every project that exposes a tuple.
Differences from C#
| Topic | Visual Basic 17.13 | C# (see Tuples, Deconstruction and Anonymous Types) |
|---|---|---|
Tuple literal |
|
|
Deconstruction |
none: no |
|
Tuple |
not defined (BC30452); use |
|
Name inference |
Visual Basic 15.3 |
C# 7.1 |
Pattern matching on tuples |
none |
tuple patterns in |
Anonymous type syntax |
|
|
Mutability of anonymous types |
non- |
always read-only |
Anonymous type equality |
value equality only for |
always value equality |
Query projections |
automatically |
read-only value-equal anonymous type |
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
BC30203, BC30035 for |
Visual Basic has no deconstruction; read the elements by name |
BC30452 on |
no |
BC30452 for |
|
BC30068 on |
the indexer returns a copy; copy, change, store back (or use an array) |
BC41009, BC37261, BC37262 |
a tuple element name is dropped, misplaced or duplicated |
BC30311, BC30512 |
tuple of another arity, or a narrowing element conversion under |
BC30526 on an anonymous type property |
the property is a |
BC30209 or BC30574 around an anonymous type |
no |
BC36547, BC36556 |
duplicate or reserved property name; or a member whose name cannot be inferred |
Group By or Join returns too many groups or no rows |
the anonymous key type has no |
A tuple prints as |
tuple elements are fields; set |
Stored tuple changes after being used as a dictionary key |
tuples are mutable: never modify a key |
Older material you may meet
Books and snippets written before 2017 predate the tuple syntax and show the pre-tuple techniques; they still compile, but newer code does not need them.
-
Tuple.Create(a, b)andDim t As Tuple(Of Integer, String): theSystem.Tupleclasses, read-only reference objects withItem1andItem2. TodayValueTupleliterals replace them, and conversions between the two families need explicit code. -
ByRefoutput parameters,Structuretypes made for a single method, and returning anObject()array or aCollectionof boxed values. VB6 and VBA code returned several results throughByRefarguments orVariantarrays. -
Material from the Visual Basic 2008 to 2015 era describes anonymous types and
Keycorrectly, but it predates tuples, so it uses an anonymous type or aStructurewhere a named tuple is now the lightest option, and it cannot mention name inference (15.3). -
Query-syntax
Selectcreates anonymous types whose properties are allKeyproperties, while an explicitNew WithwithoutKeydoes not. A snippet that moves a projection from query syntax to an explicitNew Withcan therefore change the behaviour ofDistinct,Group By,JoinandEquals.
References
-
Anonymous Types (Visual Basic) and Anonymous Type Definition
-
How to: Infer Property Names and Types in Anonymous Type Declarations
-
Key (Visual Basic) and Object Initializers: Named and Anonymous Types
-
Select Clause, Group By Clause, Join Clause and Distinct Clause
-
Tuple types (C# reference), Anonymous types (C#) and Deconstructing tuples and other types (C#)