Variables, Constants and Scope

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 and was run; the output shown is real output. The examples that are meant to fail say so, and show the compiler message that the SDK printed. The .NET SDK adds a set of project-level imports to every Visual Basic project (Microsoft.VisualBasic, System, System.Collections, System.Collections.Generic, System.Diagnostics, System.Linq, System.Xml.Linq and System.Threading.Tasks), so the examples use Console, List(Of T) or LINQ without an Imports line unless they need another namespace.

A variable is a named storage location whose type, scope, lifetime and accessibility are all fixed by the statement that declares it. In Visual Basic those four properties are controlled by different parts of the declaration, and the differences from C# are larger than they look: a Dim inside a loop does not start fresh on every pass, a local is in scope for the whole of its block (not only from the line that declares it), names are not case sensitive, and the default access level depends on the kind of declaration and on the container. The sections below take them in that order. Types and literals are on Data Types and Literals; the Option statements that change what a declaration may omit are on Option Statements; fields, properties and the members of classes are on Classes and Objects.

Statement or modifier What it declares Typical use

Dim

a local variable, or a field in a class, structure or module

the general-purpose declaration

Static

a local variable whose value survives between calls

counters, one-time initialization

Const

a compile-time constant

named literals such as limits and messages

ReadOnly

a field assigned only by a declaration initializer or a constructor

values fixed per object or per type

Shared

a field that belongs to the type, not to an instance

type-wide state

Private, Friend, Protected, Public …​

the access level of a member

controlling who may see it

Declaring variables with Dim

Dim declares one or more variables. The As clause gives the type, = gives the initial value, and one Dim may declare several variables separated by commas. A name list shares the type that follows it: in Dim shelfCount, bookCount As Integer both variables are Integer. VB6 programmers remember the opposite: there only bookCount got the type and shelfCount was a Variant. Variant does not exist in Visual Basic .NET, and the declaration means what it says. A shared type specifier cannot be combined with an initializer, because the compiler would not know which variables the value belongs to. Give each variable its own As clause when each has its own value.

Option Strict On

Module Program
    Const MaxLoanDays As Integer = 21
    Const Greeting As String = "Welcome to the shelf"
    Const LongLoanDays As Integer = MaxLoanDays * 2

    Sub Main()
        ' One Dim, several declarators: every name gets the type that follows As.
        Dim shelfCount, bookCount As Integer
        Dim title As String = Nothing, copies As Integer = 3

        ' Type inference (Option Infer On): the type comes from the initializer.
        Dim inferredInt = 42
        Dim inferredDouble = 4.5
        Dim inferredString = "Dune"
        Dim inferredDate = #1/15/2026#
        Dim nothingOnly = Nothing

        ' Default values: numbers 0, Boolean False, String/Object Nothing.
        Console.WriteLine("shelfCount = " & shelfCount & ", bookCount = " & bookCount)
        Console.WriteLine("title is Nothing: " & (title Is Nothing))
        Console.WriteLine("copies = " & copies)

        Console.WriteLine(shelfCount.GetType().Name & " " & bookCount.GetType().Name)
        Console.WriteLine(inferredInt.GetType().Name & " " & inferredDouble.GetType().Name & " " &
                          inferredString.GetType().Name & " " & inferredDate.GetType().Name)
        nothingOnly = "an Object variable accepts anything"      ' proves the inferred type is Object
        Console.WriteLine(nothingOnly)

        Console.WriteLine(Greeting & ": " & MaxLoanDays & " days, long loans " & LongLoanDays)
    End Sub
End Module
shelfCount = 0, bookCount = 0
title is Nothing: True
copies = 3
Int32 Int32
Int32 Double String DateTime
an Object variable accepts anything
Welcome to the shelf: 21 days, long loans 42

Source: Dim Statement

Every variable has a value from the moment its procedure starts: numeric types are zero, Boolean is False, Date is #1/1/0001 12:00:00 AM#, and reference types (including String and Object) are Nothing. The default value of a type is also what the keyword Nothing produces when it is assigned to a value type; the rules are on Nothing and Nullable Value Types. Reading a reference-typed local that you never assigned still compiles, but the compiler warns with BC42104 (Variable 'title' is used before it has been assigned a value) when a path through the code might dereference it. The example above assigns Nothing explicitly for that reason, and the project treats warnings as errors.

The declaration forms, from the most explicit to the most compact:

Declaration Meaning

Dim count As Integer

explicit type, default value 0

Dim count As Integer = 0

explicit type with an initializer

Dim names As New List(Of String)

As New creates the object in the declaration; the type is List(Of String)

Dim names As New List(Of String) From {"Dune", "Emma"}

collection initializer

Dim count = 0

type inferred from the initializer (see below)

Dim a, b As Integer

two Integer variables, both default value

Dim a As Integer, b As String

two declarators with different types

Dim matrix(2, 3) As Integer

array with bounds; the numbers are upper bounds (see Arrays)

Dim text$ = "x", Dim n% = 5

legacy type characters ($ String, % Integer, & Long, ! Single, # Double, @ Decimal); accepted, not recommended

Names are not case sensitive. total and Total are the same identifier, so declaring both in one scope is an error:

Option Strict On

Module Program
    Sub Main()
        Dim total As Integer = 1
        Dim Total As Integer = 2
        Console.WriteLine(total)
    End Sub
End Module
Program.vb(6,13): error BC30288: Local variable 'Total' is already declared in the current block.

The editor rewrites every reference to the declared spelling, which hides most mistakes, but the case insensitivity matters when you read code that is shared with C# (where total and Total are different variables) and when you compare names produced by reflection.

Local type inference

With Option Infer On (the SDK default) a local variable declared with an initializer and without an As clause takes the type of the initializer. It was introduced in Visual Basic 9 together with LINQ, because query results and anonymous types have no name you could write. The inferred type is fixed at compile time: it is not a Variant and not late bound. The literals follow the usual rules: an integer literal that fits is Integer, a bigger one is Long, a literal with a decimal point is Double, D makes it Decimal, "A"c is Char.

Option Strict On

Module Program
    Sub Main()
        Dim list = {1, 2, 3}                                  ' Integer()
        Dim item = New With {.Title = "Dune", .Pages = 412}   ' anonymous type
        Dim ratio = 7 / 2                                     ' Double
        Dim big = 3000000000                                  ' Long: does not fit Integer
        Dim letter = "A"c                                     ' Char
        Dim price = 9.99D                                     ' Decimal

        Console.WriteLine(TypeName(list) & ", " & TypeName(ratio) & ", " & TypeName(big) & ", " &
                          TypeName(letter) & ", " & TypeName(price))
        Console.WriteLine(item.Title & " " & item.Pages)
        For Each entry In list
            Console.Write(TypeName(entry) & " ")
        Next
        Console.WriteLine()
    End Sub
End Module
Integer(), Double, Long, Char, Decimal
Dune 412
Integer Integer Integer

Three rules complete the picture.

  • Inference applies to local variables only. A field written as Private field = 5 is not inferred: under Option Strict On the compiler reports BC30209 (Option Strict On requires all variable declarations to have an 'As' clause), and under Option Strict Off the field would be an Object. The same applies to parameters and function return types, which always need an As clause under Option Strict On.

  • Dim x = Nothing has no type to infer, so x is an Object. The example above assigns a string to such a variable to show it; avoid the pattern, because everything that follows is late bound.

  • Option Infer Off switches inference off. Combined with Option Strict On every Dim without As is then an error:

Option Strict On
Option Infer Off

Module Program
    Sub Main()
        Dim x = 5
        Console.WriteLine(x)
    End Sub
End Module
Program.vb(6,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.

An inferred variable cannot change its type later. Dim number = 5 followed by number = "five" is rejected with BC30512 (Option Strict On disallows implicit conversions from 'String' to 'Integer'), exactly as it would be for an explicit As Integer. Prefer an explicit As clause when the initializer is not obvious from the line (a constant from another file, or a method call), and inference when the type is named on the right (New, a constructor call, a literal) or cannot be named (anonymous types, query results). The Microsoft coding conventions say the same.

Constants and read-only fields

Const declares a value that the compiler substitutes at every use. A constant must have an initializer made only from literals, other constants and operators, and its type must be an intrinsic type (Boolean, Byte, Char, Date, Decimal, Double, Integer, Long, SByte, Short, Single, String, UInteger, ULong, UShort) or an enumeration. Arrays, classes and structures cannot be constants (BC30424). Const members of a class or module are implicitly Shared; Const at procedure level is local to it. Because the value is copied into the compiled code of every assembly that uses it, changing a public constant in a library does not change the callers until they are recompiled. That is the main reason to prefer ReadOnly for values that may change between releases.

ReadOnly declares a field that can be assigned only in its declaration or in a constructor of the same type (Shared Sub New for a Shared ReadOnly field). It can hold any type and any value that can be computed at run time, for example a TimeSpan built from a constant.

Option Strict On

Public Class LoanPolicy
    Public Const DefaultDays As Integer = 21
    Public Shared ReadOnly DefaultPeriod As TimeSpan = TimeSpan.FromDays(DefaultDays)
    Public ReadOnly Created As Date
    Private ReadOnly _limit As Integer

    Public Sub New(limit As Integer)
        _limit = limit                     ' ReadOnly: assignable here ...
        Created = New Date(2026, 10, 19)
    End Sub

    Public ReadOnly Property Limit As Integer
        Get
            Return _limit
        End Get
    End Property

    Public Sub Change()
        ' _limit = 5                       ' ... but not here: BC30064
    End Sub
End Class

Module Program
    Sub Main()
        Dim policy As New LoanPolicy(5)
        Console.WriteLine("DefaultDays   = " & LoanPolicy.DefaultDays)
        Console.WriteLine("DefaultPeriod = " & LoanPolicy.DefaultPeriod.ToString())
        Console.WriteLine("Limit         = " & policy.Limit)
        Console.WriteLine("Created       = " & policy.Created.ToString("yyyy-MM-dd"))
    End Sub
End Module
DefaultDays   = 21
DefaultPeriod = 21.00:00:00
Limit         = 5
Created       = 2026-10-19

Source: ReadOnly

Property Const ReadOnly field Shared ReadOnly field

Value known at

compile time

run time, once per object

run time, once per type

Allowed types

intrinsic types and enumerations

any

any

Assigned in

the declaration

declaration or any constructor

declaration or Shared Sub New

Stored

not stored; the value is inlined into callers

one slot per instance

one slot per type

Safe to change in a library

no (callers keep the old value until rebuilt)

yes

yes

Usable as an attribute argument or an optional-parameter default

yes

no (BC30059)

no (BC30059)

A ReadOnly field of a reference type is read-only only as a reference: ReadOnly _items As List(Of Book) forbids assigning a different list, but the list itself can still be changed. A ReadOnly field of a mutable Structure type is the usual source of surprises, because a method call on the field operates on a copy; see Structures and Value Types. Assigning a ReadOnly field from a method other than a constructor is error BC30064:

Option Strict On

Public Class Policy
    Public ReadOnly Limit As Integer = 5

    Public Sub Change()
        Limit = 6
    End Sub
End Class

Module Program
    Sub Main()
        Console.WriteLine(New Policy().Limit)
    End Sub
End Module
Program.vb(7,9): error BC30064: 'ReadOnly' variable cannot be the target of an assignment.

Using a ReadOnly field where the language demands a constant, for example as the default of an Optional parameter, is error BC30059 (Constant expression is required); a Const is accepted there.

Scope

The scope of a declared name is the code that can refer to it without qualification. Visual Basic has four levels.

Level Declared Available

Block

inside If, For, For Each, Do, While, Select Case, Using, SyncLock, Try or With

only in that block

Procedure

in a Sub, Function, property accessor or operator, outside any block

the rest of the procedure (and, as explained below, its whole block)

Module

as a member of a Module, Class or Structure

anywhere in that type (subject to its access level)

Namespace

a type, or a Public/Friend member of a Module (type promotion)

anywhere in the namespace and, if accessible, outside it

Option Strict On

Public Class Counter
    Private count As Integer = 100          ' field: lives as long as the object

    Public Sub Show()
        Dim count As Integer = 1            ' local shadows the field
        Console.WriteLine("local count      = " & count)
        Console.WriteLine("Me.count (field) = " & Me.count)
        If count > 0 Then
            Dim inner As Integer = count + 1
            Console.WriteLine("block-level inner = " & inner)
        End If
        ' Console.WriteLine(inner)         ' BC30451: 'inner' is not declared (out of scope)
    End Sub
End Class

Module Program
    Private moduleLevel As Integer = 7      ' module scope

    Sub Main()
        Dim c As New Counter()
        c.Show()
        Console.WriteLine("moduleLevel = " & moduleLevel)

        ' A For control variable is scoped to the loop.
        For i As Integer = 1 To 2
            Console.WriteLine("i = " & i)
        Next
        For i As Integer = 5 To 6           ' a second loop may reuse the name
            Console.WriteLine("second loop i = " & i)
        Next
    End Sub
End Module
local count      = 1
Me.count (field) = 100
block-level inner = 2
moduleLevel = 7
i = 1
i = 2
second loop i = 5
second loop i = 6

Three details deserve attention.

Shadowing across levels is allowed, shadowing inside a procedure is not. The local count in the example hides the field of the same name; Me.count reaches the field. But a block cannot redeclare a name that the enclosing block or procedure already uses. This is the rule C# calls CS0136 and Visual Basic calls BC30616:

Option Strict On

Module Program
    Sub Main()
        Dim n As Integer = 1
        If n > 0 Then
            Dim n As Integer = 2
            Console.WriteLine(n)
        End If
    End Sub
End Module
Program.vb(7,17): error BC30616: Variable 'n' hides a variable in an enclosing block.

A local variable is in scope for its whole block, not just from the line that declares it. The compiler resolves names by block, so declaring the same name after a nested block that uses it conflicts with that block, and using a variable before the line that declares it is an error of its own (BC32000), instead of silently referring to some outer variable:

Option Strict On

Module Program
    Sub Main()
        For i As Integer = 1 To 2
            Console.WriteLine("i = " & i)
        Next
        Dim i As Integer = 99
        Console.WriteLine(i)
    End Sub
End Module
Program.vb(5,13): error BC30616: Variable 'i' hides a variable in an enclosing block.
Option Strict On

Module Program
    Sub Main()
        Console.WriteLine(later)
        Dim later As Integer = 5
        Dim number = 5
        number = "five"
    End Sub
End Module
Program.vb(5,27): error BC32000: Local variable 'later' cannot be referred to before it is declared.
Program.vb(8,18): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.

The second program has two errors. The first one is the use-before-declaration; the second shows that Dim number = 5 fixes the type of number.

Types nested in a module are promoted to the namespace. An Enum, a class or a structure declared inside a Module can be referred to through the enclosing namespace without naming the module (the Language Specification and Learn call this type promotion). The promotion is defeated when the namespace already contains a type of the same name, and for Partial types. The procedures and fields of a module are different: they are reachable unqualified because every module’s members are in scope wherever the module is, not because of promotion. See Modules and Shared Members.

Lifetime and the uninitialized loop-local pitfall

Scope and lifetime are different. The lifetime of a local variable starts when control enters the procedure and ends when the procedure returns, regardless of how small its scope is. The compiler sets every local to its default value when the procedure starts, and a Dim with an initializer is simply an assignment executed when control reaches the line. The consequence is the most famous trap in the language:

A variable declared inside a loop body is not recreated on each pass. Without an initializer it keeps the value from the previous pass. In C# the same declaration would not compile until the variable is assigned (definite assignment), so the trap does not exist there.

Option Strict On

Module Program
    Sub Main()
        Dim readings As Integer() = {3, 4, 5}

        Console.WriteLine("Without an initializer the total survives every iteration:")
        For Each reading In readings
            Dim total As Integer          ' not reset on each pass
            total += reading
            Console.WriteLine("  total = " & total)
        Next

        Console.WriteLine("With an initializer the variable is reset on each pass:")
        For Each reading In readings
            Dim total As Integer = 0
            total += reading
            Console.WriteLine("  total = " & total)
        Next

        Console.WriteLine("The same trap with a While loop:")
        Dim i As Integer = 0
        While i < 3
            Dim copies As Integer
            copies += 1
            Console.WriteLine("  copies = " & copies)
            i += 1
        End While
    End Sub
End Module
Without an initializer the total survives every iteration:
  total = 3
  total = 7
  total = 12
With an initializer the variable is reset on each pass:
  total = 3
  total = 4
  total = 5
The same trap with a While loop:
  copies = 1
  copies = 2
  copies = 3

The first loop accumulates 3, 7 and 12 instead of 3, 4 and 5. The programs that fall into this trap are usually averages, running totals and "reset the counters for each group" code that was first written for one iteration and later wrapped in a loop. The remedies are, in order of preference:

  1. Give the declaration an initializer (Dim total As Integer = 0). This costs nothing and states the intent. The assignment runs on every pass, which is what resets the variable.

  2. Declare the variable outside the loop when the accumulation is wanted, and write that down in the name (runningTotal).

  3. Move the loop body into a procedure. Every call has fresh locals.

The SDK reports nothing for the value-type case above: the program compiles without a single warning, which is why the bug survives code review.

Closures capture variables, not values

The same rule decides what a lambda sees. A lambda captures the variable, so the number of variables decides what each lambda observes. Declare the variable inside the loop body and copy the value into it, as in the first program. The loop control variable of For ... Next is a single variable for the whole loop, so every lambda sees its final value:

Option Strict On

Module Program
    Sub Main()
        Dim actions As New List(Of Action)()
        For i As Integer = 1 To 3
            Dim snapshot As Integer = i * 10
            actions.Add(Sub() Console.WriteLine("lambda sees snapshot = " & snapshot))
        Next
        For Each action In actions
            action()
        Next
    End Sub
End Module
lambda sees snapshot = 10
lambda sees snapshot = 20
lambda sees snapshot = 30
Option Strict On

Module Program
    Sub Main()
        Dim late As New List(Of Action)()
        For j As Integer = 1 To 3
            late.Add(Sub() Console.WriteLine("lambda sees j = " & j))
        Next
        For Each action In late
            action()
        Next
    End Sub
End Module
Program.vb(7,67): warning BC42324: Using the iteration variable in a lambda expression may have unexpected results.  Instead, create a local variable within the loop and assign it the value of the iteration variable.
lambda sees j = 4
lambda sees j = 4
lambda sees j = 4

The second program is built with TreatWarningsAsErrors off so that it can run: the compiler warns with BC42324 (Using the iteration variable in a lambda expression may have unexpected results), the warning is an error in every other project of this page, and the output shows why. The last value, 4, is the one that made the loop condition false. A For Each control variable behaves differently: the compiler gives each iteration its own variable, so the lambdas see the items they were created for.

Option Strict On

Module Program
    Sub Main()
        Dim late As New List(Of Action)()
        For Each name In {"Dune", "Emma", "Ubik"}
            late.Add(Sub() Console.WriteLine("lambda sees name = " & name))
        Next
        For Each action In late
            action()
        Next
    End Sub
End Module
lambda sees name = Dune
lambda sees name = Emma
lambda sees name = Ubik

C# had the same difference for for and foreach until C# 5 changed foreach; it still has it for for.

Static locals

Static makes a local variable keep its value between calls. The compiler stores it in a hidden field, so it is not a local at run time. Its initializer runs once, on the first call.

Option Strict On

Module Program
    Private Function NextTicket() As Integer
        Static issued As Integer = 1000     ' initialized once, survives calls
        issued += 1
        Return issued
    End Function

    Private Sub Visit()
        Static visits As Integer
        Dim local As Integer                ' ordinary local: reset each call
        visits += 1
        local += 1
        Console.WriteLine("visits = " & visits & ", local = " & local)
    End Sub

    Sub Main()
        Console.WriteLine(NextTicket() & " " & NextTicket() & " " & NextTicket())
        Visit()
        Visit()
        Visit()
    End Sub
End Module
1001 1002 1003
visits = 1, local = 1
visits = 2, local = 1
visits = 3, local = 1

Source: Static

Where the hidden field lives depends on the procedure that contains the declaration.

Procedure Static variable begins Static variable ends

in a Module (shared by default)

at the first call

when the application stops

Shared member of a class

at the first call, on an instance or on the type

when the application stops

instance member of a class

at the first call on that instance

when the instance is collected

Each instance of a class therefore has its own copy of a Static local in an instance method, which a field would also give you; use a Static when the variable should be visible to one procedure only. A Static local cannot be declared in a method of a structure: the compiler says so with BC31400 (Local variables within methods of structures cannot be declared 'Static'), shown together with BC30671 from earlier:

Option Strict On

Public Structure Tally
    Public Sub Increment()
        Static calls As Integer
        calls += 1
    End Sub
End Structure

Module Program
    Sub Main()
        Dim a, b As Integer = 5
        Console.WriteLine(a & b)
    End Sub
End Module
Program.vb(5,9): error BC31400: Local variables within methods of structures cannot be declared 'Static'.
Program.vb(12,13): error BC30671: Explicit initialization is not permitted with multiple variables declared with a single type specifier.

A Static local is not thread safe. Two threads calling the same procedure share the variable and need the synchronization described on Threads, Parallelism and Synchronization. Static does not exist in C#, where the equivalent is a private field.

Access levels and declaration contexts

Visual Basic has six access modifiers.

Modifier Meaning C# equivalent

Public

no restriction

public

Friend

visible in the same assembly (and to friend assemblies)

internal

Protected

the declaring type and derived types

protected

Protected Friend

derived types, or any code in the same assembly

protected internal

Private Protected (Visual Basic 15.5)

derived types in the same assembly

private protected

Private

the declaring type only

private

If you omit the modifier, the default depends on what is being declared and where. The rows below were checked by compiling the declarations and reading the result with reflection; this is the whole program:

Option Strict On

Imports System.Reflection

Friend Class Container
    Dim implicitField As Integer
    Const ImplicitConst As Integer = 1
    Sub ImplicitSub()
    End Sub
    Property ImplicitProperty As Integer
    Event ImplicitEvent()
End Class

Friend Structure Pair
    Dim implicitField As Integer
    Sub ImplicitSub()
    End Sub
End Structure

Public Interface IImplicit
    Sub Member()
End Interface

Class UndeclaredLevelClass
End Class

Enum UndeclaredLevelEnum
    A
End Enum

Module Program
    Private Function Access(member As MemberInfo) As String
        Dim f = TryCast(member, FieldInfo)
        If f IsNot Nothing Then Return If(f.IsPrivate, "Private", If(f.IsPublic, "Public", If(f.IsAssembly, "Friend", "other")))
        Dim m = TryCast(member, MethodBase)
        If m IsNot Nothing Then Return If(m.IsPrivate, "Private", If(m.IsPublic, "Public", If(m.IsAssembly, "Friend", "other")))
        Return "n/a"
    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(Container), GetType(Pair)}
            Console.WriteLine(t.Name & " (" & If(t.IsNotPublic, "Friend", "Public") & ")")
            For Each member In t.GetMembers(flags).OrderBy(Function(x) x.Name)
                If member.MemberType = MemberTypes.Field OrElse member.MemberType = MemberTypes.Method Then
                    If member.Name.StartsWith("_") OrElse member.Name.StartsWith("get_") OrElse member.Name.StartsWith("set_") OrElse member.Name = ".ctor" Then Continue For
                    Console.WriteLine("  " & member.MemberType.ToString().PadRight(7) & member.Name.PadRight(22) & Access(member))
                End If
            Next
        Next
        Console.WriteLine("UndeclaredLevelClass is " & If(GetType(UndeclaredLevelClass).IsNotPublic, "Friend", "Public"))
        Console.WriteLine("UndeclaredLevelEnum  is " & If(GetType(UndeclaredLevelEnum).IsNotPublic, "Friend", "Public"))
        Dim p = GetType(Container).GetProperty("ImplicitProperty")
        Console.WriteLine("ImplicitProperty getter: " & If(p.GetMethod.IsPublic, "Public", "not public"))
        Console.WriteLine("ImplicitEvent add: " & If(GetType(Container).GetEvent("ImplicitEvent").AddMethod.IsPublic, "Public", "not public"))
    End Sub
End Module
Container (Friend)
  Method add_ImplicitEvent     Public
  Field  ImplicitConst         Private
  Field  ImplicitEventEvent    Private
  Field  implicitField         Private
  Method ImplicitSub           Public
  Method remove_ImplicitEvent  Public
Pair (Friend)
  Field  implicitField         Public
  Method ImplicitSub           Public
UndeclaredLevelClass is Friend
UndeclaredLevelEnum  is Friend
ImplicitProperty getter: Public
ImplicitEvent add: Public

A second program checks the remaining rows (nested types, constructors, structures, modules, interfaces and delegates):

Option Strict On

Imports System.Reflection

Public Class Outer
    Class NestedClass
    End Class
    Structure NestedStructure
    End Structure
    Enum NestedEnum
        A
    End Enum
    Sub New()
    End Sub
    Shared Sub New()
    End Sub
    Dim OuterField As Integer
End Class

Structure TopStructure
End Structure

Module TopModule
    Dim moduleField As Integer
    Sub ModuleSub()
    End Sub
End Module

Interface ITop
End Interface

Delegate Sub TopDelegate()

Module Program
    Private Function Vis(t As Type) As String
        If t.IsNested Then Return If(t.IsNestedPublic, "Public", If(t.IsNestedAssembly, "Friend", If(t.IsNestedPrivate, "Private", "other")))
        Return If(t.IsPublic, "Public", "Friend")
    End Function

    Sub Main()
        For Each t In {GetType(Outer), GetType(TopStructure), GetType(TopModule), GetType(ITop), GetType(TopDelegate),
                       GetType(Outer.NestedClass), GetType(Outer.NestedStructure), GetType(Outer.NestedEnum)}
            Console.WriteLine(t.Name.PadRight(18) & Vis(t))
        Next
        Const all As BindingFlags = BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
        For Each c In GetType(Outer).GetConstructors(all)
            Console.WriteLine("constructor (" & If(c.IsStatic, "Shared", "instance") & ") " & If(c.IsPublic, "Public", If(c.IsPrivate, "Private", "other")))
        Next
        Console.WriteLine("Outer.OuterField: " & If(GetType(Outer).GetField("OuterField", all).IsPrivate, "Private", "not private"))
        Console.WriteLine("TopModule.moduleField: " & If(GetType(TopModule).GetField("moduleField", all).IsPrivate, "Private", "not private"))
        Console.WriteLine("TopModule.ModuleSub: " & If(GetType(TopModule).GetMethod("ModuleSub", all).IsPublic, "Public", "not public"))
    End Sub
End Module
Outer             Public
TopStructure      Friend
TopModule         Friend
ITop              Friend
TopDelegate       Friend
NestedClass       Public
NestedStructure   Public
NestedEnum        Public
constructor (instance) Public
constructor (Shared) Private
Outer.OuterField: Private
TopModule.moduleField: Private
TopModule.ModuleSub: Public

The compiler also generates a private backing field for the event (ImplicitEventEvent) and the add_ and remove_ accessors. The table summarizes the defaults.

Declaration Default access Note

Dim field in a class or module

Private

unlike C#, Dim is the way to write a private field

Dim field in a structure

Public

one of the few places where the default is wider than in a class

Const in a class

Private

Sub, Function, Property, Event in a class, structure or module

Public

unlike C#, where members default to private

Instance constructor (Sub New)

Public

a Shared Sub New is Private and takes no modifier

Class, Structure, Module, Enum, Interface, Delegate at namespace level

Friend

the same as C# internal; a type meant for other assemblies needs Public

Class, Structure, Enum nested inside a type

Public

unlike C#, where nested types default to private

Members of an Interface

Public

no modifier is allowed

Local variables, parameters

not applicable

Public is not allowed inside a procedure

Because the defaults are not what C# programmers expect, write the access modifier on every member. The analyzer in XML Documentation, Analyzers and Code Style can enforce it.

Names, escaping and Global

A name that is a reserved word can be used as an identifier when it is written in square brackets. The brackets are not part of the name: [Class] and [Select] in the declaration are Class and Select to reflection and to other languages. Global starts a name at the top of the namespace tree, so a type of your own cannot hide System.String.

Option Strict On

Namespace Global.Shelf.Demo
    Public Class [Class]
        Public Property [Select] As String = "reserved word as a name"
    End Class
End Namespace

Module Program
    Sub Main()
        Dim item As New Shelf.Demo.[Class]()
        Console.WriteLine(item.[Select])

        ' Global.System.String reaches the real System namespace even if a local name hides it.
        Dim s As Global.System.String = "hello"
        Console.WriteLine(s.ToUpperInvariant())
    End Sub
End Module
reserved word as a name
HELLO

Naming conventions (PascalCase for types and members, camelCase for locals and parameters, a leading underscore for private fields, no Hungarian prefixes) are on Program Structure and Code Conventions.

Object variables

A variable declared As Object can hold any value, and Is Nothing / IsNot Nothing test whether it refers to an object. Under Option Strict On you cannot call members that Object does not have, because that would be late binding (BC30574):

Option Strict On

Module Program
    Sub Main()
        Dim o As Object = "text"
        Console.WriteLine(o.Length)
    End Sub
End Module
Program.vb(6,27): error BC30574: Option Strict On disallows late binding.

Use a concrete type, an interface or a generic parameter, and TryCast / TypeOf ... Is when you need to test. Late binding is the subject of Early and Late Binding.

Differences from C#

Topic Visual Basic C#

Case sensitivity

no; total and Total are the same name

yes

Definite assignment

not enforced for locals; they have default values (BC42104 warns for some reference cases)

enforced; reading an unassigned local is an error

Loop-body locals

one variable for the whole procedure, not reset per pass

a new variable on each pass

for variable captured by a lambda

one shared variable (warning BC42324)

one shared variable

foreach variable captured by a lambda

a new variable per iteration

a new variable per iteration since C# 5

Static locals

Static keyword

none; use a field

Constants

Const (intrinsic types and enumerations)

const (also null for reference types)

Default member access

Public for procedures, Private for Dim and Const in classes

private for all members

Inference

Dim x = ... (Option Infer)

var x = ...

Several variables in one declaration

Dim a, b As Integer (shared type)

int a, b;

Shadowing a local by a nested local

error BC30616

error CS0136

Older material you may meet

Introductory VB books and VBA/VBScript tutorials describe several things that are not true of Visual Basic .NET, and the mistakes are repeated in forum answers. The right-hand column is what to use today.

Older statement Current Visual Basic

Dim a, b As Integer makes a a Variant

both are Integer; there is no Variant, and an untyped declaration is an Object (late bound) or, with Option Infer On, inferred

variables can be created by using them ("implicit declaration")

Option Explicit On is the default; what looks like implicit declaration in modern code is local type inference, and it always has an initializer

a "global variable" can be declared outside any module

every variable lives in a type; use a Public Shared field or a Module member and think twice about the design

block variables are new for each loop pass

they are not, see the lifetime section above; initialize them

initializers on Dim are a VB.NET invention, a Dim is always uninitialized

initializers on Dim exist since Visual Basic .NET 2002; VB6 and VBA had none

Type ... End Type for records

Structure ... End Structure (or a Class); Type is VB6/VBA only

Static works on whole procedures

Static on a Sub header is VB6; in VB.NET only locals can be Static (use Shared for procedures)

Common errors

Code Message (abridged) Typical cause

BC30209

Option Strict On requires all variable declarations to have an As clause

field or parameter without As, or Option Infer Off

BC30288

Local variable is already declared in the current block

same name twice, possibly in a different case

BC30424

Constants must be of an intrinsic or enumerated type

Const of an array, class or structure

BC30451

Name is not declared

use outside the scope of a block variable

BC30059

Constant expression is required

ReadOnly field used as an optional-parameter default

BC30064

ReadOnly variable cannot be the target of an assignment

assignment outside a constructor

BC30574

Option Strict On disallows late binding

member access on an Object

BC30512

Option Strict On disallows implicit conversions

assigning a different type to an inferred variable

BC30616

Variable hides a variable in an enclosing block

same name in a nested block or in a later declaration

BC30671

Explicit initialization is not permitted with multiple variables declared with a single type specifier

Dim a, b As Integer = 5

BC30980

Type of a variable cannot be inferred from an expression containing the variable

Dim attribute = Attribute.GetCustomAttribute(...): names are case-insensitive, so the variable hides the type

BC31400

Local variables within methods of structures cannot be declared Static

Static in a structure method

BC32000

Local variable cannot be referred to before it is declared

use above the Dim

BC42104

Variable is used before it has been assigned a value

reference-typed local read before assignment

BC42324

Using the iteration variable in a lambda expression may have unexpected results

lambda in a For loop

Source: BC30288: Local variable is already declared in the current block; the other codes have pages of the same form (for example BC32000) where Learn publishes one.