Structures and Value 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 (except the ones that are
meant to fail, which say so) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The root namespace of the
scratch projects is Shelf, which is why runtime messages mention Shelf.Cursor.
A structure is a value type that you declare yourself: a bundle of fields, properties, methods and operators whose variables
contain the data instead of pointing at it. Date, Decimal, Integer, Guid, TimeSpan, KeyValuePair(Of TKey, TValue) and the
tuples written (Integer, String) are all structures, and so is the Isbn type of the shelf library catalogue used throughout this
reference. Visual Basic has had Structure since the first .NET release, where it replaced the VB6 Type ... End Type user-defined
type (see The VB Family: VB6, VBA and VBScript).
Structures are the right tool for small, immutable, value-like data and the wrong tool for almost everything else, and the language
makes the wrong choice easy to make by accident: several errors that C# programmers know are silent in Visual Basic.
Declaring a structure
[<attributes>] [accessmodifier] [Shadows] [Partial] Structure Name [(Of typeparameters)]
[Implements interface]
' fields, constants, properties, methods, operators, events, nested types
End Structure
-
A structure can be declared at namespace level or inside a class, structure, module or interface. Without an access modifier a top-level structure is
Friendand a nested one isPublic, as for other types. -
Members are
Publicby default, unlike a class, where aDimfield isPrivate. The example below shows it by reflection. WritingDim Field As Integerin a structure exposes a public field. -
A structure may be generic (
Structure Pair(Of T)) and may bePartial. It cannot beMustInherit,NotInheritableorReadOnly: a structure is implicitly sealed, and Visual Basic has noReadOnly Structure(error BC30395). -
A structure implicitly inherits
System.ValueTypeand cannot name any other base class. It can implement any number of interfaces withImplements. -
The fields are laid out in declaration order (sequential layout) unless a
StructLayoutattribute says otherwise (see Memory layout).
Option Strict On
Structure DefaultAccess
Dim Field As Integer
Sub Method()
End Sub
Class Nested
End Class
End Structure
Class DefaultAccessClass
Dim Field As Integer
Sub Method()
End Sub
Class Nested
End Class
End Class
Module Program
Sub Main()
Console.WriteLine("Structure type: " & If(GetType(DefaultAccess).IsPublic, "Public", "Friend"))
Console.WriteLine(" field Public: " & GetType(DefaultAccess).GetField("Field")?.IsPublic)
Console.WriteLine(" method Public: " & GetType(DefaultAccess).GetMethod("Method")?.IsPublic)
Console.WriteLine(" nested Public: " & GetType(DefaultAccess).GetNestedType("Nested")?.IsNestedPublic)
Dim flags = Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Public Or Reflection.BindingFlags.NonPublic
Console.WriteLine("Class:")
Console.WriteLine(" field Private: " & GetType(DefaultAccessClass).GetField("Field", flags)?.IsPrivate)
Console.WriteLine(" method Public: " & GetType(DefaultAccessClass).GetMethod("Method")?.IsPublic)
Console.WriteLine(" nested Public: " & GetType(DefaultAccessClass).GetNestedType("Nested")?.IsNestedPublic)
End Sub
End Module
Structure type: Friend
field Public: True
method Public: True
nested Public: True
Class:
field Private: True
method Public: True
nested Public: True
Source: Structure Statement
Rules a structure must obey
The compiler rejects each of the declarations in the next program. Several of them are legal in a C# 10 or later struct, so the list is
a good fingerprint of what a Visual Basic structure cannot be. The program fails to build on purpose and the output is the compiler’s
complete list of errors.
Option Strict On
Public Structure InitialValue
Public Pages As Integer = 100
End Structure
Public Structure DefaultConstructor
Public Sub New()
End Sub
End Structure
Public Structure Derived
Inherits Object
End Structure
Public Structure OverridableMember
Public Overridable Sub Print()
End Sub
End Structure
Public Structure AbstractMember
MustOverride Sub Print()
End Structure
Public Structure ProtectedField
Protected Hidden As Integer
End Structure
Public Structure Recursive
Public Inner As Recursive
End Structure
Public Structure SizedArray
Public Items(3) As Integer
End Structure
Public Structure PublicSharedConstructor
Public Shared Sub New()
End Sub
End Structure
ReadOnly Structure Frozen
Public Pages As Integer
End Structure
Module Program
Sub Main()
End Sub
End Module
Program.vb(4): BC31049: Initializers on structure members are valid only for 'Shared' members and constants.
Program.vb(8): BC30629: Structures cannot declare a non-shared 'Sub New' with no parameters.
Program.vb(13): BC30628: Structures cannot have 'Inherits' statements.
Program.vb(17): BC30435: Members in a Structure cannot be declared 'Overridable'.
Program.vb(22): BC30435: Members in a Structure cannot be declared 'MustOverride'.
Program.vb(26): BC30435: Members in a Structure cannot be declared 'Protected'.
Program.vb(30): BC30294: Structure 'Recursive' cannot contain an instance of itself:
Program.vb(34): BC31043: Arrays declared as structure members cannot be declared with an initial size.
Program.vb(38): BC30480: Shared 'Sub New' cannot be declared 'Public'.
Program.vb(42): BC30395: 'ReadOnly' is not valid on a Structure declaration.
Source: Structure Statement
| Error | Rule | Why and what to do |
|---|---|---|
BC31049 |
No instance field initializers |
|
BC30629 |
No parameterless instance |
Every structure has an implicit parameterless constructor that sets every field to its zero value, and you cannot replace it. Code such as |
BC30628 |
No |
A structure derives from |
BC30435 |
No |
Nothing can derive from a structure, so those modifiers have no meaning. You can still override |
BC30294 |
No field of the structure’s own type |
|
BC31043 |
No array field with a declared size |
|
BC30480 |
|
The static constructor of a class has the same rule. Write |
BC30395 |
No |
There is no readonly structure in Visual Basic. See Immutable structures. |
Two rules that the Language Reference lists are not enforced by the 17.13 compiler on SDK 10.0.402, which is why the table
does not claim them as errors: an empty Structure with no members compiles, and so does a structure that contains only Shared
members. The Learn page for structures and classes still states the older limit (a structure must have at least one non-shared
variable or event), which is an example of the text lagging the compiler.
Value semantics: assignment copies
A variable of a structure type holds the fields themselves. Assignment, argument passing by value, return values, boxing and
storing in a collection all copy the bits; a change to the copy never affects the original. The program below shows the
copy on assignment, on a ByVal call, and the ways of getting a change back to the caller (ByRef, or returning the new
value).
Option Strict On
Public Structure PageRange
Public First As Integer
Public Last As Integer
Public Sub New(first As Integer, last As Integer)
Me.First = first
Me.Last = last
End Sub
Public ReadOnly Property Count As Integer
Get
Return Last - First + 1
End Get
End Property
Public Overrides Function ToString() As String
Return First & "-" & Last
End Function
End Structure
Module Program
Sub Extend(range As PageRange)
range.Last += 10
Console.WriteLine("inside Extend: " & range.ToString())
End Sub
Sub ExtendInPlace(ByRef range As PageRange)
range.Last += 10
End Sub
Sub Main()
Dim a As New PageRange(1, 20)
Dim b As PageRange = a ' copies every field
b.Last = 99
Console.WriteLine("a = " & a.ToString() & ", b = " & b.ToString())
Extend(a) ' ByVal: the callee gets a copy
Console.WriteLine("after Extend: " & a.ToString())
ExtendInPlace(a)
Console.WriteLine("after ByRef: " & a.ToString())
Dim blank As PageRange ' all fields are zero
Dim viaNew As New PageRange() ' the implicit parameterless New does the same
Console.WriteLine("blank = " & blank.ToString() & ", viaNew = " & viaNew.ToString() & ", Count = " & blank.Count)
Dim reset As PageRange = a
reset = Nothing ' Nothing for a structure means default(PageRange)
Console.WriteLine("reset = " & reset.ToString())
Console.WriteLine("a is value type: " & GetType(PageRange).IsValueType)
End Sub
End Module
a = 1-20, b = 1-99
inside Extend: 1-30
after Extend: 1-20
after ByRef: 1-30
blank = 0-0, viaNew = 0-0, Count = 1
reset = 0-0
a is value type: True
Source: Structure Variables
Notes on the listing:
-
Dim b As PageRange = ais a copy, not an alias, sob.Last = 99does not toucha. For a class the same two lines would make two names for one object. -
Extend(a)passes a copy. The method’srange.Last += 10changes only that copy.ExtendInPlace(ByRef range ...)receives a reference to the caller’s variable and changes it. The VB6 default ofByRefis gone: a parameter isByValunless you say otherwise (see Procedures: Sub and Function). -
Dim blank As PageRange,Dim viaNew As New PageRange()andreset = Nothingall give the same all-zero value. For a structure,Nothingmeans "the default value of the type" (it is not a null reference; see Nothing and Nullable Value Types).Is Nothingon a structure does not compile (BC30020, below). -
Countis a computed property: the structure has no state beyond its two fields.
Where the storage is
The common statement that "structures live on the stack" is a simplification. The storage of a value type is where the variable is:
| The variable is | A structure value is stored |
|---|---|
a local variable or parameter |
in the method’s frame (stack or CPU register, at the JIT’s discretion) |
a field of a class |
inline in the heap object of that class |
a field of another structure |
inline in the enclosing structure |
an element of an array |
inline in the array’s heap block, so |
boxed into an |
in a separate heap object (a box), subject to garbage collection |
captured by a lambda or an |
in a heap object that the compiler generates (a closure or state machine) |
The garbage collector never runs a finalizer for a structure, and an array of structures that contain no references costs it almost nothing to scan; those are the benefits. The cost is the copy on every assignment and call, which grows with the size of the structure.
Default values and constructors
Every structure has an implicit parameterless New that zeroes all fields, so New PageRange() is always legal even when you declare
only constructors with parameters. That has a consequence for any invariant you try to protect: a validating constructor is
simply bypassed by Dim x As Isbn, by Nothing, by an array of the type and by Activator.CreateInstance. The
shelf Isbn shows the usual defence: make the constructor Private, offer Parse and TryParse, and give the zero value an
explicit meaning (IsEmpty).
Option Strict On
Public Structure Isbn
Implements IEquatable(Of Isbn)
Public ReadOnly Property Value As String
Private Sub New(value As String)
Me.Value = value
End Sub
Public ReadOnly Property IsEmpty As Boolean
Get
Return Value Is Nothing
End Get
End Property
Public Shared Function TryParse(text As String, ByRef result As Isbn) As Boolean
Dim digits = New String(If(text, "").Where(AddressOf Char.IsLetterOrDigit).ToArray())
If digits.Length = 13 AndAlso digits.All(AddressOf Char.IsDigit) AndAlso HasValidCheckDigit(digits) Then
result = New Isbn(digits)
Return True
End If
Return False
End Function
Public Shared Function Parse(text As String) As Isbn
Dim result As Isbn = Nothing ' initialised: passing an unassigned structure ByRef is BC42108
If Not TryParse(text, result) Then
Throw New FormatException("'" & text & "' is not a valid ISBN-13.")
End If
Return result
End Function
Private Shared Function HasValidCheckDigit(digits As String) As Boolean
Dim sum = 0
For i = 0 To 12
Dim d = AscW(digits(i)) - AscW("0"c)
sum += If(i Mod 2 = 0, d, d * 3)
Next
Return sum Mod 10 = 0
End Function
Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
Return Value = other.Value
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is Isbn AndAlso Equals(DirectCast(obj, Isbn))
End Function
Public Overrides Function GetHashCode() As Integer
Return If(Value, "").GetHashCode()
End Function
Public Overrides Function ToString() As String
Return If(Value, "(empty)")
End Function
End Structure
Module Program
Sub Main()
Dim good = Isbn.Parse("978-0-13-468599-1")
Console.WriteLine("parsed: " & good.ToString() & ", IsEmpty " & good.IsEmpty)
Dim blank As New Isbn() ' the default value skips the private constructor
Console.WriteLine("default: " & blank.ToString() & ", IsEmpty " & blank.IsEmpty)
Console.WriteLine("good = blank: " & good.Equals(blank))
Dim parsed As Isbn = Nothing
Console.WriteLine("TryParse bad: " & Isbn.TryParse("123", parsed) & " -> " & parsed.ToString())
Console.WriteLine("TryParse good: " & Isbn.TryParse("9780134685991", parsed) & " -> " & parsed.ToString())
Dim maybe As Isbn? = Nothing ' Nullable(Of Isbn) says "no ISBN" explicitly
Console.WriteLine("maybe.HasValue: " & maybe.HasValue)
maybe = good
Console.WriteLine("maybe.Value: " & maybe.Value.ToString())
Dim shelf(2) As Isbn ' array elements are default structures, not Nothing
shelf(1) = good
For i = 0 To shelf.Length - 1
Console.WriteLine("shelf(" & i & ") = " & shelf(i).ToString())
Next
Try
Isbn.Parse("978-0-13-468599-2")
Catch ex As FormatException
Console.WriteLine("FormatException: " & ex.Message)
End Try
End Sub
End Module
parsed: 9780134685991, IsEmpty False
default: (empty), IsEmpty True
good = blank: False
TryParse bad: False -> (empty)
TryParse good: True -> 9780134685991
maybe.HasValue: False
maybe.Value: 9780134685991
shelf(0) = (empty)
shelf(1) = 9780134685991
shelf(2) = (empty)
FormatException: '978-0-13-468599-2' is not a valid ISBN-13.
Source: Structures and Classes
Details worth keeping:
-
The private constructor takes a parameter, so it is not the forbidden parameterless one. The implicit parameterless constructor stays
Public: you cannot hide it. That is whyblankexists at all. -
A constructor need not assign every field. Unlike C# before version 11, there is no "all fields must be assigned" rule and no definite-assignment error; unassigned fields are zero.
-
A reference-type field of a structure is
Nothingin the zero value. UseIf(Value, "")or a property that substitutes a safe default, asToStringdoes above. -
Unassigned variables. A structure that contains a reference-type member gets the flow-analysis warnings that a reference variable gets. Passing
Dim p As PbyByRefbefore assigning it is BC42108 ("Variable 'p' is passed by reference before it has been assigned a value. A null reference exception could result at runtime. Make sure the structure or all the reference members are initialized before use"), and reading a field of such a variable can be BC42109. Initialising the variable with= NothingorNewsilences them, which is whyParseassignsresult = Nothingbefore callingTryParse, and why the project hasTreatWarningsAsErrors: a program that left out the initialiser would not build. -
Dim shelf(2) As Isbncreates three zero values, not threeNothingreferences.shelf(0).Value Is NothingisTrue.
Constants, shared members and operators in a structure
A structure may hold constants, Shared fields, a Shared Sub New (no access modifier), properties (including auto-implemented
properties), methods, operators, conversion operators and nested types. The program below combines them in a Dimensions
structure for the physical size of a book, and also shows Me.New(...) chaining, an Optional constructor parameter,
IComparable(Of T), an object initializer and a With block on a local structure variable.
Option Strict On
Public Structure Dimensions
Implements IComparable(Of Dimensions)
Public Const Unit As String = "cm"
Public Shared ReadOnly Empty As Dimensions
Public Shared ReadOnly A5 As New Dimensions(14.8, 21.0)
Private Shared _created As Integer
Public Property Width As Double
Public Property Height As Double
Public Property Depth As Double
Shared Sub New()
Console.WriteLine("Shared constructor runs once, before first use")
End Sub
Public Sub New(width As Double, height As Double, Optional depth As Double = 1.0)
Me.Width = width
Me.Height = height
Me.Depth = depth
_created += 1
End Sub
Public Sub New(side As Double)
Me.New(side, side) ' constructor chaining uses Me.New
End Sub
Public Shared ReadOnly Property CreatedCount As Integer
Get
Return _created
End Get
End Property
Public ReadOnly Property Volume As Double
Get
Return Width * Height * Depth
End Get
End Property
Public Function CompareTo(other As Dimensions) As Integer Implements IComparable(Of Dimensions).CompareTo
Return Volume.CompareTo(other.Volume)
End Function
Public Overrides Function ToString() As String
Return String.Format("{0} x {1} x {2} {3}", Width, Height, Depth, Unit)
End Function
Public Shared Operator +(left As Dimensions, right As Dimensions) As Dimensions
Return New Dimensions(Math.Max(left.Width, right.Width), Math.Max(left.Height, right.Height), left.Depth + right.Depth)
End Operator
Public Shared Widening Operator CType(side As Double) As Dimensions
Return New Dimensions(side)
End Operator
End Structure
Module Program
Sub Main()
Console.WriteLine("Main starts")
Console.WriteLine("Empty = " & Dimensions.Empty.ToString())
Console.WriteLine("A5 = " & Dimensions.A5.ToString() & ", volume " & Dimensions.A5.Volume)
Dim cube As New Dimensions(2.5)
Dim initialised As New Dimensions With {.Width = 10, .Height = 15, .Depth = 2}
Console.WriteLine("cube = " & cube.ToString())
Console.WriteLine("initialised = " & initialised.ToString())
Dim stack As Dimensions = cube + cube
Console.WriteLine("stack = " & stack.ToString())
Dim widened As Dimensions = 4.0 ' Widening CType: implicit under Option Strict On
Console.WriteLine("widened = " & widened.ToString())
Dim sorted As New List(Of Dimensions) From {stack, cube, initialised, Dimensions.A5}
sorted.Sort()
For Each d In sorted
Console.WriteLine(" " & d.ToString() & " -> " & d.Volume)
Next
With initialised
.Depth = 3 ' With on a local variable changes the variable
End With
Console.WriteLine("after With: " & initialised.ToString())
Console.WriteLine("constructor calls with arguments: " & Dimensions.CreatedCount)
End Sub
End Module
Main starts
Shared constructor runs once, before first use
Empty = 0 x 0 x 0 cm
A5 = 14.8 x 21 x 1 cm, volume 310.8
cube = 2.5 x 2.5 x 1 cm
initialised = 10 x 15 x 2 cm
stack = 2.5 x 2.5 x 2 cm
widened = 4 x 4 x 1 cm
2.5 x 2.5 x 1 cm -> 6.25
2.5 x 2.5 x 2 cm -> 12.5
10 x 15 x 2 cm -> 300
14.8 x 21 x 1 cm -> 310.8
after With: 10 x 15 x 3 cm
constructor calls with arguments: 4
Source: Operator Statement
Observations:
-
The
Sharedfield initializers run before theShared Sub Newbody, and both run lazily, before the first use of the structure (Dimensions.Emptyin this program), not at program start. Pay attention to the output order: "Main starts" appears first. -
New Dimensions With {.Width = 10}calls the implicit parameterless constructor and then sets the properties. It is the usual way to create a structure that has public settable members, and it bypasses every constructor you wrote: the constructor count at the end is 4 (A5,cube,stackandwidened) and does not includeinitialised. -
Shared ReadOnly Empty As Dimensionsis allowed although a field of the structure’s own type is not (BC30294 applies to instance fields only). -
Dim widened As Dimensions = 4.0works becauseWidening Operator CTypeis implicit even underOption Strict On; make a conversionNarrowingwhen information can be lost (see Operator Overloading and Equality). -
With initialised ... End Withon a local variable changes that variable. The same block on an expression that returns a copy (With list(0)) is rejected when it assigns (.V = 5is BC30068); see the next section.
Mutable structures and the lost update
The most common error in programs that use structures is changing a copy. Visual Basic makes a different part of the problem loud than C# does. The table summarises what happens when you change a structure that you reached through each kind of expression.
| You write | Result | Why |
|---|---|---|
|
works |
a local variable is a variable |
|
works |
a field is a variable |
|
works |
an array element is a variable |
|
BC30068 |
the property returns a copy, there is nothing to assign to |
|
BC30068 |
the indexer returns a copy |
|
BC30068 |
same |
|
BC30064 |
a |
|
compiles; change is lost |
the call operates on a temporary copy |
|
compiles; change is lost |
the call operates on a defensive copy |
|
compiles; change is lost |
the indexer returns a copy |
|
compiles; change is lost |
the iteration variable is a copy |
The assignment rows are errors; the method-call and For Each rows are silent. The messages below come from this program, which fails to
build on purpose:
Option Strict On
Public Structure Counter
Public Value As Integer
End Structure
Public Class Holder
Public Property AsProperty As Counter
Public ReadOnly AsReadOnlyField As Counter
End Class
Module Program
Sub Main()
Dim list As New List(Of Counter) From {New Counter()}
list(0).Value = 5
Dim h As New Holder()
h.AsProperty.Value = 5
h.AsReadOnlyField.Value = 5
Dim a As New Counter()
Dim b As New Counter()
Console.WriteLine(a = b)
Console.WriteLine(a Is b)
End Sub
End Module
Program.vb(15): BC30068: Expression is a value and therefore cannot be the target of an assignment.
Program.vb(18): BC30068: Expression is a value and therefore cannot be the target of an assignment.
Program.vb(19): BC30064: 'ReadOnly' variable cannot be the target of an assignment.
Program.vb(23): BC30452: Operator '=' is not defined for types 'Counter' and 'Counter'.
Program.vb(24): BC30020: 'Is' operator does not accept operands of type 'Counter'. Operands must be reference or nullable types.
Source: Structure Variables
The silent cases are the dangerous ones. The next program calls a mutating method through each kind of expression:
Option Strict On
Public Structure Counter
Public Value As Integer
Public Sub Increment()
Value += 1
End Sub
End Structure
Public Class Holder
Public Property AsProperty As Counter
Public AsField As Counter
Public ReadOnly AsReadOnlyField As Counter
End Class
Module Program
Sub Main()
Dim h As New Holder()
h.AsField.Increment() ' a field is a variable: changed in place
h.AsField.Value += 1
h.AsProperty.Increment() ' a property returns a copy: the change is lost
h.AsReadOnlyField.Increment() ' ReadOnly field: operates on a copy
Console.WriteLine("field = " & h.AsField.Value)
Console.WriteLine("property = " & h.AsProperty.Value)
Console.WriteLine("readonly = " & h.AsReadOnlyField.Value)
Dim list As New List(Of Counter) From {New Counter()}
list(0).Increment() ' the indexer returns a copy
Console.WriteLine("list(0) = " & list(0).Value)
Dim copy = list(0) ' correct pattern: read, change, write back
copy.Increment()
list(0) = copy
Console.WriteLine("written = " & list(0).Value)
Dim array(0) As Counter
array(0).Increment() ' an array element is a variable
Console.WriteLine("array(0) = " & array(0).Value)
For Each item In list
item.Increment() ' the loop variable is a copy
Next
Console.WriteLine("after For Each = " & list(0).Value)
Dim boxed As Object = h.AsField ' boxing copies
h.AsField.Increment()
Console.WriteLine("boxed = " & DirectCast(boxed, Counter).Value & ", field = " & h.AsField.Value)
End Sub
End Module
field = 2
property = 0
readonly = 0
list(0) = 0
written = 1
array(0) = 1
after For Each = 1
boxed = 2, field = 3
Only the public field and the array element changed. The property, the ReadOnly field, the List(Of T) indexer and the For Each
variable all operated on copies, and the program printed 0 without any diagnostic. The corrective pattern, read the element into
a variable, change it and write it back, is the one used for written = 1. Boxing (the last line) also copies: boxed kept the old
value 2 while the field went on to 3.
Visual Basic also forbids assigning to Me inside a structure method (Me = New Counter() is BC30068, where C# allows
this = new Counter()); reset the fields one by one or return a new value.
Immutable structures
Because a mutable structure is so easy to misuse, the guidance for structures in .NET is to make them immutable: all fields
ReadOnly, no setters, "mutation" methods that return a new value. Visual Basic has no ReadOnly Structure and no readonly
member keyword, so the discipline is by convention:
-
declare fields
Public ReadOnly(orPrivate ReadOnlybehindReadOnly Property), and initialise them in the constructor; -
declare auto-implemented properties as
Public ReadOnly Property X As Integer, assigned in the constructor; -
return a new value from every "change" (
Public Function WithLast(last As Integer) As PageRange); -
the
PageRangestructure in the equality example below is built that way.
A consequence is that the compiler does not mark a Visual Basic structure as readonly in metadata, so a C# compiler calling a
method on one through an in parameter or a readonly field must make defensive copies. For a hot path that C# code consumes, build
the structure in C# as a readonly struct and reference it from the Visual Basic project; see the end of this page.
Equality: Equals, GetHashCode and operators
A structure that declares nothing gets Equals(Object) and GetHashCode from System.ValueType: two values are equal when all
their fields are equal. The inherited Equals can use reflection, and the inherited GetHashCode may not mix every field into the
result, so for anything that is used as a dictionary key or in a hot loop you should override both. The operators = and <> do
not exist for a structure unless you define them (BC30452 earlier), and Is is not allowed at all, because a structure has no
reference identity (BC30020).
Option Strict On
' No overrides: Equals comes from System.ValueType and compares field by field.
Public Structure ShelfSlot
Public Row As Integer
Public Position As Integer
End Structure
' Complete equality: IEquatable(Of T), Equals(Object), GetHashCode and both operators.
Public Structure PageRange
Implements IEquatable(Of PageRange)
Public ReadOnly First As Integer
Public ReadOnly Last As Integer
Public Sub New(first As Integer, last As Integer)
Me.First = first
Me.Last = last
End Sub
Public Overloads Function Equals(other As PageRange) As Boolean Implements IEquatable(Of PageRange).Equals
Return First = other.First AndAlso Last = other.Last
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is PageRange AndAlso Equals(DirectCast(obj, PageRange))
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(First, Last)
End Function
Public Shared Operator =(left As PageRange, right As PageRange) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As PageRange, right As PageRange) As Boolean
Return Not left.Equals(right)
End Operator
Public Overrides Function ToString() As String
Return First & "-" & Last
End Function
End Structure
Module Program
Sub Main()
Dim s1 As ShelfSlot
s1.Row = 2
s1.Position = 7
Dim s2 As ShelfSlot = s1
Console.WriteLine("ValueType.Equals: " & s1.Equals(s2))
Console.WriteLine("Object.Equals: " & Object.Equals(s1, s2))
Console.WriteLine("same hash code: " & (s1.GetHashCode() = s2.GetHashCode()))
' s1 = s2 would not compile: BC30452, no operator '=' for ShelfSlot.
Dim a As New PageRange(1, 20)
Dim b As New PageRange(1, 20)
Dim c As New PageRange(5, 8)
Console.WriteLine("a = b: " & (a = b))
Console.WriteLine("a <> c: " & (a <> c))
Console.WriteLine("a.Equals(CObj(b)): " & a.Equals(CObj(b)))
Console.WriteLine("a.Equals(""x""): " & a.Equals("x"))
Dim seen As New HashSet(Of PageRange) From {a, b, c}
Console.WriteLine("distinct ranges: " & seen.Count)
Dim titles As New Dictionary(Of PageRange, String)
titles(a) = "Introduction"
Console.WriteLine("lookup by value: " & titles(New PageRange(1, 20)))
Dim nullable As PageRange? = Nothing
Console.WriteLine("Nothing for ?: " & (nullable Is Nothing) & ", HasValue " & nullable.HasValue)
nullable = a
Console.WriteLine("nullable = a: " & (nullable = a))
End Sub
End Module
ValueType.Equals: True
Object.Equals: True
same hash code: True
a = b: True
a <> c: True
a.Equals(CObj(b)): True
a.Equals("x"): False
distinct ranges: 2
lookup by value: Introduction
Nothing for ?: True, HasValue False
nullable = a: True
Source: ValueType.Equals
The complete recipe, which is also the one the shelf Isbn uses:
-
Implement
IEquatable(Of T)and writeEquals(other As T)with theOverloadskeyword. It avoids boxing inDictionary,HashSet,List.ContainsandArray.IndexOf, which all useEqualityComparer(Of T).Default. -
Override
Equals(Object):TypeOf obj Is PageRange AndAlso Equals(DirectCast(obj, PageRange)). TheTypeOf ... Istest on a value type checks the box’s type;DirectCastthen unboxes it. C# 7 pattern matching does this in one line; Visual Basic needs the two steps. -
Override
GetHashCodewith the same fields, usingHashCode.Combine(a, b, ...)(up to eight values). Equal values must have equal hash codes. Do not use a field that can change if the value is stored in a hash-based collection. -
Define
Shared Operator =andShared Operator <>together (the compiler requires a matching pair, as for<and>). -
Add
IComparable(Of T)and the four ordering operators if the type has a natural order (theDimensionsexample above).
Nullable(Of T) lifts the operators: nullable = a compiles, and nullable Is Nothing is the way to ask whether the nullable has no
value (see Operator Overloading and Equality for the full
treatment, including the three-valued logic of lifted comparisons).
Boxing, interfaces and generics
Converting a structure to Object, to ValueType, or to an interface it implements boxes it: the runtime allocates an object on the
heap and copies the value into it. Unboxing (DirectCast(o, Cursor)) copies it out again. The box is a reference type, so several
variables of the interface type can share one box, and mutation through the interface changes the box and not the original
variable. The Cursor structure below makes the three effects visible: a call through an interface parameter, a generic method
with a Structure and interface constraint (which does not box), and a shared box.
Option Strict On
Public Interface IStepper
Sub [Step]()
ReadOnly Property Position As Integer
End Interface
Public Structure Cursor
Implements IStepper
Private _position As Integer
Public Sub [Step]() Implements IStepper.Step
_position += 1
End Sub
Public ReadOnly Property Position As Integer Implements IStepper.Position
Get
Return _position
End Get
End Property
End Structure
Module Program
Sub StepTwice(Of T As {Structure, IStepper})(ByRef stepper As T)
stepper.Step() ' constrained call: no boxing, the caller's variable changes
stepper.Step()
End Sub
Sub StepTwiceBoxed(stepper As IStepper)
stepper.Step() ' the argument is a boxed copy
stepper.Step()
End Sub
Sub Main()
Dim cursor As New Cursor()
cursor.Step()
Console.WriteLine("direct call: " & cursor.Position)
StepTwiceBoxed(cursor) ' boxes a copy
Console.WriteLine("after interface arg: " & cursor.Position)
StepTwice(cursor) ' generic constraint, ByRef
Console.WriteLine("after generic call: " & cursor.Position)
Dim box As IStepper = cursor ' one box, shared through the interface
Dim alias2 As IStepper = box
box.Step()
Console.WriteLine("box = " & box.Position & ", alias = " & alias2.Position & ", cursor = " & cursor.Position)
Dim o As Object = cursor
Dim unboxed = DirectCast(o, Cursor) ' unboxing copies again
unboxed.Step()
Console.WriteLine("object = " & DirectCast(o, Cursor).Position & ", unboxed = " & unboxed.Position)
Console.WriteLine("is interface: " & (TypeOf o Is IStepper) & ", same box: " & ReferenceEquals(box, alias2))
Try
Dim notBoxed As Object = "text"
Dim c As Cursor = DirectCast(notBoxed, Cursor)
Console.WriteLine(c.Position)
Catch ex As InvalidCastException
Console.WriteLine("InvalidCastException: " & ex.Message)
End Try
Try
Dim nothingObject As Object = Nothing
Dim c As Cursor = DirectCast(nothingObject, Cursor)
Console.WriteLine("unboxing Nothing gives default: " & c.Position)
Catch ex As NullReferenceException
Console.WriteLine("NullReferenceException")
End Try
End Sub
End Module
direct call: 1
after interface arg: 1
after generic call: 3
box = 4, alias = 4, cursor = 3
object = 3, unboxed = 4
is interface: True, same box: True
InvalidCastException: Unable to cast object of type 'System.String' to type 'Shelf.Cursor'.
NullReferenceException
Source: Boxing and Unboxing (C# guide; the CLR rules are identical)
-
StepTwiceBoxed(cursor)boxes a copy, so the caller’scursor.Positionstays1. -
StepTwice(Of T As {Structure, IStepper})(ByRef stepper As T)is the allocation-free way to call interface members on a structure: the JIT specialises the method for the structure type and calls the member on the caller’s variable (3). WithoutByRefthe generic method would work on a copy. -
boxandalias2are the same object (ReferenceEqualsisTrue), sobox.Step()is seen through both, whilecursoris unchanged. -
DirectCastto the wrong type throwsInvalidCastException; unboxing a nullObjectinto a non-nullable structure throwsNullReferenceException. Test withTypeOf o Is Cursorfirst.TryCastcannot be used for a value type (TryCast(o, Cursor?)is BC30792 "operand must be reference type"). -
A
Structureconstraint (T As Structure) means "any non-nullable value type": the generic code can useT?and cannot useNothingas an unset reference (see Generics).
Typical places where boxing happens without being visible: passing a structure to a parameter of type Object, ValueType or an
interface; String.Format("{0}", value) and Console.WriteLine("{0}", value) (the ParamArray Object); using the non-generic
collections (ArrayList, Hashtable); calling the inherited GetType or the non-overridden GetHashCode on a structure that
does not override it; and late binding (Object variables, see
Early and Late Binding).
Memory layout
Fields of a structure are stored in declaration order with the padding the processor needs (sequential layout, which both the
Visual Basic and C# compilers emit by default). A structure that contains a reference-type field is laid out automatically by the CLR,
which is free to reorder; the layout of a structure that contains only blittable fields is the one you declare. Native interop,
binary file formats and sensor data need a fixed layout, which System.Runtime.InteropServices.StructLayoutAttribute and
FieldOffsetAttribute give. The numbers below were printed by this program on Linux x64:
Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Public Structure Mixed
Public Flag As Byte
Public Count As Integer
Public Small As Short
End Structure
<StructLayout(LayoutKind.Sequential, Pack:=1)>
Public Structure Packed
Public Flag As Byte
Public Count As Integer
Public Small As Short
End Structure
<StructLayout(LayoutKind.Explicit)>
Public Structure IntOrBytes
<FieldOffset(0)> Public Whole As Integer
<FieldOffset(0)> Public Low As Byte
<FieldOffset(1)> Public Second As Byte
End Structure
Public Structure WithReference
Public Title As String
Public Pages As Integer
End Structure
Module Program
Sub Main()
Console.WriteLine("Mixed: Unsafe.SizeOf = " & Unsafe.SizeOf(Of Mixed)() & ", Marshal.SizeOf = " & Marshal.SizeOf(Of Mixed)() & ", Len = " & Len(New Mixed()))
Console.WriteLine("Packed: Unsafe.SizeOf = " & Unsafe.SizeOf(Of Packed)() & ", Marshal.SizeOf = " & Marshal.SizeOf(Of Packed)())
Console.WriteLine("Offset of Count in Mixed: " & Marshal.OffsetOf(Of Mixed)("Count").ToInt32())
Console.WriteLine("Offset of Count in Packed: " & Marshal.OffsetOf(Of Packed)("Count").ToInt32())
Console.WriteLine("WithReference: Unsafe.SizeOf = " & Unsafe.SizeOf(Of WithReference)())
Console.WriteLine("Guid = " & Unsafe.SizeOf(Of Guid)() & ", Date = " & Unsafe.SizeOf(Of Date)() & ", Decimal = " & Unsafe.SizeOf(Of Decimal)() & ", Boolean = " & Unsafe.SizeOf(Of Boolean)())
Dim union As New IntOrBytes()
union.Whole = &H1234
Console.WriteLine("union low byte = " & Hex(union.Low) & ", second byte = " & Hex(union.Second))
Dim kind As StructLayoutAttribute = GetType(Mixed).StructLayoutAttribute
Console.WriteLine("Mixed layout kind: " & kind.Value.ToString())
End Sub
End Module
Mixed: Unsafe.SizeOf = 12, Marshal.SizeOf = 12, Len = 7
Packed: Unsafe.SizeOf = 7, Marshal.SizeOf = 7
Offset of Count in Mixed: 4
Offset of Count in Packed: 1
WithReference: Unsafe.SizeOf = 16
Guid = 16, Date = 8, Decimal = 16, Boolean = 1
union low byte = 34, second byte = 12
Mixed layout kind: Sequential
Source: StructLayoutAttribute
-
Mixedholds aByte, anIntegerand aShort(seven bytes of data). Natural alignment pads it to 12 bytes, andCountsits at offset 4.Pack:=1removes the padding (7 bytes, offset 1), which is what a packed binary file or network header needs and which makes unaligned reads slower on some processors. -
LayoutKind.ExplicitwithFieldOffsetcreates a union:Whole,LowandSecondoverlap. The low byte of&H1234is&H34, the second is&H12on a little-endian machine. Overlapping reference fields are rejected by the runtime. -
Unsafe.SizeOf(Of T)()gives the managed size,Marshal.SizeOf(Of T)()the size of the unmanaged copy that P/Invoke would make. For these blittable types they agree.Marshal.OffsetOfreturns anIntPtr, so you convert with.ToInt32():"offset " & Marshal.OffsetOf(Of Mixed)("Count")does not compile (BC30452, operator&is not defined forStringandIntPtr). -
Len(structureValue), the VB runtime function, gives 7 forMixed: it adds the sizes of the fields without padding, as it did for the VB6Type. Do not use it to size a buffer for interop; useMarshal.SizeOf. -
A structure with a reference field (
WithReference) is 16 bytes on a 64-bit process: 8 for the reference, 4 for theInteger, 4 of padding. -
A structure passed to a
DeclareorDllImportfunction must match the native layout, which is why interop structures normally carryStructLayout. See Native Interop: Declare and P/Invoke for the marshalling rules.
Class or structure?
Visual Basic unifies the syntax for structures and classes (constructors, methods, properties, events, interfaces, Shared members,
operators and nested types are available to both), so the choice is not about syntax. The Framework Design Guidelines, reprinted by
Microsoft and dated 2008, say to prefer a class unless the type represents a single value, is small (the guidance gives 16 bytes),
is immutable and is not boxed often. The sizes there are rules of thumb: measure in your own code before choosing a structure
for speed.
| Aspect | Structure | Class |
|---|---|---|
Kind |
value type; a variable holds the data |
reference type; a variable holds a reference or |
Assignment, |
copies all fields |
copies the reference |
Default value |
all fields zero; never |
|
|
BC30020 unless |
valid |
Parameterless constructor |
implicit, cannot be replaced (BC30629) |
optional, can be declared |
Field initializers |
not allowed (BC31049) |
allowed |
Inheritance |
none, base type |
single inheritance, |
Interfaces |
yes (assigning to the interface boxes) |
yes |
Finalizer |
never called |
called by the garbage collector |
Equality default |
field by field ( |
reference equality ( |
Members |
not allowed |
allowed |
Default access of a member |
|
|
Array of 1000 |
one block of 1000 values |
one block of 1000 references plus 1000 objects |
Typical examples |
|
|
Use a structure when all of these hold: the type has the semantics of a number, a point, a range, an identifier or a similar
value; it is immutable; it is small; copies are cheap and equality means "same contents". Use a class for entities with an identity
(Book, Loan), anything that is changed after construction, anything that is part of an inheritance hierarchy (CatalogItem,
Magazine) or anything big enough that copying hurts. See Classes and Objects
and Inheritance and Polymorphism for the other side.
Nullable structures
A structure cannot be Nothing, so "no value" needs either a sentinel (Isbn.IsEmpty) or Nullable(Of T), written T?. The
value of Dim maybe As Isbn? = Nothing has HasValue = False, and maybe.Value throws InvalidOperationException when it has no value.
The Isbn? form is itself a structure, which means a Nullable(Of T) of a large structure copies both the value and a flag.
The full treatment (lifted operators, If(a, b), GetValueOrDefault) is on
Nothing and Nullable Value Types; the equality example above
and the Isbn example show the common uses.
Related value types
-
Enumis a structure-like value type with named constants: Enumerations. -
Tuples,
(Integer, String), areSystem.ValueTuplestructures with mutable public fields; a tuple element in aList(Of T)has the same lost-update problem as any structure: Tuples and Anonymous Types. -
Collections of structures avoid one allocation per element but copy on every read through an indexer: Collections.
-
Resource wrappers that must be disposed are normally classes: Object Lifetime and Disposal.
Consuming C# readonly struct, ref struct and Span(Of T)
Visual Basic cannot declare a readonly struct, a ref struct, a readonly member, a record struct, an in parameter or a ref
return, but it can use the first two when a C# or BCL API gives them to it, with two classes of restriction.
readonly struct, in parameters
A C# readonly struct is an ordinary structure to Visual Basic: construct it with New, read its members and call its methods.
A method with in parameters is called with ordinary arguments. A ref readonly return, in contrast, is not supported (see below).
The solution that follows has two projects. The C# library:
using System;
namespace ShelfInterop;
public readonly struct Price
{
public Price(decimal amount, string currency) { Amount = amount; Currency = currency; }
public decimal Amount { get; }
public string Currency { get; }
public Price Plus(Price other) => new(Amount + other.Amount, Currency);
public override string ToString() => $"{Amount:F2} {Currency}";
}
public ref struct Tokenizer
{
private ReadOnlySpan<char> _rest;
public Tokenizer(string text) { _rest = text; Current = ""; }
public string Current { get; private set; }
public bool MoveNext()
{
if (_rest.IsEmpty) return false;
int i = _rest.IndexOf(' ');
if (i < 0) { Current = _rest.ToString(); _rest = default; }
else { Current = _rest[..i].ToString(); _rest = _rest[(i + 1)..]; }
return true;
}
}
public static class Pricing
{
public static decimal Total(in Price a, in Price b) => a.Amount + b.Amount;
public static ref readonly Price Cheaper(in Price a, in Price b) => ref (a.Amount <= b.Amount ? ref a : ref b);
public static Tokenizer Tokenize(string text) => new(text);
public static int CountWords(ReadOnlySpan<char> text) => text.Count(' ') + 1;
public static void Fill(Span<int> target, int value) => target.Fill(value);
}
The Visual Basic project references it with a ProjectReference; the .vbproj is the usual console project plus:
<ItemGroup>
<ProjectReference Include="..\CsLib\CsLib.csproj" />
</ItemGroup>
Option Strict On
Imports ShelfInterop
Module Program
Sub Main()
Dim a As New Price(12.5D, "EUR")
Dim b As New Price(9.99D, "EUR")
Console.WriteLine("readonly struct: " & a.Plus(b).ToString())
Console.WriteLine("'in' parameters: " & Pricing.Total(a, b))
Dim words = Pricing.Tokenize("one two three")
Dim count = 0
Do While words.MoveNext()
count += 1
Console.WriteLine("token " & count & ": " & words.Current)
Loop
Console.WriteLine("words: " & Pricing.CountWords("a b c d".AsSpan()))
Dim numbers = New Integer(3) {}
Pricing.Fill(numbers, 7)
Console.WriteLine("filled: " & String.Join(",", numbers))
End Sub
End Module
readonly struct: 22.49 EUR
'in' parameters: 22.49
token 1: one
token 2: two
token 3: three
words: 4
filled: 7,7,7,7
Source: Structure types (C#)
-
Priceis used like any structure; its members are read-only, so there is no lost-update trap. -
Pricing.Total(a, b)takes twoin Priceparameters. The Visual Basic call looks like aByValcall; the C# method receives a read-only reference. -
Pricing.Tokenizereturns aref struct.Dim words = Pricing.Tokenize(...)works because the type is inferred; callingMoveNextand readingCurrentworks too. -
Pricing.CountWords("a b c d".AsSpan())passes aReadOnlySpan(Of Char)created in the argument expression, andPricing.Fill(numbers, 7)passes anInteger()to aSpan<int>parameter through the implicit conversion. -
Pricing.Cheaper(a, b), declaredpublic static ref readonly Price Cheaper(in Price a, in Price b), cannot be called at all: addingDim cheaper = Pricing.Cheaper(a, b)to the program givesBC30657: 'Cheaper' has a return type that is not supported or parameter types that are not supported.(the library compiled with the extra method and the build of the Visual Basic project failed with exactly that message). Wrap such an API in C# with a method that returns a plain value.
Span(Of T) and other ref structs: local use only
Span(Of T) and ReadOnlySpan(Of T) are ref struct types: they may only live on the stack, because they can point into the
stack, into native memory or into the middle of an array. The C# compiler enforces that statically. The Visual Basic compiler does
not understand the rule. What it does instead is hide the types: naming a ref struct in a declaration (a field, a local, a
parameter or a return type) is error BC30668 ("'Span(Of Integer)' is obsolete: 'Types with embedded references are not
supported in this version of your compiler.'"), because the types carry an ObsoleteAttribute that the Visual Basic compiler
treats as an error. Inferred locals and chained calls are not named, so they compile. The conclusion for a Visual Basic programmer
is that spans are available only through inference and method chains:
Option Strict On
Module Program
' A Span(Of T) parameter cannot be declared in Visual Basic: naming the type is BC30668.
' Spans are therefore used only through inferred locals and chained calls.
Sub Main()
Dim pages = New Integer() {10, 20, 30, 40, 50}
Dim middle = pages.AsSpan().Slice(1, 3) ' a view over the same memory, no copy
middle(0) = 99 ' writes through to the array
Console.WriteLine("array after write: " & String.Join(",", pages))
Console.WriteLine("view length: " & middle.Length & ", copy: " & String.Join(",", middle.ToArray()))
Dim text = "978-0-13-468599-1".AsSpan() ' ReadOnlySpan(Of Char): no substring allocation
Dim dash = text.IndexOf("-"c)
Console.WriteLine("prefix: " & text.Slice(0, dash).ToString())
Console.WriteLine("parsed without Substring: " & Integer.Parse(text.Slice(0, dash)))
Dim total = 0
Dim view = pages.AsSpan()
For i = 0 To view.Length - 1 ' index the view
total += view(i)
Next
Console.WriteLine("total: " & total)
Dim read = pages.AsSpan(2).Length ' chained use ends in the expression
Console.WriteLine("tail length: " & read)
End Sub
End Module
array after write: 10,99,30,40,50
view length: 3, copy: 99,30,40
prefix: 978
parsed without Substring: 978
total: 229
tail length: 3
Source: Span(Of T) Struct
The span view middle points into pages: writing through it changed the array, and ToArray made a copy. text.Slice(0, dash) gave
a part of the string without allocating a substring, and Integer.Parse(ReadOnlySpan(Of Char)) parsed it in place.
Because VB does not check the lifetime rules, the following two programs compile and fail at run time. Capturing a span in a lambda makes the compiler generate a closure class with a span field:
Option Strict On
Module Program
Sub Main()
Dim arr = New Integer() {1, 2, 3}
Dim s = arr.AsSpan()
Dim f As Func(Of Integer) = Function() s.Length
Console.WriteLine(f())
End Sub
End Module
Unhandled exception. System.TypeLoadException: A ByRef or ByRef-like type cannot be used as the type for an instance field in a non-ByRef-like type.
at Shelf.Program.Main()
And converting a span to Object (boxing a type that can never be boxed) produces invalid IL:
Option Strict On
Module Program
Sub Main()
Dim arr = New Integer() {1, 2, 3}
Dim s = arr.AsSpan()
Try
Dim o As Object = s
Console.WriteLine(o)
Catch ex As Exception
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
End Sub
End Module
Unhandled exception. System.InvalidProgramException: Common Language Runtime detected an invalid program.
at Shelf.Program.Main()
The exceptions are thrown while the method is being prepared for execution, before its first line runs, so a Try ... Catch inside the
same method cannot catch them (a Try in Main around the boxing line was tried and did not catch it). The restrictions to keep in mind when you use a span in Visual Basic are therefore:
-
never assign a span to an
Object, an interface variable or a field, never use one as a generic type argument, and never put one in an array; -
never capture a span in a lambda, and do not use a span local in an
AsyncorIteratormethod that crosses anAwaitorYield; -
do not store one in a
Structure; a Visual Basic structure is not a ref struct, and naming the type is BC30668 anyway; -
if you need
stackalloc, aMemory(Of T)slice, an array pool or a span-returning API in your own code, write that code in C# and expose an ordinary (array, string orMemory(Of T)) interface to the Visual Basic caller.
Other C# features for ref structs (ref fields from C# 11, allows ref struct from C# 13, ref readonly returns) are likewise beyond Visual Basic as a consumer.
Microsoft’s language strategy for Visual Basic is to keep it stable and to consume, but not extend, new runtime features
(see History, Versions and Language Strategy),
and these gaps are what that looks like in practice. For the other side of C# interop see
Interop with C# and Other .NET Languages.
Comparison with C#
| Topic | Visual Basic | C# |
|---|---|---|
Keyword |
|
|
Immutable structure |
convention only ( |
|
Record structure |
none; write |
|
Parameterless constructor |
not allowed (BC30629) |
allowed since C# 10 |
Field initializers |
not allowed (BC31049) |
allowed since C# 10 when a constructor is declared |
|
no |
yes |
|
cannot be declared, cannot be named (BC30668); inferred locals only |
declared and enforced by the compiler |
|
|
full support |
Assignment to |
not allowed (BC30068) |
allowed in a structure |
Changing a copy |
property, indexer and field-of-property assignments are errors; method calls are silent |
property and indexer assignments are errors (CS1612, CS1654); method calls are silent |
Default value |
|
|
Equality operators |
define |
define |
Nullable shorthand |
|
|
Type test and unbox |
|
|
Default member access |
|
|
Older material you may meet
Books and tutorials written for VB6, VBA and the first .NET versions teach structures in ways that no longer apply:
-
The VB6
Type ... End TypebecomesStructure ... End Structure; fixed-length strings (Name As String * 20) become aStringwith<VBFixedString(20)>for file I/O or aMarshalAs(UnmanagedType.ByValTStr)field for interop;Dim a(10) As Integerinside aTypeis rejected (BC31043) and is allocated in a constructor instead. See Migrating Legacy Visual Basic. -
Old samples keep structures in an
ArrayListand change an element in place. Unboxing always produced a copy, so the pattern was never right; read the element into a variable, change it and write it back, as the lost-update example does forList(Of T). -
Samples use
Dim s As Structure1 = Nothingto mean "a null structure". In VB.NET it is the all-zero value, and a test forIs Nothingis a compile-time error, not a runtime one. -
Sample "serialization" of structures with
BinaryFormatterno longer works in .NET 9 and later. UseSystem.Text.Json; see Serialization: JSON and XML. -
A structure that "inherits" behaviour in old samples is almost always a class in disguise: use a class, or an interface and an extension method.
-
The Language Reference for
Structurestill lists the older limits (an empty structure is an error); the 17.13 compiler accepts such a structure.
Pitfalls checklist
-
Mutating a structure obtained from a property, an indexer, a
ReadOnlyfield or aFor Eachvariable changes a copy and gives no diagnostic when the change is a method call. -
Dim x As T,Nothing,New T(), an array element andActivator.CreateInstanceall give an all-zero value that has never run your constructor. -
Passing a structure to
Object,ValueType, an interface or aParamArray Objectboxes it, and mutating through the interface changes only the box. -
The default
EqualsandGetHashCodeofValueTypemay be slow or poor; override them, implementIEquatable(Of T)and define=and<>as a pair. -
Is Nothing,IsNot NothingandIsbetween structures are errors (BC30020); useNullable(Of T)andHasValue. -
A structure cannot have a parameterless constructor, a field initializer, an array field with a size, an
Inheritsor aProtectedmember; the error numbers are in the table above. -
There is no
ReadOnly Structure; immutability is by convention, and C# callers cannot rely on it. -
Never name
Span(Of T)or any ref struct; never capture one, box one or keep one acrossAwait; write such code in C# and expose arrays orMemory(Of T). -
Len(value)on a structure sums field sizes without padding;Marshal.SizeOfgives the interop size. -
Large structures (more than a few fields) lose to a class once copies dominate; measure before choosing.
References
-
Structures (Visual Basic), How to: Declare a Structure, Structure Variables, Structures and Other Programming Elements and Structures and Classes
-
ValueType Class, IEquatable(Of T), Object.GetHashCode and HashCode.Combine
-
StructLayoutAttribute, FieldOffsetAttribute, Marshal.SizeOf, Marshal.OffsetOf, Unsafe.SizeOf and Strings.Len
-
Span(Of T), ReadOnlySpan(Of T), Structure types (C#), ref struct types (C#) and Boxing and Unboxing
-
Compiler messages: BC30629, BC31049, BC30628, BC30435, BC30294, BC30452, BC30064, BC42108, BC42109, BC30668, BC30657, BC30395 and BC30480