Procedures: Sub and Function
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is
a complete console program that starts with Option Strict On, was built with warnings treated as errors (except where the
text says that a warning is the point) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The
examples that are meant to fail say so, and the compiler messages quoted are the ones the SDK printed. The project root
namespace of the examples is App. The project-level imports that the SDK adds (System, System.Collections.Generic,
System.Linq, System.Threading.Tasks and others) are why Console, List(Of T) and Task need no Imports line.
A procedure is a named block of code that can be called. Visual Basic has two basic kinds, and the only difference between them is
the result: a Sub returns nothing and a Function returns a value. Everything else on this page (parameters, ByRef,
Optional, overloading, Static locals) applies to both. Methods that belong to a class or structure are procedures declared inside it, and
they are covered with their type in Classes and Objects; modules, whose
procedures are implicitly Shared, are on Modules and Shared Members.
Lambda expressions, which are anonymous procedures, are on
Delegates and Lambda Expressions.
| Kind | Declared with | Notes |
|---|---|---|
Subroutine |
|
no result; leave early with |
Function |
|
has a result; leave early with |
Constructor |
|
a |
Property accessors |
|
the accessors of a |
Operator |
|
a |
Event accessors |
|
|
Iterator |
|
uses |
Async |
|
uses |
External |
|
bodies live in a native library; see Native Interop |
Lambda |
|
anonymous; no name, and parameters cannot be |
Sub procedures
A Sub performs an action. The general form is:
[attributes] [Public | Private | Protected | Friend | Protected Friend | Private Protected]
[Shared] [Overloads | Overrides | Overridable | NotOverridable | MustOverride | Shadows]
[Async | Iterator] Sub Name [(Of T ...)] [(parameters)] [Implements ...] [Handles ...]
statements
End Sub
Points that matter in practice:
-
State the access you mean. The examples on this page write
Privatefor helpers that only the module itself calls. -
The parameter list is optional for a
SuborFunctionthat takes no arguments:Sub Main()andSub Mainare the same. Always write the parentheses; the formatter adds them and the call site needs them for aFunction. -
Exit Subleaves the procedure at once.Returnwith no value does the same, and is the form used inside aTryblock or a loop when the intent is "stop here".End Subalso leaves. -
A
Subthat is the target of aHandlesclause or is registered withAddHandleris an event handler; its parameter list must match the event’s delegate. -
There is no nested
Sub. Code that would be a local function in C# is written as a lambda held in a local variable, or as a private helper next to its caller (see below).
Function procedures
A Function returns a value whose type follows As. With Option Strict On the As Type clause is required (without it the
return type would be Object, and the compiler reports BC30210, "Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause"). A function can return any type: an intrinsic type, a class, a
structure, an array, a tuple, a nullable value type, a delegate, an IEnumerable(Of T).
There are two ways to set the result:
-
Return valueassigns the result and leaves the function in one statement. This is the form to use in all new code, and the only one that works the same in a lambda, an iterator or an async function. -
Assigning to the function’s own name (
LateFee = 1D) sets the same result without leaving. The name behaves like a local variable of the return type that exists for the whole call, so execution continues with the next statement, and the value the variable holds when the function ends (byExit Functionor by reachingEnd Function) is the result. This is the Visual Basic 6 style, and it is the reason the compiler’s missing-return warning (below) exists.
Option Strict On
Imports System.Globalization
Module Program
' A Sub performs an action and produces no value.
Private Sub PrintReceipt(title As String, daysLate As Integer)
If daysLate <= 0 Then
Console.WriteLine(title & ": returned on time")
Exit Sub
End If
Console.WriteLine(title & ": " & LateFee(daysLate).ToString("F2", CultureInfo.InvariantCulture) & " owed")
End Sub
' A Function produces a value: Return sets it and leaves at once.
Private Function LateFee(daysLate As Integer) As Decimal
If daysLate <= 0 Then Return 0D
Dim perDay As Decimal = 0.25D
Return Math.Min(daysLate * perDay, 10D)
End Function
' The older style: assign to the function's name; the procedure keeps running.
Private Function LateFeeOldStyle(daysLate As Integer) As Decimal
LateFeeOldStyle = 0D
If daysLate > 0 Then
LateFeeOldStyle = daysLate * 0.25D
Exit Function ' leaves now, returning the value assigned so far
End If
Console.WriteLine("(not reached when daysLate > 0)")
End Function
' Return with no value is only legal in a Sub; Return x is only legal in a Function.
Private Function Describe(count As Integer) As String
Select Case count
Case 0
Return "none"
Case 1
Return "one"
Case Else
Return count.ToString(CultureInfo.InvariantCulture) & " items"
End Select
End Function
Sub Main()
PrintReceipt("Dune", 0)
PrintReceipt("Emma", 6)
PrintReceipt("Ulysses", 80)
Console.WriteLine(LateFeeOldStyle(4).ToString("F2", CultureInfo.InvariantCulture))
Console.WriteLine(LateFeeOldStyle(0).ToString("F2", CultureInfo.InvariantCulture))
Console.WriteLine(Describe(0) & ", " & Describe(1) & ", " & Describe(7))
' A Function used as a statement: the result is discarded (allowed, and a classic bug source).
LateFee(3)
' Call is optional and rarely used today
Call PrintReceipt("Hamlet", 1)
End Sub
End Module
The program above prints:
Dune: returned on time
Emma: 1.50 owed
Ulysses: 10.00 owed
1.00
(not reached when daysLate > 0)
0.00
none, one, 7 items
Hamlet: 0.25 owed
Source: Function Procedures
Notes on the output:
-
LateFeeOldStyle(4)printed1.00and nothing else:Exit Functionleft right after the assignment.LateFeeOldStyle(0)skipped theIfblock, reached theConsole.WriteLine(the text is only accurate whendaysLateis positive) and returned0.00, the value the name held at the end. -
LateFee(3)on its own line calls aFunctionand discards the result. That is legal, nothing was printed, and it is a classic bug when the author meant to use the value. TheCallkeyword (Call PrintReceipt("Hamlet", 1)) is optional, legal for both kinds, and rarely used in new code. -
Returnis checked against the procedure kind.Return 5in aSubis BC30647 ("'Return' statement in a Sub or a Set cannot return a value") and a bareReturnin aFunctionis BC30654 ("'Return' statement in a Function, Get, or Operator must return a value"). -
The function’s name can also be read. In a function that takes parameters,
Twice = Twice * 2compiles and reads the implicit result variable. In a function with no parameters the same name on the right-hand side is a recursive call, because it can also be written without parentheses.Private Function G() As Integer : Return G + 1 : End Functioncompiles, calls itself forever and ends in a stack overflow. UseReturnand a local variable and the ambiguity never arises.
When a path returns nothing
If control can reach End Function without a Return, the function still returns, with whatever the implicit result variable holds, which is
the default value of the type (0, False, Nothing, a zero-initialised structure) unless a name assignment ran. The compiler tells you
with a warning, which this project enables as errors on all the other examples. This one is built with warnings visible:
Option Strict On
Module Program
Private Function FirstLetter(text As String) As Char
If text.Length > 0 Then
Return text(0)
End If
' no Return on this path: the function returns the default value of Char
End Function
Private Function Find(titles As String(), wanted As String) As String
For Each t In titles
If t = wanted Then Return t
Next
End Function
Private Function Count(values As Integer()) As Integer
For Each v In values
Count += v
Next
' falls off the end: returns the running value of the implicit local
End Function
Sub Main()
Console.WriteLine("[" & AscW(FirstLetter("")).ToString() & "]")
Console.WriteLine(Find({"Dune", "Emma"}, "Ulysses") Is Nothing)
Console.WriteLine(Count({1, 2, 3}))
End Sub
End Module
The compiler printed three warnings, and the program printed the default values:
/w/e2/Program.vb(15,5): warning BC42105: Function 'Find' doesn't return a value on all code paths. A null reference exception could occur at run time when the result is used.
/w/e2/Program.vb(22,5): warning BC42353: Function 'Count' doesn't return a value on all code paths. Are you missing a 'Return' statement?
/w/e2/Program.vb(9,5): warning BC42353: Function 'FirstLetter' doesn't return a value on all code paths. Are you missing a 'Return' statement?
[0]
True
6
Source: Function Statement
-
The compiler chose the code by the return type: BC42105 for
Find, which returns a reference type (String) and so returnedNothing, with a warning that a null reference exception could follow; BC42353 forFirstLetter(aChar, which returned the character with code 0, printed as[0]) and forCount(anInteger).Countis also the pattern the older style makes legal:Count += vaccumulates in the implicit result variable and the function returns it atEnd Function, and the warning is still reported because noReturnappears. -
Treat both warnings as errors (
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>in the project, or<WarningsAsErrors>BC42105,BC42353</WarningsAsErrors>). The fix is always to add aReturnon the missing path, or to throw. -
Different from C#, where a missing return on any path is the error CS0161. In Visual Basic it is a warning because the language has always returned a default.
Calling a procedure
-
A
Functioncall used as a value needs parentheses:Dim fee = LateFee(3). ASubcall statement that passes arguments needs them too: the VB6 formTwo 1, 2is BC30800 ("Method arguments must be enclosed in parentheses"). -
Arguments are evaluated from left to right, once, before the call, whether or not the callee uses them.
-
A call to a member through an instance, a type name or a module name uses the same syntax; for a
Module, the module name is optional. -
An extra pair of parentheses around one argument turns it into an expression: it is then passed by value even to a
ByRefparameter (see below). The extra parentheses are a quiet way to stop aByRefparameter from changing the variable, and a quiet bug when they appear by accident inPrintReceipttitle), (daysLate. -
A
Functionthat returns a tuple gives a caller several results at once; the call site names them:Dim r = MinMax(values)and thenr.Minandr.Max(see below).
The entry point
The program starts in a Sub Main or Function Main in a module (or a Shared member of a class). It may take the command-line arguments
as String(), and when declared as a Function returning Integer, the value is the process exit code. There is no Async Main in Visual Basic;
a Sub Main that waits for a task is shown on
Async and Await.
Option Strict On
Module Program
' Main may be a Function that returns the process exit code.
Function Main(args As String()) As Integer
If args.Length = 0 Then
Console.Error.WriteLine("usage: shelf <isbn>")
Return 2
End If
Console.WriteLine("looking up " & args(0))
Return 0
End Function
End Module
Run once without arguments (the process exit code was 2) and once with an ISBN (exit code 0):
usage: shelf <isbn>
looking up 9780441172719
Source: Main Procedure
The same entry point rules are used by the console project template, described on
Console Applications. Windows Forms and WPF applications use the
Application Framework instead of Sub Main (see
Windows Forms Applications).
Parameters and arguments
A parameter is the variable in the procedure’s declaration; an argument is the value or variable supplied by the caller. The declaration of one parameter is:
[<attributes>] [Optional | ParamArray] [ByVal | ByRef] name [As Type] [= default]
The order of the keywords is fixed. With Option Strict On the As Type clause is mandatory for every parameter. Parameter names follow the
usual identifier rules, are case-insensitive, and must not repeat a name in the same procedure. A parameter shares its scope with the procedure’s
locals, so a Dim with the same name inside the body is BC30734 ("'n' is already declared as a parameter of this method").
ByVal and ByRef
ByVal is the default since Visual Basic .NET; writing it is legal and redundant. In Visual Basic 6, ByRef was the default; code, books
and tutorials that predate VB.NET assume that, and so do many VBA snippets. The two mechanisms behave as follows.
| Argument is | ByVal (default) |
ByRef |
|---|---|---|
a value type ( |
the callee gets a copy; changing it does not change the caller’s variable |
the callee gets a reference to the caller’s variable; assignments are visible to the caller |
a reference type (class, array, |
the callee gets a copy of the reference: it can change the object the reference points to ( |
the callee gets a reference to the caller’s variable: rebinding ( |
a literal, a constant or an expression |
a copy |
a temporary: the callee may assign to it, nothing is written back |
Passing a reference type ByVal is not passing it by reference: the confusion between "the object is shared" and "the variable is shared"
is the most common mistake on this topic. String is the extreme case, because its instances are immutable: with ByVal the callee cannot
change the caller’s string at all; with ByRef it can replace which string the caller’s variable holds.
The following program shows each row. The Swap(Of T) helper is the classic use of ByRef (see also the generic version on
Generics); Slot is a structure, so Fill works on a copy and FillByRef on the caller’s
variable.
Option Strict On
Public Class Loan
Public Property Copies As Integer
Public Property Title As String = ""
End Class
Public Structure Slot
Public Copies As Integer
End Structure
Module Program
Private Sub Fill(slot As Slot)
slot.Copies = 99 ' changes the copy
End Sub
Private Sub FillByRef(ByRef slot As Slot)
slot.Copies = 99 ' changes the caller's structure
End Sub
Private Sub Grow(items As Integer())
items(0) = 100 ' the array is shared
ReDim items(5) ' ReDim creates a new array: only the local reference changes
End Sub
Private Sub AddByVal(n As Integer) ' ByVal is the default
n += 10
Console.WriteLine(" inside AddByVal: " & n)
End Sub
Private Sub AddByRef(ByRef n As Integer)
n += 10
Console.WriteLine(" inside AddByRef: " & n)
End Sub
Private Sub RenameByVal(loan As Loan)
loan.Title = "renamed" ' mutates the shared object
loan = New Loan() With {.Title = "replaced"} ' rebinds only the local copy of the reference
End Sub
Private Sub RenameByRef(ByRef loan As Loan)
loan.Title = "renamed"
loan = New Loan() With {.Title = "replaced"} ' rebinds the caller's variable
End Sub
Private Sub Swap(Of T)(ByRef a As T, ByRef b As T)
Dim temp As T = a
a = b
b = temp
End Sub
Private Sub AddBoth(ByRef a As Integer, ByRef b As Integer)
a += 1
b += 1
End Sub
Sub Main()
Dim copies As Integer = 1
AddByVal(copies)
Console.WriteLine("after AddByVal: " & copies)
AddByRef(copies)
Console.WriteLine("after AddByRef: " & copies)
Dim loan As New Loan() With {.Title = "original"}
RenameByVal(loan)
Console.WriteLine("after RenameByVal: " & loan.Title)
RenameByRef(loan)
Console.WriteLine("after RenameByRef: " & loan.Title)
Dim a As String = "left", b As String = "right"
Swap(a, b)
Console.WriteLine(a & " " & b)
' Parentheses around an argument pass a copy, even for a ByRef parameter.
Dim n As Integer = 5
AddByRef((n))
Console.WriteLine("after AddByRef((n)): " & n)
' Literals and expressions are passed through a temporary; nothing to write back to.
AddByRef(n + 1)
AddByRef(42)
Dim slot As Slot
Fill(slot)
Console.WriteLine("structure after Fill: " & slot.Copies)
FillByRef(slot)
Console.WriteLine("structure after FillByRef: " & slot.Copies)
Dim items As Integer() = {1, 2, 3}
Grow(items)
Console.WriteLine("array after Grow: " & items.Length & " items, first = " & items(0))
' Aliasing: the same variable passed twice.
Dim x As Integer = 3
AddBoth(x, x)
Console.WriteLine("AddBoth(x, x): " & x)
End Sub
End Module
inside AddByVal: 11
after AddByVal: 1
inside AddByRef: 11
after AddByRef: 11
after RenameByVal: renamed
after RenameByRef: replaced
right left
inside AddByRef: 15
after AddByRef((n)): 5
inside AddByRef: 16
inside AddByRef: 52
structure after Fill: 0
structure after FillByRef: 99
array after Grow: 3 items, first = 100
AddBoth(x, x): 5
Reading the output line by line:
-
after AddByVal: 1andafter AddByRef: 11: theIntegerwas copied, then referenced. -
after RenameByVal: renamedandafter RenameByRef: replaced: both calls changedTitlethrough the shared object, but only theByRefcall could also replace the object, soloanwas a differentLoanafterwards. The initial object, renamed"renamed", is now unreachable. -
AddByRefnprinted 15 inside and leftnat 5: the parentheses made the argument an expression. A literal orn + 1needs no parentheses: the compiler makes a temporary. There is no error and no warning for either, so aByRefparameter that the callee assigns to and the caller never reads back goes unnoticed. -
The structure and the array:
Fill(slot)leftCopiesat 0;FillByRef(slot)set it to 99.Grow(items)wrote100into the shared array (the caller saw it) but theReDimcreated a new array held only by the callee’s copy of the reference, so the caller still had 3 elements. -
AddBoth(x, x)passes the same variable twice and added 2: withByRef, two parameters can alias each other, and a procedure that assumes they are distinct breaks.Swap(x, x)is harmless;Copy(ByRef src, ByRef dst)over the same variable may not be.
What a ByRef argument can be
When the argument is something other than a plain local, parameter or field, the compiler decides how to give the callee "a reference".
Option Strict On
Public Class Counter
Private _value As Integer
Public Property Value As Integer
Get
Console.WriteLine(" get Value")
Return _value
End Get
Set(newValue As Integer)
Console.WriteLine(" set Value = " & newValue)
_value = newValue
End Set
End Property
Public Items As Integer
End Class
Module Program
Private Sub Bump(ByRef n As Integer)
n += 1
Console.WriteLine(" Bump saw " & n)
End Sub
' Reads the property inside the callee while the caller's property still holds the old value.
Private Sub BumpAndPeek(ByRef n As Integer, c As Counter)
n += 100
Console.WriteLine(" peek through the object: " & c.Items)
End Sub
Private Function TryParseCopies(text As String, ByRef copies As Integer) As Boolean
Return Integer.TryParse(text, copies) AndAlso copies > 0
End Function
Sub Main()
Dim c As New Counter()
Console.WriteLine("property argument (copy-in, copy-out):")
Bump(c.Value)
Console.WriteLine("field argument (a real reference):")
BumpAndPeek(c.Items, c)
Console.WriteLine(" Items = " & c.Items)
Console.WriteLine("array element argument:")
Dim a As Integer() = {10, 20}
Bump(a(1))
Console.WriteLine(" a(1) = " & a(1))
Console.WriteLine("consuming a C# out parameter:")
Dim copies As Integer
Console.WriteLine(TryParseCopies("12", copies) & " " & copies)
Console.WriteLine(TryParseCopies("abc", copies) & " " & copies)
Console.WriteLine(TryParseCopies("-4", copies) & " " & copies)
End Sub
End Module
property argument (copy-in, copy-out):
get Value
Bump saw 1
set Value = 1
field argument (a real reference):
peek through the object: 100
Items = 100
array element argument:
Bump saw 21
a(1) = 21
consuming a C# out parameter:
True 12
False 0
False -4
Source: ByRef
-
A property is not a variable, so the compiler reads it into a temporary, passes the temporary, and writes the temporary back with the setter after the call (copy-in, copy-out):
get Value,Bump saw 1,set Value = 1in that order. The callee never sees the property change while it runs, and the setter runs even if the callee never assigns to the parameter (a test with an empty callee still printedset Value = 0). AReadOnlyproperty or field is accepted too: the callee changes a temporary and nothing is written back. -
A field and an array element are real variables and the callee receives their address:
peek through the object: 100shows the field already holding 100 inside the call. -
TryParseCopiesconsumes the C#outparameter ofInteger.TryParseby passing aByRefparameter. Visual Basic has nooutkeyword: a C#outorrefparameter is seen asByRef, and the variable passed need not be assigned first."abc"leftcopiesat 0 (whatTryParsestores on failure);"-4"parsed successfully, socopiesholds-4even though the function then returnedFalsefor its owncopies > 0rule. AByRefresult is written whether or not the caller’s wrapper considers the call a success. -
Option Strict On is stricter for
ByRef. AByRefargument must convert to the parameter type and back. Passing anIntegervariable to aByRef n As Longparameter widens on the way in but would have to narrow when the value is copied back, so it is rejected with BC32029 ("Option Strict On disallows narrowing from type 'Long' to type 'Integer' in copying the value of 'ByRef' parameter 'n' back to the matching argument"). Pass a variable of the exact type, and convert afterwards. (Passing aLongvariable to anIntegerparameter is the ordinary BC30512 narrowing error.)
ByRef parameters have three restrictions that follow from the parameter living in the caller’s stack frame:
-
A lambda expression cannot use one:
Dim f = Function() n + 1inside a procedure withByRef nis BC36639 ("'ByRef' parameter 'n' cannot be used in a lambda expression"). Copy it to a local first. -
An
Asyncmethod cannot have aByRefparameter (BC36926) and neither can anIterator(BC36927), because the state would outlive the call. -
A
ParamArrayparameter must beByVal(ByRef ParamArrayis BC30667, "ParamArray parameters must be declared 'ByVal'").
In new code prefer a return value: a tuple returns several results without ByRef and works with Async. Keep ByRef for Swap-like procedures, for the
Try... pattern that mirrors the BCL (Integer.TryParse, Dictionary.TryGetValue, Isbn.TryParse(text, result) in the shelf), and for interop.
Value and reference semantics: common mistakes
-
Expecting a
ByValobject parameter to be replaceable.loan = New Loan()inside aByValcallee changes nothing outside. -
Forgetting that a
ByValparameter can be assigned to (locally): assignments to aByValparameter are legal, and affect only the callee. Treat a parameter as read-only by convention. Material that says the parameter "cannot be modified" withByValis wrong for Visual Basic .NET: the compiler allows it, and only the caller’s variable is protected. -
Passing a property
ByRefand relying on side effects inside the call. The setter runs after the call (see above).
Optional, ParamArray and named arguments
Optional parameters
Optional makes an argument omissible. The rules are:
-
An optional parameter must declare a default value with
= constant. There is noIsMissingtest any more (it belonged to theVariant-basedOptionalof Visual Basic 6);Optional n As Integeris a syntax error (BC30201 together with BC30812, "Optional parameters must specify a default value"). -
The default must be a constant expression: a literal, a
Const, an enumeration member,Nothing, or a constant expression built from them.Optional d As Date = Date.Nowis BC30059 ("Constant expression is required"); useOptional d As Date? = Nothingand test forNothingin the body. -
Every parameter after an optional one must also be optional (BC30202, "'Optional' expected").
-
The default is copied into the caller’s compiled code when the call is compiled. Changing a default in a library does not change callers that were not recompiled.
-
An
Optionalparameter cannot be combined withParamArray(BC30642) and a lambda parameter cannot beOptionalat all (BC33010). -
Nothingas a default means "the default of the type":Nothingfor a reference type,0for anInteger,Falsefor aBoolean, and a zero-initialised structure. Use a nullable parameter (Integer?) when "not supplied" must be told apart from0.
ParamArray
ParamArray collects any number of trailing arguments into a one-dimensional array parameter.
-
There can be only one, it must be the last parameter (BC30192, "End of parameter list expected. Cannot define parameters after a paramarray parameter"), its type must be an array (BC30050, "ParamArray parameter must be an array"), it must be
ByVal(BC30667), and it cannot beOptional(BC30642). -
The caller may pass the arguments one by one (expanded form:
Join(", ", "a", "b"), a new array is created for the call), pass no argument for it (an empty array of length 0, neverNothing), or pass an array of the right type (normal form, the array itself, which the callee may modify). -
Because the normal form also applies to covariant arrays, an
Object()parameter receives aString()argument as it is (the output below,3 object(s), is the length of the string array, not 1). To pass an array as a single element, wrap it:Describe(CObj(texts))printed1 object(s). -
Nothingpassed to aParamArrayis the null array, not an array with one null element: bothDescribe(Nothing)andDescribe(CType(Nothing, Object()))printedNothingin tests, so the callee must be ready for a null array (If items Is Nothing) when callers may pass one.
Named arguments
A named argument is written name:=value, with the parameter’s name, and makes the order irrelevant. It is the way to skip an optional parameter in the middle
of the list, and to document a call whose meaning is not obvious (Nothing, True).
Option Strict On
Imports System.Globalization
Module Program
Private Function Label(title As String, Optional copies As Integer = 1, Optional shelf As String = "A") As String
Return title & " x" & copies.ToString(CultureInfo.InvariantCulture) & " on shelf " & shelf
End Function
Private Function Join(separator As String, ParamArray parts As String()) As String
Return parts.Length.ToString(CultureInfo.InvariantCulture) & ": " & String.Join(separator, parts)
End Function
Private Function Total(ParamArray values As Integer()) As Integer
Dim sum As Integer = 0
For Each v In values
sum += v
Next
Return sum
End Function
Private Function Describe(ParamArray items As Object()) As String
If items Is Nothing Then Return "Nothing"
Return items.Length.ToString(CultureInfo.InvariantCulture) & " object(s)"
End Function
Private Function Build(title As String, Optional year As Integer = 0, Optional isbn As String = Nothing, Optional tags As List(Of String) = Nothing) As String
Return title & "/" & year & "/" & If(isbn, "no isbn") & "/" & If(tags Is Nothing, "no tags", tags.Count.ToString(CultureInfo.InvariantCulture))
End Function
Sub Main()
Console.WriteLine(Label("Dune"))
Console.WriteLine(Label("Dune", 3))
Console.WriteLine(Label("Dune", shelf:="Z")) ' skip an optional parameter by name
Console.WriteLine(Label(shelf:="B", copies:=2, title:="Emma")) ' any order when every argument is named
Console.WriteLine(Label(title:="Hamlet", 4)) ' named first, positional after (VB 15.5)
Console.WriteLine(Join(", "))
Console.WriteLine(Join(", ", "a"))
Console.WriteLine(Join(", ", "a", "b", "c"))
Console.WriteLine(Join(", ", New String() {"x", "y"})) ' an array is passed as is
Console.WriteLine(Total() & " " & Total(1, 2, 3))
Console.WriteLine(Describe())
Console.WriteLine(Describe(1, "two", 3.0))
Console.WriteLine(Describe(New Object() {1, 2}))
Console.WriteLine(Describe(New String() {"a", "b", "c"})) ' array covariance: still the array itself
Console.WriteLine(Describe(CType(Nothing, Object())))
Console.WriteLine(Build("Dune"))
Console.WriteLine(Build("Dune", isbn:="9780441172719"))
Console.WriteLine(Build("Dune", tags:=New List(Of String) From {"sf"}))
End Sub
End Module
Dune x1 on shelf A
Dune x3 on shelf A
Dune x1 on shelf Z
Emma x2 on shelf B
Hamlet x4 on shelf A
0:
1: a
3: a, b, c
2: x, y
0 6
0 object(s)
3 object(s)
2 object(s)
3 object(s)
Nothing
Dune/0/no isbn/no tags
Dune/0/9780441172719/no tags
Dune/0/no isbn/1
Reading it:
-
Label(shelf:="Z")supplied only the second optional parameter; the first took its default. Positionally, the same call needs a placeholder comma for the skipped argument,Label("Dune", , "Z"), which also compiles; the named form does not break when someone adds a parameter before it. -
Non-trailing named arguments (Visual Basic 15.5).
Label(title:="Hamlet", 4)puts a positional argument after a named one, which is legal as long as every named argument that comes before the last positional one sits in its own position (titleis the first parameter and the first argument). Before 15.5 every argument after the first named one also had to be named. WithLangVersion15.3 the compiler says exactly that:error BC30241: Named argument expected. Please use language version 15.5 or greater to use non-trailing named arguments.The defaultLangVersionof the SDK (17.13) accepts it. -
A name must match a parameter, ignoring case. A wrong or renamed name is BC30272 (
'c' is not a parameter of 'Private Sub Two(...)'), so renaming a parameter of a public procedure is a breaking change for every caller that names it, including callers in C# (which use the same names). -
A parameter cannot be supplied twice:
Two(1, a:=2)is BC30274 ("Parameter 'a' of … already has a matching argument"), and leaving a required parameter out is BC30455 ("Argument not specified for parameter 'b' of …"). -
Build("Dune", tags:=...)shows the idiom that makes named arguments valuable: three optional parameters, and the caller names only the one that is not the default. -
The
ParamArrayfunctionJoinprinted0: ` for no arguments, which is the empty array; `Total()returned 0.
Overloading
A name can be declared more than once in the same type or module as long as the declarations differ in their signature: the number, order and types of the parameters (and for generic procedures, the number of type parameters). The following do not make a different signature, and each is an error:
| Difference only in | Error | Example |
|---|---|---|
return type |
BC30301 |
|
parameter names |
BC30269 ("multiple definitions with identical signatures") |
|
|
BC30345 |
|
|
BC30368 |
|
the default value of an optional parameter |
BC30305 |
|
These are different signatures: Sub B(x As Integer, Optional y As Integer = 0) next to Sub B(x As Integer) (the compiler accepts both, and B(1) binds the
one without optional parameters), and Sub C(Of T)(x As T) next to Sub C(Of T, U)(x As T) (different type-parameter count). An overload set that depends on an optional
parameter is easy to get subtly wrong; give each overload a different parameter list instead.
Overloads is needed in one case: a derived class that declares a member with the same name as an inherited one and wants both visible. It is optional
for overloads that are declared in the same type. Without it the derived
member shadows the base members by name; the compiler warns with BC40003 ("sub 'Print' shadows an overloadable member declared in the base class 'Printer'. If you want to overload the
base method, this method must be declared 'Overloads'"), and the base overloads disappear from the derived class’s view, so p.Print(1) then fails with BC30512.
Shadows hides all base members of that name deliberately, whatever their signature, and a call through a base-class variable still reaches the base member.
Resolution
When several overloads have the right name, the compiler chooses with the following process (the full rules are in chapter "Overload Resolution" of the language specification):
-
Collect the accessible members with the name. Any candidate that cannot accept the number and names of the arguments is dropped (an optional parameter may be omitted, a
ParamArraymay take extra arguments). -
Drop the candidates for which some argument has no implicit conversion to the parameter type. With
Option Strict On, narrowing conversions are not implicit, so they are dropped here; withOption Strict Offthey remain candidates of last resort, and conversions fromObjectcan turn the call into a late-bound call (see Early and Late Binding). -
Of those left, choose the most specific one: candidate A is better than B if, for each argument, A’s parameter type converts implicitly to B’s and not the other way (so
Integeris more specific thanLong, andLongmore specific thanObject). If no single candidate wins, the call is ambiguous. -
Further tie-breakers are listed in the specification. The ones the examples below exercised: a member that needs no omitted optional arguments is preferred, and a non-generic member is preferred over a generic one.
-
Since Visual Basic 17.13,
OverloadResolutionPriorityAttributealso takes part (see below); in the test it outranked specificity.
Option Strict On
Module Program
Private Function Show(value As Integer) As String
Return "Integer"
End Function
Private Function Show(value As Long) As String
Return "Long"
End Function
Private Function Show(value As Double) As String
Return "Double"
End Function
Private Function Show(value As Object) As String
Return "Object"
End Function
Private Function Show(value As String) As String
Return "String"
End Function
Private Function Pick(a As Integer, b As Integer) As String
Return "(Integer, Integer)"
End Function
Private Function Pick(a As Integer, ParamArray rest As Integer()) As String
Return "(Integer, ParamArray)"
End Function
Private Function Pick(a As Double, Optional b As Double = 0) As String
Return "(Double, Optional Double)"
End Function
Private Function Gen(Of T)(value As T) As String
Return "Gen(Of " & GetType(T).Name & ")"
End Function
Private Function Gen(value As Integer) As String
Return "Gen(Integer)"
End Function
Sub Main()
Dim b As Byte = 1
Dim s As Short = 2
Dim sg As Single = 1.5F
Dim d As Decimal = 1D
Console.WriteLine("Byte -> " & Show(b))
Console.WriteLine("Short -> " & Show(s))
Console.WriteLine("Integer -> " & Show(3))
Console.WriteLine("Long -> " & Show(3L))
Console.WriteLine("Single -> " & Show(sg))
Console.WriteLine("Decimal -> " & Show(d))
Console.WriteLine("Char -> " & Show("a"c))
Console.WriteLine("String -> " & Show("a"))
Console.WriteLine(Pick(1, 2))
Console.WriteLine(Pick(1))
Console.WriteLine(Pick(1, 2, 3))
Console.WriteLine(Pick(1.5, 2))
Console.WriteLine(Pick(1.5))
Console.WriteLine(Gen(5))
Console.WriteLine(Gen("x"))
Console.WriteLine(Gen(Of Integer)(5))
Console.WriteLine(Gen(5L))
End Sub
End Module
Byte -> Integer
Short -> Integer
Integer -> Integer
Long -> Long
Single -> Double
Decimal -> Double
Char -> String
String -> String
(Integer, Integer)
(Integer, ParamArray)
(Integer, ParamArray)
(Double, Optional Double)
(Double, Optional Double)
Gen(Integer)
Gen(Of String)
Gen(Of Int32)
Gen(Of Int64)
Source: Overload Resolution (language specification)
How to read the results:
-
ByteandShortarguments selectedInteger: with noByteorShortoverload, the nearest wider type is chosen, andIntegeris the narrowest ofInteger,Long,DoubleandObjectthat every smaller integral type converts to.SingleandDecimalselectedDouble(a widening conversion), notObject. -
A
Charselected theStringoverload (aCharwidens toString; it does not convert toIntegerimplicitly). -
Pick(1)chose theParamArrayoverload over(Double, Optional Double):Integeris a better match for the first parameter thanDouble, even though theDoubleversion needs no expanded array.Pick(1, 2, 3)could only useParamArray. -
Gen(5)picked the non-generic overload,Gen(5L)the generic one withTinferred asInt64, andGen(Of Integer)(5)explicitly selected the generic one: type arguments in the call remove the non-generic candidate.
Resolution is done at compile time from the static types of the arguments, not from the run-time type of an object. The only exception is the late-bound call that Option Strict Off
produces for Object arguments.
Ambiguity and failure
When no overload wins, or none applies, the compiler stops with one of these (messages from the 17.13 compiler):
| Code | Message (shortened) | Typical cause |
|---|---|---|
BC30521 |
Overload resolution failed because no accessible 'Show' is most specific for these arguments |
|
BC30518 |
Overload resolution failed because no accessible 'Show' can be called with these arguments |
|
BC30516 |
Overload resolution failed because no accessible 'Show' accepts this number of arguments |
|
BC30057 |
Too many arguments to 'Private Sub Two(…)' |
a call to a procedure that is not overloaded, with too many arguments |
BC30455 |
Argument not specified for parameter 'b' of 'Private Sub Two(…)' |
too few arguments, or a missing required parameter after named arguments |
BC30512 |
Option Strict On disallows implicit conversions from 'String' to 'Integer' |
a single remaining candidate whose parameter type needs a narrowing conversion |
The first message needs a cast to disambiguate: Show(CType(Nothing, String)) selected the String overload. For Option Strict On, the fix for BC30518 is an explicit conversion (CInt(d)) at the call
site (see Type Conversions).
Overloads in a hierarchy, and OverloadResolutionPriority
Option Strict On
Imports System.Runtime.CompilerServices
Public Class Printer
Public Sub Print(value As Integer)
Console.WriteLine("Printer.Print(Integer)")
End Sub
End Class
Public Class FancyPrinter
Inherits Printer
' Without Overloads the base overloads would be hidden by name (warning BC40003 about shadowing).
Public Overloads Sub Print(value As String)
Console.WriteLine("FancyPrinter.Print(String)")
End Sub
End Class
Public Class ShadowPrinter
Inherits Printer
' Shadows hides every base member with this name, whatever its signature.
Public Shadows Sub Print(value As String)
Console.WriteLine("ShadowPrinter.Print(String)")
End Sub
End Class
Public Module Api
<OverloadResolutionPriority(1)>
Public Sub Log(Of T)(value As T)
Console.WriteLine("Log(Of T) [priority 1] T=" & GetType(T).Name)
End Sub
Public Sub Log(value As String)
Console.WriteLine("Log(String) [priority 0]")
End Sub
End Module
Module Program
Sub Main()
Dim f As New FancyPrinter()
f.Print(1)
f.Print("x")
Dim sp As New ShadowPrinter()
sp.Print("x")
DirectCast(sp, Printer).Print(1)
' sp.Print(1) would not compile: the Integer overload is hidden by Shadows
Log("a")
Log(5)
End Sub
End Module
Printer.Print(Integer)
FancyPrinter.Print(String)
ShadowPrinter.Print(String)
Printer.Print(Integer)
Log(Of T) [priority 1] T=String
Log(Of T) [priority 1] T=Int32
Source: Overloads
-
FancyPrinterusedOverloads:f.Print(1)still reached the basePrint(Integer)andf.Print("x")the new one. -
ShadowPrinterusedShadows:sp.Print("x")called the derived member, and a cast to the base type reached the base member. The compiler rejectssp.Print(1), because the baseIntegeroverload is hidden. -
OverloadResolutionPriority(1)onLog(Of T)made it win over the more specificLog(String), so both calls printed the generic overload. Without the attributeLog("a")would have chosen theStringversion. The attribute is how the .NET libraries (and C# 13+ code) steer callers towards a new, preferred overload without changing the old one; Visual Basic 17.13 is the first Visual Basic version that honours it, and both reading it from a referenced assembly and applying it in Visual Basic source worked in this test. It is an advanced tool for library authors; ordinary programs should not need it.
Recursion
A procedure may call itself directly, or through another procedure (mutual recursion); declaration order inside a type or module does not matter. The pattern needs a base case that always stops the calls, and the depth is limited by the thread’s stack. The shelf’s categories are a tree, so the classic example is a recursive total.
Option Strict On
Imports System.Globalization
Public Class Category
Public ReadOnly Property Name As String
Public ReadOnly Property Books As Integer
Public ReadOnly Property Children As New List(Of Category)
Public Sub New(name As String, books As Integer)
Me.Name = name
Me.Books = books
End Sub
End Class
Module Program
Private Function CountBooks(category As Category) As Integer
Dim total As Integer = category.Books
For Each child In category.Children
total += CountBooks(child)
Next
Return total
End Function
Private Sub PrintTree(category As Category, depth As Integer)
Console.WriteLine(New String(" "c, depth * 2) & category.Name & " (" & CountBooks(category) & ")")
For Each child In category.Children
PrintTree(child, depth + 1)
Next
End Sub
Private Function Factorial(n As Integer) As Long
If n < 0 Then Throw New ArgumentOutOfRangeException(NameOf(n))
If n <= 1 Then Return 1
Return n * Factorial(n - 1)
End Function
Private Function Depth(n As Long) As Long
Return If(n = 0, 0, 1 + Depth(n - 1))
End Function
Sub Main()
Dim root As New Category("Library", 2)
Dim fiction As New Category("Fiction", 10)
fiction.Children.Add(New Category("Science fiction", 7))
fiction.Children.Add(New Category("Crime", 4))
root.Children.Add(fiction)
root.Children.Add(New Category("Reference", 5))
PrintTree(root, 0)
Console.WriteLine(Factorial(5))
Console.WriteLine(Factorial(20))
Try
Console.WriteLine(Factorial(21)) ' 21! does not fit in a Long
Catch ex As OverflowException
Console.WriteLine("OverflowException: " & ex.Message)
End Try
Console.WriteLine(Depth(10000))
End Sub
End Module
Library (28)
Fiction (21)
Science fiction (7)
Crime (4)
Reference (5)
120
2432902008176640000
OverflowException: Arithmetic operation resulted in an overflow.
10000
Source: Recursive Procedures
-
CountBooksterminates because the category tree is finite. A cycle in the data (a category that is its own ancestor) would not terminate. -
Factorial(21)threwOverflowException: arithmetic in Visual Basic is checked by default, unlike C#, where it wraps silently unlesscheckedis used. The project propertyRemoveIntegerChecks(and the compiler option-removeintchecks) turns the check off; see Numbers and Math. -
Depth(10000)worked. Each call uses stack, and very deep recursion ends in a stack overflow (StackOverflowException), which the runtime cannot catch: the process ends. The limit depends on the thread’s stack size and the size of each frame, so no depth is safe to promise. The same applies to the accidental recursion of a parameterless function name used as a variable. Replace deep recursion with a loop and an explicitStack(Of T), or run it on a thread created with a larger stack size (see Threads, Parallelism and Synchronization). -
Visual Basic has no tail-call guarantee; do not rely on the runtime turning a tail call into a jump.
-
A recursive lambda must be declared first and assigned afterwards (see below).
Static local variables
A Static local keeps its value between calls: it is initialised once, the first time its declaration is reached, and then retains whatever the procedure assigns to it.
Option Strict On
Public Class Scanner
Private Shared Function NextTicket() As Integer
Static counter As Integer = 100 ' initialised once, on the first call
counter += 1
Return counter
End Function
Public Function NextForThisInstance() As Integer
Static scans As Integer ' one per instance
scans += 1
Return scans
End Function
Public Shared Sub PrintTickets()
Console.WriteLine(NextTicket() & " " & NextTicket() & " " & NextTicket())
End Sub
End Class
Module Program
Private Function Calls() As Integer
Static n As Integer = 0
n += 1
Return n
End Function
Private Function Initialised() As Integer
Static stamp As Integer = Initialiser()
Return stamp
End Function
Private Function Initialiser() As Integer
Console.WriteLine(" initialiser runs")
Return 7
End Function
Sub Main()
Console.WriteLine(Calls() & " " & Calls() & " " & Calls())
Scanner.PrintTickets()
Scanner.PrintTickets()
Dim a As New Scanner(), b As New Scanner()
Console.WriteLine(a.NextForThisInstance() & " " & a.NextForThisInstance() & " " & b.NextForThisInstance())
Console.WriteLine(Initialised())
Console.WriteLine(Initialised())
End Sub
End Module
1 2 3
101 102 103
104 105 106
1 2 1
initialiser runs
7
7
Source: Static
-
In a
Sharedprocedure there is one copy for the whole program (Calls()andNextTicket()counted across all calls). In an instance procedure there is one per instance (acounted 1, 2 andbhad its own 1). -
The initialiser runs once (
initialiser runswas printed one time). -
Restrictions, with the compiler’s messages: not in a structure’s procedures (BC31400, "Local variables within methods of structures cannot be declared 'Static'"), not in a generic procedure (BC32068, "Local variables within generic methods cannot be declared 'Static'"), not in a lambda (BC36672, "Static local variables cannot be declared inside lambda expressions"), and it cannot be combined with
Shadows,SharedorReadOnly. -
A
Staticlocal is a hidden field of the type, so it is hidden shared state. Unit tests see the value left by the previous test; aModule-levelPrivatefield makes the state visible and resettable. C# has no equivalent: the same code there is a field.
Consuming C# ref returns
C# methods can return a reference to a variable (public ref int At(int index) => ref _copies[index];). Visual Basic cannot declare such a method, but since Visual Basic 15 it can call
one, and treats the result as an assignable expression. The "What’s new" page for Visual Basic documents this as support for C# reference return values: Visual Basic does not allow
authoring methods with reference return values, but it allows consuming and modifying them.
The example uses a two-project solution: a C# class library and a Visual Basic console project that references it. The test directory was mounted as a whole so that the project reference resolved.
namespace CsLib;
public class Slots
{
private readonly int[] _copies = { 3, 5, 8 };
public ref int At(int index) => ref _copies[index];
public ref readonly int ReadOnlyAt(int index) => ref _copies[index];
public int Total() { int t = 0; foreach (var c in _copies) t += c; return t; }
public bool TryGetCopies(int index, out int copies) { copies = 0; if (index < 0 || index >= _copies.Length) return false; copies = _copies[index]; return true; }
}
Option Strict On
Imports CsLib
Module Program
Private Sub Bump(ByRef n As Integer)
n += 100
End Sub
Sub Main()
Dim slots As New Slots()
' Reading a ref return gives a copy of the value.
Dim first As Integer = slots.At(0)
first = 99
Console.WriteLine("copy changed, slots total = " & slots.Total())
' Passing a ref return to a ByRef parameter writes through it.
Bump(slots.At(1))
Console.WriteLine("after Bump(slots.At(1)), total = " & slots.Total())
' Assigning to a call expression: try it
slots.At(2) = 1
Console.WriteLine("after slots.At(2) = 1, total = " & slots.Total())
' Consuming a C# out parameter as ByRef
Dim c As Integer
Console.WriteLine(slots.TryGetCopies(1, c) & " " & c)
End Sub
End Module
copy changed, slots total = 16
after Bump(slots.At(1)), total = 116
after slots.At(2) = 1, total = 109
True 105
Source: What’s new for Visual Basic
-
Dim first As Integer = slots.At(0)copies the value; changingfirstdoes not change the stored value (total stayed 16:3 + 5 + 8). -
Bump(slots.At(1))passes the reference to aByRefparameter, so the stored value became 105 (16 + 100 = 116). -
slots.At(2) = 1assigns to the call expression, which writes into the C# array (3 + 105 + 1 = 109): the left-hand side is the referenced variable. -
The compiler rejects the other C# reference features this test tried: a
ref readonlyreturn type is BC30657 ("'ReadOnlyAt' has a return type that is not supported or parameter types that are not supported"), andSpan(Of T)is BC30668 ("'Span(Of Integer)' is obsolete: 'Types with embedded references are not supported in this version of your compiler'"). Put the span code in a C# helper that exposes ordinary types (arrays,ReadOnlyMemory(Of T)) to the Visual Basic caller. -
A C#
outparameter is aByRefin Visual Basic (TryGetCopies(1, c)returnedTrue 105); see Interop with C# and Other .NET Languages.
Caller information
Three attributes from System.Runtime.CompilerServices fill an Optional parameter with information from the call site, so that a logging or validation helper does not need the caller to type it.
| Attribute | Value supplied | Since |
|---|---|---|
|
the name of the calling method or property |
Visual Basic 11 |
|
full path of the source file, line of the call |
Visual Basic 11 |
|
the source text of the argument given to parameter |
enabled in Visual Basic 17.0 (a feature of the 17.0 compiler, no |
Option Strict On
Imports System.Runtime.CompilerServices
Module Program
Private Sub Log(message As String, <CallerMemberName> Optional member As String = Nothing, <CallerLineNumber> Optional line As Integer = 0)
Console.WriteLine(member & ":" & line & " " & message)
End Sub
Private Sub Require(condition As Boolean, <CallerArgumentExpression("condition")> Optional expression As String = Nothing)
If Not condition Then Console.WriteLine("Failed: " & expression)
End Sub
Private Sub Process(copies As Integer)
Log("processing")
Require(copies > 0)
Require(copies < 5 AndAlso copies <> 3)
End Sub
Sub Main()
Log("starting")
Process(3)
Log("explicit member", "Custom")
End Sub
End Module
Main:21 starting
Process:15 processing
Failed: copies < 5 AndAlso copies <> 3
Custom:23 explicit member
Source: Caller Information
-
The attribute parameter must be
Optionaland its defaultNothingforString(or0forInteger): the attribute value is only used when the argument is omitted. The last call passed a member name explicitly (Custom), and the attribute was ignored for that call. -
CallerArgumentExpressionis the idea behind guard helpers:Require(copies < 5 AndAlso copies <> 3)printed the original text of the condition. The text is the exact source of the argument expression. -
CallerFilePathembeds a machine-specific path in the binary; use-pathmap/ thePathMapproject property for reproducible builds. It is not used in the examples so that the output stays independent of the build machine. These attributes are described further on Attributes, Caller Information and Reflection.
Generic procedures, tuples and local functions
Generic procedures
A procedure may declare type parameters with (Of T) after its name. Constraints use As: Larger(Of T As IComparable(Of T)). The type arguments are usually inferred from the arguments
(Larger(3, 9) infers Integer), and can be given explicitly (Larger(Of Long)(3, 9)). The details are on
Generics.
Procedures that replace local functions
Visual Basic has no procedure nested inside another procedure. The replacement is a lambda in a local variable, which closes over the enclosing locals. A recursive lambda needs a variable declared first so that the lambda can refer to it.
Option Strict On
Module Program
' Generic procedure with a constraint
Private Function Larger(Of T As IComparable(Of T))(a As T, b As T) As T
Return If(a.CompareTo(b) >= 0, a, b)
End Function
' VB has no nested (local) procedures; a lambda held in a local variable is the substitute.
Private Function Summary(titles As IEnumerable(Of String)) As String
Dim shorten = Function(t As String) If(t.Length > 6, t.Substring(0, 6) & "~", t)
Return String.Join(" | ", titles.Select(shorten))
End Function
' A recursive lambda must be declared before it is assigned.
Private Function Fib(n As Integer) As Integer
Dim f As Func(Of Integer, Integer) = Nothing
f = Function(k As Integer) If(k < 2, k, f(k - 1) + f(k - 2))
Return f(n)
End Function
' Methods can return tuples
Private Function MinMax(values As Integer()) As (Min As Integer, Max As Integer)
Return (values.Min(), values.Max())
End Function
Sub Main()
Console.WriteLine(Larger(3, 9) & " " & Larger("pear", "apple"))
Console.WriteLine(Summary({"Dune", "Frankenstein", "Emma"}))
Console.WriteLine(Fib(10))
Dim r = MinMax({4, 9, 1})
Console.WriteLine(r.Min & ".." & r.Max)
Dim ByRefLambdaOk As Action(Of Integer) = Sub(n As Integer) Console.WriteLine("lambda " & n)
ByRefLambdaOk(1)
End Sub
End Module
9 pear
Dune | Franke~ | Emma
55
1..9
lambda 1
Source: Function Procedures
-
shortenhas an inferred delegate type; useFunc(Of String, String)when the type has to be named (a parameter, a field). -
Fibdeclaresfas aFunc(Of Integer, Integer) = Nothingfirst;Dim f = Function(k As Integer) ... f(k - 1)fails with BC30980 ("Type of 'f' cannot be inferred from an expression containing 'f'"). -
MinMaxreturns a tuple with named elements, which avoids twoByRefparameters. Tuples are covered on Tuples and Anonymous Types. -
A lambda cannot have
OptionalorParamArrayparameters (BC33010), and cannot declareStaticlocals (BC36672). When a local helper needs those, it is a real private procedure. -
Iterator and async procedures keep their body in a state machine; see Iterators and Yield and Async and Await. An extension method is a
Sharedprocedure in a module marked with an attribute; see Extension Methods and Partial Methods.
Pitfalls
| Pitfall | What happens | Instead |
|---|---|---|
A function with a path that has no |
BC42105 or BC42353 warning; returns the default or |
treat the warnings as errors; add |
Using the function name as a variable in a parameterless function |
reads as a recursive call: infinite recursion and a stack overflow |
use |
Calling a function as a statement |
the result is silently discarded |
use the value, or write the call as a statement only when the result is meant to be ignored |
Expecting |
the callee can change the shared object’s members |
pass an immutable value, a copy, or an interface that only exposes reads |
Expecting |
only the callee’s copy of the reference changes |
return the new object, or use |
Passing the same variable to two |
the parameters alias each other ( |
avoid aliasing, or copy to locals first |
Extra parentheses |
the argument is passed by value, so the |
remove them when you want the result |
A property passed |
the getter runs before, and the setter always runs after, the call |
copy to a local, pass the local, assign it back |
An |
callers not recompiled keep the old value |
an overload, or |
|
the overload with the omitted argument and the one without compete |
give each overload a distinct parameter list |
A named argument that mentions a renamed parameter |
callers that are already compiled keep working (names are resolved at compile time); the next compile of each caller fails with BC30272 |
keep parameter names stable in public APIs |
A |
hidden state shared by all calls |
a |
Deep recursion |
|
a loop with an explicit |
|
the array is the argument list, not one element |
wrap it with |
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Result of a procedure |
|
return type or |
Setting the result |
|
|
Missing return on a path |
warning (BC42105, BC42353) |
error CS0161 |
Parameter passing default |
|
by value |
By reference |
|
|
|
no keyword; consumed as |
|
Variable number of arguments |
|
|
Optional parameters |
|
|
Named arguments |
|
|
Overloading on |
not allowed: |
allowed |
Local functions |
none (lambda in a variable, or a private procedure) |
local functions |
Expression-bodied members |
none |
|
|
consume only |
author and consume |
|
|
none (use a field) |
Arithmetic overflow |
checked by default |
unchecked by default |
Entry point |
|
|
Older material you may meet
Tutorials and books written for Visual Basic 6, VBA and VBScript, and some written for early .NET versions, describe things that are different in Visual Basic 17.13:
-
In Visual Basic 6 and VBA, parameters were
ByRefunless markedByVal; in .NET they areByValunless markedByRef. Converting old code without checking this changes behaviour in both directions. Some material also claims that aByValparameter "cannot be modified"; it can be assigned inside the procedure, and only the caller’s variable is safe. -
A VB6
Functionis often described as returning its value only by assigning to its name, andReturnas the VB6GoSubreturn. In Visual Basic .NETReturn valuesets the result and leaves, andGoSubdoes not exist. Treat name assignment as legacy, as recognised in code you maintain. -
Optionalparameters without a default value and tested withIsMissingwere Variant-era; the .NET form requires the default. -
Call Name(args)is common in VB6 code, where it was the way to call aSubwith parentheses; the keyword is still legal and unnecessary. -
Declarestatements andSub`s taking `Variantarguments are legacy; see The VB Family: VB6, VBA and VBScript and Migrating Legacy Visual Basic. -
Material from the Visual Basic 15.5 era says named arguments must all come last; since 15.5 a positional argument can follow a named one when it is in its own position.
-
Any material that says Visual Basic cannot use C# reference returns predates Visual Basic 15; consuming (not authoring) them has been supported since then.
Errors the compiler reports
| Code | Cause |
|---|---|
BC30647, BC30654 |
|
BC42105, BC42353 (warnings) |
a |
BC30210 |
a |
BC30800 |
a |
BC30455, BC30057, BC30516 |
too few, too many, or no overload that accepts this number of arguments |
BC30272, BC30274, BC30241 |
unknown parameter name, parameter supplied twice, non-trailing named argument below |
BC30512, BC32029 |
narrowing under |
BC30518, BC30521 |
no overload applies, or none is the most specific |
BC30301, BC30269, BC30345, BC30368, BC30305 |
overloads that differ only by return type, parameter names, |
BC40003 (warning) |
a member shadows an overloadable base member without |
BC30812, BC30202, BC30059, BC30642 |
|
BC30192, BC30050, BC30667 |
a parameter after |
BC30734 |
a local with the name of a parameter |
BC36639, BC36926, BC36927 |
a |
BC31400, BC32068, BC36672 |
|
BC30657, BC30668 |
a C# |
BC33010 |
an |
References
-
Procedures (programming guide), including Sub Procedures, Function Procedures, Procedure Parameters and Arguments and Differences Between Parameters and Arguments
-
Passing Arguments by Value and by Reference, Passing Arguments by Position and by Name, Optional Parameters and Parameter Arrays
-
Procedure Overloading, Considerations in Overloading Procedures, Overload Resolution and Recursive Procedures
-
Sub Statement, Function Statement, Return Statement, Exit Statement and Call Statement
-
ByVal, ByRef, Optional, ParamArray, Overloads, Shadows and Static
-
What’s new for Visual Basic (non-trailing named arguments in 15.5, C# reference returns in 15, overload resolution priority in 17.13) and OverloadResolutionPriorityAttribute