Modules and Shared Members

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 examples 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 scratch projects use the root namespace Shelf, which matters for the namespace examples below. The project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq and others) are why Console, List(Of T) and Where need no Imports.

Instance members belong to an object; shared members belong to the type. Visual Basic has two ways to put code and data on a type instead of on an object: a Module, which is a type whose members are all shared, and the Shared modifier on the members of a class or structure. They compile to the same thing (static members of a class), but they differ in how names are found, in what they may contain and in what they communicate to a reader. The page covers both, plus the two things that are only about shared state: how and when it is initialized, and how to keep it correct when threads touch it. The running example is the shelf library catalogue: Isbn helpers, a counter of Book objects and a Catalog singleton.

Module statements

[<attributes>] [accessmodifier] Module Name
    [statements]
End Module

A module is declared at file or namespace level, never inside another type or a procedure (a nested module is BC30617, "Module statements can occur only at file or namespace level"). Without an access modifier a top-level module is Friend, as for every other type, so a library that wants its modules visible to other assemblies must say Public Module. Module has been in the language since Visual Basic .NET 1.0 (2002).

The whole point of a module is that its members need no object and, in most cases, no qualification:

Option Strict On

Imports Shelf.Helpers

Namespace Helpers
    ' A module: every member is implicitly Shared, nothing can be instantiated.
    Public Module IsbnTools
        Public Const Prefix As String = "978"          ' constants are fine
        Private ParsedCount As Integer                  ' a Private field
        Dim LastText As String = ""                     ' Dim in a module is Private

        Public Function Normalize(text As String) As String
            ParsedCount += 1
            LastText = text
            Return text.Replace("-", "").Replace(" ", "")
        End Function

        Public Function HasPrefix(text As String) As Boolean
            Return Normalize(text).StartsWith(Prefix, StringComparison.Ordinal)
        End Function

        Public ReadOnly Property Count As Integer
            Get
                Return ParsedCount
            End Get
        End Property

        Public Function Last() As String
            Return LastText
        End Function
    End Module
End Namespace

Namespace App
    Module Program
        Sub Main()
            ' Unqualified through the enclosing namespace's Imports ...
            Console.WriteLine(Normalize("978-0-13-235088-4"))
            ' ... or qualified by the module name.
            Console.WriteLine(Helpers.IsbnTools.HasPrefix("978 1 80512 531 0"))
            Console.WriteLine(IsbnTools.Count)
            Console.WriteLine(Last())
            Console.WriteLine(Prefix)
        End Sub
    End Module
End Namespace
9780132350884
True
2
978 1 80512 531 0
978

Points to read from the listing:

  • Normalize, HasPrefix, Count and Last carry no Shared keyword and are shared anyway. A module’s members are implicitly Shared, and writing the keyword is an error (see below).

  • Imports Shelf.Helpers made the module’s members visible without naming the module in Main; Helpers.IsbnTools shows the qualified form. Both reach the same method.

  • Dim LastText As String has no access modifier and is Private; a Const is allowed, and so are properties, events, nested types and a constructor.

  • The state in a module (ParsedCount) is one copy per process, as for every shared field. Nothing in the language prevents a module from turning into a bag of global variables; see When to choose which.

What a module cannot do

A module has no instances, no base type and no interface list, and its members are always shared. The compiler checks each rule with its own message:

Attempt Error Meaning

Dim m As New IsbnTools(), Dim m As IsbnTools, Inherits IsbnTools

BC30371

"Module 'IsbnTools' cannot be used as a type." The name of a module is a namespace-like container, not a type you can declare variables of. GetType(IsbnTools) is allowed.

Inherits inside a module

BC30230

A module has no base class other than System.Object.

Implements inside a module, or Implements on a member

BC30231, BC31083

A module cannot implement an interface, because interface members are instance members.

Public Shared Function ... in a module

BC30433

The modifier is redundant and rejected.

Protected, Overridable, MustOverride, Overrides or Shadows on a module member

BC30433

Inheritance modifiers make no sense without derived types.

Public Module inside a class

BC30617

Modules are top-level or namespace-level only.

Extension methods in a class

BC36551

Extension methods can be defined only in modules (see below).

Option Strict On

Public Module IsbnTools
    Public Shared Function Normalize(text As String) As String   ' 1
        Return text
    End Function

    Protected Sub Hidden()                                        ' 2
    End Sub

    Public Overridable Sub Extend()                               ' 3
    End Sub

    Public Const Prefix As String = "978"
End Module

Public Class Derived
    Inherits IsbnTools                                            ' 4
End Class

Public Module Combined
    Inherits Object                                               ' 5
End Module

Public Class Loans
    Private Count As Integer

    Public Shared Sub Reset()
        Count = 0                                                 ' 6
    End Sub
End Class

Public Module Program
    Sub Main()
        Dim tools As New IsbnTools()                              ' 8
        Dim t As IsbnTools = Nothing                              ' 9
        Dim x = GetType(IsbnTools)                                ' fine
        Console.WriteLine(x.Name)
    End Sub
End Module
Program.vb(22,5): error BC30230: 'Inherits' not valid in Modules.
Program.vb(18,14): error BC30371: Module 'IsbnTools' cannot be used as a type.
Program.vb(18,14): error BC30258: Classes can inherit only from other classes.
Program.vb(4,12): error BC30433: Methods in a Module cannot be declared 'Shared'.
Program.vb(8,5): error BC30433: Methods in a Module cannot be declared 'Protected'.
Program.vb(11,12): error BC30433: Methods in a Module cannot be declared 'Overridable'.

The messages come in the order the compiler found them. Note that line 18 produces two errors, since a module is not a type and Inherits wants a class. Declaration errors stop the compiler before it checks method bodies, which is why the next program exists: the errors in using a module appear only when its declaration is valid.

Option Strict On

Public Module IsbnTools
    Public Function Normalize(text As String) As String
        Return text
    End Function
End Module

Public Class Loans
    Private Count As Integer

    Public Shared Sub Reset()
        Count = 0                                  ' instance field from a Shared method
    End Sub
End Class

Public Module Program
    Sub Main()
        Dim tools As New IsbnTools()               ' a module is not a type
        Dim other As IsbnTools = Nothing
        Dim parts = IsbnTools.Normalize("1").Length
        Dim loan As New Loans()
        loan.Reset()                               ' Shared member through an instance
        Console.WriteLine(parts)
    End Sub
End Module
Program.vb(13,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.
Program.vb(19,26): error BC30371: Module 'IsbnTools' cannot be used as a type.
Program.vb(20,22): error BC30371: Module 'IsbnTools' cannot be used as a type.
Program.vb(23,9): warning BC42025: Access of shared member, constant member, enum member or nested type through an instance; qualifying expression will not be evaluated.
  • Dim tools As New IsbnTools() and Dim other As IsbnTools both give BC30371.

  • BC30369 is the rule for shared methods in general: Count is an instance field, and a Shared method has no Me to read it from. See Shared members of classes and structures.

  • BC42025 is a warning (an error under TreatWarningsAsErrors): loan.Reset() compiles, but the compiler discards loan and calls Loans.Reset(). Write Loans.Reset().

Default access of module members

Members of a module have the same default access as members of a class: Dim and Private fields are Private, while Sub, Function and Property without a modifier are Public (the member is as visible as its module). The module itself is Friend by default. The next program prints what the compiler actually emitted for a module and for a class.

Option Strict On

Imports System.Reflection

Public Module Defaults
    Dim Hidden As Integer
    Dim Computed As Integer = 5
    Sub Quiet()
    End Sub
    Function Loud() As Integer
        Return Hidden + Computed
    End Function
End Module

Friend Module Internal
End Module

Public Class Library
    Dim Plain As Integer
    Sub Quiet()
    End Sub
End Class

Module Program
    Sub Main()
        Const flags = BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Static Or BindingFlags.Instance
        For Each t In {GetType(Defaults), GetType(Library)}
            Console.WriteLine(t.Name)
            For Each f In t.GetFields(flags)
                Console.WriteLine("  field  " & f.Name & ": " & If(f.IsPublic, "Public", If(f.IsPrivate, "Private", "other")))
            Next
            For Each m In t.GetMethods(flags Or BindingFlags.DeclaredOnly)
                Console.WriteLine("  method " & m.Name & ": " & If(m.IsPublic, "Public", If(m.IsPrivate, "Private", "other")))
            Next
        Next
        Console.WriteLine("Internal visible outside assembly: " & GetType(Internal).IsVisible & ", Defaults: " & GetType(Defaults).IsVisible)
    End Sub
End Module
Defaults
  field  Hidden: Private
  field  Computed: Private
  method Quiet: Public
  method Loud: Public
Library
  field  Plain: Private
  method Quiet: Public
Internal visible outside assembly: False, Defaults: True

Defaults.Quiet is Public even though nobody wrote the word. Only the module needs Public for the member to cross the assembly boundary, which is the reason that exposing a Friend Module to the test project needs InternalsVisibleTo (see Testing).

Source: Modifiers

What the compiler emits

A module is not a separate runtime concept. The compiler produces a class that has Shared (static) members, no public constructor, and an attribute that tells VB tooling it came from a module.

Option Strict On

Imports System.Reflection
Imports Microsoft.VisualBasic.CompilerServices

Public Module Util
    Public Function Twice(n As Integer) As Integer
        Return n * 2
    End Function
    Public Const Limit As Integer = 10
End Module

Public Class WithInitializer
    Public Shared ReadOnly Value As Integer = 1
End Class

Public Class WithConstructor
    Public Shared ReadOnly Value As Integer

    Shared Sub New()
        Value = 1
    End Sub
End Class

Public Class Plain
    Public Shared Sub Run()
    End Sub
End Class

Module Program
    Sub Main()
        Dim m = GetType(Util)
        Console.WriteLine("IsAbstract=" & m.IsAbstract & " IsSealed=" & m.IsSealed & " IsClass=" & m.IsClass)
        Console.WriteLine("StandardModule=" & (m.GetCustomAttribute(Of StandardModuleAttribute)() IsNot Nothing))
        Console.WriteLine("Twice is static: " & m.GetMethod("Twice").IsStatic)
        Console.WriteLine("Public constructors: " & m.GetConstructors().Length)

        For Each t In {GetType(WithInitializer), GetType(WithConstructor), GetType(Plain), GetType(Util)}
            Console.WriteLine(t.Name & ": BeforeFieldInit=" & t.Attributes.HasFlag(TypeAttributes.BeforeFieldInit) & ", TypeInitializer=" & (t.TypeInitializer IsNot Nothing))
        Next
    End Sub
End Module
IsAbstract=False IsSealed=True IsClass=True
StandardModule=True
Twice is static: True
Public constructors: 0
WithInitializer: BeforeFieldInit=True, TypeInitializer=True
WithConstructor: BeforeFieldInit=False, TypeInitializer=True
Plain: BeforeFieldInit=False, TypeInitializer=False
Util: BeforeFieldInit=False, TypeInitializer=False
  • IsSealed=True and StandardModule=True: a module is a sealed class marked with Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute. The attribute is how the Visual Basic compiler, when it reads another assembly (written in VB or not), knows that the class is a module and that its members may be called without qualification.

  • IsAbstract=False: the C# static class is both abstract and sealed in metadata. A VB module is only sealed, and its constructor is private, so no one can create an instance. The behaviour is the same; the metadata is not.

  • The last four lines show initialization metadata, covered in Shared constructors and initialization order.

A C# project that references a VB library sees the module as an ordinary static class:

Option Strict On

Public Module IsbnTools
    Public Const Prefix As String = "978"

    Public Function Normalize(text As String) As String
        Return text.Replace("-", "").Replace(" ", "")
    End Function
End Module

Public Class Library
    Public Shared ReadOnly Property Name As String = "Shelf"

    Public Shared Function Greeting() As String
        Return "Welcome to " & Name
    End Function
End Class
using Shelf;

// A Visual Basic Module is consumed like a C# static class.
Console.WriteLine(IsbnTools.Normalize("978-0-13-235088-4"));
Console.WriteLine(IsbnTools.Prefix);
Console.WriteLine(Library.Greeting());
Console.WriteLine(typeof(IsbnTools).IsAbstract + " " + typeof(IsbnTools).IsSealed);
9780132350884
978
Welcome to Shelf
False True

The VB library has the root namespace Shelf, and the C# program called both IsbnTools.Normalize and Library.Greeting() the same way. A C# developer will therefore not notice whether a VB utility class is a module or a class of Shared members. A VB consumer does notice: the module’s members can be used unqualified after Imports, the class’s members cannot be called without writing the class name (importing a class with Imports, as in Imports System.Math, also makes its Shared members available unqualified, but nothing forces a caller to do so).

Names, namespaces and type promotion

Promotion of types declared in a module

An Enum, Class, Structure, Interface or Delegate declared inside a module can be named through the enclosing namespace without the module’s name. The Language Specification and Learn call this type promotion; it was part of Visual Basic .NET 1.0. It is purely a rule of name lookup in the compiler: the type is still nested in the module in metadata.

Option Strict On

Imports Shelf.Helpers.Labels      ' importing the module itself, not the namespace

Namespace Helpers
    Public Module Labels
        Public Enum Style         ' nested in a module: promoted to the namespace
            Compact
            Full
        End Enum

        Public Class Options
            Public Property Style As Style = Style.Compact
        End Class

        Public Function Describe(state As LoanState) As String
            Return "state " & state.ToString()
        End Function
    End Module
End Namespace

Public Enum LoanState
    Active = 1
    Returned = 2
End Enum

Module Program
    Sub Main()
        ' Three ways to name the same enumeration.
        Dim a As Helpers.Style = Helpers.Style.Full
        Dim b As Helpers.Labels.Style = Helpers.Labels.Style.Full
        Console.WriteLine(a = b)
        Console.WriteLine(GetType(Helpers.Options).FullName)
        Console.WriteLine(GetType(Helpers.Labels.Options).FullName)

        ' The module's members are reachable unqualified because of the Imports line.
        Console.WriteLine(Describe(LoanState.Returned))
        Console.WriteLine(Helpers.Labels.Describe(LoanState.Active))
    End Sub
End Module
True
Shelf.Helpers.Labels+Options
Shelf.Helpers.Labels+Options
state Returned
state Active
  • Helpers.Style and Helpers.Labels.Style are two names for the same enumeration, and GetType(...).FullName reports the real location Shelf.Helpers.Labels+Options, a type nested in a class named after the module.

  • Because of the root namespace Shelf, Namespace Helpers in this file really declares Shelf.Helpers. With <RootNamespace>Shelf</RootNamespace> in the project file, Imports Shelf.Helpers.Labels at the top of the file is the full name. If the project has a different root namespace (the default is the assembly name), the file must say Imports TheRoot.Helpers.Labels; an Imports of a name that does not exist is BC40056, a warning that this project’s TreatWarningsAsErrors turns into an error.

  • Imports Shelf.Helpers.Labels imports a module, so Describe is visible unqualified, but the word Labels is not: Labels.Describe(...) is BC30451 unless Shelf.Helpers is also imported (or the call is written Helpers.Labels.Describe(...)). Importing a module imports its members; importing a namespace imports the module names and promoted types.

  • A type is promoted unless the namespace already contains a type of the same name, or unless the type is Partial. In both cases write the module name.

Two modules with the same member

Because unqualified names reach into every imported module, two modules that each declare Render collide the moment one calls Render without a qualifier:

Option Strict On

Public Module TitleFormat
    Public Function Render(text As String) As String
        Return text.ToUpperInvariant()
    End Function
End Module

Public Module AuthorFormat
    Public Function Render(text As String) As String
        Return text.ToLowerInvariant()
    End Function
End Module

Public Module Program
    Sub Main()
        Console.WriteLine(Render("Dune"))                  ' which module?
        Console.WriteLine(TitleFormat.Render("Dune"))
        Console.WriteLine(AuthorFormat.Render("Herbert"))
    End Sub
End Module
Program.vb(17,27): error BC30562: 'Render' is ambiguous between declarations in Modules 'Shelf.TitleFormat, Shelf.AuthorFormat'.

BC30562 is the module-specific form of the ambiguity error. The fix is a qualifier at the call site (TitleFormat.Render), a rename, or removing one of the Imports. Choose function names that say what they act on (RenderTitle) in a module that every other file imports, since the module gives its member names to the whole namespace.

Sub Main and the startup object

A console or Windows Forms application needs one entry point: a Shared Sub Main (or a Function Main As Integer) in a module or in a class, or, for Windows Forms, the application framework’s startup form. The module is the usual home, and dotnet new console -lang vb generates Module Program with Sub Main(args As String()). A class with Public Shared Sub Main works exactly the same. Visual Basic has no top-level statements and no Async Main (see Program Structure and Code Conventions).

When two types declare Main, the compiler does not guess:

Option Strict On

Public Class Launcher
    Public Shared Sub Main()
        Console.WriteLine("Launcher.Main")
    End Sub
End Class

Public Module Program
    Public Sub Main()
        Console.WriteLine("Program.Main")
    End Sub
End Module
vbc : error BC30738: 'Sub Main' is declared more than once in 'Shelf.Cli': Shelf.Launcher.Main(), Shelf.Program.Main()

The fix is to delete one, or to choose it in the project file with StartupObject, which takes the full name of the module or class (the root namespace included):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Shelf</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <OptionExplicit>On</OptionExplicit>
    <OptionInfer>On</OptionInfer>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <StartupObject>Shelf.Program</StartupObject>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
</Project>
Launcher.Main

In Visual Studio the same property is Startup object on the Application tab of the project properties. A Windows Forms project that has a custom Sub Main must also set MyType to WindowsFormsWithCustomSubMain (see Windows Forms Applications).

Shared members of classes and structures

Shared on a field, property, method, event or constructor makes it belong to the type. All the objects see the same copy; a Shared member is reached with the type name, and inside the type with no prefix at all. Shared has been in Visual Basic since .NET 1.0. In the other languages the keyword is static; in Visual Basic Static exists but means something else (see Static local variables).

Option Strict On

Public Class Book
    Private Shared NextId As Integer = 1                    ' one copy for the whole class
    Public Shared ReadOnly DefaultLoanDays As Integer = 21  ' shared constant-like value
    Public Const MaxTitleLength As Integer = 120            ' Const is implicitly Shared

    Public ReadOnly Property Id As Integer                  ' one copy per object
    Public ReadOnly Property Title As String

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

    Public Shared ReadOnly Property Created As Integer      ' Shared property
        Get
            Return NextId - 1
        End Get
    End Property

    Public Shared Function Create(title As String) As Book  ' Shared factory method
        Return New Book(title)
    End Function
End Class

Public Class Magazine
    Inherits Book

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Public Class Cache(Of T)
    Public Shared Count As Integer                          ' one copy per closed type
End Class

Module Program
    Sub Main()
        Dim dune = Book.Create("Dune")
        Dim wired = New Magazine("Wired")
        Console.WriteLine(dune.Id & " " & wired.Id)
        Console.WriteLine(Book.Created)                     ' through the class
        Console.WriteLine(Magazine.Created)                 ' through a derived class: same member
        Console.WriteLine(Book.DefaultLoanDays & " " & Book.MaxTitleLength)

        Cache(Of Integer).Count = 5
        Cache(Of String).Count += 1
        Console.WriteLine(Cache(Of Integer).Count & " " & Cache(Of String).Count & " " & Cache(Of Date).Count)

        Dim p = GetType(Book).GetProperty("Created")
        Console.WriteLine(p.GetMethod.IsStatic)
    End Sub
End Module
1 2
2
2
21 120
5 1 0
True
  • NextId is a private shared field. The constructor increments it, so each Book gets the next number, and Created exposes the count through a Shared read-only property.

  • Const is implicitly Shared. Shared ReadOnly is the usual choice when the value is not a compile-time constant (a Date, an object, a computed value); its value is fixed after the type initializer has run. Public Const values are copied into the callers when they are compiled, so changing one needs a rebuild of every client (a Shared ReadOnly field is read at run time).

  • Magazine.Created and Book.Created are the same property: a derived class inherits the shared members of its base but does not get its own copy. There is no way to ask "how many magazines" without a second counter on Magazine.

  • Cache(Of T).Count has a separate copy for every closed generic type. Cache(Of Integer).Count and Cache(Of String).Count are unrelated, and Cache(Of Date).Count has never been touched. This is the same in C# and surprises people who use a generic class to hold "the" cache.

  • The reflection check shows that Created compiles to a static getter.

A Shared member cannot use Me, MyBase or MyClass or the instance members of its type without an object, and the compiler says so with a distinct message for each:

Option Strict On

Imports System.Runtime.CompilerServices

Public Class Tools
    Public Shared Sub Describe()
        Console.WriteLine(Me.ToString())                         ' 3
    End Sub
End Class

Public Module Program
    Sub Main()
    End Sub
End Module
Program.vb(7,27): error BC30043: 'Me' is valid only within an instance method.

Other modifiers that cannot be combined with Shared, or are rejected on a shared member, each have a message of their own:

Option Strict On

Imports System.Reflection
Imports System.Runtime.CompilerServices

Public Class Tools
    <Extension>
    Public Shared Function Shout(text As String) As String       ' extension method in a class
        Return text.ToUpperInvariant()
    End Function

    Public Shared Overridable Sub Reset()                        ' Shared and Overridable
    End Sub
End Class

Public Interface ICatalogLike
    Shared Sub Refresh()                                         ' Shared interface member
End Interface

Public Module Program
    Dim Hidden As Integer
    Sub Main()
    End Sub
End Module
Program.vb(12,19): error BC30501: 'Shared' cannot be combined with 'Overridable' on a method declaration.
Program.vb(17,5): error BC30270: 'Shared' is not valid on an interface method declaration.
Program.vb(7,6): error BC36551: Extension methods can be defined only in modules.
  • Shared and Overridable (and Overrides, MustOverride, NotOverridable) are contradictory, since virtual dispatch needs an instance to select the override. A shared member can still be Shadows-ed in a derived class (see Inheritance and Polymorphism).

  • Visual Basic cannot declare Shared interface members (BC30270). C# 11 can declare static abstract members in an interface, but Visual Basic has no syntax to write one (see Interfaces).

  • Extension methods are written <Extension()> on a Shared method of a module, never of a class (BC36551), which is the one place where a module is required (see Extension Methods and Partial Methods).

Source: Shared

Shared constructors and initialization order

A shared constructor is Shared Sub New(). It runs once per type, before the first instance is created or the first shared member is used. It is the Visual Basic name for the C# static constructor, and in metadata it is the type initializer (.cctor). A module may declare a plain Sub New, which is implicitly shared.

Option Strict On

Public Class Settings
    Public Shared ReadOnly Started As Date = Trace("field initializer 1", Date.MinValue)
    Public Shared ReadOnly Name As String = Trace("field initializer 2", "shelf")

    Shared Sub New()
        Trace("Shared Sub New", 0)
        Total = Name.Length
    End Sub

    Public Shared ReadOnly Total As Integer

    Private Shared Function Trace(Of T)(message As String, value As T) As T
        Console.WriteLine("  [Settings] " & message)
        Return value
    End Function

    Public Shared Sub Touch()
        Console.WriteLine("  [Settings] Touch")
    End Sub
End Class

Public Module Registry
    Public ReadOnly Items As New List(Of String)

    Sub New()                                        ' a module constructor: implicitly Shared
        Console.WriteLine("  [Registry] Sub New")
        Items.Add("first")
    End Sub
End Module

Public Class Broken
    Public Shared ReadOnly Value As Integer = Integer.Parse("not a number")
End Class

Module Program
    Sub Main()
        Console.WriteLine("Main starts")
        Console.WriteLine("calling Settings.Touch")
        Settings.Touch()
        Console.WriteLine("Total = " & Settings.Total)

        Console.WriteLine("using Registry")
        Console.WriteLine(Registry.Items.Count)

        For attempt = 1 To 2
            Try
                Console.WriteLine(Broken.Value)
            Catch ex As TypeInitializationException
                Console.WriteLine("attempt " & attempt & ": " & ex.GetType().Name & " / " & ex.InnerException.GetType().Name)
            End Try
        Next
    End Sub
End Module
Main starts
calling Settings.Touch
  [Settings] field initializer 1
  [Settings] field initializer 2
  [Settings] Shared Sub New
  [Settings] Touch
Total = 5
using Registry
  [Registry] Sub New
1
attempt 1: TypeInitializationException / FormatException
attempt 2: TypeInitializationException / FormatException
  • The field initializers ran first, in textual order, then the body of Shared Sub New. The type is initialized when Settings.Touch() is first called, not when Main starts.

  • Registry was initialized when Registry.Items was first used, not at program start.

  • A TypeInitializationException wraps whatever the initializer threw (here a FormatException). It is thrown on every later use of the type as well: the runtime does not retry. The second attempt in the output shows the same exception from the cached failure. A failing Shared initializer therefore makes the type unusable for the life of the process, so keep initializers free of I/O and parsing of external data.

The rules for the constructor itself are enforced by the compiler:

Option Strict On

Public Class A
    Shared Sub New(x As Integer)
    End Sub
End Class

Public Class B
    Public Shared Sub New()
    End Sub
End Class

Public Class C
    Shared Sub New()
    End Sub

    Shared Sub New()
    End Sub
End Class

Public Class D
    Private Shared Total As Integer = Count()
    Private Shared Offset As Integer = 5
    Private Shared Function Count() As Integer
        Return Offset + 1
    End Function
    Public Shared Sub Show()
        Console.WriteLine(Total)
    End Sub
End Class

Public Module Program
    Sub Main()
        D.Show()
    End Sub
End Module
Program.vb(9,5): error BC30480: Shared 'Sub New' cannot be declared 'Public'.
Program.vb(4,19): error BC30479: Shared 'Sub New' cannot have any parameters.
Program.vb(14,16): error BC30269: 'Private Shared Sub New()' has multiple definitions with identical signatures.
  • A shared constructor has no parameters (BC30479) and no access modifier (BC30480; it is implicitly Private), and there can be only one (BC30269).

  • The runtime calls it; the program does not.

Textual order, and a pitfall

Field initializers run in the order they appear in the source. A shared field that reads another shared field is a bug if the other one is declared later:

Option Strict On

Public Class Limits
    ' Initializers run in textual order: Total is computed while Offset is still 0.
    Public Shared ReadOnly Total As Integer = Compute()
    Public Shared ReadOnly Offset As Integer = 5

    Private Shared Function Compute() As Integer
        Return Offset + 1
    End Function
End Class

Module Program
    Sub Main()
        Console.WriteLine("Total=" & Limits.Total & " Offset=" & Limits.Offset)
    End Sub
End Module
Total=1 Offset=5

Total was computed while Offset still had its default 0. The compiler does not warn. A class split into several files with Partial initializes the parts in an order that depends on how the files are passed to the compiler, so never rely on it: put dependent values in one Shared Sub New, or make them computed Shared ReadOnly Property values.

When the initializer runs

Whether the runtime may initialize a type early depends on a flag called beforefieldinit, visible in the Util, WithInitializer, WithConstructor and Plain lines of the reflection output above:

  • A class whose only shared initialization is field initializers (WithInitializer) is marked BeforeFieldInit: the runtime may run the initializer at any time before the first access to a field, including earlier than the first call of a method.

  • A class with an explicit Shared Sub New (WithConstructor) is not marked: the initializer runs precisely when the type is first used.

  • Modules and classes with nothing to initialize have no type initializer.

The practical rule: if the exact moment matters (a log line, a registration, an order dependency between two types), use Shared Sub New. If it does not, field initializers are simpler. The observable output above came from Shared Sub New and from Sub New in a module, which are the precise forms.

Static local variables

Static declares a local variable that keeps its value between calls. It has been in the language since the first version of Visual Basic. It is shared state in disguise, and the sharing depends on where the procedure lives:

Option Strict On

Public Class Ticket
    Public Function NextInstanceNumber() As Integer
        Static count As Integer = 0          ' one copy per object (instance method)
        count += 1
        Return count
    End Function

    Public Shared Function NextSharedNumber() As Integer
        Static count As Integer = 0          ' one copy for the class (Shared method)
        count += 1
        Return count
    End Function
End Class

Module Counters
    Function NextModuleNumber() As Integer
        Static count As Integer = 100        ' one copy, initialised once
        count += 1
        Return count
    End Function
End Module

Module Program
    Sub Main()
        Dim a As New Ticket()
        Dim b As New Ticket()
        Console.WriteLine(a.NextInstanceNumber() & " " & a.NextInstanceNumber() & " " & b.NextInstanceNumber())
        Console.WriteLine(Ticket.NextSharedNumber() & " " & Ticket.NextSharedNumber())
        Console.WriteLine(NextModuleNumber() & " " & NextModuleNumber())

        ' What the compiler generates: private fields in the type.
        Dim generated = GetType(Ticket).GetFields(Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Static).Where(Function(f) f.Name.StartsWith("$STATIC$", StringComparison.Ordinal)).ToList()
        Console.WriteLine("generated fields: " & generated.Count & ", Shared ones: " & generated.Where(Function(f) f.IsStatic).Count())
    End Sub
End Module
1 2 1
1 2
101 102
generated fields: 4, Shared ones: 2
  • In an instance method the variable is per object (a and b count separately); in a Shared method or in a module it is per type.

  • The compiler turns the variable into a field plus a second $Init field that guards the initializer, which is why the initializer = 0 runs only once. In a multithreaded program that guard is the only protection the variable has: Static locals are not synchronized beyond the initialization, so count += 1 above has the data race described in the next section.

  • C# has no equivalent. Move the variable to a private field when porting.

Source: Static

Thread safety of shared state

Every shared field is visible to every thread of the process, and every instance of the type shares it. Visual Basic adds nothing: no guard, no volatile semantics, no atomicity. Three tools cover almost every case.

Option Strict On

Imports System.Threading
Imports System.Threading.Tasks

Public Class LoanCounter
    Private Shared ReadOnly Gate As New Object()    ' private lock object, never Me or GetType(...)
    Private Shared Plain As Integer
    Private Shared Locked As Integer
    Private Shared Atomic As Integer

    Public Shared Sub AddPlain()
        Plain += 1                                  ' read, add, write: three steps, not atomic
    End Sub

    Public Shared Sub AddLocked()
        SyncLock Gate
            Locked += 1
        End SyncLock
    End Sub

    Public Shared Sub AddAtomic()
        Interlocked.Increment(Atomic)
    End Sub

    Public Shared ReadOnly Property Totals As String
        Get
            Return "plain<=expected: " & (Plain <= 400000) & ", locked=" & Locked & ", atomic=" & Atomic
        End Get
    End Property
End Class

Public NotInheritable Class Catalog
    Private Shared ReadOnly Holder As New Lazy(Of Catalog)(Function() New Catalog())   ' thread-safe by default
    Private Shared Created As Integer

    Private Sub New()
        Interlocked.Increment(Created)
    End Sub

    Public Shared ReadOnly Property Instance As Catalog
        Get
            Return Holder.Value
        End Get
    End Property

    Public Shared ReadOnly Property InstancesCreated As Integer
        Get
            Return Created
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Parallel.For(0, 400000, Sub(i As Integer)
                                    LoanCounter.AddPlain()
                                    LoanCounter.AddLocked()
                                    LoanCounter.AddAtomic()
                                End Sub)
        Console.WriteLine(LoanCounter.Totals)

        Parallel.For(0, 100, Sub(i As Integer)
                                 Dim c = Catalog.Instance
                             End Sub)
        Console.WriteLine("catalogs created: " & Catalog.InstancesCreated)
    End Sub
End Module
plain<=expected: True, locked=400000, atomic=400000
catalogs created: 1
  • Plain += 1 is read, add and write, so two threads can read the same value and one increment is lost. The unsafe total is nondeterministic, which is why the program prints only that it is not more than expected; run it with enough threads and it is usually smaller.

  • SyncLock over a private Shared ReadOnly Gate As New Object() makes the compound operation atomic. Do not lock on Me, on GetType(...) or on a string: any other code can lock on the same object and cause a deadlock (see Threads, Parallelism and Synchronization).

  • Interlocked.Increment is the cheapest option for one number.

  • A singleton is a shared field initialized once. Lazy(Of T) is thread-safe by default (LazyThreadSafetyMode.ExecutionAndPublication), so a hundred threads ask for Catalog.Instance and one Catalog is created. Do not hand-write double-checked locking with If instance Is Nothing unless you also understand memory barriers.

  • A shared type initializer is itself run once, and thread-safely, by the runtime, so Shared ReadOnly Instance As New Catalog() is already a correct, simple singleton when the construction is cheap and must not be lazy.

  • Shared mutable collections (List(Of T), Dictionary(Of K, V)) are not thread-safe for concurrent writes; use a lock around every access or System.Collections.Concurrent (see Collections).

When to choose which

Module Class with Shared members C# static class

Can contain instance members

No

Yes

No

Needs the Shared keyword on members

No (implicit; an error if written)

Yes

No (the class is static)

Can be instantiated

No

Yes, unless the constructor is Private

No

Can be inherited

No

Yes, unless NotInheritable

No

Can implement interfaces

No

Yes

No

Members usable unqualified after Imports

Yes

Only when the class itself is imported

Only with using static

Types nested in it are reachable through the namespace

Yes (type promotion)

No

No

Holds extension methods

Yes (the only place)

No

Yes

Compiled form

Sealed class with StandardModuleAttribute

Ordinary class

Abstract sealed class

Has a shared constructor

Sub New (implicitly shared)

Shared Sub New

static constructor

Guidelines used in the shelf solution:

  • Use a Module for a bag of pure helpers that the whole project uses (IsbnTools, formatting, extension methods), for Sub Main, and for the home of extension methods.

  • Use a NotInheritable Class with Shared members when the type will have instance members later, when it must implement an interface or be a generic type, or when you want callers to have to write the class name (clearer in a big code base).

  • Do not use a module as a place to park mutable global state. A module’s public field is a global variable, with every property that implies: hidden coupling, order-of-initialization bugs, no test isolation. Pass dependencies in (see Design Patterns in Visual Basic).

  • A library meant to be used from other languages should prefer a class with Shared members or a Public Module with unambiguous names; both appear to C# as a static class.

Older material you may meet

  • Visual Basic 6 and VBA. Code lived in .bas files called standard modules, and Public variables there were global to the whole project; Global was a synonym for Public. In Visual Basic .NET the Module keeps the "no instances, call without qualification" behaviour, but everything about it is a real type with a namespace: the Global keyword now means "the root namespace" (Global.System.Console), and there is no Global Const. Class modules (.cls) became classes; there is no Class_Initialize: use Sub New, or Shared Sub New for type-wide setup.

  • Tutorials for Visual Basic .NET 2002-2010 often show a module full of public fields as the way to share data between forms. It works, but see the guideline above; a class with Shared members or a dependency passed in is easier to test, and My.Settings or a configuration object is the usual home for settings.

  • Books and courses that open with Module Module1 use a name generated by Visual Studio 2008-era templates. The current dotnet new console -lang vb template names it Program and gives Sub Main(args As String()); name the module for its job.

  • The Shared and Static confusion. The C# keyword static maps to Shared for members and to nothing (a Module) for classes. Static in Visual Basic means a local that survives calls.

  • Multithreaded examples from the Framework era use Monitor.Enter or SyncLock Me; the guidance above (private lock object, Interlocked, Lazy(Of T)) is current.

Pitfalls

Pitfall What happens Fix

Dim x As New SomeModule()

BC30371

Call SomeModule.Member directly; there is no object

Two modules, same member name, both imported

BC30562 on the unqualified call

Qualify, rename or drop one Imports

Calling a shared member through a variable (loan.Reset())

BC42025 warning, the variable is not evaluated

Call it on the type

Shared field initializers that depend on each other

Silently read 0 or Nothing

Use Shared Sub New or computed properties

Throwing in a type initializer

TypeInitializationException on every later use

Keep initializers free of I/O and parsing

A shared counter updated by several threads

Lost updates

Interlocked or SyncLock on a private Shared ReadOnly object

SyncLock Me or SyncLock GetType(X) in shared state

Any code can contend for the same lock

Lock on a private object

Cache(Of T).Count assumed to be shared by all T

One copy per closed type

Use a non-generic holder

Public Const changed in a library

Callers keep the old value until recompiled

Shared ReadOnly for values that may change

Extension method in a class

BC36551

Move it to a Module

Two Sub Main in the project

BC30738

Delete one or set StartupObject

Imports of the wrong root namespace

BC40056

Check RootNamespace; Namespace X is relative to it