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 Friend and a nested one is Public, as for other types.

  • Members are Public by default, unlike a class, where a Dim field is Private. The example below shows it by reflection. Writing Dim Field As Integer in a structure exposes a public field.

  • A structure may be generic (Structure Pair(Of T)) and may be Partial. It cannot be MustInherit, NotInheritable or ReadOnly: a structure is implicitly sealed, and Visual Basic has no ReadOnly Structure (error BC30395).

  • A structure implicitly inherits System.ValueType and cannot name any other base class. It can implement any number of interfaces with Implements.

  • The fields are laid out in declaration order (sequential layout) unless a StructLayout attribute 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

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.
Error Rule Why and what to do

BC31049

No instance field initializers

Public Pages As Integer = 100 is rejected: only Shared members and constants may have an initializer. Assign values in a constructor or, for a default other than zero, expose a factory method or a property that interprets zero. C# 10 added field initializers to structures that declare a constructor; Visual Basic did not follow.

BC30629

No parameterless instance Sub New

Every structure has an implicit parameterless constructor that sets every field to its zero value, and you cannot replace it. Code such as Dim a As New PageRange(), Dim a As PageRange and Nothing all give that zero value, so a structure can never guarantee that a constructor ran. C# 10 allows a parameterless constructor in a structure; Visual Basic does not.

BC30628

No Inherits

A structure derives from System.ValueType and nothing else. Reuse behaviour through interfaces, composition and extension methods.

BC30435

No Overridable, MustOverride or Protected members

Nothing can derive from a structure, so those modifiers have no meaning. You can still override ToString, Equals and GetHashCode, which are inherited from Object and ValueType.

BC30294

No field of the structure’s own type

Public Inner As Recursive would need infinite size. Use a reference type in between: a class, an array or a List(Of T). The same message is given for a field of type Recursive?, because Nullable(Of T) is itself a structure that contains the value.

BC31043

No array field with a declared size

Public Items(3) As Integer is rejected. Declare Public Items() As Integer and allocate in a constructor (a reference to an array copies as a reference; see Arrays).

BC30480

Shared Sub New cannot have an access modifier

The static constructor of a class has the same rule. Write Shared Sub New(), not Public Shared Sub New().

BC30395

No ReadOnly Structure

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

Notes on the listing:

  • Dim b As PageRange = a is a copy, not an alias, so b.Last = 99 does not touch a. For a class the same two lines would make two names for one object.

  • Extend(a) passes a copy. The method’s range.Last += 10 changes only that copy. ExtendInPlace(ByRef range ...) receives a reference to the caller’s variable and changes it. The VB6 default of ByRef is gone: a parameter is ByVal unless you say otherwise (see Procedures: Sub and Function).

  • Dim blank As PageRange, Dim viaNew As New PageRange() and reset = Nothing all give the same all-zero value. For a structure, Nothing means "the default value of the type" (it is not a null reference; see Nothing and Nullable Value Types). Is Nothing on a structure does not compile (BC30020, below).

  • Count is 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 Dim a(999) As PageRange is one allocation of contiguous memory

boxed into an Object or an interface variable

in a separate heap object (a box), subject to garbage collection

captured by a lambda or an Async method

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.

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 why blank exists 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 Nothing in the zero value. Use If(Value, "") or a property that substitutes a safe default, as ToString does 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 P by ByRef before 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 = Nothing or New silences them, which is why Parse assigns result = Nothing before calling TryParse, and why the project has TreatWarningsAsErrors: a program that left out the initialiser would not build.

  • Dim shelf(2) As Isbn creates three zero values, not three Nothing references. shelf(0).Value Is Nothing is True.

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

Observations:

  • The Shared field initializers run before the Shared Sub New body, and both run lazily, before the first use of the structure (Dimensions.Empty in 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, stack and widened) and does not include initialised.

  • Shared ReadOnly Empty As Dimensions is allowed although a field of the structure’s own type is not (BC30294 applies to instance fields only).

  • Dim widened As Dimensions = 4.0 works because Widening Operator CType is implicit even under Option Strict On; make a conversion Narrowing when information can be lost (see Operator Overloading and Equality).

  • With initialised ... End With on 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 = 5 is 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

local.Value = 1

works

a local variable is a variable

obj.Field.Value = 1 (public field)

works

a field is a variable

array(0).Value = 1

works

an array element is a variable

obj.Prop.Value = 1 (property)

BC30068

the property returns a copy, there is nothing to assign to

list(0).Value = 1 (List(Of T) indexer)

BC30068

the indexer returns a copy

dict("k").Value = 1

BC30068

same

obj.ReadOnlyField.Value = 1

BC30064

a ReadOnly field cannot be the target of an assignment

obj.Prop.Method() where Method changes Me

compiles; change is lost

the call operates on a temporary copy

obj.ReadOnlyField.Method()

compiles; change is lost

the call operates on a defensive copy

list(0).Method()

compiles; change is lost

the indexer returns a copy

For Each item In list : item.Method() or item.Value = 1

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.

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 (or Private ReadOnly behind ReadOnly 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 PageRange structure 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

The complete recipe, which is also the one the shelf Isbn uses:

  1. Implement IEquatable(Of T) and write Equals(other As T) with the Overloads keyword. It avoids boxing in Dictionary, HashSet, List.Contains and Array.IndexOf, which all use EqualityComparer(Of T).Default.

  2. Override Equals(Object): TypeOf obj Is PageRange AndAlso Equals(DirectCast(obj, PageRange)). The TypeOf ... Is test on a value type checks the box’s type; DirectCast then unboxes it. C# 7 pattern matching does this in one line; Visual Basic needs the two steps.

  3. Override GetHashCode with the same fields, using HashCode.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.

  4. Define Shared Operator = and Shared Operator <> together (the compiler requires a matching pair, as for < and >).

  5. Add IComparable(Of T) and the four ordering operators if the type has a natural order (the Dimensions example 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’s cursor.Position stays 1.

  • 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). Without ByRef the generic method would work on a copy.

  • box and alias2 are the same object (ReferenceEquals is True), so box.Step() is seen through both, while cursor is unchanged.

  • DirectCast to the wrong type throws InvalidCastException; unboxing a null Object into a non-nullable structure throws NullReferenceException. Test with TypeOf o Is Cursor first. TryCast cannot be used for a value type (TryCast(o, Cursor?) is BC30792 "operand must be reference type").

  • A Structure constraint (T As Structure) means "any non-nullable value type": the generic code can use T? and cannot use Nothing as 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
  • Mixed holds a Byte, an Integer and a Short (seven bytes of data). Natural alignment pads it to 12 bytes, and Count sits at offset 4. Pack:=1 removes 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.Explicit with FieldOffset creates a union: Whole, Low and Second overlap. The low byte of &H1234 is &H34, the second is &H12 on 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.OffsetOf returns an IntPtr, so you convert with .ToInt32(): "offset " & Marshal.OffsetOf(Of Mixed)("Count") does not compile (BC30452, operator & is not defined for String and IntPtr).

  • Len(structureValue), the VB runtime function, gives 7 for Mixed: it adds the sizes of the fields without padding, as it did for the VB6 Type. Do not use it to size a buffer for interop; use Marshal.SizeOf.

  • A structure with a reference field (WithReference) is 16 bytes on a 64-bit process: 8 for the reference, 4 for the Integer, 4 of padding.

  • A structure passed to a Declare or DllImport function must match the native layout, which is why interop structures normally carry StructLayout. 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.

flowchart TD A[New type] --> B{A single value, like Isbn or PageRange?} B -- no --> C[Class] B -- yes --> D{Immutable and small, 16 bytes or less?} D -- no --> C D -- yes --> E{Boxed often, or needs inheritance or identity?} E -- yes --> C E -- no --> F[Structure]
Aspect Structure Class

Kind

value type; a variable holds the data

reference type; a variable holds a reference or Nothing

Assignment, ByVal argument

copies all fields

copies the reference

Default value

all fields zero; never Nothing unless declared T?

Nothing

Is Nothing

BC30020 unless T?

valid

Parameterless constructor

implicit, cannot be replaced (BC30629)

optional, can be declared

Field initializers

not allowed (BC31049)

allowed

Inheritance

none, base type ValueType

single inheritance, MustInherit, NotInheritable

Interfaces

yes (assigning to the interface boxes)

yes

Finalizer

never called

called by the garbage collector

Equality default

field by field (ValueType.Equals)

reference equality (Object.Equals)

Members Protected, Overridable, MustOverride

not allowed

allowed

Default access of a member

Public

Private for fields, Public for the rest

Array of 1000

one block of 1000 values

one block of 1000 references plus 1000 objects

Typical examples

Date, Decimal, Guid, Isbn, PageRange

Book, Loan, Catalog

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.

  • Enum is a structure-like value type with named constants: Enumerations.

  • Tuples, (Integer, String), are System.ValueTuple structures with mutable public fields; a tuple element in a List(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
  • Price is used like any structure; its members are read-only, so there is no lost-update trap.

  • Pricing.Total(a, b) takes two in Price parameters. The Visual Basic call looks like a ByVal call; the C# method receives a read-only reference.

  • Pricing.Tokenize returns a ref struct. Dim words = Pricing.Tokenize(...) works because the type is inferred; calling MoveNext and reading Current works too.

  • Pricing.CountWords("a b c d".AsSpan()) passes a ReadOnlySpan(Of Char) created in the argument expression, and Pricing.Fill(numbers, 7) passes an Integer() to a Span<int> parameter through the implicit conversion.

  • Pricing.Cheaper(a, b), declared public static ref readonly Price Cheaper(in Price a, in Price b), cannot be called at all: adding Dim cheaper = Pricing.Cheaper(a, b) to the program gives BC30657: '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

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 Async or Iterator method that crosses an Await or Yield;

  • 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, a Memory(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 or Memory(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

Structure ... End Structure

struct

Immutable structure

convention only (ReadOnly fields, no setters)

readonly struct, readonly members

Record structure

none; write IEquatable, Equals, GetHashCode and ToString by hand

record struct, readonly record struct

Parameterless constructor

not allowed (BC30629)

allowed since C# 10

Field initializers

not allowed (BC31049)

allowed since C# 10 when a constructor is declared

with expression, init accessors

no

yes

ref struct, Span<T> as a type

cannot be declared, cannot be named (BC30668); inferred locals only

declared and enforced by the compiler

in, ref readonly parameters and returns

in accepted when calling, ref readonly returns unsupported (BC30657)

full support

Assignment to this / Me

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

Nothing, Dim x As T or New T()

default, default(T), new T()

Equality operators

define Operator = and Operator <> as a pair; Is is invalid

define == and != as a pair; ReferenceEquals on boxes

Nullable shorthand

Dim x As Isbn?

Isbn? x

Type test and unbox

TypeOf o Is T then DirectCast

o is T t

Default member access

Public

private

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 Type becomes Structure ... End Structure; fixed-length strings (Name As String * 20) become a String with <VBFixedString(20)> for file I/O or a MarshalAs(UnmanagedType.ByValTStr) field for interop; Dim a(10) As Integer inside a Type is rejected (BC31043) and is allocated in a constructor instead. See Migrating Legacy Visual Basic.

  • Old samples keep structures in an ArrayList and 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 for List(Of T).

  • Samples use Dim s As Structure1 = Nothing to mean "a null structure". In VB.NET it is the all-zero value, and a test for Is Nothing is a compile-time error, not a runtime one.

  • Sample "serialization" of structures with BinaryFormatter no longer works in .NET 9 and later. Use System.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 Structure still 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 ReadOnly field or a For Each variable changes a copy and gives no diagnostic when the change is a method call.

  • Dim x As T, Nothing, New T(), an array element and Activator.CreateInstance all give an all-zero value that has never run your constructor.

  • Passing a structure to Object, ValueType, an interface or a ParamArray Object boxes it, and mutating through the interface changes only the box.

  • The default Equals and GetHashCode of ValueType may be slow or poor; override them, implement IEquatable(Of T) and define = and <> as a pair.

  • Is Nothing, IsNot Nothing and Is between structures are errors (BC30020); use Nullable(Of T) and HasValue.

  • A structure cannot have a parameterless constructor, a field initializer, an array field with a size, an Inherits or a Protected member; 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 across Await; write such code in C# and expose arrays or Memory(Of T).

  • Len(value) on a structure sums field sizes without padding; Marshal.SizeOf gives the interop size.

  • Large structures (more than a few fields) lose to a class once copies dominate; measure before choosing.