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 |
|---|---|---|
|
a local variable, or a field in a class, structure or module |
the general-purpose declaration |
|
a local variable whose value survives between calls |
counters, one-time initialization |
|
a compile-time constant |
named literals such as limits and messages |
|
a field assigned only by a declaration initializer or a constructor |
values fixed per object or per type |
|
a field that belongs to the type, not to an instance |
type-wide state |
|
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 |
|---|---|
|
explicit type, default value 0 |
|
explicit type with an initializer |
|
|
|
collection initializer |
|
type inferred from the initializer (see below) |
|
two |
|
two declarators with different types |
|
array with bounds; the numbers are upper bounds (see Arrays) |
|
legacy type characters ( |
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.
Source: Declared Element Names
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
Source: Local Type Inference
Three rules complete the picture.
-
Inference applies to local variables only. A field written as
Private field = 5is not inferred: underOption Strict Onthe compiler reports BC30209 (Option Strict On requires all variable declarations to have an 'As' clause), and underOption Strict Offthe field would be anObject. The same applies to parameters and function return types, which always need anAsclause underOption Strict On. -
Dim x = Nothinghas no type to infer, soxis anObject. 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 Offswitches inference off. Combined withOption Strict OneveryDimwithoutAsis 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.
Source: Option Infer Statement
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 |
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 |
only in that block |
Procedure |
in a |
the rest of the procedure (and, as explained below, its whole block) |
Module |
as a member of a |
anywhere in that type (subject to its access level) |
Namespace |
a type, or a |
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
Source: Scope in Visual Basic
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.
Source: Shadowing in Visual Basic
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
Source: Lifetime in Visual Basic
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:
-
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. -
Declare the variable outside the loop when the accumulation is wanted, and write that down in the name (
runningTotal). -
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
Source: For Each…Next Statement
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 |
at the first call |
when the application stops |
|
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 |
|---|---|---|
|
no restriction |
|
|
visible in the same assembly (and to friend assemblies) |
|
|
the declaring type and derived types |
|
|
derived types, or any code in the same assembly |
|
|
derived types in the same assembly |
|
|
the declaring type only |
|
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 |
|---|---|---|
|
|
unlike C#, |
|
|
one of the few places where the default is wider than in a class |
|
|
|
|
|
unlike C#, where members default to |
Instance constructor ( |
|
a |
|
|
the same as C# |
|
|
unlike C#, where nested types default to |
Members of an |
|
no modifier is allowed |
Local variables, parameters |
not applicable |
|
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
Source: References to Declared Elements
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; |
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 |
|
one shared variable (warning BC42324) |
one shared variable |
|
a new variable per iteration |
a new variable per iteration since C# 5 |
Static locals |
|
none; use a field |
Constants |
|
|
Default member access |
|
|
Inference |
|
|
Several variables in one declaration |
|
|
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 |
|---|---|
|
both are |
variables can be created by using them ("implicit declaration") |
|
a "global variable" can be declared outside any module |
every variable lives in a type; use a |
block variables are new for each loop pass |
they are not, see the lifetime section above; initialize them |
initializers on |
initializers on |
|
|
|
|
Common errors
| Code | Message (abridged) | Typical cause |
|---|---|---|
BC30209 |
Option Strict On requires all variable declarations to have an |
field or parameter without |
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 |
|
BC30451 |
Name is not declared |
use outside the scope of a block variable |
BC30059 |
Constant expression is required |
|
BC30064 |
|
assignment outside a constructor |
BC30574 |
Option Strict On disallows late binding |
member access on an |
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 |
|
BC30980 |
Type of a variable cannot be inferred from an expression containing the variable |
|
BC31400 |
Local variables within methods of structures cannot be declared |
|
BC32000 |
Local variable cannot be referred to before it is declared |
use above the |
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 |
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.