Classes and Objects

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors (unless the text says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so, and show the compiler messages that the SDK printed. The projects use the root namespace Shelf, which is why the runtime messages below mention Shelf.Magazine and not just Magazine. The project-level imports that the SDK adds (Microsoft.VisualBasic, System, System.Collections.Generic, System.Linq and others) are why Console, List(Of T) and LINQ need no Imports line.

A class is the basic unit of object-oriented code in Visual Basic: a reference type that bundles data (fields), behaviour (methods) and the rules for creating and exposing them. Almost everything you write for a real application is a class: the shelf library catalogue used throughout this reference is built from Book, Magazine, Author, Loan and Catalog. This page covers the class itself, how an object comes into existence, how its members are declared and how visible each one is, and how two references to objects compare. Properties and default properties are on Properties and Default Properties, Shared members and Module on Modules and Shared Members, value types on Structures and Value Types, inheritance on Inheritance and Polymorphism, interfaces on Interfaces, events on Events and Handlers, equality operators on Operator Overloading and Equality and disposal on Object Lifetime and Disposal. If you come from C#, the same topic is on Classes and Objects in C#; the differences are collected in a table near the end of this page.

Kind of type Declared with Main characteristic

Class

Class ... End Class

reference type; created with New; may inherit and be inherited unless NotInheritable

Structure

Structure ... End Structure

value type, copied on assignment (see Structures and Value Types)

Module

Module ... End Module

a type whose members are all Shared; cannot be instantiated (see Modules and Shared Members)

Interface

Interface ... End Interface

a contract with no state (see Interfaces)

Enumeration

Enum ... End Enum

a named set of integral constants (see Enumerations)

Delegate

Delegate Sub, Delegate Function

a type for method references (see Delegates and Lambda Expressions)

Declaring a class

[<attributes>] [accessmodifier] [Shadows] [MustInherit | NotInheritable] [Partial] Class Name [(Of typeparameters)]
    [Inherits BaseClass]
    [Implements Interface1, Interface2]
    members
End Class
  • The members are declarations between Class and End Class: fields, Const, methods (Sub and Function), properties, events, operators, constructors, nested types, and Declare statements. Executable statements are only allowed inside procedures, never directly in the class body.

  • Class has no mandatory Public. A top-level class without an access modifier is Friend, visible inside its assembly only (the reflection example in the access modifiers section prints it). Write Public for every type that is part of a library’s API.

  • MustInherit makes the class abstract (it cannot be created with New, BC30569), NotInheritable makes it sealed. They are mutually exclusive. Both are developed on Inheritance and Polymorphism.

  • Shadows on a nested class hides a base-class member of the same name.

  • Partial splits the declaration over several files (see below), and Of adds type parameters (see Generics).

  • A file may contain any number of classes, but the usual convention is one type per file named after the type. The layout of a source file is on Program Structure and Code Conventions.

  • Names are not case sensitive. Book, BOOK and book are the same identifier, so a local variable called book of type Book is legal and common (Dim book As Book), but a class and a method of the same spelling in one scope are not.

The first complete example shows the vocabulary used in the rest of the page: private fields, a constructor, an instance method that uses the fields, a Shared field that belongs to the class and an overridden ToString.

Option Strict On

Public Class Loan
    Private ReadOnly _title As String
    Private _renewals As Integer

    Public Shared ReadOnly MaxRenewals As Integer = 2
    Public Shared Created As Integer

    Public Sub New(title As String)
        _title = title
        Created += 1
    End Sub

    Public Function Renew() As Boolean
        If _renewals >= MaxRenewals Then
            Return False
        End If
        _renewals += 1
        Return True
    End Function

    Public Overrides Function ToString() As String
        Return _title & " (renewed " & _renewals & " times)"
    End Function
End Class

Module Program
    Sub Main()
        Dim first As New Loan("Refactoring")
        Dim second As Loan = New Loan("Domain-Driven Design")

        Console.WriteLine(first.Renew())
        Console.WriteLine(first.Renew())
        Console.WriteLine(first.Renew())
        Console.WriteLine(first)
        Console.WriteLine(second)
        Console.WriteLine("Loans created: " & Loan.Created)
    End Sub
End Module
True
True
False
Refactoring (renewed 2 times)
Domain-Driven Design (renewed 0 times)
Loans created: 2

Source: Class Statement

Three details to note. Dim first As New Loan("Refactoring") and Dim second As Loan = New Loan("...") do the same thing; the first form is shorter, the second can also be written with a different type on the left, as in Dim item As CatalogItem = New Book(...). The third Renew() returns False because the limit lives in a Shared field that every instance reads. And Console.WriteLine(first) printed the result of the overridden ToString, because WriteLine(Object) calls it.

What goes inside a class

Member What it is Default accessibility (checked on this page)

Field (Dim x As T)

a variable stored in each object (or once per class if Shared)

Private

Constant (Const)

a compile-time value

Private

Sub, Function

a method

Public

Property

a field-like member with accessors

Public

Event

a notification

Public

Sub New

an instance constructor

Public

Shared Sub New

the type initializer, run once before first use

Private; no accessibility can be written

Operator, Widening/Narrowing conversion

a user-defined operator

must be written Public Shared

Nested Class, Structure, Enum

a type declared in the class

Public

The defaults differ from C#, where every member is private unless stated. Always write the modifier: the defaults are a frequent source of surprise when you read code that omits them, and a code review rule that every member has an explicit access modifier costs nothing. The example in the access modifiers section prints these defaults by reflection.

Fields

A field is a variable declared in the class body. Dim in a class means Private, so Dim Count As Integer is a private field, while Public Count As Integer is a public one. Prefer Private fields, exposed through properties when other code needs them: a public field cannot later change into a property without recompiling every caller, cannot be used in data binding, and cannot carry validation.

  • The usual naming convention is a leading underscore (_title), which also avoids the clash with a parameter of the same name described under "Case-insensitivity" below.

  • ReadOnly fields may be assigned only in their declaration or in a constructor of the same class (BC30064 otherwise). Shared ReadOnly fields may be assigned in their declaration or in Shared Sub New.

  • Const fields are compile-time constants, implicitly Shared, and only of an intrinsic type (numbers, Boolean, Char, Date, String) or an Enum. Use Shared ReadOnly for anything else, such as an object or a computed value.

  • WithEvents fields make event wiring with Handles possible; see Events and Handlers.

  • A field may be declared with an initializer, Private _count As Integer = 5, or with As New, Private ReadOnly _log As New List(Of String). The initializers of instance fields can use Me and the fields declared before them (unlike C#, where an instance initializer cannot refer to another instance member).

Option Strict On

Public Class Order
    Private ReadOnly _quantity As Integer = 3
    Private ReadOnly _price As Decimal = 2.5D
    Private ReadOnly _total As Decimal = _quantity * _price   ' an instance initializer may use earlier instance fields
    Private ReadOnly _created As Date = Date.Today.Date

    Public ReadOnly Property Total As Decimal
        Get
            Return _total
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Console.WriteLine(New Order().Total)
    End Sub
End Module
7.5

Source: ReadOnly

When the initializers run

The order in which a new object is built matters when constructors call overridable methods. The rule in Visual Basic is:

  1. memory for the object is allocated and every field has its default value (0, False, Nothing);

  2. the constructor starts. If its first statement calls MyBase.New(...), the base constructor runs first, to the end; if it calls Me.New(...), that other constructor of the same class runs first and the initializers are not repeated;

  3. immediately after the base constructor has returned, the instance field initializers of this class run in the order they are written;

  4. then the rest of the constructor body runs.

C# differs: field initializers run before the base constructor. The consequence in Visual Basic is that a base constructor that calls an Overridable method sees the derived class’s fields before their initializers ran:

Option Strict On

Public Class Base
    Public Sub New()
        Console.WriteLine("Base.New starts")
        Console.WriteLine("  Describe() says: " & Describe())
        Console.WriteLine("Base.New ends")
    End Sub

    Public Overridable Function Describe() As String
        Return "base"
    End Function
End Class

Public Class Derived
    Inherits Base

    Private ReadOnly _label As String = "initialized"
    Private _count As Integer = 5
    Private ReadOnly _upper As String = _label.ToUpperInvariant()

    Public Sub New()
        MyBase.New()
        Console.WriteLine("Derived.New body: " & Describe())
    End Sub

    Public Overrides Function Describe() As String
        Return "label=" & If(_label, "<Nothing>") & ", count=" & _count
    End Function
End Class

Module Program
    Sub Main()
        Dim d As New Derived()
        Console.WriteLine(d.Describe())
    End Sub
End Module
Base.New starts
  Describe() says: label=<Nothing>, count=0
Base.New ends
Derived.New body: label=initialized, count=5
label=initialized, count=5

While Base.New ran, _label was still Nothing and _count was 0, although both have initializers. Calling an overridable member from a constructor is a known design problem in every .NET language; in Visual Basic it is slightly worse than in C# because even the initializer values are missing. Keep constructors free of virtual calls, or make the member NotOverridable/Private, or move the work into an explicit Initialize step.

Constructors

A constructor is a Sub named New. It runs once for every object created with New, after the memory has been zeroed.

[accessmodifier] Sub New([parameters])
    [MyBase.New(...) | MyClass.New(...) | Me.New(...)]
    statements
End Sub
  • A class that declares no Sub New receives a default public parameterless constructor from the compiler. As soon as you declare any constructor, the default one disappears: New Reader() then fails with BC30455 when only Sub New(name As String) exists.

  • Constructors can be overloaded like any method. They may use Optional parameters and named arguments (New Reader(limit:=9)), though many designs prefer two or three explicit overloads so the intent is clear in the call.

  • To reuse one constructor from another, call Me.New(...) as the first statement. Me.New anywhere else is BC30282. A cycle of constructor calls is BC30298.

  • The constructor of the base class is called with MyBase.New(...), also as the first statement. If you omit the call, the compiler inserts MyBase.New() and that must exist and be accessible: otherwise BC30387 (when the derived class declares no Sub New at all) or BC30148 (when it declares one without the call). A constructor cannot have both a Me.New and a MyBase.New call.

  • A constructor does not return a value; Return takes no expression there (BC30647).

  • A Private Sub New prevents creating objects from outside, which is how factory methods, singletons and classes of only Shared members are written.

  • Shared Sub New is the type initializer. It has no parameters and no access modifier, runs once, before the first instance is created or the first Shared member is read, and runs in a thread-safe way. If it throws, the type becomes unusable: every later access throws TypeInitializationException (see "Instance and Shared members").

The next program prints the order for an object built through a chain of three constructors and shows the type initializer running once:

Option Strict On

Public Class Reader
    Private ReadOnly _log As New List(Of String)
    Private ReadOnly _id As Integer = Seed("field initializer")

    Public ReadOnly Property Name As String
    Public ReadOnly Property Limit As Integer

    Shared Sub New()
        Console.WriteLine("Shared Sub New runs once, before first use")
    End Sub

    Public Sub New()
        Me.New("anonymous")
        Console.WriteLine("New(): body, after New(String)")
    End Sub

    Public Sub New(name As String)
        Me.New(name, 3)
        Console.WriteLine("New(String): body, after New(String, Integer)")
    End Sub

    Public Sub New(name As String, limit As Integer)
        Console.WriteLine("New(String, Integer): body starts, _id = " & _id)
        Me.Name = name
        Me.Limit = limit
    End Sub

    Private Shared Function Seed(origin As String) As Integer
        Console.WriteLine("  initializer called from: " & origin)
        Return 42
    End Function
End Class

Module Program
    Sub Main()
        Console.WriteLine("Main starts")
        Dim a As New Reader()
        Console.WriteLine(a.Name & " " & a.Limit)
        Console.WriteLine("---")
        Dim b As New Reader("Ada", 10)
        Console.WriteLine(b.Name & " " & b.Limit)
    End Sub
End Module
Main starts
Shared Sub New runs once, before first use
  initializer called from: field initializer
New(String, Integer): body starts, _id = 42
New(String): body, after New(String, Integer)
New(): body, after New(String)
anonymous 3
---
  initializer called from: field initializer
New(String, Integer): body starts, _id = 42
Ada 10

Source: Sub Statement

Read the output from the top. Main starts appears before the Shared Sub New line: the type initializer is not run when Main starts but just before Reader is first used. The field initializer ran exactly once for a, in the constructor that does not delegate (New(String, Integer)), and the delegating constructors ran their bodies afterwards, in reverse order. The second object shows that Shared Sub New did not run again.

Optional parameters, named arguments and returning Me

Option Strict On

Public Class Notifier
    Private ReadOnly _watchers As New List(Of Watcher)

    Public Sub Register(w As Watcher)
        _watchers.Add(w)
    End Sub

    Public Function Count() As Integer
        Return _watchers.Count
    End Function
End Class

Public Class Watcher
    Public Sub New(owner As Notifier)
        owner.Register(Me)   ' Me escapes from the constructor: the object is not finished yet
    End Sub
End Class

Public Class Query
    Private _text As String = ""
    Private _limit As Integer = 10
    Private _descending As Boolean

    ' Returning Me allows a fluent call chain
    Public Function Containing(text As String) As Query
        _text = text
        Return Me
    End Function

    Public Function Take(limit As Integer) As Query
        _limit = limit
        Return Me
    End Function

    Public Function Descending() As Query
        _descending = True
        Return Me
    End Function

    Public Overrides Function ToString() As String
        Return $"text='{_text}', limit={_limit}, descending={_descending}"
    End Function
End Class

Public Class Reader
    Public ReadOnly Property Name As String
    Public ReadOnly Property Limit As Integer

    Public Sub New(Optional name As String = "anonymous", Optional limit As Integer = 3)
        Me.Name = name
        Me.Limit = limit
    End Sub
End Class

Module Program
    Sub Main()
        Dim n As New Notifier()
        Dim w As New Watcher(n)
        Console.WriteLine("watchers: " & n.Count())

        Console.WriteLine(New Query().Containing("vb").Take(5).Descending())

        Dim a As New Reader()
        Dim b As New Reader(limit:=9)
        Dim c As New Reader("Ada", 1)
        Console.WriteLine(a.Name & a.Limit & " " & b.Name & b.Limit & " " & c.Name & c.Limit)
    End Sub
End Module
watchers: 1
text='vb', limit=5, descending=True
anonymous3 anonymous9 Ada1

The Watcher constructor registers Me with another object before it finishes; the notifier now holds a reference to an object that is not completely built. It is legal and sometimes needed (for example in event wiring), but it is the second reason, with virtual calls, to keep constructors short. The Query class shows the other use of Me as a value: a method that returns Me allows a fluent chain.

Private constructors and factories

A private constructor and a Shared factory method control how objects are created: they can validate, cache, or choose among subclasses. NotInheritable closes the class so that no derived class can bypass the factory. A class that only groups Shared functions can use the same two tools, although a Module is simpler (see Modules and Shared Members).

Option Strict On

' A class that is only ever created through a Shared factory method
Public NotInheritable Class Isbn
    Private Shared ReadOnly Known As New Dictionary(Of String, Isbn)

    Public ReadOnly Property Value As String

    Private Sub New(value As String)
        Me.Value = value
    End Sub

    Public Shared Function Create(value As String) As Isbn
        Dim existing As Isbn = Nothing
        If Known.TryGetValue(value, existing) Then
            Return existing
        End If
        Dim created As New Isbn(value)
        Known.Add(value, created)
        Return created
    End Function

    Public Overrides Function ToString() As String
        Return Value
    End Function
End Class

' A class that only has Shared members: NotInheritable plus a Private constructor
Public NotInheritable Class Strings
    Private Sub New()
    End Sub

    Public Shared Function Initials(name As String) As String
        Return String.Concat(name.Split(" "c).Select(Function(part) part.Substring(0, 1)))
    End Function
End Class

Module Program
    Sub Main()
        Dim a = Isbn.Create("9780134757599")
        Dim b = Isbn.Create("9780134757599")
        Console.WriteLine(a.ToString() & " same instance: " & (a Is b))
        Console.WriteLine(Strings.Initials("Martin Fowler"))
        ' Dim bad As New Isbn("1")     ' BC30517: 'Sub New' is not accessible (private)
    End Sub
End Module
9780134757599 same instance: True
MF

Source: NotInheritable

The singleton and factory patterns built on these ideas are on Design Patterns in Visual Basic.

Creating objects and the meaning of Nothing

New allocates an object and runs a constructor. A class-typed variable holds a reference to the object, or Nothing (null) when it refers to none; Nothing is the default of every reference-type variable and field. The semantics of Nothing, including its meaning for value types, are on Nothing and Nullable Value Types.

  • Dim x As New T(args) declares and creates in one statement. With several names, Dim x, y As New T() creates one object per name; Dim p, q As T declares two variables of type T, both Nothing, with no object .

  • Assigning a variable of class type copies the reference, not the object. Two variables then name the same object, and a change through one is visible through the other. Passing an object ByVal copies the reference too: the callee can change the object but cannot make the caller’s variable refer to another object, which ByRef can.

  • A method call on Nothing throws NullReferenceException. The compiler warns about the obvious cases: reading a local that has not been assigned on every path gives BC42104, an error here because the project treats warnings as errors.

Option Strict On

Public Class Basket
    Public Property Items As New List(Of String)
    Public Property Label As String = "basket"
End Class

Module Program
    Private Sub AddByVal(b As Basket)
        b.Items.Add("added through the reference")   ' the caller sees this
        b = New Basket()                              ' the caller does not see this
        b.Label = "new basket, local only"
    End Sub

    Private Sub ReplaceByRef(ByRef b As Basket)
        b = New Basket With {.Label = "replaced"}
    End Sub

    Sub Main()
        Dim first As New Basket()
        Dim alias1 = first
        alias1.Label = "renamed through alias"
        Console.WriteLine(first.Label & " / same object: " & (first Is alias1))

        AddByVal(first)
        Console.WriteLine(first.Label & " / items: " & first.Items.Count)

        ReplaceByRef(first)
        Console.WriteLine(first.Label & " / items: " & first.Items.Count & " / alias still: " & alias1.Label)

        ' As New with several names creates one object per name; a separate As clause does not create any.
        Dim x, y As New Basket()
        Dim p, q As Basket
        p = Nothing
        q = x
        Console.WriteLine("x Is y: " & (x Is y))
        Console.WriteLine("p Is Nothing: " & (p Is Nothing) & ", q Is x: " & (q Is x))

        Try
            Console.WriteLine(p.Label)
        Catch ex As NullReferenceException
            Console.WriteLine("NullReferenceException: " & ex.Message)
        End Try
    End Sub
End Module
renamed through alias / same object: True
renamed through alias / items: 1
replaced / items: 0 / alias still: renamed through alias
x Is y: False
p Is Nothing: True, q Is x: True
NullReferenceException: Object reference not set to an instance of an object.

Source: New Operator

Object initializers

An object initializer creates the object and assigns accessible fields and properties in the same expression: New Book With {.Title = "...", .Year = 2004}. They were added in Visual Basic 9.0 (Visual Studio 2008). The rules:

  • Each assignment starts with a dot and names a member that is accessible, settable and not ReadOnly (BC30526 for a read-only property). The assignments run in the order written.

  • The constructor runs first, with the arguments in parentheses if any (New Book("title", 2004) With {.Pages = 1}), then the assignments run top to bottom.

  • Initializers nest: a value can itself be New Author With {...}. They can appear anywhere an expression is allowed: as an argument, in a Return, in a From collection initializer.

  • A ReadOnly collection property cannot be assigned in an initializer; add to it afterwards, or provide a constructor parameter.

  • The matching With statement (below) is a different feature, and the initializer’s list uses braces, while the statement is a block.

Option Strict On

Public Class Author
    Public Property Name As String = ""
    Public Property BornOn As Date?
End Class

Public Class Book
    Public Property Title As String = ""
    Public Property Year As Integer
    Public Property Lead As Author
    Public ReadOnly Property Tags As New List(Of String)
    Public Property Pages As Integer = 100
End Class

Module Program
    Sub Main()
        Dim book As New Book With {
            .Title = "Working Effectively with Legacy Code",
            .Year = 2004,
            .Lead = New Author With {.Name = "Michael Feathers"},
            .Pages = 456
        }
        Console.WriteLine(book.Title & " / " & book.Lead.Name & " / " & book.Pages & " pages")
        Console.WriteLine("Born: " & If(book.Lead.BornOn.HasValue, "known", "unknown"))

        ' Tags has no setter: fill it after creation.
        book.Tags.Add("testing")
        book.Tags.Add("refactoring")
        Console.WriteLine(String.Join(", ", book.Tags))

        ' Object initializer as a method argument and with an inferred local
        Dim shortBook = New Book With {.Title = "Short"}
        Print(New Book With {.Title = "Inline", .Pages = 12})
        Print(shortBook)
    End Sub

    Private Sub Print(b As Book)
        Console.WriteLine(b.Title & ": " & b.Pages)
    End Sub
End Module
Working Effectively with Legacy Code / Michael Feathers / 456 pages
Born: unknown
testing, refactoring
Inline: 12
Short: 100

Order of evaluation, and the With statement:

Option Strict On

Public Class Pair
    Public Sub New()
        Console.WriteLine("Pair.New")
    End Sub

    Public Property First As Integer
    Public Property Second As Integer
End Class

Module Program
    Private Function Trace(label As String, value As Integer) As Integer
        Console.WriteLine("evaluating " & label)
        Return value
    End Function

    Sub Main()
        ' The constructor runs first, then the members are assigned in the order written.
        Dim p As New Pair With {.Second = Trace("second", 2), .First = Trace("first", 1)}
        Console.WriteLine(p.First & "," & p.Second)

        ' With ... End With evaluates its expression once; a leading dot refers to it.
        With p
            .First = 10
            .Second = .First + 1
        End With
        Console.WriteLine(p.First & "," & p.Second)

        ' A With block over a New expression.
        With New Pair()
            .First = 5
            Console.WriteLine(.First)
        End With
    End Sub
End Module
Pair.New
evaluating second
evaluating first
1,2
10,11
Pair.New
5

The constructor ran before either argument expression was evaluated; the initializer expressions were then evaluated in the order they are written (second, then first), not in the order of the members' declarations. A With block evaluates its object expression once, so With New Pair() is a convenient way to configure a temporary object. Object initializers for anonymous types (New With {.A = 1}) are on Tuples and Anonymous Types, and collection initializers (From {...}) on Collections.

Me, MyBase and MyClass

Keyword Meaning

Me

the current object. An implicit receiver for unqualified instance members; explicit when a parameter or local has the same name as a member, when passing the object to another method, and when returning it. Not valid in a Shared member (BC30043).

MyBase

the base-class part of the object: calls the base-class implementation (MyBase.Describe()), or its constructor (MyBase.New(...)).

MyClass

the current class: calls a method of this class non-virtually, ignoring overrides in derived classes. Only used with Overridable members.

MyClass is a reserved word in Visual Basic. It cannot be used as the name of a class, a variable or a member (BC30183 at the declaration and BC30180 at a use), unless you escape it with brackets, [MyClass]. The escape works for any reserved word ([Class], [Error], [Property]), and the escaped name is the real name, so [MyClass] from VB is the same type as MyClass from C#. Some tutorials for Visual Basic use MyClass as the sample class name; they do not compile, and the fix is to rename the class (Sample, Book) or to escape it.

Option Strict On

Public Class Item
    Public Overridable Function Label() As String
        Return "Item"
    End Function

    Public Function ViaMe() As String
        Return Me.Label()
    End Function

    Public Function ViaMyClass() As String
        Return MyClass.Label()
    End Function

    Public Function Identify() As String
        Return "Me is a " & Me.GetType().Name & ", MyClass is always " & GetType(Item).Name
    End Function
End Class

Public Class Magazine
    Inherits Item

    Public Overrides Function Label() As String
        Return "Magazine"
    End Function

    Public Function Base() As String
        Return MyBase.Label()
    End Function
End Class

Public Class [MyClass]
    Public Function Hello() As String
        Return "escaped class name"
    End Function
End Class

Module Program
    Sub Main()
        Dim m As New Magazine()
        Console.WriteLine("ViaMe      : " & m.ViaMe())
        Console.WriteLine("ViaMyClass : " & m.ViaMyClass())
        Console.WriteLine("MyBase     : " & m.Base())
        Console.WriteLine(m.Identify())

        Dim legal As New [MyClass]()
        Console.WriteLine(legal.Hello() & " (" & GetType([MyClass]).FullName & ")")
    End Sub
End Module
ViaMe      : Magazine
ViaMyClass : Item
MyBase     : Item
Me is a Magazine, MyClass is always Item
escaped class name (Shelf.MyClass)

ViaMe called the override in Magazine, ViaMyClass the implementation in Item, whatever the runtime type is. In practice MyClass is rare, and it has no C# equivalent: it is used when a base class wants to be sure that its own algorithm calls its own version of a helper that derived classes are allowed to replace. MyClass.Method on a MustOverride member is rejected (BC30614): there is nothing to call. Overriding and Shadows are on Inheritance and Polymorphism.

Instance and Shared members

An instance member belongs to one object. A Shared member belongs to the class itself: there is one copy, it can be used without an object (Loan.Created), and it is the equivalent of static in C#. The rules:

  • A Shared member cannot use Me or an instance member without an object reference (BC30043, BC30369, BC30469). An instance member can use Shared members freely.

  • Accessing a Shared member through an object variable (c.Level) compiles, but warns: BC42025, "qualifying expression will not be evaluated". The warning exists because GetCounter().Total would not call GetCounter(). Always qualify with the class name.

  • Static local variables keep their value between calls. In an instance method there is one copy per object; in a Shared method, one for the class. They are not thread safe.

  • Shared Sub New runs once before first use; Shared field initializers run at the same time, in written order.

  • Module members are implicitly Shared; a Module is the right container for helper procedures that have no state.

Option Strict On

Public Class Counter
    Public Shared Total As Integer
    Public Count As Integer

    Public Sub Hit()
        Count += 1
        Total += 1
    End Sub

    Public Shared Sub Reset()
        Total = 0
        ' Count = 0   ' BC30369: Cannot refer to an instance member of a class from within a shared method
    End Sub

    ' Static locals: one per instance in an instance method, one for the whole class in a Shared method
    Public Function PerInstance() As Integer
        Static calls As Integer
        calls += 1
        Return calls
    End Function

    Public Shared Function PerClass() As Integer
        Static calls As Integer
        calls += 1
        Return calls
    End Function
End Class

Module Program
    Sub Main()
        Dim a As New Counter()
        Dim b As New Counter()
        a.Hit()
        a.Hit()
        b.Hit()
        Console.WriteLine("a.Count=" & a.Count & " b.Count=" & b.Count & " Total=" & Counter.Total)

        Console.WriteLine(a.PerInstance() & " " & a.PerInstance() & " " & b.PerInstance())
        Console.WriteLine(Counter.PerClass() & " " & Counter.PerClass())

        Counter.Reset()
        Console.WriteLine("Total after Reset=" & Counter.Total)
    End Sub
End Module
a.Count=2 b.Count=1 Total=3
1 2 1
1 2
Total after Reset=0

Source: Shared

The compiler messages for the commented-out line, and for an instance member used in a Shared initializer:

Option Strict On

Public Class Counter
    Public Count As Integer
    Public Shared Total As Integer = Count + 1

    Public Shared Sub Reset()
        Count = 0
    End Sub

    Public Sub Hit()
        Count += 1
    End Sub
End Class

Module Program
    Sub Main()
        Counter.Hit()
    End Sub
End Module
Program.vb(5,38): error BC30369: Cannot refer to an instance member of a class from within a shared method or shared member initializer without an explicit instance of the class. [App.vbproj]
Program.vb(18,9): error BC30469: Reference to a non-shared member requires an object reference. [App.vbproj]
Program.vb(8,9): error BC30369: Cannot refer to an instance member of a class from within a shared method or shared member initializer without an explicit instance of the class. [App.vbproj]

Source: Shared

Type initialization failures

If a Shared Sub New or a Shared field initializer throws, the CLR wraps the exception in a TypeInitializationException and remembers the failure. The type is then unusable for the rest of the process: every later access throws the same exception, and the initializer does not run again.

Option Strict On

Public Class Config
    Public Shared ReadOnly Value As Integer = Load()

    Private Shared Function Load() As Integer
        Console.WriteLine("Loading configuration")
        Throw New InvalidOperationException("config file missing")
    End Function
End Class

Module Program
    Sub Main()
        For attempt = 1 To 2
            Try
                Console.WriteLine(Config.Value)
            Catch ex As TypeInitializationException
                Console.WriteLine("attempt " & attempt & ": " & ex.GetType().Name & " -> " & ex.InnerException.Message)
            End Try
        Next
    End Sub
End Module
Loading configuration
attempt 1: TypeInitializationException -> config file missing
attempt 2: TypeInitializationException -> config file missing

The log line Loading configuration appears once; the second attempt did not re-run the loader. Initializers should not do I/O or depend on configuration that may be missing; a Lazy(Of T) or an explicit Initialize method lets the caller handle the failure and retry.

Access modifiers

Every type and every member has an access level. The six levels, from the widest to the narrowest:

Modifier Visible to CLR name

Public

all code that can see the type, in any assembly

public

Protected Friend

the same assembly, plus derived classes in any assembly

protected internal

Friend

the same assembly (and friend assemblies, see below)

internal

Protected

this class and derived classes, in any assembly

protected

Private Protected

derived classes in the same assembly only (Visual Basic 15.5)

private protected

Private

this class (and its nested types) only

private

  • Protected members are accessible inside the class and derived classes, through Me, MyBase or an object of the derived type; from other code BC30389 is reported.

  • A type nested in a class can use the container’s Private members; the container cannot use the nested type’s.

  • Accessibility cannot widen: a Public member cannot expose a Friend type in its signature (BC30909).

  • A derived class cannot be more accessible than its base class.

The defaults and the six levels, read back from the compiled assembly by reflection:

Option Strict On

Imports System.Reflection

Public Class Sample
    Dim PlainField As Integer
    Const Limit As Integer = 10
    Public Event Changed As EventHandler
    Sub PlainSub()
    End Sub
    Function PlainFunction() As Integer
        Return 0
    End Function
    Property PlainProperty As Integer
    Class NestedClass
    End Class
    Structure NestedStructure
        Dim Value As Integer
    End Structure
    Enum NestedEnum
        A
    End Enum
    Shared Sub New()
    End Sub
    Sub New()
    End Sub
End Class

Structure SampleStructure
    Dim Field As Integer
    Sub Method()
    End Sub
End Structure

Class TopLevelDefault
End Class

Module Program
    Private Function Describe(access As MethodBase) As String
        If access.IsPublic Then Return "Public"
        If access.IsPrivate Then Return "Private"
        If access.IsAssembly Then Return "Friend"
        If access.IsFamily Then Return "Protected"
        Return "other"
    End Function

    Sub Main()
        Const flags As BindingFlags = BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
        For Each t In {GetType(Sample), GetType(SampleStructure)}
            Console.WriteLine("-- " & t.Name & " (top level: " & If(t.IsPublic, "Public", "Friend") & ")")
            For Each f In t.GetFields(flags).Where(Function(x) Not x.IsDefined(GetType(System.Runtime.CompilerServices.CompilerGeneratedAttribute), False))
                Console.WriteLine("field    " & f.Name & ": " & If(f.IsPublic, "Public", If(f.IsPrivate, "Private", "other")))
            Next
            For Each m In t.GetMethods(flags)
                If Not m.IsSpecialName Then Console.WriteLine("method   " & m.Name & ": " & Describe(m))
            Next
            For Each c In t.GetConstructors(flags)
                Console.WriteLine("ctor     " & If(c.IsStatic, "Shared Sub New", "Sub New") & ": " & Describe(c))
            Next
            For Each p In t.GetProperties(flags)
                Console.WriteLine("property " & p.Name & ": " & Describe(p.GetMethod))
            Next
            For Each e In t.GetEvents(flags)
                Console.WriteLine("event    " & e.Name & ": " & Describe(e.AddMethod))
            Next
            For Each n In t.GetNestedTypes(flags)
                Console.WriteLine("nested   " & n.Name & ": " & If(n.IsNestedPublic, "Public", If(n.IsNestedPrivate, "Private", "other")))
            Next
        Next
        Console.WriteLine("-- " & GetType(TopLevelDefault).Name & " (top level: " & If(GetType(TopLevelDefault).IsPublic, "Public", "Friend") & ")")
    End Sub
End Module
-- Sample (top level: Public)
field    PlainField: Private
field    Limit: Private
method   PlainSub: Public
method   PlainFunction: Public
ctor     Shared Sub New: Private
ctor     Sub New: Public
property PlainProperty: Public
event    Changed: Public
nested   NestedClass: Public
nested   NestedStructure: Public
nested   NestedEnum: Public
-- SampleStructure (top level: Friend)
field    Field: Public
method   Method: Public
-- TopLevelDefault (top level: Friend)

Note that Shared Sub New is Private (a Sub New cannot be called by the code that uses the class), that the event’s hidden field and the property’s backing field were filtered out, and that a Dim field in a Structure is Public, unlike in a class.

Option Strict On

Imports System.Reflection

Public Class Levels
    Public Sub PublicSub()
    End Sub
    Protected Friend Sub ProtectedFriendSub()
    End Sub
    Friend Sub FriendSub()
    End Sub
    Protected Sub ProtectedSub()
    End Sub
    Private Protected Sub PrivateProtectedSub()
    End Sub
    Private Sub PrivateSub()
    End Sub
End Class

Module Program
    Sub Main()
        Const flags As BindingFlags = BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
        For Each m In GetType(Levels).GetMethods(flags)
            Dim clr As String =
                If(m.IsPublic, "public",
                If(m.IsFamilyOrAssembly, "protected internal",
                If(m.IsAssembly, "internal",
                If(m.IsFamily, "protected",
                If(m.IsFamilyAndAssembly, "private protected", "private")))))
            Console.WriteLine(m.Name.PadRight(22) & clr)
        Next
    End Sub
End Module
PublicSub             public
ProtectedFriendSub    protected internal
FriendSub             internal
ProtectedSub          protected
PrivateProtectedSub   private protected
PrivateSub            private

The access-level table is also on Variables, Constants and Scope.

Friend and assemblies

Friend is the default for types and is the level of "internal to this project". A library hides its implementation by making the helper classes Friend and exposing a small set of Public types. Two projects can nevertheless share their Friend members when the library names the other assembly with InternalsVisibleTo: the usual use is a unit test project (see Testing). The next solution has a Lib project and an App project that references it:

Folder layout
Lib/Lib.vbproj
Lib/Catalog.vb
App/App.vbproj      (ProjectReference to ..\Lib\Lib.vbproj)
App/Program.vb

Lib/Catalog.vb

Option Strict On

Public Class Catalog
    Friend Function InternalCount() As Integer
        Return 3
    End Function

    Public Function PublicCount() As Integer
        Return InternalCount()
    End Function
End Class

Friend Class Helper
    Friend Shared Function Banner() As String
        Return "helper in Lib"
    End Function
End Class

App/Program.vb

Option Strict On

Imports Shelf.Lib

Module Program
    Sub Main()
        Dim c As New Catalog()
        Console.WriteLine(c.PublicCount())
        Console.WriteLine(c.InternalCount())
        Console.WriteLine(Helper.Banner())
    End Sub
End Module

dotnet build App prints:

App/Program.vb(9,27): error BC30456: 'InternalCount' is not a member of 'Catalog'.
App/Program.vb(10,27): error BC30389: 'Shelf.Lib.Helper' is not accessible in this context because it is 'Friend'.

Source: Friend

The same programs after adding a file to Lib that names the App assembly:

Lib/Catalog.vb

Option Strict On

Public Class Catalog
    Friend Function InternalCount() As Integer
        Return 3
    End Function

    Public Function PublicCount() As Integer
        Return InternalCount()
    End Function
End Class

Friend Class Helper
    Friend Shared Function Banner() As String
        Return "helper in Lib"
    End Function
End Class

Lib/Friends.vb (new)

Imports System.Runtime.CompilerServices

<Assembly: InternalsVisibleTo("App")>

dotnet run --project App prints:

3
3
helper in Lib

Two points. The first build message for the Friend method is "not a member of" (BC30456), because from another assembly the method is not visible at all, while the Friend class gets the clearer BC30389. And a strong-named library needs the public key in the attribute argument. The attribute can also be set in the project file instead of a source file:

<ItemGroup>
  <InternalsVisibleTo Include="App" />
</ItemGroup>

Source: Friend

The exposure rule

Option Strict On

Friend Class Hidden
End Class

Public Class Exposer
    Public Function Make() As Hidden
        Return New Hidden()
    End Function

    Public Property Current As Hidden

    Public Sub Use(h As Hidden)
    End Sub
End Class

Module Program
    Sub Main()
    End Sub
End Module
Program.vb(7,31): error BC30909: 'Make' cannot expose type 'Hidden' outside the project through class 'Exposer'. [App.vbproj]
Program.vb(11,32): error BC30909: 'Current' cannot expose type 'Hidden' outside the project through class 'Exposer'. [App.vbproj]
Program.vb(13,25): error BC30909: 'h' cannot expose type 'Hidden' outside the project through class 'Exposer'. [App.vbproj]

Source: Public

A Public method, property or parameter cannot use a Friend type, because a caller outside the assembly could not name that type. The fix is to make the type Public, make the member Friend, or return a public interface or base class.

Nested types

A type declared inside a class is a nested type. Use one when a type is meaningful only to its container (a private Entry record in a Catalog), or when you want the container to act as a namespace for a closely related type (Catalog.Options, Catalog.SortOrder). Rules: nested types default to Public; they are named through the container (Catalog.Options) or by Imports with an alias; the CLR name uses a plus sign (Shelf.Catalog+Options); a nested type sees the container’s Private members, but it needs an object reference to use instance members (BC30469 without one).

Option Strict On

Public Class Catalog
    Private ReadOnly _items As New List(Of Entry)
    Private Shared _nextId As Integer = 1

    Public Sub Add(title As String)
        _items.Add(New Entry(title))
    End Sub

    ' A nested type can use the private members of its container
    Private Class Entry
        Public ReadOnly Property Id As Integer
        Public ReadOnly Property Title As String

        Public Sub New(title As String)
            Id = _nextId
            _nextId += 1
            Me.Title = title
        End Sub
    End Class

    Public Function Titles() As IEnumerable(Of String)
        Return _items.Select(Function(e) e.Id & ":" & e.Title)
    End Function

    ' A public nested type is part of the API and is named through its container
    Public Enum SortOrder
        ByTitle
        ByYear
    End Enum

    Public Class Options
        Public Property Order As SortOrder = SortOrder.ByTitle
        Public Overrides Function ToString() As String
            Return "Options(" & Order.ToString() & ")"
        End Function
    End Class
End Class

Module Program
    Sub Main()
        Dim c As New Catalog()
        c.Add("Refactoring")
        c.Add("Clean Code")
        Console.WriteLine(String.Join("; ", c.Titles()))

        Dim o As New Catalog.Options With {.Order = Catalog.SortOrder.ByYear}
        Console.WriteLine(o)
        Console.WriteLine(GetType(Catalog.Options).FullName)
        Console.WriteLine(GetType(Catalog.Options).DeclaringType.Name)
    End Sub
End Module
1:Refactoring; 2:Clean Code
Options(ByYear)
Shelf.Catalog+Options
Catalog

Source: Class Statement

Do not nest types merely to hide them from a namespace listing, and avoid public nested types in libraries unless callers really name them rarely: Catalog.Options is awkward in an Imports list. The common alternative, a separate CatalogOptions class in the same namespace, is usually clearer.

Partial classes

Partial (Visual Basic 2005) lets one class be declared in several files of the same project. They are merged by the compiler into one type. Typical uses are generated code (Windows Forms designer files and XAML code-behind files that contain the Partial part generated by a tool, next to the part you edit) and keeping a very large class manageable.

  • All parts must belong to the same project (assembly) and namespace, have the same class name and type parameters, and the same base class and interfaces if they name them (the union of Implements is used).

  • Partial is required on at least one part. In a test with two parts, the project built when only one of them had the keyword (either one); with neither, the compiler reported BC30179, "class 'Loan' and class 'Loan' conflict in namespace 'Shelf'".

  • Access modifiers written on several parts must agree. MustInherit or NotInheritable on one part applies to the whole class.

  • Partial methods (a declaration in one part and an implementation in another) are on Extension Methods and Partial Methods.

Program.vb

Option Strict On

Module Program
    Sub Main()
        Dim loan As New Loan("Refactoring", #2026-10-19#)
        Console.WriteLine(loan.Describe())
        Console.WriteLine(loan.IsOverdue(#2026-11-20#))
        Console.WriteLine(GetType(Loan).Assembly.GetTypes().Count(Function(t) t.Name = "Loan") & " Loan type in the assembly")
    End Sub
End Module

Loan.vb

Option Strict On

' Loan.vb: data and constructor
Public Partial Class Loan
    Private ReadOnly _title As String
    Private ReadOnly _dueOn As Date

    Public Sub New(title As String, dueOn As Date)
        _title = title
        _dueOn = dueOn
    End Sub
End Class

Loan.Rules.vb

Option Strict On

' Loan.Rules.vb: behaviour. Partial is written on both parts here.
Partial Public Class Loan
    Public Function Describe() As String
        Return _title & " due " & _dueOn.ToString("yyyy-MM-dd")
    End Function

    Public Function IsOverdue(today As Date) As Boolean
        Return today > _dueOn
    End Function
End Class
Refactoring due 2026-10-19
True
1 Loan type in the assembly

Source: Partial

The compiled assembly has one Loan type. Private members declared in one file are visible in the other, because after merging they are members of one class.

Identity and equality

Two questions are different and both are common:

  1. Identity: do two variables refer to the same object? This is the Is operator (and IsNot). It compares references only, it cannot be overloaded, and it works on any pair of reference or nullable operands, including Nothing. For value types the compiler rejects it (BC30020).

  2. Equality: do two objects have the same value, as defined by their class? That is Equals(Object), and, if the class overloads it, the = operator. The default Object.Equals of a class is a reference comparison, so it returns False for two distinct but identical-looking objects unless the class overrides it.

= on two class-typed operands that do not define an operator is a compile error (BC30452), in contrast with C#, where == on classes silently compares references. Visual Basic forces you to say which one you want: Is for identity, Equals or the overloaded = for value equality. Overriding Equals requires overriding GetHashCode as well, because hash-based collections (HashSet, Dictionary) use both; the full set of rules, IEquatable(Of T), and user-defined operators are on Operator Overloading and Equality.

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Edition
    Public ReadOnly Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub

    Public Overrides Function Equals(obj As Object) As Boolean
        Dim other = TryCast(obj, Edition)
        Return other IsNot Nothing AndAlso other.Title = Title
    End Function

    Public Overrides Function GetHashCode() As Integer
        Return Title.GetHashCode()
    End Function
End Class

Module Program
    Sub Main()
        Dim a As New Book("Refactoring")
        Dim b As New Book("Refactoring")
        Dim c = a

        Console.WriteLine("a Is b: " & (a Is b))
        Console.WriteLine("a Is c: " & (a Is c))
        Console.WriteLine("a IsNot b: " & (a IsNot b))
        Console.WriteLine("a.Equals(b): " & a.Equals(b))
        Console.WriteLine("Object.ReferenceEquals(a, c): " & Object.ReferenceEquals(a, c))
        Console.WriteLine("same hash for a and c: " & (a.GetHashCode() = c.GetHashCode()))

        Dim e1 As New Edition("Refactoring")
        Dim e2 As New Edition("Refactoring")
        Console.WriteLine("e1 Is e2: " & (e1 Is e2))
        Console.WriteLine("e1.Equals(e2): " & e1.Equals(e2))
        Console.WriteLine("Equals(e1, e2): " & Object.Equals(e1, e2))
        Console.WriteLine("Equals(Nothing, Nothing): " & Object.Equals(Nothing, Nothing))

        Dim none As Book = Nothing
        Console.WriteLine("none Is Nothing: " & (none Is Nothing))
        Console.WriteLine("a IsNot Nothing: " & (a IsNot Nothing))

        ' Collections use Equals and GetHashCode
        Dim set1 As New HashSet(Of Book) From {a, b, c}
        Dim set2 As New HashSet(Of Edition) From {e1, e2}
        Console.WriteLine("Books in set: " & set1.Count & ", Editions in set: " & set2.Count)

        ' = is not defined for a class without an Operator
        ' Console.WriteLine(a = b)   ' BC30452
    End Sub
End Module
a Is b: False
a Is c: True
a IsNot b: True
a.Equals(b): False
Object.ReferenceEquals(a, c): True
same hash for a and c: True
e1 Is e2: False
e1.Equals(e2): True
Equals(e1, e2): True
Equals(Nothing, Nothing): True
none Is Nothing: True
a IsNot Nothing: True
Books in set: 2, Editions in set: 1

Source: Is Operator

The HashSet(Of Book) kept two entries (a and c are one object, b another), the HashSet(Of Edition) one, because Edition overrides Equals and GetHashCode. Identity versus equality has a few subtle cases:

Option Strict On

Module Program
    Sub Main()
        Dim literal1 As String = "shelf"
        Dim literal2 As String = "shelf"
        Dim built As String = New String("shelf".ToCharArray())

        Console.WriteLine("literals Is: " & (literal1 Is literal2))      ' literals are interned
        Console.WriteLine("built Is literal: " & (built Is literal1))    ' different object
        Console.WriteLine("built = literal: " & (built = literal1))      ' = on String compares text
        Console.WriteLine("Equals: " & built.Equals(literal1))

        Dim boxed1 As Object = 5
        Dim boxed2 As Object = 5
        Console.WriteLine("boxed Is: " & (boxed1 Is boxed2))            ' two separate boxes
        Console.WriteLine("boxed Equals: " & boxed1.Equals(boxed2))
        Console.WriteLine("Nullable Is Nothing: " & (New Integer?() Is Nothing))
    End Sub
End Module
literals Is: True
built Is literal: False
built = literal: True
Equals: True
boxed Is: False
boxed Equals: True
Nullable Is Nothing: True
  • Two string literals with the same text are the same object (the compiler interns them), but a string built at run time is not. Is on strings is therefore almost never what you want; = on String compares the characters, and String.Equals or String.Compare give control of culture and case (see Strings and Text).

  • Boxing a value creates a new object each time, so Is on two boxed 5`s is `False while Equals is True.

  • Is on a nullable value type with Nothing tests HasValue.

The value-type error:

Option Strict On

Module Program
    Sub Main()
        Dim a As Integer = 1
        Dim b As Integer = 1
        Console.WriteLine(a Is b)
    End Sub
End Module
Program.vb(7,27): error BC30020: 'Is' operator does not accept operands of type 'Integer'. Operands must be reference or nullable types. [App.vbproj]
Program.vb(7,32): error BC30020: 'Is' operator does not accept operands of type 'Integer'. Operands must be reference or nullable types. [App.vbproj]

Source: Is Operator

The Object members every class has

All classes derive from System.Object (the Visual Basic keyword Object is the same type), so every object has these members.

Member Notes

ToString()

returns the type name by default; override it for a readable text. Used by Console.WriteLine(Object), by string interpolation and by the debugger display. & does not convert objects: item & " text" is BC30452 unless the class defines the operator, so write item.ToString() & ... or use interpolation.

Equals(Object)

reference equality by default; override together with GetHashCode.

GetHashCode()

a hash code consistent with Equals; it must not change while the object is a key in a hash-based collection.

GetType()

the runtime Type; GetType(Book) is the compile-time operator form. x.GetType() Is GetType(Book) is an exact type test, TypeOf x Is Book accepts derived classes.

MemberwiseClone()

Protected; a shallow copy of the object. Useful inside a Clone or copy method, see below.

Finalize()

Protected; called by the garbage collector; almost never written by hand, see Object Lifetime and Disposal.

ReferenceEquals(a, b)

Shared; the function behind Is (and safe for classes that overload =).

Copying and comparing objects

MemberwiseClone copies the fields of an object into a new object but does not copy the objects that the fields refer to, so a "clone" shares its collections with the original. A deep copy copies those too. IComparable(Of T) makes the natural order of a class available to Sort, SortedSet and Order By; a Comparison(Of T) lambda gives an alternative order for one call. Avoid ICloneable: it does not say whether the copy is shallow or deep.

Option Strict On

Public Class Author
    Public Property Name As String = ""
End Class

Public Class Book
    Implements IComparable(Of Book)

    Public Property Title As String = ""
    Public Property Year As Integer
    Public Property Authors As New List(Of Author)

    Public Function ShallowCopy() As Book
        Return DirectCast(MemberwiseClone(), Book)
    End Function

    Public Function DeepCopy() As Book
        Dim copy = ShallowCopy()
        copy.Authors = New List(Of Author)(Authors.Select(Function(a) New Author With {.Name = a.Name}))
        Return copy
    End Function

    Public Function CompareTo(other As Book) As Integer Implements IComparable(Of Book).CompareTo
        If other Is Nothing Then Return 1
        Return Year.CompareTo(other.Year)
    End Function

    Public Overrides Function ToString() As String
        Return Title & " (" & Year & ")"
    End Function
End Class

Module Program
    Sub Main()
        Dim original As New Book With {.Title = "Clean Code", .Year = 2008}
        original.Authors.Add(New Author With {.Name = "Robert C. Martin"})

        Dim shallow = original.ShallowCopy()
        Dim deep = original.DeepCopy()
        shallow.Title = "Changed"
        original.Authors(0).Name = "R. Martin"

        Console.WriteLine(original.ToString() & " | " & shallow.ToString() & " | " & deep.ToString())
        Console.WriteLine("shallow shares authors: " & (shallow.Authors Is original.Authors) & ", " & shallow.Authors(0).Name)
        Console.WriteLine("deep author: " & deep.Authors(0).Name)

        Console.WriteLine(original.GetType().FullName & " / " & GetType(Book).Name)
        Console.WriteLine(original.GetType() Is GetType(Book))

        Dim shelf As New List(Of Book) From {
            New Book With {.Title = "Refactoring", .Year = 1999},
            New Book With {.Title = "Clean Code", .Year = 2008},
            New Book With {.Title = "DDD", .Year = 2003}
        }
        shelf.Sort()
        Console.WriteLine(String.Join(" < ", shelf))
        shelf.Sort(Function(x, y) String.Compare(x.Title, y.Title, StringComparison.Ordinal))
        Console.WriteLine(String.Join(" < ", shelf))
    End Sub
End Module
Clean Code (2008) | Changed (2008) | Clean Code (2008)
shallow shares authors: True, R. Martin
deep author: Robert C. Martin
Shelf.Book / Book
True
Refactoring (1999) < DDD (2003) < Clean Code (2008)
Clean Code (2008) < DDD (2003) < Refactoring (1999)

The shallow copy shallow changed Title independently, but its Authors list was the same object as the original’s, so the rename of the author through original was visible in shallow. The deep copy kept the old name. The line with & and a Book would have failed with BC30452, which is why the program calls ToString().

Type tests and casts

An object can be treated as any of its base classes or interfaces. The conversion from derived to base is implicit; the other direction needs a check at run time.

Expression Behaviour

TypeOf x Is T

True if x is not Nothing and its type is T or derives from or implements T. Used in If, not in Select Case.

TryCast(x, T)

the object as T, or Nothing when it is not one; T must be a reference type (or nullable). Never throws.

DirectCast(x, T)

the object as T, or InvalidCastException. Fast, no user-defined conversions.

CType(x, T)

like DirectCast, but also runs defined conversions, such as numeric ones.

x.GetType()

the exact runtime type, for an equality test with GetType(T).

Option Strict On

Public MustInherit Class CatalogItem
    Public Property Title As String = ""
End Class

Public Class Book
    Inherits CatalogItem
End Class

Public Class Magazine
    Inherits CatalogItem
End Class

Module Program
    Private Sub Show(item As CatalogItem)
        If TypeOf item Is Book Then
            Console.WriteLine("a Book: " & item.GetType().Name)
        ElseIf TypeOf item Is Magazine Then
            Console.WriteLine("a Magazine")
        End If

        Dim asBook = TryCast(item, Book)
        Console.WriteLine("TryCast to Book: " & If(asBook Is Nothing, "Nothing", "ok"))

        Try
            Dim forced = DirectCast(item, Book)
            Console.WriteLine("DirectCast ok: " & forced.GetType().Name)
        Catch ex As InvalidCastException
            Console.WriteLine("InvalidCastException: " & ex.Message)
        End Try
    End Sub

    Sub Main()
        Show(New Book())
        Show(New Magazine())
        Console.WriteLine(GetType(Book).BaseType.Name & " / " & GetType(CatalogItem).IsAbstract)
    End Sub
End Module
a Book: Book
TryCast to Book: ok
DirectCast ok: Book
a Magazine
TryCast to Book: Nothing
InvalidCastException: Unable to cast object of type 'Shelf.Magazine' to type 'Shelf.Book'.
CatalogItem / True

Source: TypeOf Operator

The message of InvalidCastException includes the root namespace (Shelf.Magazine). Conversions between value types are on Type Conversions; the "which cast?" flowchart is there.

Case-insensitivity and name clashes

Names are not case sensitive, and a parameter hides a member of the same spelling. The most common consequence is in constructors and setters. With a field or property Name and a parameter name, the unqualified Name refers to the parameter (the innermost scope), so the statement Name = name assigns the parameter to itself. It compiles without a warning and silently loses the value. Use Me.Name = name, or give the fields an underscore prefix.

Option Strict On

Public Class ReaderBroken
    Public Property Name As String = "unset"

    Public Sub New(name As String)
        ' VB is case-insensitive: "Name" here is the parameter "name", so this assigns the parameter to itself.
        Name = name
    End Sub
End Class

Public Class ReaderFixed
    Public Property Name As String = "unset"

    Public Sub New(name As String)
        Me.Name = name
    End Sub
End Class

Module Program
    Sub Main()
        Console.WriteLine("without Me: " & New ReaderBroken("Ada").Name)
        Console.WriteLine("with Me   : " & New ReaderFixed("Ada").Name)
    End Sub
End Module
without Me: unset
with Me   : Ada

A local variable with the same name as its type is legal (Dim book As Book), and is the convention for a single object of a type. Because identifiers are not case sensitive, such a variable and the type differ only in context.

Errors the compiler reports

Each snippet below is a complete program that fails to compile; the messages are the real compiler output from the .NET 10 SDK (built with warnings as errors turned off so that warnings print as warnings).

Option Strict On

Public Class Reader
    Public Sub New(name As String)
    End Sub
End Class

Public Class Student
    Inherits Reader
End Class

Public Class Tutor
    Inherits Reader

    Public Sub New()
        Console.WriteLine("tutor")
    End Sub
End Class

Public Class Counter
    Public Total As Integer
    Public ReadOnly Limit As Integer = 3

    Public Shared Sub Reset()
        Me.Total = 0
    End Sub

    Public Sub Change()
        Limit = 4
    End Sub

    Public Sub New()
        Return 1
    End Sub
End Class

Public MustInherit Class Shape
    Public MustOverride Function Area() As Double

    Public Function Twice() As Double
        Return MyClass.Area() * 2
    End Function
End Class

Public Class Secret
    Private Hidden As Integer
    Protected Guarded As Integer
End Class

Module Program
    Sub Main()
        Dim r As New Reader()
        Dim s As New Shape()
        Dim sec As New Secret()
        sec.Hidden = 1
        sec.Guarded = 2
        Dim a As New Counter()
        Dim b As New Counter()
        Console.WriteLine(a = b)
        Console.WriteLine(a.Limit)
    End Sub
End Module
Program.vb(8,14): error BC30387: Class 'Student' must declare a 'Sub New' because its base class 'Reader' does not have an accessible 'Sub New' that can be called with no arguments. [App.vbproj]
Program.vb(25,9): error BC30043: 'Me' is valid only within an instance method. [App.vbproj]
Program.vb(29,9): error BC30064: 'ReadOnly' variable cannot be the target of an assignment. [App.vbproj]
Program.vb(33,9): error BC30647: 'Return' statement in a Sub or a Set cannot return a value. [App.vbproj]
Program.vb(15,16): error BC30148: First statement of this 'Sub New' must be a call to 'MyBase.New' or 'MyClass.New' because base class 'Reader' of 'Tutor' does not have an accessible 'Sub New' that can be called with no arguments. [App.vbproj]
Program.vb(52,22): error BC30455: Argument not specified for parameter 'name' of 'Public Sub New(name As String)'. [App.vbproj]
Program.vb(53,18): error BC30569: 'New' cannot be used on a class that is declared 'MustInherit'. [App.vbproj]
Program.vb(55,9): error BC30389: 'Shelf.Secret.Hidden' is not accessible in this context because it is 'Private'. [App.vbproj]
Program.vb(56,9): error BC30389: 'Shelf.Secret.Guarded' is not accessible in this context because it is 'Protected'. [App.vbproj]
Program.vb(59,27): error BC30452: Operator '=' is not defined for types 'Counter' and 'Counter'. [App.vbproj]
Program.vb(41,16): error BC30614: 'MustOverride' method 'Public MustOverride Function Area() As Double' cannot be called with 'MyClass'. [App.vbproj]

Source: BC30387

The compiler listed BC30387, a check on the class declaration, after the errors found in method bodies. More:

Option Strict On

Public Class Chain
    Public Sub New(x As Integer)
        Console.WriteLine("work first")
        Me.New()
    End Sub

    Public Sub New()
        Me.New(1)
    End Sub
End Class

Public Class Config
    Public Shared Level As Integer = 1
    Public Name As String = "x"
End Class

Public Class Book
    Public ReadOnly Property Title As String = ""
End Class

Module Program
    Sub Main()
        Dim c As New Config()
        Console.WriteLine(c.Level)
        Dim b As New Book With {.Title = "x"}
    End Sub
End Module
Program.vb(6,9): error BC30282: Constructor call is valid only as the first statement in an instance constructor. [App.vbproj]
Program.vb(26,27): warning BC42025: Access of shared member, constant member, enum member or nested type through an instance; qualifying expression will not be evaluated. [App.vbproj]
Program.vb(27,33): error BC30526: Property 'Title' is 'ReadOnly'. [App.vbproj]

Source: BC30282

A constructor chain with a cycle, and a reserved word as a class name:

Option Strict On

Public Class Chain
    Public Sub New(x As Integer)
        Me.New()
    End Sub

    Public Sub New()
        Me.New(1)
    End Sub
End Class

Module Program
    Sub Main()
        Dim c As New Chain()
    End Sub
End Module
Program.vb(4,16): error BC30298: Constructor 'Public Sub New(x As Integer)' cannot call itself: [App.vbproj]
Program.vb(8,16): error BC30298: Constructor 'Public Sub New()' cannot call itself: [App.vbproj]

Source: Sub Statement

Option Strict On

Public Class MyClass
    Public Sub Run()
    End Sub
End Class

Module Program
    Sub Main()
        Dim x As New MyClass()
    End Sub
End Module
Program.vb(3,14): error BC30183: Keyword is not valid as an identifier. [App.vbproj]
Program.vb(10,22): error BC30180: Keyword does not name a type. [App.vbproj]

Source: BC30183

The accessibility of constructors (a private Sub New) in a base class and in a user:

Option Strict On

Public NotInheritable Class Isbn
    Private Sub New()
    End Sub
End Class

Public Class Outer
    Private _value As Integer

    Public Class Inner
        Public Function Read() As Integer
            Return _value
        End Function
    End Class
End Class

Public Class Plain
    Private Sub New()
    End Sub
End Class

Public Class Child
    Inherits Plain
End Class

Module Program
    Sub Main()
        Dim bad As New Isbn()
    End Sub
End Module
Program.vb(23,14): error BC31399: Class 'Plain' has no accessible 'Sub New' and cannot be inherited. [App.vbproj]
Program.vb(29,24): error BC30517: Overload resolution failed because no 'New' is accessible. [App.vbproj]
Program.vb(13,20): error BC30469: Reference to a non-shared member requires an object reference. [App.vbproj]

Source: BC30517

Code Cause

BC30020

Is applied to a value type

BC30043

Me in a Shared method

BC30064

assignment to a ReadOnly field outside a constructor

BC30148

a Sub New that does not call a base constructor when the base class has no parameterless one

BC30179

all parts of a class lack Partial, or a duplicated class name

BC30180, BC30183

a keyword (such as MyClass) used as a type name or identifier

BC30282

Me.New or MyBase.New not the first statement of the constructor

BC30298

constructors that call each other in a cycle

BC30299

a class derived from a NotInheritable class

BC30369

instance member used in a Shared method or initializer

BC30387

derived class without a usable base Sub New()

BC30389

inaccessible member or type (Private, Protected, Friend) through the dot

BC30452

= or & on a class that defines no such operator

BC30455

a constructor argument is missing

BC30456

a member does not exist from the caller’s point of view (e.g. a Friend member of another assembly)

BC30469

instance member used without an object reference

BC30517

no accessible constructor

BC30526

object initializer assigns a ReadOnly property

BC30569

New on a MustInherit class

BC30614

MyClass call of a MustOverride method

BC30647

Return with a value in a Sub, including Sub New

BC30909

a Public signature that exposes a Friend type

BC31399

inheriting from a class with no accessible constructor

BC42025

Shared member accessed through an instance (warning)

BC42104

a variable used before it has been assigned (warning)

Learn has an error page for most of these numbers (the Source lines above link some of them) but not for all, which is why the compiler’s own text is shown. BC42025 and BC42104 are warnings; this project turns them into errors.

Runtime exceptions

Exception Typical cause

NullReferenceException

a member call, field read or For Each on a Nothing reference; the commonest runtime error. Initialize collections in the constructor or with an initializer; return empty collections instead of Nothing.

InvalidCastException

DirectCast/CType to a type the object is not (example above); use TryCast and test.

TypeInitializationException

an exception in a Shared Sub New or Shared field initializer; see above. The real cause is InnerException.

StackOverflowException

unbounded recursion, for example a property whose getter reads itself instead of the backing field, or two constructors that create each other; it cannot be caught and ends the process.

ObjectDisposedException

use of an object after Dispose; see Object Lifetime and Disposal.

MissingMethodException, MissingMemberException

late binding to a member that is not there (see Early and Late Binding)

Differences from C#

Topic Visual Basic C#

Current object, base class

Me, MyBase

this, base

Current-class call without virtual dispatch

MyClass

no equivalent

Constructor

Sub New, chained with Me.New(...)/MyBase.New(...) as the first statement

a method named after the class; chained with : this(...)/: base(...)

Static members, static constructor

Shared, Shared Sub New

static, static T()

Static class

Module, or NotInheritable plus a Private Sub New

static class

Internal, sealed, abstract

Friend, NotInheritable, MustInherit

internal, sealed, abstract

Protected internal, private protected

Protected Friend, Private Protected

protected internal, private protected

Default member accessibility

fields from Dim are Private; Sub, Function, Property, Event and nested types are Public

private for all

Default accessibility of a top-level type

Friend

internal

Instance initializers vs the base constructor

run after the base constructor

run before it

Instance initializer using another instance field

allowed

not allowed

== on two classes

not defined: BC30452; use Is or Equals

compares references

Object initializer

New T With {.P = 1}

new T { P = 1 }

With statement

yes

no

Target-typed new()

not available

yes

Primary constructors, record, init, required

not declarable in VB (VB consumes init properties from C#); see Properties and Default Properties

yes

partial

required on at least one part

required on every part

Case sensitivity

no: name and Name are one identifier

yes

Method hiding

Shadows

new

Finalizer

Protected Overrides Sub Finalize

~TypeName()

Static local variable

yes

no (use a field)

Nested type default accessibility

Public

private

Older material you may meet

Visual Basic 6 and VBA had classes too, but of a different kind. Old tutorials, and some books that were written from them, differ in these points:

Old code you may meet What it is today

Set x = New Foo

assignment needs no Set for objects: Dim x As New Foo() or x = New Foo(). Let and Set do not exist for properties either.

Class_Initialize, Class_Terminate events

Sub New and, for resources, IDisposable.Dispose; Finalize is not a destructor you can time (see Object Lifetime and Disposal).

Dim x As New Foo creating the object on first use

VB.NET creates the object at the declaration. The VB6 behaviour (auto-instantiation) is gone; a Nothing check is meaningful again.

a class module’s Public, Private, Friend members and Instancing

Public/Friend types in the assembly; Instancing has no equivalent.

default instances of forms (Form1.Show)

still present in Windows Forms projects through My.Forms when the project type is WindowsForms (see The My Namespace); avoid for new code and pass form instances explicitly.

Is Nothing vs = Nothing

Is Nothing for references; x = Nothing on a value type means the default value.

Global module variables

a Shared field in a class, or a Module field, if at all

The books this reference was compared with (see the bibliography on the landing page) contain two kinds of outdated or incorrect material for this page. First, a few examples name the class MyClass. That is a reserved word and the code does not compile (BC30183), as shown above; the working versions use another name or the escaped [MyClass]. Second, the book examples are written for the "Console (.NET Framework)" project template and for Visual Studio 2019 or 2022 with .NET Framework 4.x; everything about classes works unchanged on .NET 10 with an SDK-style .vbproj, but the projects, Option Strict default (Off in the SDK defaults, so write Option Strict On in every file) and the root namespace that prefixes type names in messages are different.

Pitfalls

  • Writing Name = name in a constructor with a parameter name: the field stays unset (use Me.Name or _name).

  • Forgetting that adding any Sub New removes the default parameterless constructor (BC30455 at the old New T() calls).

  • Calling an overridable method from a constructor: the derived class’s initializers have not run.

  • Comparing objects with = (BC30452) and then "fixing" it by comparing ToString() results; use Is for identity and override Equals and GetHashCode for value equality.

  • Using Is on strings or boxed numbers.

  • Relying on the defaults of access modifiers; Dim fields are Private, Sub is Public, nested types are Public.

  • A class with a Shared Sub New or Shared field initializer that can throw: the type is unusable after the first failure (TypeInitializationException).

  • Giving a Public member a Friend type (BC30909).

  • Using MyClass as a name; rename it or write [MyClass].

  • Accessing a Shared member through an instance (BC42025).

  • A shallow MemberwiseClone that shares collections with the original.

  • Hiding bugs by exposing public fields; use properties (see Properties and Default Properties).