Program Structure and Code Conventions
|
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. It describes what a Visual Basic source file may contain and in which order, how the compiler finds the first line of code to run, the lexical rules that decide what counts as a name, a keyword, a comment or a line, and the conventions that Microsoft’s documentation and tooling follow. It is the page to read before writing the first file of the shelf library-catalogue solution that the rest of this section builds on.
The page covers the following, in this order: the shape of a file and of a program, the Imports statement and project-level
imports, namespaces and the root namespace, the default access levels of every declaration, the forms of Main and the startup
object, the lexical rules (case, identifiers, keywords, line continuation, comments, labels, special characters), the
conventions, regions and the documented limits. The Option statements that open every file are described on
Option Statements; the variable declaration statements on
Variables, Constants and Scope; the project file
on Projects, MSBuild and the dotnet CLI; and
console programs on Console Applications.
Every example is a complete program. Each one was built on the .NET 10 SDK with Option Strict On, Option Explicit On,
Option Infer On and warnings treated as errors, then run, and the output shown is the real one. Examples that are deliberately
wrong are labelled and show the compiler’s messages instead.
From solution to statement
A Visual Basic program is built from nested containers. Learn’s wording is that a solution comprises one or more projects, a
project produces an assembly, an assembly is compiled from one or more source files, and a source file defines classes,
structures, modules and interfaces that contain all the code. Executable code lives only inside procedures: Sub, Function,
Operator, property accessors (Get, Set) and the event accessors AddHandler, RemoveHandler and RaiseEvent. A
Visual Basic file has no top-level statements: unlike C# 9 and later, a program that is only a list of statements does not
compile, and even a three-line program needs a Module and a Sub Main.
The diagram shows the order the compiler requires inside one source file and what each level may contain.
Explicit, Strict, Infer, Compare"] O --> I["2. Imports statements
namespaces, aliases, types, XML namespaces"] I --> A["3. Assembly and module attributes
<Assembly: ...>"] A --> N["4. Namespace members"] N --> NS["Namespace blocks (nested)"] N --> T["Types
Class, Structure, Module, Interface"] N --> D["Enum and Delegate"] NS --> T T --> M["Members
Sub, Function, Property, Event, Operator, Declare,
fields, Const, nested types"] M --> S["Statements
only inside procedures, operators and accessors"] C["Conditional compilation directives
#If, #Region, #Const may appear almost anywhere"] -.-> N
The numbers 1 to 4 are an order, not a menu: a statement that arrives early in the wrong place is an error. The messages are
the compiler’s own (see the examples further down): Option after any other statement is BC30627, Imports after a
declaration or an attribute is BC30465. Directives such as #If, #Const and #Region are the exception and may be placed
between the elements; they are described on
Conditional Compilation and Directives.
The levels also have names that appear in compiler messages and in Learn: file-level elements (Option, Imports, attributes),
namespace-level elements (types, Enum, Delegate, nested Namespace), module-level elements (procedures, properties,
events, fields, constants) and procedure-level elements (local variables, local constants and the executable statements). The
data elements allowed at each level differ: enumerations and delegates at namespace level, variables, constants, enumerations and
delegates at module level, and only local variables and constants inside procedures.
A file with every level
The next program is one file that contains an Option statement, five kinds of Imports, namespaces (including the escape
Global), an enumeration, a class, a module and a Main that returns an exit code. The project sets <RootNamespace>Shelf</RootNamespace>,
which the output makes visible.
Option Strict On
Option Infer On
Imports System.Collections.Generic
Imports System.Math
Imports Cat = Shelf.Catalog
Imports Titles = System.Collections.Generic.List(Of String)
Imports <xmlns:b="urn:shelf:books">
Namespace Catalog
Public Enum LoanState
Available
OnLoan
End Enum
Public Class Book
Public ReadOnly Property Title As String
Public Property State As LoanState
Public Sub New(title As String)
Me.Title = title
End Sub
Public Overrides Function ToString() As String
Return Title & " (" & State.ToString() & ")"
End Function
End Class
End Namespace
Namespace Global.Elsewhere
Public Module Tools
Public Function Describe(item As Object) As String
Return item.GetType().FullName
End Function
End Module
End Namespace
Module Program
Function Main(args As String()) As Integer
Dim book As New Cat.Book("Dune")
book.State = Cat.LoanState.OnLoan
Dim titles As New Titles From {book.Title, "Emma"}
Dim entry = <b:book isbn="9780441013593"><b:title><%= book.Title %></b:title></b:book>
Console.WriteLine(book)
Console.WriteLine(Elsewhere.Tools.Describe(book))
Console.WriteLine(Elsewhere.Tools.Describe(New Object()))
Console.WriteLine(GetType(Program).FullName)
Console.WriteLine(titles.GetType().Name & " has " & titles.Count.ToString() & " titles")
Console.WriteLine("Max(3, 7) = " & Max(3, 7).ToString() & ", Abs(-2) = " & Abs(-2).ToString())
Console.WriteLine(entry.Name.NamespaceName & " " & entry.Name.LocalName)
Console.WriteLine("arguments: " & args.Length.ToString() & " -> " & String.Join(" | ", args))
Return args.Length
End Function
End Module
Run with the arguments one "two words" 3:
Dune (OnLoan)
Shelf.Catalog.Book
System.Object
Shelf.Program
List`1 has 2 titles
Max(3, 7) = 7, Abs(-2) = 2
urn:shelf:books book
arguments: 3 -> one | two words | 3
The process exit code was 3, the number of arguments, because Main is a Function that returns an Integer. Source:
Structure of a Visual Basic Program
Four lines of the output carry the lesson:
-
Shelf.Catalog.Book:Namespace Catalogwas written inside a file whose project has the root namespaceShelf, so the type isShelf.Catalog.Book. The root namespace is prepended to everyNamespacestatement. -
Namespace Global.Elsewheredeclares the namespaceElsewhere, notShelf.Elsewhere:Globalstarts the name at the very top, outside the root namespace, which is whyMaincallsElsewhere.Tools.Describewithout theShelfprefix. (DescribereportsSystem.Objectfor the argumentNew Object(), the line that follows theBookone.) -
Shelf.Program: a type declared outside anyNamespacestatement belongs to the root namespace. -
urn:shelf:books book: the XML namespace import gave the prefixba meaning at file level, and the XML literal<b:book>carries it.
Imports
Imports makes names visible without qualification. It never loads an assembly, never changes what a name means and has no
run-time effect: an assembly comes in through a package or project reference, and Imports only shortens the names inside it.
Visual Basic has five forms:
| Form | Example | Effect |
|---|---|---|
Namespace |
|
Every type of the namespace can be used by its simple name. |
Namespace alias |
|
|
Type alias |
|
A constructed generic type gets a short name. An alias written as a statement is local to its file. |
Type or namespace members |
|
The shared members of a type (here |
XML namespace |
|
The prefix |
Rules worth keeping in mind:
-
When two imported namespaces contain a type with the same simple name, the simple name is ambiguous (BC30561) and has to be qualified or aliased. An alias is the shortest fix.
-
Importing
System.MathorSystem.Consoleas a type removes the need to write the type name, and also removes the signal it gave:Max(3, 7)looks like the call of a function of the program. Use it for mathematical code, not for general purpose names. -
The XML namespace form is described on XML Literals and Axis Properties.
Project-level imports and the SDK defaults
A name that every file needs can be imported once, for the whole project. In an SDK-style project the item is Import, and the
build passes the list to vbc as the -imports option. The next project adds two imports and the program uses names from them
without any Imports statement of its own:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Shelf</RootNamespace>
<OptionStrict>On</OptionStrict>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Import Include="System.Text" />
<Import Include="Culture=System.Globalization.CultureInfo" />
</ItemGroup>
</Project>
Module Program
Sub Main()
' System, System.Collections.Generic, System.Linq and Microsoft.VisualBasic come from the SDK's default imports.
Dim names As New List(Of String) From {"Emma", "Dune"}
Dim builder As New StringBuilder()
For Each name In names.OrderBy(Function(n) n)
builder.Append(name).Append(" ")
Next
Console.WriteLine(builder.ToString().Trim())
Console.WriteLine(Culture.InvariantCulture.EnglishName)
Console.WriteLine(Len("Shelf"))
End Sub
End Module
Dune Emma
Invariant Language (Invariant Country)
5
Source: -imports
The second Import is a project-wide alias, written Alias=TypeOrNamespace in the item’s Include. This form is not described on the
-imports page of the documentation; it is what vbc accepts. Writing the alias as item metadata
(<Import Include="System.Globalization.CultureInfo" Alias="Culture" />) did not work in the build that was tried: the metadata
was ignored and the members of CultureInfo were imported instead, which made unrelated names ambiguous. Use the Culture=...
form shown above, or avoid project-level aliases.
The SDK also imports a fixed set of namespaces into every Visual Basic project, which is why the program above uses
List, Console, OrderBy and Len without any Imports. On SDK 10.0.402 the compiler command line of a console project
contained -imports with: Microsoft.VisualBasic, System, System.Collections, System.Collections.Generic,
System.Diagnostics, System.Linq, System.Xml.Linq and System.Threading.Tasks. An Import Remove item for one of these
(System.Xml.Linq was tried) left it in the list in the build that was inspected. Consequences:
-
A file does not need
Imports SystemorImports System.Linq. Thedotnet new console -lang vbtemplate still emitsImports Systemat the top ofProgram.vb; it is harmless and redundant. -
A class named
Task,DebugorEnumerablein your project is shadowed by, or ambiguous with, the imported type depending on the namespaces involved. Name your types so that they do not collide with these eight namespaces. -
Code copied from a project with another set of imports (a .NET Framework project, a Visual Studio template with My Extensions) may need
Importsstatements to compile in a library project.
Namespaces
Namespace groups types and gives them a qualified name. The name is prefixed with the project’s root namespace, the MSBuild
property RootNamespace (the default is the project name, and dotnet new writes it into the project file). Three consequences:
-
The dotted name in a
Namespacestatement is relative to the root namespace:Namespace Catalogin theShelfproject isShelf.Catalog. Repeating the root (Namespace Shelf.Catalog) givesShelf.Shelf.Catalog, one of the most common surprises when moving from C#, where the namespace is written in full. -
Types outside any
Namespacestatement are in the root namespace itself. With an empty root namespace, they are in the global namespace. -
Namespace Global.Elsewhereopts out of the root namespace and declares the namespaceElsewhere. In expressions,Global.System.Stringstarts the lookup at the global namespace, and is the way to reachSystemwhen some enclosing namespace has a member of the same name.
A namespace declared in the project as System or Collections under a root namespace can hide the framework one. In an
experiment, a file with Namespace System inside the Shelf root made the compiler fail on the generated AssemblyInfo.vb
with BC31424 ("Type 'System.Reflection.AssemblyCompanyAttribute' … has been forwarded to assembly 'System.Runtime'")
because the generated file’s System resolved to Shelf.System. Do not name project namespaces after framework ones.
Other facts:
-
Namespaces are open: the same namespace can be declared in many files and projects, and its members merge.
-
Namespaceblocks nest, andNamespace A.Bis the same asNamespace AcontainingNamespace B. -
Only namespace-level elements go inside a namespace: types, enumerations, delegates and nested namespaces. A
Subor a variable directly in a namespace is BC30001 ("Statement is not valid in a namespace."). -
The
Importsstatements apply to the whole file, whichever namespace block follows; there is no per-namespaceImports. -
A namespace is not a run-time entity; it is only a prefix of the names of its types.
Source: Namespaces in Visual Basic
Default access levels
A declaration without an access modifier gets the default of its kind. The next program prints the real defaults with reflection, for a class, a structure, a module, an interface and an enumeration declared with no modifiers at all:
Option Strict On
Imports System.Reflection
Class DefaultType
Dim plainField As Integer
Const Limit As Integer = 3
Sub DoWork()
End Sub
Function Compute() As Integer
Return 1
End Function
Property Name As String
Event Changed As EventHandler
Sub New()
End Sub
End Class
Structure DefaultStructure
Dim plainField As Integer
Sub DoWork()
End Sub
End Structure
Module DefaultModule
Dim plainField As Integer
Sub DoWork()
End Sub
End Module
Interface IDefault
Sub DoWork()
End Interface
Enum DefaultEnum
First
End Enum
Class [MyClass]
End Class
Module Program
Private Function Access(info As MethodBase) As String
If info.IsPublic Then Return "Public"
If info.IsPrivate Then Return "Private"
If info.IsAssembly Then Return "Friend"
If info.IsFamily Then Return "Protected"
Return "other"
End Function
Private Function TypeAccess(t As Type) As String
Return If(t.IsPublic, "Public", If(t.IsNotPublic, "Friend", "nested"))
End Function
Private Sub Dump(t As Type)
Console.WriteLine(t.Name & " -> " & TypeAccess(t))
Const all As BindingFlags = BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
For Each f In t.GetFields(all)
Dim kind = If(f.IsLiteral, "constant", "field")
Dim vis = If(f.IsPublic, "Public", If(f.IsPrivate, "Private", If(f.IsAssembly, "Friend", "other")))
Console.WriteLine(" " & kind & " " & f.Name & " = " & vis)
Next
For Each m In t.GetMethods(all)
Console.WriteLine(" " & If(m.IsSpecialName, "accessor", "method") & " " & m.Name & " = " & Access(m))
Next
For Each c In t.GetConstructors(all)
Console.WriteLine(" constructor = " & Access(c))
Next
End Sub
Sub Main()
Dump(GetType(DefaultType))
Dump(GetType(DefaultStructure))
Dump(GetType(DefaultModule))
Console.WriteLine(GetType(IDefault).Name & " -> " & TypeAccess(GetType(IDefault)))
Console.WriteLine(GetType(DefaultEnum).Name & " -> " & TypeAccess(GetType(DefaultEnum)))
Console.WriteLine(GetType([MyClass]).Name)
End Sub
End Module
DefaultType -> Friend
field plainField = Private
field _Name = Private
field ChangedEvent = Private
constant Limit = Private
method DoWork = Public
method Compute = Public
accessor get_Name = Public
accessor set_Name = Public
accessor add_Changed = Public
accessor remove_Changed = Public
constructor = Public
DefaultStructure -> Friend
field plainField = Public
method DoWork = Public
DefaultModule -> Friend
field plainField = Private
method DoWork = Public
IDefault -> Friend
DefaultEnum -> Friend
MyClass
The pattern:
-
Every type declared at namespace level is
Friend(the CLR’sinternal): visible inside the assembly only.Public Classhas to be written for a library. -
Sub,Function,Property,EventandSub Newdefault toPublic: theaccessorlines of the output are the property and event accessors, and they are public too. This is the opposite of C#, where members default toprivate. -
A field declared with
DimisPrivatein a class and a module, butPublicin a structure.Constin a class isPrivate. Always writePrivateorPublicon a field. -
The
Eventin the output is stored in a private field namedChangedEvent, and an auto-property namedNamein a private field_Name. Both fields are generated and unrelated to your own naming. -
Interface members and enumeration members are
Public, and an interface member cannot carry an access modifier.
The class is also named with an escaped identifier on purpose: Class [MyClass] compiles because MyClass is a reserved word that
the brackets turn into a plain name (see below).
The Main procedure
A project that builds an executable (OutputType Exe) must have exactly one entry point named Main. Visual Basic accepts four
signatures, written as a Sub or a Function:
| Declaration | Meaning |
|---|---|
|
No arguments, exit code 0 unless |
|
Receives the command-line arguments. This is what |
|
The returned value is the process exit code. |
|
Both. |
Source: Main Procedure in Visual Basic
The rules around them, each verified with the compiler:
-
Maincan be in aModule(the form used throughout this section) or be aSharedmember of a class or structure. In aModuletheSharedkeyword is implicit. Unlike C#, the procedure must be accessible:Private Shared Function Mainis rejected with BC30737, so usePublicorFriend. -
The name is case-insensitive (
main,MAIN), and the array does not contain the program name: the three arguments of the first example arrived as three elements. -
There is no
Async Main.Async Function Main() As Taskis BC30737 and a program that needsAwaitat the top calls an async method with.GetAwaiter().GetResult()from a synchronousMain(see Async and Await). This is a difference from C# 7.1 and later. -
A Windows Forms project with the Application Framework enabled has a generated
Mainand the project’sMyTypeisWindowsForms. If you write your ownSub Mainin such a project, setMyTypetoWindowsFormsWithCustomSubMain(see Windows Forms Applications). -
A class library has no
Main. -
When more than one
Mainexists, the build needs to know which one to start, and nothing but the propertyStartupObject(the-mainoption ofvbc) decides.
A shared Main in a class, with the explicit access modifier it requires, returning an exit code:
Option Strict On
Public NotInheritable Class Launcher
Private Sub New()
End Sub
' In a class, Main must be Shared. In a Module it would not need the keyword.
Public Shared Function Main(args As String()) As Integer
Console.WriteLine("entry point: " & GetType(Launcher).FullName)
Console.WriteLine("exit code requested: " & Exitcode(args).ToString())
Return Exitcode(args)
End Function
Private Shared Function Exitcode(args As String()) As Integer
Return If(args.Length = 0, 0, 64)
End Function
End Class
entry point: Shelf.Launcher
exit code requested: 0
Source: -main
The entry-point failures appear as errors of the compiler and not of the language, with no line number. These are the real messages
for the four cases. The first one has two Main procedures in Module Importer and Module Reporter:
Option Strict On
Module Importer
Sub Main()
Console.WriteLine("importer")
End Sub
End Module
Module Reporter
Sub Main()
Console.WriteLine("reporter")
End Sub
End Module
vbc : error BC30738: 'Sub Main' is declared more than once in 'TwoMains': TwoMains.Importer.Main(), TwoMains.Reporter.Main()
vbc : error BC30737: No accessible 'Main' method with an appropriate signature was found in 'AsyncMain'.
vbc : error BC30420: 'Sub Main' was not found in 'NoEntry'.
vbc : error BC30737: No accessible 'Main' method with an appropriate signature was found in 'InstanceMain'.
The second line is the Async Function Main() As Task file (BC30737), the third a module with a Sub Start and no Main at
all (BC30420), and the fourth a class with a non-Shared Sub Main (BC30737). The project that builds the first listing is repaired by naming
the module in StartupObject. Visual Studio’s Application type and Startup object boxes on the project’s Application
page edit the same properties:
<StartupObject>Shelf.Reporter</StartupObject>
With that property in the project (root namespace Shelf, same source file), the program starts Reporter.Main:
reporter
The value is the full name, including the root namespace. A value that is only the module name (Reporter) fails with BC30420 ("'Sub Main'
was not found in 'Reporter'"), because the root namespace is part of the type’s name.
Lexical rules
The compiler first turns the characters of a file into tokens and then into logical lines. Learn and the specification describe the grammar in the chapter Lexical Grammar. The next program exercises the rules in this section; its output is shown after it.
Option Strict On
Imports System.Collections.Generic
Imports System.Linq
Module Program
Private Function [Select](items As IEnumerable(Of String), [Class] As String) As IEnumerable(Of String)
Return items.Where(Function(item) item.StartsWith([Class], StringComparison.Ordinal))
End Function
Sub Main()
' Identifiers are case-insensitive; the declared spelling is what tooling displays.
Dim shelfCount As Integer = 3
SHELFCOUNT += 1
Console.WriteLine("shelfCount = " & ShelfCount.ToString())
console.writeline("this also compiles")
' Unicode letters are legal in identifiers, and so is a leading underscore plus a letter.
Dim año As Integer = 2026
Dim _draft As String = "v1"
Console.WriteLine(año.ToString() & " " & _draft)
' A reserved word can be used as a name when it is escaped with square brackets.
Dim [Loop] As String = "ring"
Dim titles As String() = {"Dune", "Dubliners", "Emma"}
Console.WriteLine([Loop] & ": " & String.Join(", ", [Select](titles, "Du")))
' Implicit line continuation: after a comma, an operator, an open bracket and
' before a close bracket, and before and after a query operator.
Dim summary = String.Join(
", ",
titles.Where(Function(t) t.Length > 4
).OrderBy(Function(t) t))
Dim total = shelfCount +
2 *
5
Dim longTitles = From t In titles
Where t.Length > 4
Order By t Descending
Select t
Console.WriteLine(summary & " | " & total.ToString() & " | " & String.Join("+", longTitles))
' Explicit continuation with _ ; since Visual Basic 16 a comment may follow it.
Dim padded = "[" & _ ' the opening bracket
titles(0).PadRight(8) & _ ' the title
"]"
Console.WriteLine(padded)
' Several statements on a physical line: legal, but Microsoft's conventions say not to.
Dim x As Integer = 1 : Dim y As Integer = 2 : x += y
Console.WriteLine("x = " & x.ToString())
REM Old-style comment; the keyword REM works only at the start of a statement.
Dim attempts As Integer = 0
Retry:
attempts += 1
If attempts < 3 Then GoTo Retry
Console.WriteLine("attempts = " & attempts.ToString())
End Sub
End Module
shelfCount = 4
this also compiles
2026 v1
ring: Dune, Dubliners
Dubliners | 14 | Dubliners
[Dune ]
x = 3
attempts = 3
Case insensitivity
Identifiers and keywords are case-insensitive: ShelfCount, SHELFCOUNT and shelfcount are the same name, and so are Console
and console. The Unicode simple one-to-one case mappings are used and locale-specific mappings are ignored, so the Turkish dotted
and dotless i do not cause trouble. Two declarations that differ only in case in the same scope are a duplicate:
Option Strict On
Module Program
Sub Main()
Dim Count As Integer = 1
Dim COUNT As Integer = 2
Console.WriteLine(count)
End Sub
End Module
Program.vb(6,13): error BC30288: Local variable 'COUNT' is already declared in the current block.
The same rule produces BC30269 ("has multiple definitions with identical signatures") for Sub Print() and Sub PRINT() in one
module. The Visual Studio editor rewrites the case of every use of a name to the case of its declaration while you type, which is why VB code
looks uniformly cased. The case-insensitivity has two practical consequences for a team that also writes C#:
-
A C# library that exposes two public members differing only in case (
Nameandname) is not CLS-compliant and cannot be used reliably from Visual Basic, because VB cannot tell the two apart. Keep public names unique ignoring case. -
Case-insensitivity applies to names only. String contents, XML element names in literals and
Dictionarykeys remain case-sensitive (unlessOption Compare Textor a comparer says otherwise).
Identifiers
An identifier starts with a letter or with an underscore that is followed by at least one more identifier character (a lone _
would be a line continuation, and Dim _ As Integer is BC30203). The remaining characters can be letters, decimal digits,
combining marks, formatting characters and underscores. Letters are those of Unicode classes Lu, Ll, Lt, Lm, Lo and Nl, so
Dim año As Integer and Greek or Cyrillic names are legal, as the program shows. Names that start with an underscore are not
CLS-compliant, which matters only for public members; the private-field style _title is common and fine. Identifiers can carry a type character (name$, count%), described on
Data Types and Literals, and no identifier may carry one
after being escaped.
Escaped identifiers use square brackets so that a reserved word can be a name: [Select], [Class], [End], [Loop]. In the
program above, the method is [Select] and its parameter is [Class]; the brackets are needed at the declaration and at each use,
and they are not part of the name. After a dot a reserved word is accepted as a member name without brackets: GetType(String).Module
and System.Linq.Enumerable.Select(words, ...) compile on SDK 10.0.402, which is how members of the base class library whose names
are Visual Basic keywords are called. Brackets are only needed where the word would otherwise start a statement or declare a name. A type named
MyClass is written [MyClass], as in the access-level listing above.
Keywords
The Visual Basic Language Specification lists 152 reserved words (the table in the Keywords section of the lexical-grammar
chapter, counted from the grammar), among them Alias, AndAlso, ByRef, CType, Handles, IsNot, Narrowing,
NotInheritable, MyClass, Overloads, Partial, Shadows, Widening and the conversion keywords CBool to CUShort. The specification
retains EndIf, GoSub, Let, Variant and Wend as reserved although the language no longer uses them. A reserved word cannot
be a name unless it is escaped:
Option Strict On
Imports System.Reflection
<Assembly: AssemblyTitle("Shelf")>
Imports System.Text
Module Program
Sub Main()
Dim _ As Integer = 3
Dim end As Integer = 4
Dim class As Integer = 5
End Sub
End Module
Program.vb(11,13): error BC30203: Identifier expected.
Program.vb(12,13): error BC30183: Keyword is not valid as an identifier.
Program.vb(13,13): error BC30183: Keyword is not valid as an identifier.
Program.vb(7,1): error BC30465: 'Imports' statements must precede any declarations.
That listing shows three more errors in one go: the lone underscore (BC30203), End and Class used as names (BC30183) and the Imports
statement placed after an attribute (BC30465). The compiler reports the errors of the lexical phase first, which is why the order
is not the order of the lines.
Many words that look like keywords are contextual: they have a meaning only in one syntactic place and are legal names
elsewhere. A test compiled local variables with each of the names Aggregate, Ansi, Assembly, Async, Auto, Await, Binary, Compare,
Custom, Distinct, Equals, Explicit, From, Group, Infer, Into, Iterator, Join, Key, Mid, Off, Order, Out,
Preserve, Region, Skip, Strict, Take, Text, Unicode, Until, Where and Yield without any error on SDK 10.0.402,
while Overloads and Partial were rejected with BC30183. Legal does not mean wise: the names that belong to the query syntax
(From, Where, Order, Group, Join, Into, Distinct, Take, Skip) and to Async/Await/Iterator/Yield read badly as variable
names. The practical test of any word is the compiler.
Line terminators and line continuation
A statement ends at the end of the physical line, unless the line is continued. Two mechanisms exist:
-
Implicit line continuation, introduced in Visual Basic 10 (2010). The line may be broken, without any character, after a comma, an open parenthesis, brace or embedded expression
<%=, after a member qualifier.that qualifies something, before a close parenthesis, brace or embedded expression, after a binary operator in an expression and after an assignment operator, around query operators (Where,Order By,Select, …) and in attribute lists (after<and before>). -
Explicit line continuation: a space followed by an underscore as the last character of the line. Visual Basic 16 (2019) allows a comment after the underscore, as the program above does (
& _ ' the opening bracket). Learn’s page on comments still says that a comment cannot follow a continuation; the compiler of SDK 10.0.402 accepts it.
Implicit continuation does not combine with an explicit one: Learn’s specification shows that Dim x = _ followed by y on the next
line is an error, because the underscore is not a place where implicit continuation applies. Implicit continuation is also not available inside
conditional-compilation conditions, because the text in an excluded block need not be valid. Everything else is a matter of style: Microsoft’s
conventions say to prefer implicit continuation and avoid the _ where the language allows it, which is the style used in this section. The
reverse risk is a statement that looks continued but is not. A line that ends in a value is complete, so a next line that
begins with an operator is a new statement:
Dim total = first
+ second
The compiler reports BC30035 ("Syntax error.") on the second line of that fragment, which it reads as a new statement that starts with a unary plus.
Put the operator at the end of the line, as Dim total = first + followed by second, so that the line cannot be mistaken for finished. In a query,
every clause starts a new line without any character, and the clauses can be indented under From.
Statement separator, labels and the one-line If
The colon : joins statements on one physical line (Dim x As Integer = 1 : Dim y As Integer = 2 : x += y). Microsoft’s conventions say to use
one statement per line. The colon also ends a label: Retry: at the start of a line names a statement for GoTo. A label is an
identifier or an integer (line numbers from Visual Basic 6 are labels), may be followed by a statement on the same line, and is
local to the procedure. GoTo is legal but rarely needed; the structured alternatives are on
Control Flow. The single-line If condition Then statement : statement
treats everything after Then to the end of the line as the Then branch, so an Else follows on the same line; this is
one of the places where the colon changes meaning and makes code harder to read.
Comments
A comment starts with an apostrophe ' or with the keyword REM and ends at the end of the physical line. The comment marker may be
any of three characters: the ASCII apostrophe, the left single quotation mark U+2018 and the right single quotation mark U+2019. The
Unicode ones exist because word processors insert them, and pasted code from a document therefore comments out the line where the
quote was meant to be a string delimiter: Visual Basic strings use the double quote only. REM works at the start of a line and also after a
statement on the same line (Dim a As Integer = 1 REM note and Dim b As Integer = 2 : REM note both compiled), but the apostrophe is the form
that Learn and the tooling expect. There are no block comments. ''' starts an XML documentation comment, which is the subject of
XML Documentation, Analyzers and Code Style.
A comment cannot continue on the next line with an underscore.
Source: Comments in Code
Special characters
The following program uses the characters that are not letters or digits and that beginners find confusing, among them the
dictionary-access operator !, named arguments :=, the null-conditional ?., the compound assignments and the doubling of a double
quote in a string:
Option Strict On
Imports System.Collections.Generic
Module Program
Function Pad(text As String, Optional width As Integer = 10, Optional fill As Char = "."c) As String
Return text.PadRight(width, fill)
End Function
Sub Main()
Dim stock As New Dictionary(Of String, Integer) From {{"Dune", 3}, {"Emma", 1}}
' ! is the dictionary-access operator: stock!Dune means stock("Dune") (the default property).
Console.WriteLine("copies of Dune: " & stock!Dune.ToString())
stock!Emma += 4
Console.WriteLine("copies of Emma: " & stock("Emma").ToString())
' := names an argument, and the names can come in any order.
Console.WriteLine(Pad("Title", fill:="-"c, width:=8) & "|")
' ?. short-circuits on Nothing and gives Nothing (or a Nullable) instead of throwing.
Dim missing As String = Nothing
Console.WriteLine("length of missing: " & If(missing?.Length.ToString(), "none"))
' Compound assignment operators: &= += -= *= /= \= ^= <<= >>=
Dim label As String = "shelf"
label &= " 7"
Dim total As Integer = 17
total \= 5
Dim power As Double = 2
power ^= 10
Console.WriteLine(label & " | " & total.ToString() & " | " & power.ToString())
' Double quotes inside a string are doubled; there are no backslash escapes.
Console.WriteLine("She said ""hello"" in C:\temp\shelf")
Console.WriteLine($"{"Dune",-8}|{3,4}|{19.5R,8:F2}|")
End Sub
End Module
copies of Dune: 3
copies of Emma: 5
Title---|
length of missing: none
shelf 7 | 3 | 1024
She said "hello" in C:\temp\shelf
Dune | 3| 19.50|
Source: Special Characters in Code
| Character(s) | Use |
|---|---|
|
Argument lists, grouping, indexing of arrays (arrays use parentheses, not brackets). |
|
Array and collection initializers, |
|
Member access; a leading |
|
Dictionary access ( |
|
Null-conditional member and index access. |
|
Statement separator and label terminator; named argument. |
|
String concatenation. |
|
Integer division and exponentiation. The backslash in a string is an ordinary character. |
|
Comments. |
|
Explicit line continuation (last character of a line) and a leading or embedded character of an identifier. |
|
Escaped identifiers. |
|
Date literals ( |
|
Interpolated string. |
|
|
|
Embedded expression in an XML literal. |
The ! operator, the XML ones and the date-literal forms are covered on
Collections,
XML Literals and Axis Properties and
Dates, Times and Formatting.
Naming and coding conventions
Microsoft’s documentation and samples follow a set of conventions that Learn publishes as the Visual Basic Coding Conventions and the Visual Basic Naming Conventions. They are suggestions, not rules enforced by the compiler, and the .NET design guidelines for names apply to the public surface of a library. The table summarises the ones that matter for readable code; the reference solution of this section follows them.
| Topic | Convention | Example |
|---|---|---|
Types, members, properties, events, enumerations |
PascalCase: start each word with a capital letter. Types and properties are nouns, methods begin with a verb. |
|
Locals and parameters |
camelCase. |
|
Private fields |
An underscore plus camelCase is the usual team convention; the official page does not prescribe it. |
|
Interfaces |
Prefix |
|
Event arguments, handlers and events |
Suffix |
|
Hungarian prefixes |
Do not use them: |
|
|
Do not use |
not |
Abbreviations |
Use them sparingly and consistently; a well-known one ( |
|
Scope shadowing |
Avoid an inner-scope name that equals an outer-scope name. |
a local |
Source: Visual Basic Naming Conventions
The layout and language conventions of the coding-conventions page, as a checklist:
-
Indent by four spaces and insert tabs as spaces; one statement and one declaration per line; no
:separator. -
Prefer implicit line continuation over
_; align the items of a continued list. -
Put comments on their own line, start them with a capital letter, end them with a period, put one space after the
', and do not frame comments with asterisks. -
Use string interpolation for short strings and
StringBuilderfor appending in loops. Use&and never+to join text. -
Use
Integerrather than unsigned types unless they are necessary; use the short array formDim names As String() = {...}, with the array designator on the type. -
Use
Withfor a series of calls on one object,IsNot Nothinginstead ofNot ... Is Nothing, shortNewinstantiation (Dim book As New Book()) and object initializers. -
Use
Try…CatchandUsing; do not useOn Error. Do not test an event forNothingbeforeRaiseEvent; the statement does it. -
In event handlers, use
Handlesrather thanAddHandler, and useAddressOfwithout creating the delegate explicitly. If the handler ignores the arguments, leave them out with relaxed delegate conversion. -
Call
Sharedmembers by the type name, not through an instance. -
Use XML literals and axis properties for XML, and
Wherebefore other query clauses,Joinrather than an implicit join, and inferred types for range variables.
The next program follows these conventions in one place: an I interface, an EventArgs class, a Private field with an
underscore, an event, an object initializer, With, IsNot Nothing, interpolation, StringBuilder, a #Region and an
event handler with relaxed arguments.
Option Strict On
Option Infer On
Imports System.Text
Namespace Lending
Public Interface ILendable
ReadOnly Property IsAvailable As Boolean
Sub Lend(borrower As String)
End Interface
Public Class OverdueEventArgs
Inherits EventArgs
Public ReadOnly Property DaysLate As Integer
Public Sub New(daysLate As Integer)
Me.DaysLate = daysLate
End Sub
End Class
Public Class Loan
Implements ILendable
#Region "State"
Private ReadOnly _title As String
Private _borrower As String
Private _overdueDays As Integer
#End Region
Public Event Overdue As EventHandler(Of OverdueEventArgs)
Public Sub New(title As String)
_title = title
End Sub
Public ReadOnly Property IsAvailable As Boolean Implements ILendable.IsAvailable
Get
Return _borrower Is Nothing
End Get
End Property
Public Sub Lend(borrower As String) Implements ILendable.Lend
_borrower = borrower
End Sub
Public Sub MarkLate(days As Integer)
_overdueDays = days
RaiseEvent Overdue(Me, New OverdueEventArgs(days))
End Sub
Public Function Describe() As String
Dim state = If(IsAvailable, "available", $"lent to {_borrower}")
Return $"{_title}: {state}"
End Function
End Class
Public Class Reminder
Public Property Prefix As String = "Reminder"
Public Property Sent As Integer
End Class
End Namespace
Module Program
Private WithEvents CurrentLoan As Lending.Loan
' Relaxed delegate: a Handles method may leave out the sender and e parameters.
Private Sub OnOverdue() Handles CurrentLoan.Overdue
Console.WriteLine("overdue handler ran")
End Sub
Sub Main()
CurrentLoan = New Lending.Loan("Dune")
Dim loan = CurrentLoan
Console.WriteLine(loan.Describe())
loan.Lend("Ana")
Console.WriteLine(loan.Describe())
loan.MarkLate(3)
Dim reminder As New Lending.Reminder With {.Prefix = "Overdue notice"}
With reminder
.Sent += 1
Console.WriteLine($"{.Prefix}: {.Sent} sent")
End With
Dim titles As String() = {"Dune", "Emma", "Ulysses"}
Dim builder As New StringBuilder()
For Each title In titles
builder.Append(title).Append(";"c)
Next
Console.WriteLine(builder.ToString())
If loan IsNot Nothing AndAlso Not loan.IsAvailable Then
Console.WriteLine("still on loan")
End If
End Sub
End Module
Dune: available
Dune: lent to Ana
overdue handler ran
Overdue notice: 1 sent
Dune;Emma;Ulysses;
still on loan
Source: Visual Basic Coding Conventions
Two details of that program are easy to get wrong. The relaxed handler OnOverdue() works because it is bound with Handles on a WithEvents
variable. The same method bound with AddHandler loan.Overdue, AddressOf OnOverdue does not compile under Option Strict On:
the compiler reports BC36663, "Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub
OnOverdue()' and delegate 'Delegate Sub EventHandler(Of OverdueEventArgs)(sender As Object, e As OverdueEventArgs)'". The
same happens when a function without parameters is converted with AddressOf to a Func(Of Integer, Integer). A lambda
that ignores its parameters (Sub() ...) is accepted. The rules for delegates are on
Delegates and Lambda Expressions and for events on
Events and Handlers.
Compiler and editor settings can enforce part of the conventions. The .editorconfig file supports dotnet_naming_rule,
dotnet_naming_symbols and dotnet_naming_style entries for Visual Basic, and dotnet_diagnostic.<id>.severity for analyzer rules. The tools
are covered on
XML Documentation, Analyzers and Code Style.
Generated code (designer files, My.Settings) does not have to be renamed to fit the guidelines.
Regions and outlining
#Region "name" and #End Region mark a block that the editor can collapse. They have no run-time effect and may contain any elements
of the surrounding level. The program above has a region around three fields. Learn documents BC32025 (a region inside a method body or a
multiline lambda); a region around statements in an ordinary Sub compiled on SDK 10.0.402, so treat the restriction as dependent on
the context. Regions help with generated code and with long files of constants; wrapping every method group in a region, however, is
now widely regarded as a smell, because a long class is a signal to split it. The directive is documented on the page of the
language reference for #Region, and the other directives on
Conditional Compilation and Directives.
Limits
The Visual Basic compiler on .NET has few fixed limits, and Learn’s Limitations page lists those that can matter in extreme cases: a maximum for the length of a declared name (applied to the whole qualified string), 65535 characters in a physical line of source (a longer logical line is possible with continuation), a maximum for the number of dimensions of an array, a maximum string length, and 32768 characters for an environment string used as a command-line argument. Physical limits are mostly memory. Earlier Visual Basic versions limited module size and the number of variables; Visual Basic .NET relaxed those restrictions.
Source: Visual Basic Limitations
Errors on this page
| Code | Cause |
|---|---|
BC30627 |
An |
BC30465 |
An |
BC30738 |
Two |
BC30737 |
No |
BC30420 |
An executable with no |
BC30183 |
A reserved word used as a name without brackets. |
BC30203 |
A lone underscore, or a declaration with a missing identifier. |
BC30001 |
A |
BC30035 |
A line that starts with an operator because the previous line was already complete. |
BC30288 |
A name declared twice in the same block, ignoring case. |
BC30269 |
Two members with the same name ignoring case and the same signature. |
BC30205 |
Extra tokens after a complete statement, as in |
BC30561 |
An ambiguous simple name from two imported namespaces. |
BC31424 |
A project namespace that hides |
The error numbers above have no page of their own on this site; the messages shown in the listings are the compiler’s output, and the documentation index of Visual Basic errors is Visual Basic error messages.
Differences from C#
| Concern | Visual Basic | C# |
|---|---|---|
Top-level statements |
None. A |
C# 9 and later. |
|
|
|
Case |
Insensitive for all names. |
Sensitive. |
Namespace |
Relative to the root namespace; |
Written in full. |
Types without modifiers |
|
|
Members without modifiers |
|
|
Line end |
The physical line, with implicit continuation. |
|
Block end |
|
|
Comment |
|
|
Imports |
|
|
Where the books are outdated
The two reference books for this section were written around the .NET Framework templates. Their program-structure material needs these corrections, stated in the form that works today:
-
Both books start from a Console (.NET Framework) project template. The current template is
Consolefor .NET 10 (dotnet new console -lang vb), the file isProgram.vbwithModule Program, andSub Main(args As String())is the default. A console program does not need .NET Framework. -
Quickstart Guide pp. 126-127 use
Class MyClass, which does not compile becauseMyClassis reserved (BC30183 in the listing above); writeClass [MyClass]or choose a different name. Its pp. 133-134 writePublic Class Dog Inherits Animalon one line, which fails with BC30205; putInheritson the next line, or writeClass Dog : Inherits Animal. -
Quickstart Guide p. 32 mentions "global variables declared outside of any module". No such declaration exists: code and variables live inside a type, and a
Public Sharedfield or aModulefield is the closest thing. -
Mastering VB.NET pp. 161-162 call
Async Main, which Visual Basic does not support (BC30737). Use a synchronousMainthat waits on the task. -
The object-oriented project of the Quickstart Guide (pp. 151-161) uses
clsGrade-style names (Hungarian prefixes) next to unprefixed ones and does not compile as printed. The conventions above say to leave the prefix out. -
Learn’s comments page and older sources say that a comment cannot follow a line continuation. That changed with Visual Basic 16.