Delegates and Lambda Expressions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so and show the diagnostics the compiler printed, and the two that show a warning were built with warnings allowed. The scratch projects use the root namespace ShelfDocs, which is why a type name printed by the runtime can begin with ShelfDocs.. 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), Task and the LINQ operators need no Imports line; System.Reflection and System.Linq.Expressions are imported where they are used.

A delegate is a type-safe object that refers to a method, and a lambda expression is an inline method written where a delegate is expected. Together they are how Visual Basic passes behaviour around: sort orders, filters, callbacks, event handlers, the operators of LINQ and the continuations of Async code are all delegates. Both are plain .NET features, so what is said here also applies to C# and F# code that calls Visual Basic code, and the platform detail (how the runtime represents delegates, their cost, the rules of variance) is covered once on the C# pages Delegates, Lambdas and Events and Expression Trees and Dynamic. This page explains the Visual Basic syntax and the Visual Basic rules. Events, which are built on delegates, are on Events and Handlers; procedures, whose signatures delegates copy, are on Procedures: Sub and Function.

Construct What it creates Typical use

Delegate Sub or Delegate Function

a new delegate type with a fixed signature

named callback types, event signatures, ByRef parameters

AddressOf Method

a delegate instance that refers to an existing method (a method group conversion)

passing a named procedure, AddHandler

Function(x) ...

an inline function; a delegate instance (or an expression tree)

predicates, selectors, comparers

Sub(x) ...

an inline procedure that returns nothing

callbacks, event handlers, Action

Async Function and Async Sub lambdas

an inline Async method

Task.Run, retry helpers, event handlers

Iterator Function lambda

an inline iterator that `Yield`s a sequence

small generators

Expression(Of TDelegate)

the lambda as a data structure that can be inspected

LINQ providers such as EF Core

The Delegate statement

Delegate declares a type, not a variable. The statement names a signature, and the compiler generates a sealed class that derives from System.MulticastDelegate, with a constructor that takes an object and a method, and an Invoke method that has exactly the declared parameters and result. You never write that class; you write the declaration and use the instances.

flowchart TD D["System.Delegate"] --> M["System.MulticastDelegate"] M --> B["BookFilter (your Delegate declaration)"] M --> F["Func(Of T, TResult) and Action(Of T) (framework delegates)"] B --> I["instance: Method, Target and an invocation list"] F --> I

The general form is [access] [Shadows] Delegate Sub|Function name[(Of type parameters)][(parameters)] [As type]. A Delegate Sub has no result; a Delegate Function has one, written with As. Under Option Strict On the result type is required, as it is for any Function. The access modifiers are the same as for any type (Public, Friend, and Private or Protected when the delegate is nested in a class); at namespace or module level the default is Friend. A delegate can be generic, and can be declared inside a class or module like any nested type. Parameters can be ByVal (the default) or ByRef, which is something the framework’s Func and Action cannot express, and is one of the few reasons to declare your own delegate type today.

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub
End Class

Public Delegate Function BookFilter(book As Book) As Boolean

Public Class Rules
    Private ReadOnly _minYear As Integer

    Public Sub New(minYear As Integer)
        _minYear = minYear
    End Sub

    Public Function IsRecent(book As Book) As Boolean
        Return book.Year >= _minYear
    End Function

    Public Shared Function HasLongTitle(book As Book) As Boolean
        Return book.Title.Length > 12
    End Function
End Class

Module Program
    Private Function Pick(books As List(Of Book), filter As BookFilter) As List(Of Book)
        Dim result As New List(Of Book)
        For Each b In books
            If filter(b) Then result.Add(b)
        Next
        Return result
    End Function

    Sub Main()
        Dim books As New List(Of Book) From {
            New Book("Dune", 1965),
            New Book("The Left Hand of Darkness", 1969),
            New Book("Neuromancer", 1984),
            New Book("Parable of the Sower", 1993),
            New Book("Ancillary Justice", 2013)
        }
        Dim rules As New Rules(1980)

        ' Three ways to create the same kind of delegate instance
        Dim recent As New BookFilter(AddressOf rules.IsRecent)
        Dim longTitle As BookFilter = AddressOf Rules.HasLongTitle
        Dim inline As BookFilter = Function(b) b.Title.StartsWith("The ", StringComparison.Ordinal)

        Console.WriteLine("recent:    " & Pick(books, recent).Count)
        Console.WriteLine("longTitle: " & Pick(books, longTitle).Count)
        Console.WriteLine("inline:    " & Pick(books, inline).Count)
        Console.WriteLine("direct:    " & Pick(books, AddressOf rules.IsRecent).Count)

        ' What a delegate instance carries
        Console.WriteLine(recent.Method.Name & ", target is Rules: " & (TypeOf recent.Target Is Rules))
        Console.WriteLine(longTitle.Method.Name & ", target is Nothing: " & (longTitle.Target Is Nothing))

        ' Invoking: the delegate name followed by arguments, or Invoke
        Dim first = books(0)
        Console.WriteLine(recent(first) & " " & recent.Invoke(books(4)))

        ' Delegate equality compares method and target
        Dim again As New BookFilter(AddressOf rules.IsRecent)
        Console.WriteLine("equal: " & recent.Equals(again) & ", same object: " & ReferenceEquals(recent, again))
    End Sub
End Module
recent:    3
longTitle: 3
inline:    1
direct:    3
IsRecent, target is Rules: True
HasLongTitle, target is Nothing: True
False True
equal: True, same object: False

Points to take away from the example:

  • New BookFilter(AddressOf rules.IsRecent) is the explicit form. Dim longTitle As BookFilter = AddressOf Rules.HasLongTitle and passing AddressOf rules.IsRecent straight to a BookFilter parameter are the implicit forms of the same conversion; the compiler inserts the constructor call. A lambda (inline) converts to the delegate the same way.

  • A delegate instance carries the method (Method) and, for an instance method, the object it will be called on (Target). A delegate created from a Shared method has a Nothing target. That Target keeps the object reachable: a long-lived delegate (an event subscription is the classic case) keeps that object alive.

  • Calling the delegate as recent(first) is shorthand for recent.Invoke(first). Both are checked at compile time against the declared signature.

  • Two delegate instances are equal (Equals) when they have the same type, the same method and the same target, even though they are different objects, so ReferenceEquals is False. This is what lets RemoveHandler find a handler that was added with a separate AddressOf expression, and it is why it cannot find a lambda (see the section on events below).

  • A delegate type has no Nothing value of its own; an unassigned delegate variable is Nothing, and calling it raises NullReferenceException. Guard with If handler IsNot Nothing Then handler(x) or, since Visual Basic 14, call handler?.Invoke(x). The explicit Invoke is the clearest spelling of the null-conditional call.

A generic delegate and a nested one:

Option Strict On

' A generic delegate: the type parameters follow the name
Public Delegate Function Transform(Of TIn, TOut)(value As TIn) As TOut

' A delegate declared inside a class is a nested type
Public Class Importer
    Public Delegate Sub Progress(done As Integer, total As Integer)

    Public Sub Run(onProgress As Progress)
        For i = 1 To 3
            onProgress(i, 3)
        Next
    End Sub
End Class

Module Program
    Private Function Map(Of TIn, TOut)(items As IEnumerable(Of TIn), f As Transform(Of TIn, TOut)) As List(Of TOut)
        Dim result As New List(Of TOut)
        For Each item In items
            result.Add(f(item))
        Next
        Return result
    End Function

    Sub Main()
        Dim lengths = Map({"Dune", "Kindred"}, Function(s As String) s.Length)
        Console.WriteLine(String.Join(",", lengths))

        Dim t As Transform(Of Integer, String) = Function(n) "#" & n
        Console.WriteLine(t(5))
        Console.WriteLine(GetType(Transform(Of Integer, String)).BaseType?.Name)
        Console.WriteLine(GetType(Importer.Progress).FullName)

        Dim importer As New Importer()
        importer.Run(Sub(done, total) Console.WriteLine(done & "/" & total))
        Dim members = GetType(Transform(Of Integer, String)).GetMethods().Select(Function(m) m.Name).Where(Function(n) n = "Invoke" OrElse n = "BeginInvoke").OrderBy(Function(n) n)
        Console.WriteLine(String.Join(",", members))
    End Sub
End Module
4,7
#5
MulticastDelegate
ShelfDocs.Importer+Progress
1/3
2/3
3/3
BeginInvoke,Invoke

The type parameters follow the delegate name (Transform(Of TIn, TOut)), and Map infers TOut from the lambda’s return type, the same inference that runs for generic methods (see Generics). The nested delegate’s full name, ShelfDocs.Importer+Progress, shows that it is a type inside Importer, like a nested Class. The BeginInvoke and EndInvoke members still exist on the generated type (the last line lists Invoke and BeginInvoke), but see the section on invoking delegates for what happens when you call them on .NET 10.

AddressOf and method groups

AddressOf is a unary operator whose operand is a method group: the name of one or more overloads of a procedure. It has no type of its own. It only produces a value when the compiler can convert it to a delegate type, so it must appear where a delegate type is expected: an assignment to a typed variable, an argument to a delegate-typed parameter, AddHandler, a New SomeDelegate(...), or a cast. Dim d = AddressOf M has nothing to infer from and is an error (BC30581, shown further down). The operand can be:

  • an instance method (AddressOf rules.IsRecent, AddressOf Me.Save, AddressOf MyBase.Save and AddressOf MyClass.Save for the base-class and non-overridable-view variants), which captures that object as the Target;

  • a Shared method or a procedure in a Module (AddressOf Rules.HasLongTitle, AddressOf Char.IsDigit), which has no target;

  • a constructed generic method (AddressOf Map(Of String, Object)) or an overloaded one, in which case the overload is selected by the signature of the target delegate, not by arguments.

The overload selection is shown by the next example, which also shows relaxed conversion, covered in the next section.

Option Strict On

Imports System.Reflection

Public Delegate Function Combine(a As Integer, b As Integer) As Integer
Public Delegate Sub Swap(ByRef a As Integer, ByRef b As Integer)

Module Program
    Private Function Add(a As Integer, b As Integer) As Integer
        Return a + b
    End Function

    Private Sub SwapValues(ByRef a As Integer, ByRef b As Integer)
        Dim t = a
        a = b
        b = t
    End Sub

    Private Function Describe(value As Integer) As String
        Return "Integer " & value
    End Function

    Private Function Describe(value As String) As String
        Return "String " & value
    End Function

    Sub Main()
        Dim add2 As New Combine(AddressOf Add)

        ' Late-bound invocation: slower, wraps exceptions in TargetInvocationException
        Console.WriteLine(add2.DynamicInvoke(2, 3))
        Try
            add2.DynamicInvoke("x", 3)
        Catch ex As ArgumentException
            Console.WriteLine("DynamicInvoke bad argument: " & ex.GetType().Name)
        End Try

        ' Delegate.CreateDelegate from reflection
        Dim m = GetType(Program).GetMethod("Add", BindingFlags.NonPublic Or BindingFlags.Static)
        Dim viaReflection = CType([Delegate].CreateDelegate(GetType(Combine), m), Combine)
        Console.WriteLine("CreateDelegate: " & viaReflection(10, 20))

        ' ByRef parameters work with delegates
        Dim swapper As Swap = AddressOf SwapValues
        Dim x = 1, y = 2
        swapper(x, y)
        Console.WriteLine("swapped: " & x & "," & y)

        ' AddressOf resolves overloads by the delegate signature
        Dim forInt As Func(Of Integer, String) = AddressOf Describe
        Dim forStr As Func(Of String, String) = AddressOf Describe
        Console.WriteLine(forInt(7) & " / " & forStr("seven"))

        ' The asynchronous delegate pattern of .NET Framework is not supported
        Try
            add2.BeginInvoke(1, 2, Nothing, Nothing)
        Catch ex As PlatformNotSupportedException
            Console.WriteLine("BeginInvoke: " & ex.GetType().Name)
        End Try

        ' Variance: generic delegates are covariant in results, contravariant in parameters
        Dim makeText As Func(Of String) = Function() "text"
        Dim makeObject As Func(Of Object) = makeText
        Dim handleObject As Action(Of Object) = Sub(o) Console.WriteLine("object: " & o.ToString())
        Dim handleString As Action(Of String) = handleObject
        handleString("variance")
        Console.WriteLine(makeObject())

        ' Delegate types in the Framework: Func/Action/Predicate/Comparison/Converter
        Dim toLength As Converter(Of String, Integer) = Function(s) s.Length
        Console.WriteLine(Array.ConvertAll({"a", "bcd"}, toLength).Sum())
    End Sub
End Module
5
DynamicInvoke bad argument: ArgumentException
CreateDelegate: 30
swapped: 2,1
Integer 7 / String seven
BeginInvoke: PlatformNotSupportedException
object: variance
text
4

What the example shows besides overload selection:

  • DynamicInvoke is the late-bound call: it takes the arguments as Object values and checks them at run time. A wrong argument type is an ArgumentException rather than a compile error, and an exception thrown by the target is wrapped in a TargetInvocationException. Use Invoke (or the call syntax) when the delegate type is known; reserve DynamicInvoke for plumbing code that really does not know the signature.

  • Delegate.CreateDelegate builds a delegate from a MethodInfo. It is how dependency-injection and serialization libraries create fast callable delegates from reflected methods, and it is the only way to turn a name into a delegate (AddressOf needs the name at compile time).

  • ByRef parameters work: swapper(x, y) changes x and y. The framework’s generic Func and Action cannot declare ByRef parameters, so a method that must update its arguments needs a declared delegate type.

  • The BeginInvoke and EndInvoke members that the compiler generates implement the asynchronous delegate pattern of .NET Framework. They are not implemented on .NET Core and later; the call above throws PlatformNotSupportedException. Code in older books and samples that starts work with BeginInvoke has to move to Task.Run, Async and Await, or a thread (see Async and Await and Threads, Parallelism and Synchronization).

  • Variance. Func(Of String) converts to Func(Of Object) (the result type is covariant, declared Out), and Action(Of Object) converts to Action(Of String) (the parameter type is contravariant, declared In). The variance of a generic delegate is fixed by its declaration, and a Delegate you write can declare it too: Delegate Function Factory(Of Out T)() As T. Variance applies to reference types only, and is a separate feature from relaxed conversion.

Relaxed delegate conversion

The signature of the method (or lambda) and the signature of the delegate do not have to be identical. Since Visual Basic 9, the conversion succeeds when each argument the delegate supplies can be converted to the corresponding parameter, and the method’s result can be converted to the delegate’s result, using the conversions allowed by Option Strict. The rules, with Option Strict On:

Difference Rule Example in this page

Parameter types

the delegate’s parameter type must widen to the method’s parameter type (a Magazine to a Book, anything to Object)

AddressOf Show takes Object for a Book argument

Result type

the method’s result type must widen to the delegate’s result type (Integer to Object)

AddressOf Length for a Counter that returns Object

Function for Sub delegate

allowed: the result is discarded

AddressOf Store and Function() 1 for an Action

Fewer parameters than the delegate

allowed for a lambda (it may declare none); not allowed for AddressOf under Option Strict On

Sub() Announce() accepted, AddressOf Announce rejected

More parameters than the delegate

never allowed, because there is nothing to supply the argument

BC31143

Narrowing (an Integer parameter for a String argument, a String result for an Integer result)

rejected under Option Strict On with BC36663; accepted with strict typing off and checked at run time

BC36663 below

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String
    Public Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Magazine
    Inherits Book
    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Public Delegate Sub BookHandler(sender As Object, book As Book)
Public Delegate Function Counter(text As String) As Object

Module Program
    ' Wider parameter types than the delegate: allowed (argument widens)
    Private Sub Show(sender As Object, item As Object)
        Console.WriteLine("show: " & item.ToString())
    End Sub

    ' Narrower return type than the delegate result: allowed (result widens)
    Private Function Length(text As String) As Integer
        Return text.Length
    End Function

    ' A Function can be used where a Sub delegate is expected: result discarded
    Private Function Store(sender As Object, book As Book) As Integer
        Console.WriteLine("store: " & book.Title)
        Return 42
    End Function

    Private Sub ShowBook(book As Book)
        Console.WriteLine("book: " & book.Title)
    End Sub

    Sub Main()
        ' A lambda may declare no parameters at all, although AddressOf may not drop them
        Dim handler As BookHandler = Sub() Console.WriteLine("announce: something happened")
        handler(Nothing, New Book("Dune"))

        handler = AddressOf Show
        handler(Nothing, New Magazine("Wired"))

        handler = AddressOf Store
        handler(Nothing, New Book("Kindred"))

        Dim count As Counter = AddressOf Length
        Console.WriteLine("count boxed: " & count("catalogue").GetType().Name & " " & count("catalogue").ToString())

        ' A lambda gets the same relaxation: its parameters take the delegate types
        Dim viaLambda As BookHandler = Sub(s, b) ShowBook(b)
        viaLambda(Nothing, New Book("Hyperion"))

        ' Func(Of ...) accepts a method group with a narrower return
        Dim f As Func(Of String, Object) = AddressOf Length
        Console.WriteLine(f("abc"))
    End Sub
End Module
announce: something happened
show: ShelfDocs.Magazine
store: Kindred
count boxed: Int32 9
book: Hyperion
3

Notice the first line: handler = Sub() Console.WriteLine(...) declares no parameters, although BookHandler supplies two. A lambda can ignore the arguments it does not need. Its counterpart for named procedures needs a small wrapper lambda, as the next example shows, because AddressOf Announce is rejected. Descriptions of relaxed conversion often list dropping all the arguments of a handler as an unconditional relaxation; the compiler of the SDK used for this page rejects it for AddressOf with Option Strict On, and that is what is documented here.

The failures, in one program (this one is expected to fail, so it was built with warnings allowed and the diagnostics are shown):

Option Strict On

Public Delegate Sub TextHandler(text As String)
Public Delegate Function Parser(text As String) As Integer

Module Program
    Private Sub TakesInteger(value As Integer)
    End Sub

    Private Sub TakesObject(value As Object)
    End Sub

    Private Function ReturnsString(text As String) As String
        Return text
    End Function

    Private Sub MoreParameters(a As String, b As String)
    End Sub

    Sub Main()
        Dim a As TextHandler = AddressOf TakesInteger   ' narrowing Integer parameter
        Dim b As Parser = AddressOf ReturnsString   ' String result to Integer
        Dim c As TextHandler = AddressOf MoreParameters   ' extra parameter
        Dim d = AddressOf TakesObject   ' no target type
        Dim e As TextHandler = Function(t) t   ' accepted: result is discarded
        Dim f As Parser = Function(t) t.Length.ToString()   ' String result to Integer
    End Sub
End Module
Program.vb(21,42): error BC36663: Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub TakesInteger(value As Integer)' and delegate 'Delegate Sub TextHandler(text As String)'.
Program.vb(22,37): error BC36663: Option Strict On does not allow narrowing in implicit type conversions between method 'Private Function ReturnsString(text As String) As String' and delegate 'Delegate Function Parser(text As String) As Integer'.
Program.vb(23,42): error BC31143: Method 'Private Sub MoreParameters(a As String, b As String)' does not have a signature compatible with delegate 'Delegate Sub TextHandler(text As String)'.
Program.vb(24,17): error BC30581: 'AddressOf' expression cannot be converted to 'Object' because 'Object' is not a delegate type.
Program.vb(26,39): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.

Source: BC36663

BC36663 is the narrowing diagnostic for a delegate conversion, BC31143 the signature mismatch (a parameter too many), and BC30581 the missing target type. The line Dim e As TextHandler = Function(t) t compiled without a diagnostic: a Function lambda is a valid Sub delegate, and the value is dropped. The last line fails for a different reason: the lambda’s result is String and Parser returns Integer, which is the ordinary narrowing error BC30512 you get from any String to Integer assignment.

The AddressOf rule on its own, with the accepted lambda next to it:

Option Strict On

Public Delegate Sub BookHandler(sender As Object, title As String)

Module Program
    Private Sub Announce()
        Console.WriteLine("announce")
    End Sub

    Sub Main()
        ' Rejected: AddressOf may not drop parameters under Option Strict On
        Dim viaMethod As BookHandler = AddressOf Announce

        ' Accepted: a lambda may declare fewer parameters
        Dim viaLambda As BookHandler = Sub() Announce()
        viaLambda(Nothing, "Dune")
    End Sub
End Module
Program.vb(12,50): error BC36663: Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub Announce()' and delegate 'Delegate Sub BookHandler(sender As Object, title As String)'.

With strict typing turned off (the SDK default for a project that does not set OptionStrict, see Option Statements) the same assignments compile. A method that takes Integer can then be assigned to a String delegate, the dropped-parameter assignment is accepted, and the argument is converted at run time: when this was tried on .NET 10, a delegate that took an Integer received "42" and printed it, and received "forty-two" and threw an InvalidCastException with the message Conversion from string "forty-two" to type 'Integer' is not valid. That is the usual argument for keeping Option Strict On: the same code is a compile-time error instead of a run-time surprise.

Relaxed conversion is also what makes event handlers flexible: a Handles clause or AddHandler can attach a handler whose sender parameter is Object to an event that declares a more specific type, and a handler that takes the specific event arguments can serve an event that supplies a derived class. In Visual Basic 6 an event handler had a fixed shape; today it only has to be compatible.

Func, Action, Predicate and the other framework delegates

Most code never declares a delegate type. The base class library provides generic ones, and they are interchangeable with any delegate of the same shape only through a lambda or method group (two different delegate types with identical signatures are not convertible to each other directly).

Delegate Signature Used by

Func(Of TResult) up to Func(Of T1, ..., T16, TResult)

takes up to 16 arguments and returns a TResult

LINQ selectors, factories, Task.Run

Action, Action(Of T) up to 16 arguments

takes arguments, returns nothing

callbacks, List(Of T).ForEach, Task.Run

Predicate(Of T)

T in, Boolean out

List(Of T).FindAll, Exists, RemoveAll, Array.Find

Comparison(Of T)

two T in, Integer out

List(Of T).Sort, Array.Sort

Converter(Of TInput, TOutput)

one in, one out

Array.ConvertAll, List(Of T).ConvertAll

EventHandler and EventHandler(Of TEventArgs)

(sender As Object, e As TEventArgs)

the standard event pattern

Func(Of T, Boolean)

the same shape as Predicate(Of T) but a different type

LINQ Where

A lambda or method group converts to any of them. A Func(Of T, Boolean) is not a Predicate(Of T): passing a Func variable to List(Of T).FindAll is a type error even though the shapes match, which is the case where you wrap it, list.FindAll(Function(x) predicateFunc(x)). Prefer Func and Action in new code and declare a named delegate when the name documents a contract (BookFilter), when you need ByRef, or when the signature is part of a public API that you want to read clearly in IntelliSense and in the object browser.

Invoking delegates and null delegates

The ways to call a delegate:

Form Notes

d(args)

the usual form; compiles to d.Invoke(args)

d.Invoke(args)

explicit; required with the null-conditional operator: d?.Invoke(args)

d.DynamicInvoke(args)

late-bound, slower, arguments are Object; target exceptions are wrapped in TargetInvocationException

d.BeginInvoke(...) and d.EndInvoke(...)

asynchronous delegate pattern of .NET Framework; throws PlatformNotSupportedException on .NET 10

RaiseEvent Name(args)

for a delegate declared as an Event; does nothing when no handler is attached

A delegate variable that was never assigned, or from which the last handler was removed, is Nothing. Invoking it throws NullReferenceException — except in the case of an Event, where RaiseEvent checks for you.

Multicast delegates

Every delegate is a multicast delegate: it holds an invocation list of one or more (method, target) pairs, called in the order they were combined. Single delegates are lists of length one. Visual Basic has no operator for combining delegates: unlike C#, + and += are not defined for delegate types (BC30452, shown below). You combine with Delegate.Combine and Delegate.Remove, both of which return the base type Delegate, so the result needs a CType (or DirectCast) back to the specific delegate. The only syntax for subscribing and unsubscribing is AddHandler and RemoveHandler on an Event.

Option Strict On

Public Delegate Sub Notify(message As String)
Public Delegate Function Score(text As String) As Integer

Module Program
    Private Sub ToConsole(message As String)
        Console.WriteLine("console: " & message)
    End Sub

    Private Sub ToLog(message As String)
        Console.WriteLine("log:     " & message)
    End Sub

    Private Sub Explode(message As String)
        Throw New InvalidOperationException("sink failed for " & message)
    End Sub

    Private Function One(text As String) As Integer
        Return 1
    End Function

    Private Function Two(text As String) As Integer
        Return 2
    End Function

    Sub Main()
        ' Delegate.Combine / Remove build and shrink an invocation list
        Dim notify As Notify = AddressOf ToConsole
        notify = CType([Delegate].Combine(notify, New Notify(AddressOf ToLog)), Notify)
        notify("loan created")
        Console.WriteLine("handlers: " & notify.GetInvocationList().Length)

        notify = CType([Delegate].Remove(notify, New Notify(AddressOf ToConsole)), Notify)
        notify("loan returned")

        ' Removing the last handler yields Nothing, so guard before invoking
        notify = CType([Delegate].Remove(notify, New Notify(AddressOf ToLog)), Notify)
        Console.WriteLine("empty list is Nothing: " & (notify Is Nothing))
        If notify IsNot Nothing Then notify("never printed")
        notify?.Invoke("also never printed")

        ' With a return value only the last result is returned
        Dim score As Score = AddressOf One
        score = CType([Delegate].Combine(score, New Score(AddressOf Two)), Score)
        Console.WriteLine("last result wins: " & score("x"))
        Dim total = 0
        For Each d In score.GetInvocationList()
            total += CType(d, Score).Invoke("x")
        Next
        Console.WriteLine("sum over the list: " & total)

        ' An exception stops the rest of the list
        Dim chain As Notify = AddressOf ToConsole
        chain = CType([Delegate].Combine(chain, New Notify(AddressOf Explode), New Notify(AddressOf ToLog)), Notify)
        Try
            chain("audit")
        Catch ex As InvalidOperationException
            Console.WriteLine("caught: " & ex.Message)
        End Try

        ' Invoke each handler separately to isolate failures
        For Each d In chain.GetInvocationList()
            Try
                CType(d, Notify).Invoke("audit 2")
            Catch ex As InvalidOperationException
                Console.WriteLine("isolated: " & ex.Message)
            End Try
        Next
    End Sub
End Module
console: loan created
log:     loan created
handlers: 2
log:     loan returned
empty list is Nothing: True
last result wins: 2
sum over the list: 3
console: audit
caught: sink failed for audit
console: audit 2
isolated: sink failed for audit 2
log:     audit 2

The rules the example demonstrates:

  • Delegates are immutable. Delegate.Combine and Delegate.Remove return a new delegate and leave the old one untouched, so the result must be assigned back. Remove removes the last occurrence of a matching entry, and matching uses delegate equality (the same method and the same target).

  • Removing the last entry returns Nothing, not an empty delegate. Always guard with If d IsNot Nothing Then or d?.Invoke(...).

  • With a result type only the value of the last method is returned; the earlier results are lost. Use GetInvocationList() to call each method and collect results.

  • An unhandled exception in one method stops the chain, and the methods after it never run. GetInvocationList() is also how you call every handler and handle each failure separately, as in the last loop.

  • Combine does not deduplicate: combining the same method twice calls it twice.

  • Combining is not thread-safe as a pattern: d = Delegate.Combine(d, x) reads and writes a shared field. The Event keyword’s generated Add and Remove accessors do it with an atomic compare-and-exchange, which is a reason to prefer events for subscription.

For comparison, C# writes d += handler. In Visual Basic the same intention is AddHandler obj.Event, handler for events and Delegate.Combine for a plain delegate field; see Events and Handlers for Custom Event accessors.

Lambda expressions

A lambda expression is an anonymous method written inline. Visual Basic 9 (2008) introduced single-line Function lambdas, whose body is one expression; Visual Basic 10 (2010) added single-line Sub lambdas and multi-line lambdas for both; Visual Basic 11 (2012) added the Async and Iterator modifiers. The grammar:

Form Syntax Result

Single-line Function

Function(x As Integer) x * 2

the value of the expression is returned

Single-line Sub

Sub(s As String) Console.WriteLine(s)

one statement, no value

Multi-line Function

Function(n As Integer) As String, a block, End Function

returns with Return value

Multi-line Sub

Sub(x), a block, End Sub

Return with no value to leave early

Async lambda

Async Function() ... or Async Sub() ...

Func(Of Task(Of T)), Func(Of Task) or Action

Iterator lambda

Iterator Function() As IEnumerable(Of Integer)

a lazily produced sequence

Option Strict On

Module Program
    Sub Main()
        ' Single-line Function lambda: the expression is the return value
        Dim doubled = Function(x As Integer) x * 2
        Console.WriteLine(doubled(21))

        ' Single-line Sub lambda: the body is one statement
        Dim greet = Sub(name As String) Console.WriteLine("Hello, " & name)
        greet("reader")

        ' The inferred type is an anonymous delegate, convertible to Func and Action
        Console.WriteLine(doubled.GetType().Name.StartsWith("VB$AnonymousDelegate", StringComparison.Ordinal))
        Dim asFunc As Func(Of Integer, Integer) = doubled
        Console.WriteLine(asFunc(4))

        ' Multi-line lambdas use End Function / End Sub and an explicit Return
        Dim classify = Function(copies As Integer) As String
                           If copies = 0 Then
                               Return "out"
                           ElseIf copies < 3 Then
                               Return "scarce"
                           End If
                           Return "plenty"
                       End Function
        Console.WriteLine(classify(0) & " " & classify(2) & " " & classify(9))

        Dim report = Sub(titles As IEnumerable(Of String))
                         For Each t In titles
                             Console.WriteLine("- " & t)
                         Next
                     End Sub
        report({"Dune", "Kindred"})

        ' Parameter types come from the target delegate when there is one
        Dim shout As Func(Of String, String) = Function(s) s.ToUpperInvariant()
        Console.WriteLine(shout("dune"))

        ' The return type is the dominant type of the Return expressions
        Dim pick = Function(flag As Boolean)
                       If flag Then Return 1
                       Return 2.5
                   End Function
        Console.WriteLine(pick(True).GetType().Name & " " & pick(True))

        ' In a Sub lambda "=" is assignment; in a Function lambda it is comparison
        Dim counter = 0
        Dim assign = Sub() counter = 5
        Dim compare = Function() counter = 5
        assign()
        Console.WriteLine("counter after Sub lambda: " & counter)
        counter = 0
        Console.WriteLine("Function lambda result: " & compare())

        ' A Function lambda may be used where a Sub delegate is expected: result discarded
        Dim sink As Action = Function() 1
        sink()

        ' Lambdas can be returned and curried
        Dim adder = Function(n As Integer) Function(m As Integer) n + m
        Console.WriteLine(adder(3)(4))
    End Sub
End Module
42
Hello, reader
True
8
out scarce plenty
- Dune
- Kindred
DUNE
Double 1
counter after Sub lambda: 5
Function lambda result: False
7

Things the example makes visible:

  • Type inference. Dim doubled = Function(x As Integer) x * 2 has no delegate type to start from, so the compiler infers an anonymous delegate type (printed as VB$AnonymousDelegate...), which converts to Func(Of Integer, Integer) and to any delegate of a compatible shape. When the target is a typed variable or parameter the lambda’s parameter types come from it (shout), so they can be left out: Function(s) s.ToUpperInvariant().

  • Parameter types under Option Strict On. If the compiler cannot infer a parameter’s type from a target, it must be declared with As (BC36642, below). Leaving it out makes the parameter an Object only with strict typing off.

  • Return type. A single-line lambda’s return type is the type of its expression. A multi-line Function lambda without As infers the dominant type of its Return expressions (Integer and Double give Double); say As String when it should be fixed. A Function lambda that has a code path without a Return produces BC42353, as the "no return" case below shows.

  • The = trap. In a Sub lambda, = after a name is an assignment statement (Sub() counter = 5 sets counter). In a Function lambda, the same text is a comparison (Function() counter = 5 returns False). Which one you get depends only on the word before the parentheses. If you want to assign inside a Function lambda use a multi-line lambda; if you want a comparison inside a Sub lambda there is nothing useful to do with the result.

  • A Function lambda converts to a Sub delegate (Dim sink As Action = Function() 1), per relaxed conversion; the opposite, a Sub lambda for a Func, is an error (BC36670), and so is passing a Func(Of Integer, Boolean) where a Predicate(Of Integer) is expected (BC30311).

  • Line continuation. A multi-line lambda is an expression; it may begin on the line of the assignment and the End Function closes it. Following lines are continuation lines, which is why a lambda can be an argument in the middle of an expression: Task.Run(Async Function() ...). Since Visual Basic 10 most statements need no explicit _ continuation (see Program Structure and Code Conventions).

  • Lambdas returning lambdas. adder(3)(4) calls the inner lambda; the inner one captures n.

The compile-time rules for lambdas, which are errors and can be seen together here (also built with warnings allowed):

Option Strict On

Structure Counter
    Public Total As Integer

    Public Sub Run()
        Dim f = Function() Total
    End Sub
End Structure

Module Program
    Private Sub Bump(ByRef value As Integer)
        Dim f = Function() value + 1
    End Sub

    Sub Main()
        Dim a = Function(Optional x As Integer = 1) x
        Dim b = Function(ParamArray xs() As Integer) xs.Length
        Dim c As Func(Of Integer) = Function() As Integer
                                        Console.WriteLine("no return")
                                    End Function
        Dim d = Function() Console.WriteLine("hi")
    End Sub
End Module
Program.vb(7,28): error BC36638: Instance members and 'Me' cannot be used within a lambda expression in structures.
Program.vb(13,28): error BC36639: 'ByRef' parameter 'value' cannot be used in a lambda expression.
Program.vb(17,26): error BC33010: 'Lambda' parameters cannot be declared 'Optional'.
Program.vb(18,26): error BC33009: 'Lambda' parameters cannot be declared 'ParamArray'.
Program.vb(21,37): warning BC42353: Function '<anonymous method>' doesn't return a value on all code paths. Are you missing a 'Return' statement?
Program.vb(22,28): error BC30491: Expression does not produce a value.
  • BC36639: a lambda cannot use a ByRef parameter of its enclosing method, because the lambda may run after the method has returned and the caller’s variable would no longer exist. Copy the parameter into a local first.

  • BC36638: a lambda in a Structure cannot use Me or instance members, for the same reason (a copy of the structure would be captured, not the original). Copy what you need into a local variable.

  • BC33010 and BC33009: lambda parameters cannot be Optional or ParamArray. Use overloads of a named method, or a delegate type that declares the parameters.

  • The Function() Console.WriteLine("hi") line is BC30491: a Function lambda needs an expression, and Console.WriteLine is a Sub. Write Sub() Console.WriteLine("hi").

  • BC42353 (a warning, an error here because the project treats warnings as errors): the multi-line Function lambda has a path that never returns a value.

The three mistakes that come from habits in other languages, and the lambda-type error:

Option Strict On

Module Program
    Sub Main()
        Dim twice = Function(x As Integer) x * 2
        Dim combined = twice + twice

        Dim log As Action = Sub() Console.WriteLine("log")
        log += log

        Dim untyped = Function(x) x
    End Sub
End Module
Program.vb(6,24): error BC30452: Operator '+' is not defined for types 'Function <generated method>(x As Integer) As Integer' and 'Function <generated method>(x As Integer) As Integer'.
Program.vb(9,9): error BC30452: Operator '+' is not defined for types 'Action' and 'Action'.
Program.vb(11,32): error BC36642: Option Strict On requires each lambda expression parameter to be declared with an 'As' clause if its type cannot be inferred.

Source: BC30452

No +, no += on delegates, and an inferred lambda type is not an exception to the rule that Option Strict On wants a declared parameter type.

Closures and captured variables

A lambda can use local variables and parameters of the method that contains it (and Me). The compiler moves those variables into a hidden class, a closure, and the lambda becomes a method of it. The variable and the lambda now share the same storage, so:

  • the lambda sees the current value of the variable, not the value it had when the lambda was created;

  • the variable lives as long as the lambda, which can be longer than the method that declared it;

  • each call of the method creates a new closure, so two lambdas made by two calls do not share state.

Option Strict On

Module Program
    Private Function MakeCounter() As Func(Of Integer)
        Dim count = 0
        Return Function()
                   count += 1
                   Return count
               End Function
    End Function

    Sub Main()
        ' A captured variable lives as long as the delegate and is shared
        Dim next1 = MakeCounter()
        Dim next2 = MakeCounter()
        Console.WriteLine(next1() & " " & next1() & " " & next1() & " | " & next2())

        ' The lambda sees the variable, not a copy of its value at creation time
        Dim limit = 10
        Dim isOver As Func(Of Integer, Boolean) = Function(n) n > limit
        Console.WriteLine(isOver(15))
        limit = 20
        Console.WriteLine(isOver(15))

        ' For Next: one loop variable shared by every lambda
        Dim viaFor As New List(Of Func(Of Integer))
        For i = 1 To 3
            viaFor.Add(Function() i)
        Next
        Console.WriteLine("For:      " & String.Join(",", viaFor.Select(Function(f) f())))

        ' For Each: a fresh iteration variable per iteration
        Dim viaForEach As New List(Of Func(Of Integer))
        For Each n In {1, 2, 3}
            viaForEach.Add(Function() n)
        Next
        Console.WriteLine("For Each: " & String.Join(",", viaForEach.Select(Function(f) f())))

        ' A variable declared inside the loop body is new for every iteration
        Dim viaLocal As New List(Of Func(Of Integer))
        For i = 1 To 3
            Dim copy = i
            viaLocal.Add(Function() copy)
        Next
        Console.WriteLine("Local:    " & String.Join(",", viaLocal.Select(Function(f) f())))

        ' While loop: variable declared outside is shared
        Dim viaWhile As New List(Of Func(Of Integer))
        Dim w = 1
        While w <= 3
            viaWhile.Add(Function() w)
            w += 1
        End While
        Console.WriteLine("While:    " & String.Join(",", viaWhile.Select(Function(f) f())))
    End Sub
End Module
1 2 3 | 1
True
False
For:      4,4,4
For Each: 1,2,3
Local:    1,2,3
While:    4,4,4

The loop results are the part to learn by heart:

Loop What a lambda captures Result above

For i = 1 To 3

one i for the whole loop

4,4,4: every lambda sees the final value, which Next left in i after the last step

For Each n In ...

a new n for each iteration (Visual Basic 11 and later)

1,2,3

a Dim inside the body

a new variable per iteration

1,2,3

While, Do loops with a variable declared outside

one variable for the whole loop

4,4,4

The compiler gives a warning for the first case (BC42324), and the program above was built with warnings allowed so that it could be shown, because with TreatWarningsAsErrors the first loop is an error:

Program.vb(28,35): warning BC42324: Using the iteration variable in a lambda expression may have unexpected results.  Instead, create a local variable within the loop and assign it the value of the iteration variable.

The fix is the one the message states: declare a local variable inside the loop, give it the value of the loop variable, and capture that. This is also why For Each changed in Visual Basic 11: code that used to print the final element three times now prints each element, a behaviour change for old code that depended on the sharing. C# has the same rule for for (one shared variable) and since C# 5 for foreach (one variable per iteration), so the two languages agree on every row of the table.

Costs and cautions of closures:

  • A closure that captures a variable allocates an object when the enclosing method runs, and each lambda that captures is a delegate allocation per evaluation. A lambda that captures nothing is created once and cached in a static field. The test on .NET 10 above (NoCapture returns the same instance twice, WithCapture does not) is the reason hot loops should prefer capture-free lambdas or method groups.

  • Capturing a variable that you also change from the enclosing method makes the data flow hard to read. Keep captured variables effectively read-only, or put the state in a class and pass the object.

  • Capturing a Using variable or a loop variable that is disposed later extends the lifetime of the delegate, not of the resource: the lambda may run after Dispose. See Object Lifetime and Disposal.

  • A lambda captured by a long-lived object (a static event, a cache) keeps everything the closure refers to alive: the usual source of "leaks" in a managed program.

  • Capturing a ByRef parameter or Me in a structure is not allowed (BC36639 and BC36638, above).

Async and Iterator lambdas

Async (Visual Basic 11) can be applied to a lambda. Async Function produces a Task or Task(Of T) (a Func(Of Task) or Func(Of Task(Of T))); Async Sub produces a Sub that returns nothing the caller can await. The conversion rules are the same as for named methods: an Async Function lambda can go where a Func(Of Task(Of T)) is expected, such as Task.Run, and it picks the overload by the type of the value in Return.

Option Strict On

Module Program
    Private Async Function RunTwiceAsync(work As Func(Of Task(Of Integer))) As Task(Of Integer)
        Dim a = Await work()
        Dim b = Await work()
        Return a + b
    End Function

    Private Async Function RunAsync() As Task
        ' Async Function lambda: Func(Of Task(Of Integer))
        Dim calls = 0
        Dim lookup As Func(Of Task(Of Integer)) =
            Async Function()
                calls += 1
                Await Task.Delay(10)
                Return calls * 10
            End Function
        Console.WriteLine("sum: " & Await RunTwiceAsync(lookup))

        ' Async Function lambda with no result: Func(Of Task)
        Dim save As Func(Of String, Task) =
            Async Function(title)
                Await Task.Delay(5)
                Console.WriteLine("saved " & title)
            End Function
        Await save("Dune")

        ' Async Sub lambda: fire-and-forget, an Action; nobody can await it
        Dim fireAndForget As Action =
            Async Sub()
                Await Task.Delay(5)
                Console.WriteLine("async sub finished")
            End Sub
        fireAndForget()
        Console.WriteLine("returned before the async sub finished")
        Await Task.Delay(100)

        ' Task.Run takes either shape; an Async lambda is picked as Func(Of Task(Of T))
        Dim viaRun = Await Task.Run(Async Function()
                                        Await Task.Delay(5)
                                        Return "from Task.Run"
                                    End Function)
        Console.WriteLine(viaRun)

        ' Exceptions of an Async Function lambda travel in the Task
        Dim failing As Func(Of Task) =
            Async Function()
                Await Task.Delay(1)
                Throw New InvalidOperationException("lookup failed")
            End Function
        Try
            Await failing()
        Catch ex As InvalidOperationException
            Console.WriteLine("caught after Await: " & ex.Message)
        End Try
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
sum: 30
saved Dune
returned before the async sub finished
async sub finished
from Task.Run
caught after Await: lookup failed

Source: Async

  • An Async Function lambda surfaces its exception when the Task is awaited (the last test above). That is what you want.

  • An Async Sub lambda cannot be awaited and has nowhere to put an exception: it is raised on the synchronization context that was current at the start (or on the thread pool), where it typically ends the process. The only legitimate use is an event handler whose delegate is a Sub. In the example fireAndForget() returns first (the "returned before…​" line) and the work finishes later. When an API has both a Func(Of Task) and an Action overload, an Async Function lambda picks the first; write Async Function unless the delegate really is a Sub.

  • Visual Basic has no Async Main, so the example’s Sub Main runs the Async method with GetAwaiter().GetResult() (see Async and Await).

  • An Async or Iterator lambda cannot be converted to an expression tree (BC37050, below).

Iterator lambdas and recursion:

Option Strict On

Module Program
    Sub Main()
        ' Iterator lambda (VB 11)
        Dim evens = Iterator Function(limit As Integer) As IEnumerable(Of Integer)
                        For i = 0 To limit Step 2
                            Yield i
                        Next
                    End Function
        Console.WriteLine(String.Join(",", evens(8)))

        ' Recursion: the variable must be declared before the lambda that uses it
        Dim factorial As Func(Of Integer, Long) = Nothing
        factorial = Function(n) If(n <= 1, 1L, n * factorial(n - 1))
        Console.WriteLine(factorial(10))

        ' Lambda that returns Nothing-typed values needs a target for inference
        Dim maybe As Func(Of Integer, String) = Function(n) If(n > 0, "positive", Nothing)
        Console.WriteLine(If(maybe(0), "(nothing)"))
    End Sub
End Module
0,2,4,6,8
3628800
(nothing)

Source: Iterator

  • An Iterator Function lambda is lazy like a named iterator. See Iterators and Yield.

  • A lambda cannot refer to itself by name before it exists: the variable that will hold it is declared first (assigned Nothing) and then the lambda is assigned and refers to the variable (factorial). This works because the lambda captures the variable, not the value, as the previous section explained.

  • The maybe lambda has a declared target type (Func(Of Integer, String)), so Nothing in the If operator is a String. If(maybe(0), "(nothing)") is the null-coalescing form of the If operator.

Lambdas in LINQ and in the class library

Every LINQ operator in method syntax takes a delegate, and a lambda is how you write it; query syntax (From ... Where ... Select) is translated by the compiler into the same method calls with lambdas (see LINQ Query Expressions and the shared LINQ page).

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Author As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, author As String, year As Integer)
        Me.Title = title
        Me.Author = author
        Me.Year = year
    End Sub
End Class

Module Program
    Sub Main()
        Dim books As New List(Of Book) From {
            New Book("Dune", "Frank Herbert", 1965),
            New Book("Dune Messiah", "Frank Herbert", 1969),
            New Book("Neuromancer", "William Gibson", 1984),
            New Book("Kindred", "Octavia Butler", 1979),
            New Book("Parable of the Sower", "Octavia Butler", 1993)
        }

        ' Method syntax: every operator takes a lambda
        Dim titles = books.
            Where(Function(b) b.Year < 1990).
            OrderBy(Function(b) b.Title).
            Select(Function(b) b.Title)
        Console.WriteLine(String.Join(" | ", titles))

        ' Query syntax is translated to the same calls
        Dim sameTitles = From b In books
                         Where b.Year < 1990
                         Order By b.Title
                         Select b.Title
        Console.WriteLine(String.Join(" | ", sameTitles))

        ' Grouping and aggregation with lambdas
        For Each g In books.GroupBy(Function(b) b.Author)
            Console.WriteLine(g.Key & ": " & g.Count() & " books, newest " & g.Max(Function(b) b.Year))
        Next

        ' A method group works wherever a lambda taking the same arguments would
        Dim words = {"12", "x", "7"}
        Console.WriteLine(String.Join(",", words.Where(AddressOf IsDigits)))

        ' Deferred execution reads the captured variable when the query runs
        Dim minYear = 1980
        Dim recent = books.Where(Function(b) b.Year >= minYear)
        Console.WriteLine("minYear 1980: " & recent.Count())
        minYear = 1990
        Console.WriteLine("minYear 1990: " & recent.Count())

        ' ToList() takes a snapshot
        Dim snapshot = books.Where(Function(b) b.Year >= minYear).ToList()
        minYear = 1960
        Console.WriteLine("snapshot:     " & snapshot.Count)

        ' Comparison(Of T) and a lambda for List.Sort
        books.Sort(Function(x, y) y.Year.CompareTo(x.Year))
        Console.WriteLine(books(0).Title)

        ' Predicate(Of T) for List methods
        Console.WriteLine(books.FindAll(Function(b) b.Author.StartsWith("Frank", StringComparison.Ordinal)).Count)
    End Sub

    Private Function IsDigits(text As String) As Boolean
        Return text.All(AddressOf Char.IsDigit)
    End Function
End Module
Dune | Dune Messiah | Kindred | Neuromancer
Dune | Dune Messiah | Kindred | Neuromancer
Frank Herbert: 2 books, newest 1969
William Gibson: 1 books, newest 1984
Octavia Butler: 2 books, newest 1993
12,7
minYear 1980: 2
minYear 1990: 1
snapshot:     1
Parable of the Sower
2
  • Method syntax breaks lines after the . (Visual Basic lets a line end after a dot: no _).

  • AddressOf IsDigits is a method group. It can be passed wherever a lambda taking the same argument could be (Where(AddressOf ...)); words.All(AddressOf Char.IsDigit) is the idiom the shelf’s Isbn.TryParse uses.

  • Deferred execution. books.Where(...) produces a query, not a list. The lambda runs, and the captured minYear is read, every time the query is enumerated. After minYear = 1990 the same recent query gives a different count. Call ToList() or ToArray() to take a snapshot, as snapshot does.

  • List(Of T).Sort(Comparison), FindAll(Predicate), ForEach(Action), Array.ConvertAll and similar members take the older framework delegates; lambdas convert to them. The sort used y.Year.CompareTo(x.Year) for descending order.

Lambdas as event handlers

An event is a delegate field with restricted access, so a lambda can serve as a handler. The command-table example shows the other common use: a dictionary of names to Sub lambdas.

Option Strict On

Public Class Loans
    Public Event Lent As Action(Of String)

    Public Sub Lend(title As String)
        RaiseEvent Lent(title)
    End Sub
End Class

Module Program
    Private Function NoCapture() As Func(Of Integer, Integer)
        Return Function(x) x + 1
    End Function

    Private Function WithCapture(offset As Integer) As Func(Of Integer, Integer)
        Return Function(x) x + offset
    End Function

    Sub Main()
        ' Command table: names mapped to Sub lambdas
        Dim commands As New Dictionary(Of String, Action(Of String))(StringComparer.OrdinalIgnoreCase) From {
            {"add", Sub(arg) Console.WriteLine("adding " & arg)},
            {"find", Sub(arg) Console.WriteLine("finding " & arg)}
        }
        For Each line In {"ADD Dune", "find Kindred", "drop Emma"}
            Dim parts = line.Split(" "c, 2)
            Dim handler As Action(Of String) = Nothing
            If commands.TryGetValue(parts(0), handler) Then
                handler(parts(1))
            Else
                Console.WriteLine("unknown command " & parts(0))
            End If
        Next

        ' Lambdas as event handlers; RemoveHandler needs the same delegate instance
        Dim loans As New Loans()
        Dim log As Action(Of String) = Sub(t) Console.WriteLine("lent " & t)
        AddHandler loans.Lent, log
        AddHandler loans.Lent, Sub(t) Console.WriteLine("also lent " & t)
        loans.Lend("Dune")

        RemoveHandler loans.Lent, log
        loans.Lend("Kindred")

        ' A lambda that captures nothing is created once and reused
        Console.WriteLine("no capture, same instance: " & ReferenceEquals(NoCapture(), NoCapture()))
        Console.WriteLine("capture, same instance:    " & ReferenceEquals(WithCapture(1), WithCapture(1)))
    End Sub
End Module
adding Dune
finding Kindred
unknown command drop
lent Dune
also lent Dune
also lent Kindred
no capture, same instance: True
capture, same instance:    False
  • RemoveHandler loans.Lent, log worked because log is a variable that holds the very delegate that was added. The second handler was added with an inline lambda, so it was never removed: it printed again for "Kindred" (the last output line), and there is no variable to remove it with. A lambda written again in RemoveHandler is a different delegate, created from a different closure method, so it does not match. The compiler warns about exactly this (BC42326), and the program that follows triggers the warning (built with warnings allowed):

Option Strict On

Public Class Loans
    Public Event Lent As Action(Of String)

    Public Sub Lend(title As String)
        RaiseEvent Lent(title)
    End Sub
End Class

Module Program
    Sub Main()
        Dim loans As New Loans()
        AddHandler loans.Lent, Sub(t) Console.WriteLine("lent " & t)
        RemoveHandler loans.Lent, Sub(t) Console.WriteLine("lent " & t)
        loans.Lend("Dune")
    End Sub
End Module
Program.vb(15,35): warning BC42326: Lambda expression will not be removed from this event handler. Assign the lambda expression to a variable and use the variable to add and remove the event.

Assign the lambda to a variable first, and use that variable with both statements; or use a named method and AddressOf, which compares equal. The handler still printed "lent Dune" because the removal found nothing. A handler that is attached for the whole life of the object does not need removing. The rule matters for objects that subscribe to a longer-lived publisher: forgetting to unsubscribe keeps the subscriber alive (see Events and Handlers).

Expression trees

When the target type of a lambda is Expression(Of TDelegate) (in System.Linq.Expressions), the compiler does not generate a method. It generates code that builds a tree of objects describing the lambda’s expression: a data structure that a library can inspect, translate to SQL, or compile to a delegate at run time. The same source text is a delegate in one place and a tree in the other.

Option Strict On

Imports System.Linq.Expressions

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub
End Class

Module Program
    Sub Main()
        ' The same lambda text becomes data when the target type is Expression(Of ...)
        Dim tree As Expression(Of Func(Of Book, Boolean)) = Function(b) b.Year >= 1980 AndAlso b.Title.Length > 4
        Console.WriteLine("tree:       " & tree.ToString())
        Console.WriteLine("node type:  " & tree.Body.NodeType.ToString())
        Console.WriteLine("parameters: " & tree.Parameters.Count & " (" & tree.Parameters(0).Name & " As " & tree.Parameters(0).Type.Name & ")")

        Dim andNode = CType(tree.Body, BinaryExpression)
        Console.WriteLine("left:       " & andNode.Left.ToString() & " [" & andNode.Left.NodeType.ToString() & "]")
        Console.WriteLine("right:      " & andNode.Right.ToString() & " [" & andNode.Right.NodeType.ToString() & "]")

        ' Compile turns the tree into a delegate
        Dim test = tree.Compile()
        Console.WriteLine(test(New Book("Neuromancer", 1984)) & " " & test(New Book("Dune", 1965)))

        ' Captured variables appear as a member access on a closure object
        Dim minYear = 1990
        Dim captured As Expression(Of Func(Of Book, Boolean)) = Function(b) b.Year >= minYear
        Console.WriteLine("captured:   " & captured.ToString())
        minYear = 2000
        Console.WriteLine("evaluated later: " & captured.Compile()(New Book("Kindred", 1995)))

        ' The same tree can be built by hand with the Expression factory methods
        Dim p = Expression.Parameter(GetType(Book), "b")
        Dim body = Expression.GreaterThanOrEqual(Expression.Property(p, "Year"), Expression.Constant(1980))
        Dim built = Expression.Lambda(Of Func(Of Book, Boolean))(body, p)
        Console.WriteLine("built:      " & built.ToString())

        ' Query providers receive trees through IQueryable(Of T)
        Dim books = {New Book("Dune", 1965), New Book("Neuromancer", 1984)}.AsQueryable()
        Dim q = books.Where(Function(b) b.Year >= 1980)
        Console.WriteLine("query expression: " & q.Expression.ToString().Replace("ShelfDocs.", ""))
    End Sub
End Module
tree:       b => ((b.Year >= 1980) AndAlso (b.Title.Length > 4))
node type:  AndAlso
parameters: 1 (b As Book)
left:       (b.Year >= 1980) [GreaterThanOrEqual]
right:      (b.Title.Length > 4) [GreaterThan]
True False
captured:   b => (b.Year >= value(ShelfDocs.Program+_Closure$__0-0).$VB$Local_minYear)
evaluated later: False
built:      b => (b.Year >= 1980)
query expression: Book[].Where(b => (b.Year >= 1980))
  • The tree’s Body is an ExpressionType.AndAlso node whose Left and Right are GreaterThanOrEqual and GreaterThan nodes. A provider such as Entity Framework Core walks that tree and emits WHERE Year >= 1980 AND LEN(Title) > 4 (or its dialect). That is why IQueryable(Of T) operators take Expression(Of Func(...)) and IEnumerable(Of T) operators take Func(...): the first gives the provider something to translate, the second runs in memory.

  • Compile() turns the tree into a delegate; it is the slow, one-off cost, so cache the result when you evaluate many times.

  • A captured variable appears as a field of the closure object (value(ShelfDocs.Program+_Closure$__0-0).$VB$Local_minYear), and its value is read when the tree is evaluated, not when it is built — the same live-capture rule as for delegates. A provider that wants a constant must evaluate it. The names are compiler-generated and not API.

  • A tree can be built by hand with Expression.Parameter, Expression.Property, Expression.Constant and Expression.Lambda. This is the way to assemble a filter from user input without string concatenation.

  • AsQueryable() on an array or list lets LINQ build a tree (shown on the last line) while still running in memory. A provider evaluates the tree differently.

Not every lambda can be a tree. The compiler enforces the subset that a tree can represent:

Option Strict On

Imports System.Linq.Expressions

Module Program
    Sub Main()
        Dim ok As Expression(Of Func(Of Integer, Integer)) = Function(x) x + 1
        Dim assign As Expression(Of Action(Of Integer)) = Sub(x) Console.WriteLine(x)
        Dim multi As Expression(Of Func(Of Integer, Integer)) = Function(x)
                                                                    Return x + 1
                                                                End Function
        Dim cond As Expression(Of Func(Of Integer, Integer)) = Function(x) If(x > 0, x, -x)
        Dim a As Expression(Of Func(Of Integer, Boolean)) = Function(x) x = 1
        Dim async As Expression(Of Func(Of Task)) = Async Function() Await Task.Delay(1)
    End Sub
End Module
Program.vb(14,70): error BC30491: Expression does not produce a value.
Program.vb(9,65): error BC36675: Statement lambdas cannot be converted to expression trees.
Program.vb(14,53): error BC37050: Lambdas with the 'Async' or 'Iterator' modifiers cannot be converted to expression trees.
  • Statement lambdas cannot become trees (BC36675). A single-line Function(x) ... and a single-line Sub(x) call can.

  • Async and Iterator lambdas cannot (BC37050).

  • The single-line lambdas in the same file (Function(x) x + 1, If(x > 0, x, -x) and x = 1 as a comparison) compiled without a diagnostic.

  • A tree can contain a call to any method, but a provider may not know how to translate it. EF Core throws at run time (not at compile time) when a Where lambda calls a method that has no SQL equivalent. The compiler cannot tell.

Expression trees are the one feature of this page where the C# and Visual Basic compilers differ visibly: C# trees cannot contain assignments or statement bodies either, but each compiler’s list of unsupported nodes is its own, so consult the compiler diagnostics when a tree does not compile. See also Expression Trees and Dynamic.

Differences from C# and older Visual Basic

Topic Visual Basic C#

Lambda syntax

Function(x) x + 1, Sub(x) Console.WriteLine(x); the kind of lambda is stated by the keyword

x => x + 1; the compiler decides from the body

Combining delegates

Delegate.Combine, Delegate.Remove, AddHandler; + is not defined

+, +=, -=

Null-safe call

d?.Invoke(x)

d?.Invoke(x)

Method group conversion

AddressOf M, always written

M alone is enough

Dropping parameters

lambdas may declare fewer parameters; AddressOf may not (Option Strict On)

discards in lambdas ((_, _) => ...) must list every parameter

Optional and ParamArray in lambdas

not allowed (BC33010, BC33009)

lambda parameters can have default values

static lambda (no capture enforced)

no keyword, but non-capturing lambdas are cached

static modifier since C# 9

Assignments in an expression-bodied lambda

Sub() x = 1 is a statement lambda

() => x = 1 is an expression

Captured loop variable

For shared, For Each per iteration

the same

Async void lambda

Async Sub

async void

Natural function type of a lambda

an anonymous delegate type

Func or Action (C# 10)

Statement lambdas in expression trees

not allowed

not allowed

Where older books and samples are outdated, in short:

  • Delegates in Visual Basic 6 and early .NET. VB6 has no delegates; AddressOf there passed a function pointer to a Windows API (see Native Interop: Declare and P/Invoke). The .NET meaning of AddressOf is different: it creates a managed, type-checked delegate.

  • Lambdas. Visual Basic 2005 and earlier have no lambda expressions, and code written for those versions used AddressOf and small named procedures, or anonymous-method style classes. Books written for VB 2008 describe single-line Function lambdas only; Sub lambdas, multi-line lambdas (VB 10) and Async/Iterator lambdas (VB 11) are later additions.

  • The asynchronous delegate pattern (BeginInvoke/EndInvoke, IAsyncResult and callbacks) does not exist on .NET Core and later; use Task and Await.

  • Delegate and MulticastDelegate combination by operators is C#. Books that discuss it for Visual Basic really describe AddHandler and Delegate.Combine.

  • Loop capture. Text that says every closure captured in a For Each sees the last item describes Visual Basic 10 and earlier; since Visual Basic 11 each iteration has its own variable.

Common mistakes and diagnostics

Mistake What happens Fix

Lambdas capture the loop variable of For Next

all see the final value; BC42324 warning

copy to a local inside the loop

AddressOf assigned to an untyped variable

BC30581, no target type

declare the delegate type

Narrowing signature (Integer parameter for a String delegate)

BC36663 under Option Strict On; run-time InvalidCastException with strict typing off

match the signature, or wrap in a lambda that converts

d1 + d2, d += x on delegates

BC30452

Delegate.Combine and CType

Invoking a delegate whose last handler was removed

NullReferenceException

d?.Invoke(x)

Function() x = 5 meaning assignment

a comparison; no assignment happens

Sub() x = 5

RemoveHandler with a lambda

does nothing; BC42326

keep the delegate in a variable

Async Sub lambda passed to Task.Run-like APIs

exceptions are unobservable

use Async Function

ByRef parameter or structure Me used in a lambda

BC36639, BC36638

copy to a local

BeginInvoke from older code

PlatformNotSupportedException

Task.Run, Async/Await

Capturing a mutable variable and enumerating a deferred query later

different result each time

ToList() or copy the variable

A Func variable passed where Predicate(Of T) is required

type error, same shape but different types

wrap in a lambda

A statement lambda in an Expression(Of ...)

BC36675

use a single-line lambda, or build the tree by hand

The common diagnostics of this page: BC31143 (delegate signature mismatch), BC36663 (narrowing in delegate conversion), BC30581 (AddressOf needs a delegate type), BC36642 (lambda parameter needs a type), BC36639 and BC36638 (lambda cannot capture a ByRef parameter or a structure’s Me), BC33009 and BC33010 (ParamArray and Optional lambda parameters), BC30491 (the lambda body does not produce a value), BC42324 (iteration variable in a lambda), BC42326 (lambda in RemoveHandler), BC42353 (not all paths return a value), BC36675 and BC37050 (lambda not allowed in an expression tree).

Delegates and the shelf

In the running scenario the shelf’s Catalog.Search and the filtering in the command-line program are the natural places for delegates. The Isbn.TryParse method uses AddressOf Char.IsLetterOrDigit and AddressOf Char.IsDigit as method groups (see Data Types and Literals), the command table above has the shape of a command-line dispatcher for the shelf, and the Windows Forms and WPF projects wire their buttons with Handles and AddHandler, which are delegates under the surface (see Windows Forms Applications and WPF Applications). The extension methods that make the LINQ operators look like members are on Extension Methods and Partial Methods.