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 |
|---|---|---|
|
a new delegate type with a fixed signature |
named callback types, event signatures, |
|
a delegate instance that refers to an existing method (a method group conversion) |
passing a named procedure, |
|
an inline function; a delegate instance (or an expression tree) |
predicates, selectors, comparers |
|
an inline procedure that returns nothing |
callbacks, event handlers, |
|
an inline |
|
|
an inline iterator that `Yield`s a sequence |
small generators |
|
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.
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
Source: Delegate Statement
Points to take away from the example:
-
New BookFilter(AddressOf rules.IsRecent)is the explicit form.Dim longTitle As BookFilter = AddressOf Rules.HasLongTitleand passingAddressOf rules.IsRecentstraight to aBookFilterparameter 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 aSharedmethod has aNothingtarget. ThatTargetkeeps 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 forrecent.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, soReferenceEqualsisFalse. This is what letsRemoveHandlerfind a handler that was added with a separateAddressOfexpression, and it is why it cannot find a lambda (see the section on events below). -
A delegate type has no
Nothingvalue of its own; an unassigned delegate variable isNothing, and calling it raisesNullReferenceException. Guard withIf handler IsNot Nothing Then handler(x)or, since Visual Basic 14, callhandler?.Invoke(x). The explicitInvokeis 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
Source: Delegate Statement
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.SaveandAddressOf MyClass.Savefor the base-class and non-overridable-view variants), which captures that object as theTarget; -
a
Sharedmethod or a procedure in aModule(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
Source: AddressOf Operator
What the example shows besides overload selection:
-
DynamicInvokeis the late-bound call: it takes the arguments asObjectvalues and checks them at run time. A wrong argument type is anArgumentExceptionrather than a compile error, and an exception thrown by the target is wrapped in aTargetInvocationException. UseInvoke(or the call syntax) when the delegate type is known; reserveDynamicInvokefor plumbing code that really does not know the signature. -
Delegate.CreateDelegatebuilds a delegate from aMethodInfo. 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 (AddressOfneeds the name at compile time). -
ByRefparameters work:swapper(x, y)changesxandy. The framework’s genericFuncandActioncannot declareByRefparameters, so a method that must update its arguments needs a declared delegate type. -
The
BeginInvokeandEndInvokemembers that the compiler generates implement the asynchronous delegate pattern of .NET Framework. They are not implemented on .NET Core and later; the call above throwsPlatformNotSupportedException. Code in older books and samples that starts work withBeginInvokehas to move toTask.Run,AsyncandAwait, or a thread (see Async and Await and Threads, Parallelism and Synchronization). -
Variance.
Func(Of String)converts toFunc(Of Object)(the result type is covariant, declaredOut), andAction(Of Object)converts toAction(Of String)(the parameter type is contravariant, declaredIn). The variance of a generic delegate is fixed by its declaration, and aDelegateyou 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 |
|
Result type |
the method’s result type must widen to the delegate’s result type ( |
|
|
allowed: the result is discarded |
|
Fewer parameters than the delegate |
allowed for a lambda (it may declare none); not allowed for |
|
More parameters than the delegate |
never allowed, because there is nothing to supply the argument |
BC31143 |
Narrowing (an |
rejected under |
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
Source: Relaxed Delegate Conversion
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)'.
Source: Relaxed Delegate Conversion
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 |
|---|---|---|
|
takes up to 16 arguments and returns a |
LINQ selectors, factories, |
|
takes arguments, returns nothing |
callbacks, |
|
|
|
|
two |
|
|
one in, one out |
|
|
|
the standard event pattern |
|
the same shape as |
LINQ |
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 |
|---|---|
|
the usual form; compiles to |
|
explicit; required with the null-conditional operator: |
|
late-bound, slower, arguments are |
|
asynchronous delegate pattern of .NET Framework; throws |
|
for a delegate declared as an |
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
Source: Delegate.Combine Method
The rules the example demonstrates:
-
Delegates are immutable.
Delegate.CombineandDelegate.Removereturn a new delegate and leave the old one untouched, so the result must be assigned back.Removeremoves 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 withIf d IsNot Nothing Thenord?.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. -
Combinedoes 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. TheEventkeyword’s generatedAddandRemoveaccessors 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 |
|
the value of the expression is returned |
Single-line |
|
one statement, no value |
Multi-line |
|
returns with |
Multi-line |
|
|
|
|
|
|
|
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
Source: Lambda Expressions
Things the example makes visible:
-
Type inference.
Dim doubled = Function(x As Integer) x * 2has no delegate type to start from, so the compiler infers an anonymous delegate type (printed asVB$AnonymousDelegate...), which converts toFunc(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 withAs(BC36642, below). Leaving it out makes the parameter anObjectonly with strict typing off. -
Return type. A single-line lambda’s return type is the type of its expression. A multi-line
Functionlambda withoutAsinfers the dominant type of itsReturnexpressions (IntegerandDoublegiveDouble); sayAs Stringwhen it should be fixed. AFunctionlambda that has a code path without aReturnproduces BC42353, as the "no return" case below shows. -
The
=trap. In aSublambda,=after a name is an assignment statement (Sub() counter = 5setscounter). In aFunctionlambda, the same text is a comparison (Function() counter = 5returnsFalse). Which one you get depends only on the word before the parentheses. If you want to assign inside aFunctionlambda use a multi-line lambda; if you want a comparison inside aSublambda there is nothing useful to do with the result. -
A
Functionlambda converts to aSubdelegate (Dim sink As Action = Function() 1), per relaxed conversion; the opposite, aSublambda for aFunc, is an error (BC36670), and so is passing aFunc(Of Integer, Boolean)where aPredicate(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 Functioncloses 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 capturesn.
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.
Source: Lambda Expressions
-
BC36639: a lambda cannot use a
ByRefparameter 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
Structurecannot useMeor 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
OptionalorParamArray. Use overloads of a named method, or a delegate type that declares the parameters. -
The
Function() Console.WriteLine("hi")line is BC30491: aFunctionlambda needs an expression, andConsole.WriteLineis aSub. WriteSub() Console.WriteLine("hi"). -
BC42353 (a warning, an error here because the project treats warnings as errors): the multi-line
Functionlambda 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
Source: Lambda Expressions
The loop results are the part to learn by heart:
| Loop | What a lambda captures | Result above |
|---|---|---|
|
one |
|
|
a new |
|
a |
a new variable per iteration |
|
|
one variable for the whole loop |
|
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.
Source: For…Next Statement
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 (
NoCapturereturns the same instance twice,WithCapturedoes 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
Usingvariable or a loop variable that is disposed later extends the lifetime of the delegate, not of the resource: the lambda may run afterDispose. 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
ByRefparameter orMein 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 Functionlambda surfaces its exception when theTaskis awaited (the last test above). That is what you want. -
An
Async Sublambda 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 aSub. In the examplefireAndForget()returns first (the "returned before…" line) and the work finishes later. When an API has both aFunc(Of Task)and anActionoverload, anAsync Functionlambda picks the first; writeAsync Functionunless the delegate really is aSub. -
Visual Basic has no
Async Main, so the example’sSub Mainruns theAsyncmethod withGetAwaiter().GetResult()(see Async and Await). -
An
AsyncorIteratorlambda 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 Functionlambda 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
maybelambda has a declared target type (Func(Of Integer, String)), soNothingin theIfoperator is aString.If(maybe(0), "(nothing)")is the null-coalescing form of theIfoperator.
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
Source: Language-Integrated Query (LINQ)
-
Method syntax breaks lines after the
.(Visual Basic lets a line end after a dot: no_). -
AddressOf IsDigitsis 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’sIsbn.TryParseuses. -
Deferred execution.
books.Where(...)produces a query, not a list. The lambda runs, and the capturedminYearis read, every time the query is enumerated. AfterminYear = 1990the samerecentquery gives a different count. CallToList()orToArray()to take a snapshot, assnapshotdoes. -
List(Of T).Sort(Comparison),FindAll(Predicate),ForEach(Action),Array.ConvertAlland similar members take the older framework delegates; lambdas convert to them. The sort usedy.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
Source: AddHandler Statement
-
RemoveHandler loans.Lent, logworked becauselogis 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 inRemoveHandleris 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.
Source: RemoveHandler Statement
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))
Source: Expression<TDelegate> Class
-
The tree’s
Bodyis anExpressionType.AndAlsonode whoseLeftandRightareGreaterThanOrEqualandGreaterThannodes. A provider such as Entity Framework Core walks that tree and emitsWHERE Year >= 1980 AND LEN(Title) > 4(or its dialect). That is whyIQueryable(Of T)operators takeExpression(Of Func(...))andIEnumerable(Of T)operators takeFunc(...): 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.ConstantandExpression.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.
Source: Expression<TDelegate> Class
-
Statement lambdas cannot become trees (BC36675). A single-line
Function(x) ...and a single-lineSub(x) callcan. -
AsyncandIteratorlambdas cannot (BC37050). -
The single-line lambdas in the same file (
Function(x) x + 1,If(x > 0, x, -x)andx = 1as 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
Wherelambda 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 |
|
|
Combining delegates |
|
|
Null-safe call |
|
|
Method group conversion |
|
|
Dropping parameters |
lambdas may declare fewer parameters; |
discards in lambdas ( |
Optional and |
not allowed (BC33010, BC33009) |
lambda parameters can have default values |
|
no keyword, but non-capturing lambdas are cached |
|
Assignments in an expression-bodied lambda |
|
|
Captured loop variable |
|
the same |
Async void lambda |
|
|
Natural function type of a lambda |
an anonymous delegate type |
|
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;
AddressOfthere passed a function pointer to a Windows API (see Native Interop: Declare and P/Invoke). The .NET meaning ofAddressOfis 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
AddressOfand small named procedures, or anonymous-method style classes. Books written for VB 2008 describe single-lineFunctionlambdas only;Sublambdas, multi-line lambdas (VB 10) andAsync/Iteratorlambdas (VB 11) are later additions. -
The asynchronous delegate pattern (
BeginInvoke/EndInvoke,IAsyncResultand callbacks) does not exist on .NET Core and later; useTaskandAwait. -
DelegateandMulticastDelegatecombination by operators is C#. Books that discuss it for Visual Basic really describeAddHandlerandDelegate.Combine. -
Loop capture. Text that says every closure captured in a
For Eachsees 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 |
all see the final value; BC42324 warning |
copy to a local inside the loop |
|
BC30581, no target type |
declare the delegate type |
Narrowing signature ( |
BC36663 under |
match the signature, or wrap in a lambda that converts |
|
BC30452 |
|
Invoking a delegate whose last handler was removed |
|
|
|
a comparison; no assignment happens |
|
|
does nothing; BC42326 |
keep the delegate in a variable |
|
exceptions are unobservable |
use |
|
BC36639, BC36638 |
copy to a local |
|
|
|
Capturing a mutable variable and enumerating a deferred query later |
different result each time |
|
A |
type error, same shape but different types |
wrap in a lambda |
A statement lambda in an |
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.
References
-
Delegates (programming guide) and Relaxed Delegate Conversion
-
Delegate Statement, AddressOf Operator, AddHandler and RemoveHandler
-
Delegate, MulticastDelegate, Func<T,TResult>, Action<T>, Predicate<T> and Comparison<T>
-
Delegates and lambdas (the shared .NET overview)