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

Sub Name(...) …​ End Sub

no result; leave early with Exit Sub or Return

Function

Function Name(...) As Type …​ End Function

has a result; leave early with Return value or Exit Function

Constructor

Sub New(...)

a Sub that runs when an object is created

Property accessors

Get …​ End Get, Set …​ End Set

the accessors of a Property; see Properties

Operator

Public Shared Operator and an operator symbol

a Function-like procedure with fixed names; see Operator Overloading

Event accessors

AddHandler, RemoveHandler, RaiseEvent blocks of a Custom Event

see Events and Handlers

Iterator

Iterator Function

uses Yield; see Iterators and Yield

Async

Async Sub, Async Function

uses Await; see Async and Await

External

Declare Function, Declare Sub

bodies live in a native library; see Native Interop

Lambda

Function(x) x * 2, Sub() Console.WriteLine()

anonymous; no name, and parameters cannot be Optional (BC33010)

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 Private for helpers that only the module itself calls.

  • The parameter list is optional for a Sub or Function that takes no arguments: Sub Main() and Sub Main are the same. Always write the parentheses; the formatter adds them and the call site needs them for a Function.

  • Exit Sub leaves the procedure at once. Return with no value does the same, and is the form used inside a Try block or a loop when the intent is "stop here". End Sub also leaves.

  • A Sub that is the target of a Handles clause or is registered with AddHandler is 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 value assigns 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 (by Exit Function or by reaching End 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

Notes on the output:

  • LateFeeOldStyle(4) printed 1.00 and nothing else: Exit Function left right after the assignment. LateFeeOldStyle(0) skipped the If block, reached the Console.WriteLine (the text is only accurate when daysLate is positive) and returned 0.00, the value the name held at the end.

  • LateFee(3) on its own line calls a Function and discards the result. That is legal, nothing was printed, and it is a classic bug when the author meant to use the value. The Call keyword (Call PrintReceipt("Hamlet", 1)) is optional, legal for both kinds, and rarely used in new code.

  • Return is checked against the procedure kind. Return 5 in a Sub is BC30647 ("'Return' statement in a Sub or a Set cannot return a value") and a bare Return in a Function is 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 * 2 compiles 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 Function compiles, calls itself forever and ends in a stack overflow. Use Return and 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
  • The compiler chose the code by the return type: BC42105 for Find, which returns a reference type (String) and so returned Nothing, with a warning that a null reference exception could follow; BC42353 for FirstLetter (a Char, which returned the character with code 0, printed as [0]) and for Count (an Integer). Count is also the pattern the older style makes legal: Count += v accumulates in the implicit result variable and the function returns it at End Function, and the warning is still reported because no Return appears.

  • Treat both warnings as errors (<TreatWarningsAsErrors>true</TreatWarningsAsErrors> in the project, or <WarningsAsErrors>BC42105,BC42353</WarningsAsErrors>). The fix is always to add a Return on 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 Function call used as a value needs parentheses: Dim fee = LateFee(3). A Sub call statement that passes arguments needs them too: the VB6 form Two 1, 2 is 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 ByRef parameter (see below). The extra parentheses are a quiet way to stop a ByRef parameter from changing the variable, and a quiet bug when they appear by accident in PrintReceipttitle), (daysLate.

  • A Function that returns a tuple gives a caller several results at once; the call site names them: Dim r = MinMax(values) and then r.Min and r.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 (Integer, Date, a Structure, an Enum)

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, String, delegate)

the callee gets a copy of the reference: it can change the object the reference points to (loan.Title = ..., items(0) = ...) but rebinding the parameter (loan = New Loan()) changes only its own copy

the callee gets a reference to the caller’s variable: rebinding (loan = New Loan()) replaces the object the caller’s variable holds

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: 1 and after AddByRef: 11: the Integer was copied, then referenced.

  • after RenameByVal: renamed and after RenameByRef: replaced: both calls changed Title through the shared object, but only the ByRef call could also replace the object, so loan was a different Loan afterwards. The initial object, renamed "renamed", is now unreachable.

  • AddByRefn printed 15 inside and left n at 5: the parentheses made the argument an expression. A literal or n + 1 needs no parentheses: the compiler makes a temporary. There is no error and no warning for either, so a ByRef parameter that the callee assigns to and the caller never reads back goes unnoticed.

  • The structure and the array: Fill(slot) left Copies at 0; FillByRef(slot) set it to 99. Grow(items) wrote 100 into the shared array (the caller saw it) but the ReDim created 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: with ByRef, 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 = 1 in 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 printed set Value = 0). A ReadOnly property 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: 100 shows the field already holding 100 inside the call.

  • TryParseCopies consumes the C# out parameter of Integer.TryParse by passing a ByRef parameter. Visual Basic has no out keyword: a C# out or ref parameter is seen as ByRef, and the variable passed need not be assigned first. "abc" left copies at 0 (what TryParse stores on failure); "-4" parsed successfully, so copies holds -4 even though the function then returned False for its own copies > 0 rule. A ByRef result is written whether or not the caller’s wrapper considers the call a success.

  • Option Strict On is stricter for ByRef. A ByRef argument must convert to the parameter type and back. Passing an Integer variable to a ByRef n As Long parameter 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 a Long variable to an Integer parameter 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 + 1 inside a procedure with ByRef n is BC36639 ("'ByRef' parameter 'n' cannot be used in a lambda expression"). Copy it to a local first.

  • An Async method cannot have a ByRef parameter (BC36926) and neither can an Iterator (BC36927), because the state would outlive the call.

  • A ParamArray parameter must be ByVal (ByRef ParamArray is 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 ByVal object parameter to be replaceable. loan = New Loan() inside a ByVal callee changes nothing outside.

  • Forgetting that a ByVal parameter can be assigned to (locally): assignments to a ByVal parameter are legal, and affect only the callee. Treat a parameter as read-only by convention. Material that says the parameter "cannot be modified" with ByVal is wrong for Visual Basic .NET: the compiler allows it, and only the caller’s variable is protected.

  • Passing a property ByRef and 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 no IsMissing test any more (it belonged to the Variant-based Optional of Visual Basic 6); Optional n As Integer is 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.Now is BC30059 ("Constant expression is required"); use Optional d As Date? = Nothing and test for Nothing in 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 Optional parameter cannot be combined with ParamArray (BC30642) and a lambda parameter cannot be Optional at all (BC33010).

  • Nothing as a default means "the default of the type": Nothing for a reference type, 0 for an Integer, False for a Boolean, and a zero-initialised structure. Use a nullable parameter (Integer?) when "not supplied" must be told apart from 0.

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 be Optional (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, never Nothing), 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 a String() 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)) printed 1 object(s).

  • Nothing passed to a ParamArray is the null array, not an array with one null element: both Describe(Nothing) and Describe(CType(Nothing, Object())) printed Nothing in 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 (title is the first parameter and the first argument). Before 15.5 every argument after the first named one also had to be named. With LangVersion 15.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 default LangVersion of 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 ParamArray function Join printed 0: ` 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

Function Same(x As Integer) As Integer and Function Same(x As Integer) As Long

parameter names

BC30269 ("multiple definitions with identical signatures")

Sub E(x As Integer) and Sub E(y As Integer)

ByVal versus ByRef

BC30345

Sub A(x As Integer) and Sub A(ByRef x As Integer)

ParamArray versus an array parameter

BC30368

Sub Log(value As String()) and Sub Log(ParamArray values As String())

the default value of an optional parameter

BC30305

Sub D(x As Integer, Optional y As Integer = 1) and ... = 2

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):

  1. 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 ParamArray may take extra arguments).

  2. 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; with Option Strict Off they remain candidates of last resort, and conversions from Object can turn the call into a late-bound call (see Early and Late Binding).

  3. 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 Integer is more specific than Long, and Long more specific than Object). If no single candidate wins, the call is ambiguous.

  4. 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.

  5. Since Visual Basic 17.13, OverloadResolutionPriorityAttribute also 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:

  • Byte and Short arguments selected Integer: with no Byte or Short overload, the nearest wider type is chosen, and Integer is the narrowest of Integer, Long, Double and Object that every smaller integral type converts to. Single and Decimal selected Double (a widening conversion), not Object.

  • A Char selected the String overload (a Char widens to String; it does not convert to Integer implicitly).

  • Pick(1) chose the ParamArray overload over (Double, Optional Double): Integer is a better match for the first parameter than Double, even though the Double version needs no expanded array. Pick(1, 2, 3) could only use ParamArray.

  • Gen(5) picked the non-generic overload, Gen(5L) the generic one with T inferred as Int64, and Gen(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

Show(Nothing) with Integer, Long and String overloads: Nothing converts to all of them

BC30518

Overload resolution failed because no accessible 'Show' can be called with these arguments

Show(d) with d As Double: Double narrows to every overload, so under Option Strict On none applies

BC30516

Overload resolution failed because no accessible 'Show' accepts this number of arguments

Show("a", "b")

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

  • FancyPrinter used Overloads: f.Print(1) still reached the base Print(Integer) and f.Print("x") the new one.

  • ShadowPrinter used Shadows: sp.Print("x") called the derived member, and a cast to the base type reached the base member. The compiler rejects sp.Print(1), because the base Integer overload is hidden.

  • OverloadResolutionPriority(1) on Log(Of T) made it win over the more specific Log(String), so both calls printed the generic overload. Without the attribute Log("a") would have chosen the String version. 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
  • CountBooks terminates because the category tree is finite. A cycle in the data (a category that is its own ancestor) would not terminate.

  • Factorial(21) threw OverflowException: arithmetic in Visual Basic is checked by default, unlike C#, where it wraps silently unless checked is used. The project property RemoveIntegerChecks (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 explicit Stack(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 Shared procedure there is one copy for the whole program (Calls() and NextTicket() counted across all calls). In an instance procedure there is one per instance (a counted 1, 2 and b had its own 1).

  • The initialiser runs once (initialiser runs was 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, Shared or ReadOnly.

  • A Static local is a hidden field of the type, so it is hidden shared state. Unit tests see the value left by the previous test; a Module-level Private field 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
  • Dim first As Integer = slots.At(0) copies the value; changing first does not change the stored value (total stayed 16: 3 + 5 + 8).

  • Bump(slots.At(1)) passes the reference to a ByRef parameter, so the stored value became 105 (16 + 100 = 116).

  • slots.At(2) = 1 assigns 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 readonly return type is BC30657 ("'ReadOnlyAt' has a return type that is not supported or parameter types that are not supported"), and Span(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# out parameter is a ByRef in Visual Basic (TryGetCopies(1, c) returned True 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

CallerMemberName

the name of the calling method or property

Visual Basic 11

CallerFilePath, CallerLineNumber

full path of the source file, line of the call

Visual Basic 11

CallerArgumentExpression("param")

the source text of the argument given to parameter param

enabled in Visual Basic 17.0 (a feature of the 17.0 compiler, no LangVersion value of its own)

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
  • The attribute parameter must be Optional and its default Nothing for String (or 0 for Integer): 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.

  • CallerArgumentExpression is 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.

  • CallerFilePath embeds a machine-specific path in the binary; use -pathmap / the PathMap project 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
  • shorten has an inferred delegate type; use Func(Of String, String) when the type has to be named (a parameter, a field).

  • Fib declares f as a Func(Of Integer, Integer) = Nothing first; Dim f = Function(k As Integer) ... f(k - 1) fails with BC30980 ("Type of 'f' cannot be inferred from an expression containing 'f'").

  • MinMax returns a tuple with named elements, which avoids two ByRef parameters. Tuples are covered on Tuples and Anonymous Types.

  • A lambda cannot have Optional or ParamArray parameters (BC33010), and cannot declare Static locals (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 Shared procedure 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 Return

BC42105 or BC42353 warning; returns the default or Nothing

treat the warnings as errors; add Return or Throw

Using the function name as a variable in a parameterless function

reads as a recursive call: infinite recursion and a stack overflow

use Return and a local

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 ByVal to protect an object

the callee can change the shared object’s members

pass an immutable value, a copy, or an interface that only exposes reads

Expecting loan = New Loan() to replace the caller’s object with ByVal

only the callee’s copy of the reference changes

return the new object, or use ByRef deliberately

Passing the same variable to two ByRef parameters

the parameters alias each other (AddBoth(x, x) added 2)

avoid aliasing, or copy to locals first

Extra parentheses Fx or F(x + 0)

the argument is passed by value, so the ByRef result is lost

remove them when you want the result

A property passed ByRef

the getter runs before, and the setter always runs after, the call

copy to a local, pass the local, assign it back

An Optional default that changes in a library

callers not recompiled keep the old value

an overload, or Nothing and a default in the body

Optional plus overloads that differ only by it

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 Static local in code that tests need to reset

hidden state shared by all calls

a Private field

Deep recursion

StackOverflowException, uncatchable

a loop with an explicit Stack(Of T)

ParamArray of Object and an array argument

the array is the argument list, not one element

wrap it with CObj

Differences from C#

Topic Visual Basic C#

Result of a procedure

Function ... As T / Sub

return type or void

Setting the result

Return value, or assigning to the function’s name

return value;

Missing return on a path

warning (BC42105, BC42353)

error CS0161

Parameter passing default

ByVal

by value

By reference

ByRef (the caller writes nothing special)

ref, out, in, written at the call site too

out parameters

no keyword; consumed as ByRef, no definite-assignment guarantee for the callee

out: must be assigned before the method returns

Variable number of arguments

ParamArray (an array only)

params (arrays, and in recent versions spans and collections)

Optional parameters

Optional x As T = constant; Nothing allowed

T x = constant; also default

Named arguments

name:=value

name: value

Overloading on ref/out

not allowed: ByVal and ByRef do not differ

allowed

Local functions

none (lambda in a variable, or a private procedure)

local functions

Expression-bodied members

none

=> expression

ref returns and ref locals

consume only

author and consume

Static locals

Static x As T keeps a value between calls

none (use a field)

Arithmetic overflow

checked by default

unchecked by default

Entry point

Sub Main or Function Main in a module; no Async Main

static void Main, top-level statements, async Task Main

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 ByRef unless marked ByVal; in .NET they are ByVal unless marked ByRef. Converting old code without checking this changes behaviour in both directions. Some material also claims that a ByVal parameter "cannot be modified"; it can be assigned inside the procedure, and only the caller’s variable is safe.

  • A VB6 Function is often described as returning its value only by assigning to its name, and Return as the VB6 GoSub return. In Visual Basic .NET Return value sets the result and leaves, and GoSub does not exist. Treat name assignment as legacy, as recognised in code you maintain.

  • Optional parameters without a default value and tested with IsMissing were Variant-era; the .NET form requires the default.

  • Call Name(args) is common in VB6 code, where it was the way to call a Sub with parentheses; the keyword is still legal and unnecessary.

  • Declare statements and Sub`s taking `Variant arguments 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

Return value in a Sub, or Return without a value in a Function

BC42105, BC42353 (warnings)

a Function path reaches End Function without Return

BC30210

a Function without As Type under Option Strict On

BC30800

a Sub called with arguments but without parentheses (Two 1, 2)

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 LangVersion 15.5

BC30512, BC32029

narrowing under Option Strict On, including the copy-back of a ByRef argument

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, ByVal/ByRef, ParamArray, or the optional default

BC40003 (warning)

a member shadows an overloadable base member without Overloads or Shadows

BC30812, BC30202, BC30059, BC30642

Optional without a default, an Optional followed by a required parameter, a non-constant default, or Optional with ParamArray

BC30192, BC30050, BC30667

a parameter after ParamArray, a ParamArray that is not an array, or ByRef ParamArray

BC30734

a local with the name of a parameter

BC36639, BC36926, BC36927

a ByRef parameter used in a lambda, in an Async method, or in an Iterator

BC31400, BC32068, BC36672

Static local in a structure, a generic procedure, or a lambda

BC30657, BC30668

a C# ref readonly return, or Span(Of T), used from Visual Basic

BC33010

an Optional lambda parameter