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
Source: Module Statement
Points to read from the listing:
-
Normalize,HasPrefix,CountandLastcarry noSharedkeyword and are shared anyway. A module’s members are implicitlyShared, and writing the keyword is an error (see below). -
Imports Shelf.Helpersmade the module’s members visible without naming the module inMain;Helpers.IsbnToolsshows the qualified form. Both reach the same method. -
Dim LastText As Stringhas no access modifier and isPrivate; aConstis 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 |
|---|---|---|
|
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. |
|
BC30230 |
A module has no base class other than |
|
BC30231, BC31083 |
A module cannot implement an interface, because interface members are instance members. |
|
BC30433 |
The modifier is redundant and rejected. |
|
BC30433 |
Inheritance modifiers make no sense without derived types. |
|
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()andDim other As IsbnToolsboth give BC30371. -
BC30369 is the rule for shared methods in general:
Countis an instance field, and aSharedmethod has noMeto read it from. See Shared members of classes and structures. -
BC42025 is a warning (an error under
TreatWarningsAsErrors):loan.Reset()compiles, but the compiler discardsloanand callsLoans.Reset(). WriteLoans.Reset().
Source: Module Statement
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=TrueandStandardModule=True: a module is a sealed class marked withMicrosoft.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 classis bothabstractandsealedin 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).
Source: StandardModuleAttribute Class
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.StyleandHelpers.Labels.Styleare two names for the same enumeration, andGetType(...).FullNamereports the real locationShelf.Helpers.Labels+Options, a type nested in a class named after the module. -
Because of the root namespace
Shelf,Namespace Helpersin this file really declaresShelf.Helpers. With<RootNamespace>Shelf</RootNamespace>in the project file,Imports Shelf.Helpers.Labelsat 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 sayImports TheRoot.Helpers.Labels; anImportsof a name that does not exist is BC40056, a warning that this project’sTreatWarningsAsErrorsturns into an error. -
Imports Shelf.Helpers.Labelsimports a module, soDescribeis visible unqualified, but the wordLabelsis not:Labels.Describe(...)is BC30451 unlessShelf.Helpersis also imported (or the call is writtenHelpers.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).
Source: -main (Visual Basic); Main Procedure
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
-
NextIdis a private shared field. The constructor increments it, so eachBookgets the next number, andCreatedexposes the count through aSharedread-only property. -
Constis implicitlyShared.Shared ReadOnlyis the usual choice when the value is not a compile-time constant (aDate, an object, a computed value); its value is fixed after the type initializer has run. PublicConstvalues are copied into the callers when they are compiled, so changing one needs a rebuild of every client (aShared ReadOnlyfield is read at run time). -
Magazine.CreatedandBook.Createdare 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 onMagazine. -
Cache(Of T).Counthas a separate copy for every closed generic type.Cache(Of Integer).CountandCache(Of String).Countare unrelated, andCache(Of Date).Counthas 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
Createdcompiles 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.
-
SharedandOverridable(andOverrides,MustOverride,NotOverridable) are contradictory, since virtual dispatch needs an instance to select the override. A shared member can still beShadows-ed in a derived class (see Inheritance and Polymorphism). -
Visual Basic cannot declare
Sharedinterface members (BC30270). C# 11 can declarestatic abstractmembers in an interface, but Visual Basic has no syntax to write one (see Interfaces). -
Extension methods are written
<Extension()>on aSharedmethod 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 whenSettings.Touch()is first called, not whenMainstarts. -
Registrywas initialized whenRegistry.Itemswas first used, not at program start. -
A
TypeInitializationExceptionwraps whatever the initializer threw (here aFormatException). 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 failingSharedinitializer 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 markedBeforeFieldInit: 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 (
aandbcount separately); in aSharedmethod or in a module it is per type. -
The compiler turns the variable into a field plus a second
$Initfield that guards the initializer, which is why the initializer= 0runs only once. In a multithreaded program that guard is the only protection the variable has:Staticlocals are not synchronized beyond the initialization, socount += 1above 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 += 1is 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. -
SyncLockover a privateShared ReadOnly Gate As New Object()makes the compound operation atomic. Do not lock onMe, onGetType(...)or on a string: any other code can lock on the same object and cause a deadlock (see Threads, Parallelism and Synchronization). -
Interlocked.Incrementis 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 forCatalog.Instanceand oneCatalogis created. Do not hand-write double-checked locking withIf instance Is Nothingunless 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 orSystem.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 |
No (implicit; an error if written) |
Yes |
No (the class is |
Can be instantiated |
No |
Yes, unless the constructor is |
No |
Can be inherited |
No |
Yes, unless |
No |
Can implement interfaces |
No |
Yes |
No |
Members usable unqualified after |
Yes |
Only when the class itself is imported |
Only with |
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 |
Ordinary class |
Abstract sealed class |
Has a shared constructor |
|
|
|
Guidelines used in the shelf solution:
-
Use a
Modulefor a bag of pure helpers that the whole project uses (IsbnTools, formatting, extension methods), forSub Main, and for the home of extension methods. -
Use a
NotInheritable ClasswithSharedmembers 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
Sharedmembers or aPublic Modulewith unambiguous names; both appear to C# as a static class.
Older material you may meet
-
Visual Basic 6 and VBA. Code lived in
.basfiles called standard modules, andPublicvariables there were global to the whole project;Globalwas a synonym forPublic. In Visual Basic .NET theModulekeeps the "no instances, call without qualification" behaviour, but everything about it is a real type with a namespace: theGlobalkeyword now means "the root namespace" (Global.System.Console), and there is noGlobal Const. Class modules (.cls) became classes; there is noClass_Initialize: useSub New, orShared Sub Newfor 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
Sharedmembers or a dependency passed in is easier to test, andMy.Settingsor a configuration object is the usual home for settings. -
Books and courses that open with
Module Module1use a name generated by Visual Studio 2008-era templates. The currentdotnet new console -lang vbtemplate names itProgramand givesSub Main(args As String()); name the module for its job. -
The
SharedandStaticconfusion. The C# keywordstaticmaps toSharedfor members and to nothing (aModule) for classes.Staticin Visual Basic means a local that survives calls. -
Multithreaded examples from the Framework era use
Monitor.EnterorSyncLock Me; the guidance above (private lock object,Interlocked,Lazy(Of T)) is current.
Pitfalls
| Pitfall | What happens | Fix |
|---|---|---|
|
BC30371 |
Call |
Two modules, same member name, both imported |
BC30562 on the unqualified call |
Qualify, rename or drop one |
Calling a shared member through a variable ( |
BC42025 warning, the variable is not evaluated |
Call it on the type |
Shared field initializers that depend on each other |
Silently read |
Use |
Throwing in a type initializer |
|
Keep initializers free of I/O and parsing |
A shared counter updated by several threads |
Lost updates |
|
|
Any code can contend for the same lock |
Lock on a private object |
|
One copy per closed type |
Use a non-generic holder |
|
Callers keep the old value until recompiled |
|
Extension method in a class |
BC36551 |
Move it to a |
Two |
BC30738 |
Delete one or set |
|
BC40056 |
Check |
References
-
Modules (Language Reference overview) and Main Procedure
-
SyncLock Statement, Interlocked, Lazy(Of T) and TypeInitializationException
-
Static Classes and Static Class Members (C#) and Static Class (Framework Design Guidelines)