Early and Late Binding
|
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 program on it was built (with
warnings treated as errors, except the ones that are meant to fail) and run in the .NET 10 SDK container (Linux, arm64); the programs that are meant to fail show the compiler
messages the SDK printed. The one place where a program was not run in full is the Excel automation branch of the late-bound file, which
needs Windows: it is compiled on every build, guarded with OperatingSystem.IsWindows(), and labelled "built, not run" where it appears.
The root namespace of the scratch project is LateApp, because the CLR and compiler messages include it.
Binding is the step that connects the name in item.Title to a real member. When the compiler does it, the call is early bound; when the
decision is deferred to run time, the call is late bound. Visual Basic is unusual among .NET languages because late binding is an
implicit, per-file mode of the ordinary dot operator: with Option Strict Off, a member access on an Object variable compiles and is
resolved by the run-time binder, with no dynamic keyword. That makes it the most visible difference between a Visual Basic file that
is safe and one that merely looks safe. This section of the site uses Option Strict On everywhere (see
Option Statements); this is the only page that shows Option Strict Off, in a single labelled file
(LateBinding.vb, below), so that you can see what the switch really does and why you keep it out of the rest of the project.
Early and late binding at a glance
| Form | How the member is found | What it costs and what it gives |
|---|---|---|
Early binding |
The compiler looks the member up in the static type of the expression, checks arguments, and emits a direct call (or a virtual or interface call) |
Fastest; every mistake is a compile-time error; refactoring tools, IntelliSense and trimming see the call |
Late binding (Option Strict Off, |
The compiler emits a call to the run-time helper |
Slow (see the measurements below); a typo is a |
Name-based calls ( |
The same run-time lookup, written as an explicit function call |
Allowed under |
Dynamic objects ( |
The receiver itself decides, through |
Needs a late-bound call site on the Visual Basic side; the receiver chooses semantics |
The comparison with C# is short. In C#, a member access on object is always an error; late binding is requested per variable with the dynamic type,
and the run-time binder is the C# one. In Visual Basic there is no dynamic keyword: the Object type plays that part in files compiled with
Option Strict Off. A C# member declared dynamic appears to Visual Basic as plain Object. Visual Basic 6 and VBA had the same implicit
behaviour for Object and for Variant, and no Option Strict at all, which is why so much old Visual Basic code is late bound without its authors knowing.
The scratch project has one .vbproj and the files below. Only LateBinding.vb has Option Strict Off at the top; the project itself sets
OptionStrict to On, so a file that says nothing is strict.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>LateApp</RootNamespace>
<OptionExplicit>On</OptionExplicit>
<OptionStrict>On</OptionStrict>
<OptionInfer>On</OptionInfer>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
</Project>
| File | Role |
|---|---|
|
the Shelf domain ( |
|
calls each demonstration in turn; |
|
the strict, early-bound way to do each thing; |
|
name-based access under |
|
dynamic helper classes and a structure used by the late-bound file; |
|
the only file with |
|
times five ways of making the same call; |
Program.vb is the entry point. Its output, section by section, is quoted through the page.
Option Strict On
Module Program
Sub Main()
Console.WriteLine("-- early binding")
EarlyBinding.Run()
Console.WriteLine("-- CallByName under Option Strict On")
CallByNameDemo.Run()
Console.WriteLine("-- reflection under Option Strict On")
ReflectionDemo.Run()
Console.WriteLine("-- late binding (LateBinding.vb)")
LateBinding.Run()
Console.WriteLine("-- relative cost, one million calls")
Performance.Run()
Console.WriteLine("-- a typed facade over the late-bound file")
Console.WriteLine(LateBinding.DescribeAny(New Book("Emma", 1815, Isbn.Parse("9780306406157"), New Author("Jane Austen"))))
Console.WriteLine(LateBinding.DescribeAny(New Magazine("Byte", 1982, 7)))
End Sub
End Module
What Option Strict On forbids
Option Strict controls three separate things, and the compiler message tells you which one you hit.
Rule under Option Strict On |
Error | Why the rule exists |
|---|---|---|
No late binding: a member access on |
BC30574 |
the compiler cannot check that the member exists |
No implicit narrowing conversions: |
BC30512 |
a narrowing conversion can lose data or throw; you must write |
Every declaration needs an |
BC30209, BC30210 |
a missing type silently means |
No |
BC30038, BC32013 |
the operator would be late bound |
|
BC32023 |
the collection type is unknown |
Each error below comes from a short program that was built on its own (the whole file is shown, so the line and column numbers match).
The first has three late-bound uses of a String held in an Object variable: a property read, a method call, and an index.
Option Strict On
Module Program
Sub Main()
Dim item As Object = "text"
Console.WriteLine(item.Length)
item.Trim()
Dim n As Object = item(0)
End Sub
End Module
Program.vb(6,27): error BC30574: Option Strict On disallows late binding.
Program.vb(7,9): error BC30574: Option Strict On disallows late binding.
Program.vb(8,27): error BC30574: Option Strict On disallows late binding.
Source: BC30574
Narrowing is the second rule. Each of the four lines below compiles under Option Strict Off and does something surprising; under
Option Strict On each is an error that names the two types:
Option Strict On
Module Program
Sub Main()
Dim rounded As Integer = 3.7
Dim parsed As Double = "3.5"
Dim shown As String = 42
Dim number As Integer = "12"
End Sub
End Module
Program.vb(5,34): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(6,32): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.
Program.vb(7,31): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'String'.
Program.vb(8,33): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.
Source: BC30512
(The surprises: 3.7 becomes 4 by rounding, not truncation; "3.5" is parsed with the current culture; 42 becomes "42"; and "12" is parsed.)
The third rule applies when Option Infer is off, as it is in this file (Option Infer is on by default in SDK projects, so Dim x = 1 normally
infers Integer). The compiler also reports the unused variable, because warnings are not errors in this scratch project:
Option Strict On
Option Infer Off
Module Program
Sub Main()
Dim x = 1
Dim y
End Sub
End Module
Program.vb(6,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Program.vb(7,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Program.vb(7,13): warning BC42024: Unused local variable: 'y'.
Source: BC30209
The same rule applies to results: a method or property with no As clause returns Object, which is then late bound at every call.
Option Strict On
Module Program
Function Twice(n As Integer)
Return n * 2
End Function
ReadOnly Property Label
Get
Return "x"
End Get
End Property
Sub Main()
End Sub
End Module
Program.vb(4,14): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Program.vb(8,23): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Source: BC30210
Operators on Object operands are the fourth rule. Object + Object would run Operators.AddObject at run time, which looks at the actual types
and may add numbers, concatenate strings or throw. Object = Object is rejected with a hint about reference identity (Is):
Option Strict On
Module Program
Sub Main()
Dim a As Object = 1
Dim b As Object = 2
Console.WriteLine(a + b)
Console.WriteLine(a = b)
Console.WriteLine(a < b)
Console.WriteLine(a And b)
End Sub
End Module
Program.vb(7,27): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(7,31): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(8,27): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(8,31): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(9,27): error BC30038: Option Strict On prohibits operands of type Object for operator '<'.
Program.vb(9,31): error BC30038: Option Strict On prohibits operands of type Object for operator '<'.
Program.vb(10,27): error BC30038: Option Strict On prohibits operands of type Object for operator 'And'.
Program.vb(10,33): error BC30038: Option Strict On prohibits operands of type Object for operator 'And'.
For Each needs a known collection type, so an Object variable cannot be enumerated even when it holds an array, with or without an element type:
Option Strict On
Module Program
Sub Main()
Dim items As Object = New Integer() {1, 2}
For Each n In items
Console.WriteLine(n)
Next
For Each n As Integer In items
Console.WriteLine(n)
Next
End Sub
End Module
Program.vb(6,23): error BC32023: Expression is of type 'Object', which is not a collection type.
Program.vb(9,34): error BC32023: Expression is of type 'Object', which is not a collection type.
Source: BC32023
Two last cases. A method call on a variable that is declared Object is late bound even when the object is a StringBuilder, and the indexer of a Hashtable returns Object, which needs a conversion before it can become an Integer:
Option Strict On
Module Program
Sub Main()
Dim book As Object = New System.Text.StringBuilder()
book.Append("x")
Dim d As New System.Collections.Hashtable()
Dim v As Integer = d("k")
End Sub
End Module
Program.vb(6,9): error BC30574: Option Strict On disallows late binding.
Program.vb(8,28): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Integer'.
Early-bound code gets back what these errors take away by a cast. The fix is almost always one of the forms below, and EarlyBinding.vb
shows them on the Shelf domain:
Option Strict On
Module EarlyBinding
Sub Run()
Dim isbn As Isbn = Isbn.Parse("9780306406157")
Dim book As New Book("Dune", 1965, isbn, New Author("Frank Herbert"))
' Every member is resolved by the compiler against the static type.
Console.WriteLine(book.Title)
book.Title = "Dune Messiah"
Console.WriteLine(book.Describe())
' Through the base class: the call is still bound at compile time (a virtual call).
Dim item As CatalogItem = book
Console.WriteLine(item.Describe())
' An Object variable needs a cast to get early binding back.
Dim anything As Object = book
Dim asBook As Book = DirectCast(anything, Book)
Console.WriteLine(asBook.Kind)
Dim asMagazine As Magazine = TryCast(anything, Magazine)
Console.WriteLine(asMagazine Is Nothing)
Try
Dim wrong As Magazine = DirectCast(anything, Magazine)
Console.WriteLine(wrong.IssueNumber)
Catch ex As InvalidCastException
Console.WriteLine(ex.Message)
End Try
If TypeOf anything Is CatalogItem Then
Console.WriteLine(DirectCast(anything, CatalogItem).Year)
End If
End Sub
End Module
The output (the early binding section of the run):
Dune
Book: Dune Messiah (1965) by Frank Herbert
Book: Dune Messiah (1965) by Frank Herbert
Book
True
Unable to cast object of type 'LateApp.Book' to type 'LateApp.Magazine'.
1965
Source: Option Strict Statement
| Form | Result type and failure | When to use it |
|---|---|---|
|
|
you know the type and want a cast that never converts |
|
|
you are not sure; test the result with |
|
|
you want a conversion rather than a cast |
|
|
a guard before |
|
|
you need exactness |
Visual Basic has no pattern-matching is with a declared variable, so the guard and the cast are two steps (C# folds them: x is Book b).
The message of a failed DirectCast names both types, including the root namespace, as the output above shows.
Option Strict Custom and the warnings the SDK hides
The project property OptionStrict is Boolean in the SDK’s MSBuild task: it accepts On or Off, and an attempt to set it to Custom stops with
error MSB4030: "Custom" is an invalid value for the "OptionStrict" parameter of the "Vbc" task. The "OptionStrict" parameter is of type "System.Boolean".
The compiler itself has a third mode, -optionstrict:custom, in which the same problems are warnings instead of errors (BC42016 to BC42022).
It is reached through AdditionalOptions. There is a trap, though, that I checked in the SDK’s Microsoft.NET.Sdk.VisualBasic.props: when the project’s NoWarn is empty,
the SDK sets it to 41999,42016,42017,42018,42019,42020,42021,42022,42032,42036, so these warnings are silently suppressed. A project that
wants to see them must replace NoWarn. The project used for the next program is:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>ErrorsApp</RootNamespace>
<OptionStrict>Off</OptionStrict>
<AdditionalOptions>-optionstrict:custom</AdditionalOptions>
<NoWarn>1701</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
This is a project-level mode, not an Option Strict Off file: the file below says only Option Infer Off, and the project decides the rest.
Option Infer Off
Module Program
Function Twice(n As Integer)
Return n * 2
End Function
Sub Main()
Dim x = 1
Dim rounded As Integer = 3.7
Dim item As Object = "text"
Console.WriteLine(item.Length)
Dim a As Object = 1
Dim b As Object = 2
Console.WriteLine(a + b)
Console.WriteLine(a = b)
Console.WriteLine(a < b)
Console.WriteLine(a And b)
End Sub
End Module
Program.vb(4,14): warning BC42021: Function without an 'As' clause; return type of Object assumed.
Program.vb(9,13): warning BC42020: Variable declaration without an 'As' clause; type of Object assumed.
Program.vb(10,34): warning BC42016: Implicit conversion from 'Double' to 'Integer'.
Program.vb(12,27): warning BC42017: Late bound resolution; runtime errors could occur.
Program.vb(15,27): warning BC42019: Operands of type Object used for operator '+'; runtime errors could occur.
Program.vb(15,31): warning BC42019: Operands of type Object used for operator '+'; runtime errors could occur.
Program.vb(16,27): warning BC42018: Operands of type Object used for operator '='; use the 'Is' operator to test object identity.
Program.vb(16,31): warning BC42018: Operands of type Object used for operator '='; use the 'Is' operator to test object identity.
Program.vb(17,27): warning BC42019: Operands of type Object used for operator '<'; runtime errors could occur.
Program.vb(17,31): warning BC42019: Operands of type Object used for operator '<'; runtime errors could occur.
Program.vb(18,27): warning BC42019: Operands of type Object used for operator 'And'; runtime errors could occur.
Program.vb(18,33): warning BC42019: Operands of type Object used for operator 'And'; runtime errors could occur.
| Warning | Meaning |
|---|---|
BC42016 |
implicit conversion (narrowing) between two types |
BC42017 |
late-bound resolution; run-time errors could occur |
BC42018 |
|
BC42019 |
|
BC42020 |
a variable declared with no |
BC42021, BC42022 |
a function or property with no |
Source: -optionstrict
The same codes exist as errors in the Option Strict On table above. Custom is useful for migrating old code: you turn the specific warnings into errors (WarningsAsErrors) one family
at a time while the rest still compiles. For new code, On.
CallByName under Option Strict On
CallByName (in Microsoft.VisualBasic.Interaction) takes an object, the member’s name and a CallType, and returns Object. It performs the same run-time lookup as a late-bound
call, but it is an ordinary function call, so Option Strict On allows it; the risk is visible in the source and greppable. It is the right tool when the member name
comes from data (a configuration file, a column header, a plug-in contract) rather than from the program text.
Option Strict On
Imports Microsoft.VisualBasic
Public Class Printer
Public Property Prefix As String = ">"
Public Function Show(value As Integer) As String
Return Prefix & " Integer " & value.ToString()
End Function
Public Sub Increment(ByRef value As Integer)
value += 1
End Sub
Private Function Hidden() As String
Return "hidden"
End Function
Public Sub Fail()
Throw New InvalidOperationException("boom")
End Sub
Public Function Pad(value As String, width As Integer) As String
Return value.PadLeft(width, "."c)
End Function
End Class
Module CallByNameDemo
Sub Run()
Dim book As New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))
' CallType.Get reads a property, CallType.Let / Set writes it, CallType.Method calls a method.
Console.WriteLine(CallByName(book, "Title", CallType.Get))
CallByName(book, "Title", CallType.Let, "Dune Messiah")
Console.WriteLine(CallByName(book, "Describe", CallType.Method))
' Names can come from data; the result type is Object, so convert it.
Dim printer As New Printer()
Dim shown As String = CStr(CallByName(printer, "Show", CallType.Method, 7))
Console.WriteLine(shown)
Console.WriteLine(CallByName(printer, "Pad", CallType.Method, "7", 4))
ProbeCreateObject()
' Member names are matched without regard to case, and non-public members are not found.
Console.WriteLine(CallByName(book, "TITLE", CallType.Get))
Try
Console.WriteLine(CallByName(printer, "Hidden", CallType.Method))
Catch ex As MissingMemberException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
' An exception thrown by the target arrives as itself, not wrapped.
Try
CallByName(printer, "Fail", CallType.Method)
Catch ex As InvalidOperationException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
Dim memberNames As String() = {"Kind", "Year", "Missing"}
For Each memberName In memberNames
Try
Console.WriteLine(memberName & " = " & CStr(CallByName(book, memberName, CallType.Get)))
Catch ex As MissingMemberException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
Next
End Sub
#Disable Warning CA1416 ' CreateObject is Windows-only; this probe exists to show the failure elsewhere
Sub ProbeCreateObject()
Try
Dim fso As Object = CreateObject("Scripting.FileSystemObject")
Console.WriteLine(CStr(CallByName(fso, "GetFileName", CallType.Method, "C:\data\books.txt")))
Catch ex As Exception
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
End Sub
#Enable Warning CA1416
End Module
The output (the CallByName under Option Strict On section; the line Exception: Cannot create ActiveX component. is the Linux behaviour of CreateObject, whose ProgID lookup needs Windows):
Dune
Book: Dune Messiah (1965) by Frank Herbert
> Integer 7
...7
Exception: Cannot create ActiveX component.
Dune Messiah
MissingMemberException: Public member 'Hidden' on type 'Printer' not found.
InvalidOperationException: boom
Kind = Book
Year = 1965
MissingMemberException: Public member 'Missing' on type 'Book' not found.
What the output shows, and what to remember:
-
CallType.Getreads a property (or calls a method that takes no arguments),CallType.LetandCallType.Setassign (Set for object references, Let for values), andCallType.Methodcalls a method, with the arguments as theParamArray. The value comes back asObject; convert it withCStr,CIntorDirectCast. -
Names are matched without regard to case (
"TITLE"foundTitle) and only public members are found: the privateHiddenmethod raisedMissingMemberException: Public member 'Hidden' on type 'Printer' not found.The message is the same for a misspelling and for a non-public member. -
An exception thrown by the target arrives as itself (
InvalidOperationException: boom), not wrapped in aTargetInvocationException. Reflection does wrap it (see below). -
CreateObjectcompiles underOption Strict On(it returnsObject) but raises a plainSystem.Exceptionwith the textCannot create ActiveX component.when the ProgID cannot be created, which is every ProgID on Linux. The analyzer CA1416 flags the call as Windows-only, which is why the program disables that warning around the probe.
Reflection under Option Strict On
Reflection is the other name-based tool. It is stricter in one way (it returns Nothing for an unknown member rather than throwing at the call site) and looser in another (a
MemberInfo can reach non-public members when you ask).
Option Strict On
Imports System.Reflection
Module ReflectionDemo
Sub Run()
Dim book As New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))
Dim bookType As Type = book.GetType()
' The same lookups, written with reflection. Nothing here is late bound.
Dim title As PropertyInfo = bookType.GetProperty("Title")
Console.WriteLine(title.GetValue(book))
title.SetValue(book, "Dune Messiah")
Console.WriteLine(bookType.GetMethod("Describe").Invoke(book, Nothing))
Console.WriteLine(bookType.InvokeMember("Year", BindingFlags.GetProperty, Nothing, book, Nothing))
' A missing member returns Nothing from GetMethod instead of throwing at the call.
Console.WriteLine(bookType.GetMethod("Missing") Is Nothing)
' Reflection is case sensitive unless you ask otherwise, and it wraps exceptions.
Console.WriteLine(bookType.GetProperty("title") Is Nothing)
Console.WriteLine(bookType.GetProperty("title", BindingFlags.Public Or BindingFlags.Instance Or BindingFlags.IgnoreCase) Is Nothing)
Dim printer As New Printer()
Try
GetType(Printer).GetMethod("Fail").Invoke(printer, Nothing)
Catch ex As TargetInvocationException
Console.WriteLine(ex.GetType().Name & " -> " & ex.InnerException.GetType().Name)
End Try
' Look up once, then call through a typed delegate: the fast form of the same idea.
Dim getter As Func(Of Book, String) = DirectCast(
System.Delegate.CreateDelegate(GetType(Func(Of Book, String)), bookType.GetProperty("Title").GetGetMethod()),
Func(Of Book, String))
Console.WriteLine(getter(book))
End Sub
End Module
The output (the reflection under Option Strict On section):
Dune
Book: Dune Messiah (1965) by Frank Herbert
1965
True
True
False
TargetInvocationException -> InvalidOperationException
Dune Messiah
Source: Type.InvokeMember and Delegate.CreateDelegate
The differences from CallByName are worth a table, because converting between the two without noticing them is a common source of bugs.
| Aspect | CallByName and late binding |
Type.GetMethod and Invoke |
|---|---|---|
Name matching |
case-insensitive ( |
case-sensitive; |
Exceptions from the target |
arrive as themselves |
wrapped in |
Overload selection |
Visual Basic rules at run time, narrowing conversions allowed |
you pick the |
Unknown member |
|
|
Typed delegate cache |
not available |
|
The last row is the practical advice: when names must be dynamic and speed matters, look the member up once, turn it into a typed delegate
(Func(Of Book, String) above) and keep the delegate.
The labelled Option Strict Off file
Everything above was written under Option Strict On. The file below is the only one in the project that is not. Its first line says so, the project’s other files
reach into it only through DescribeAny and LateAddLoop, whose signatures are fully typed, and the module is Friend so that nothing outside the assembly sees it.
Read the comments in the file: each block is one behaviour of the Visual Basic run-time binder, and the output follows.
' LateBinding.vb: the ONLY file in this project with Option Strict Off.
' Everything else, including the project itself, uses Option Strict On.
Option Strict Off
Imports System.Dynamic
Imports Microsoft.VisualBasic
Friend Module LateBinding
' The typed facade: callers see strict, early-bound signatures.
Public Function DescribeAny(item As Object) As String
Return item.Kind & " -> " & item.Describe()
End Function
Public Function LateAddLoop(target As Object, count As Integer) As Integer
Dim last As Integer
For i = 1 To count
last = target.Bump()
Next
Return last
End Function
Public Sub Run()
Dim item As Object = New Book("Dune", 1965, Isbn.Parse("9780306406157"), New Author("Frank Herbert"))
' Property get, property set and method call on an Object variable.
Console.WriteLine(item.Title)
item.Title = "Dune Messiah"
Console.WriteLine(item.Describe())
Console.WriteLine(item.Authors.Count)
Console.WriteLine(item.Authors(0).Name)
Console.WriteLine(item.TITLE)
' An exception thrown by the target arrives as itself.
Try
Dim failing As Object = New Printer()
failing.Fail()
Catch ex As InvalidOperationException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
' The compiler cannot see the members, so the run-time binder looks them up.
Try
Console.WriteLine(item.Pages)
Catch ex As MissingMemberException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
Try
item.Rewind(3)
Catch ex As MissingMemberException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
' A Nothing receiver fails at run time too.
Dim nobody As Object = Nothing
Try
Console.WriteLine(nobody.ToString())
Catch ex As NullReferenceException
Console.WriteLine(ex.GetType().Name)
End Try
' Overloads are chosen from the run-time types of the arguments.
Dim printer As Object = New Printer()
Dim small As Object = CShort(7)
Console.WriteLine(printer.Show(small))
Dim text As Object = "8"
Console.WriteLine(printer.Show(text))
Try
Console.WriteLine(printer.Show("nine"))
Catch ex As InvalidCastException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
' Named arguments work too.
Console.WriteLine(printer.Pad(width:=6, value:="x"))
' ByRef arguments are copied back into the caller's variable after the late call.
Dim counter As Integer = 1
printer.Increment(counter)
Console.WriteLine(counter)
' Setting a field of a boxed structure through an Object variable.
Dim corner As Object = New Point2D()
corner.X = 10
corner.Y = 20
Console.WriteLine(corner.X & "," & corner.Y)
' Assigning to a field of a structure that is itself the result of a late-bound read is refused.
Dim holder As Object = New Corner()
Try
holder.Position.X = 5
Catch ex As Exception
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
' Operators on Object operands are resolved at run time as well.
Dim a As Object = 10
Dim b As Object = "2.5"
Console.WriteLine(a + b)
Console.WriteLine(a & b)
Console.WriteLine(a > b)
Console.WriteLine((a + b).GetType().Name)
' Turning the switch off also allows implicit narrowing and string-to-number conversions.
Dim rounded As Integer = 3.7
Dim parsed As Double = "3.5"
Dim shown As String = 42
Console.WriteLine(rounded & " " & parsed & " " & shown)
' Dynamic objects: IDynamicMetaObjectProvider is honoured by the Visual Basic late binder.
Dim bag As Object = New ExpandoObject()
bag.Title = "Emma"
bag.Pages = 474
Console.WriteLine(bag.Title & " " & bag.Pages)
Dim settings As Object = New SettingsBag()
settings.Theme = "dark"
Console.WriteLine(settings.THEME)
' DynamicObject.TryInvokeMember answers calls; a member read without parentheses tries the
' call first, so it reaches TryInvokeMember when the class overrides it.
Dim recorder As Object = New Recorder()
Console.WriteLine(recorder.Open("a", 2))
Console.WriteLine(recorder.Status)
' A type parameter must be boxed to an Object variable first to be used late-bound.
Console.WriteLine(LengthOf("hello"))
Console.WriteLine(LengthOf(New Integer() {1, 2, 3}))
' COM automation is late bound through CreateObject; the Windows branch is not run here.
If OperatingSystem.IsWindows() Then
Dim excel As Object = CreateObject("Excel.Application")
excel.Visible = False
Dim workbook As Object = excel.Workbooks.Add()
workbook.Worksheets(1).Cells(1, 1).Value = "Shelf"
workbook.Close(False)
excel.Quit()
Else
Console.WriteLine("COM automation needs Windows; skipped on " & Runtime.InteropServices.RuntimeInformation.OSDescription.Split(" "c)(0))
End If
End Sub
Private Function LengthOf(Of T)(value As T) As Integer
Dim boxed As Object = value
Return boxed.Length
End Function
End Module
The output (the late binding section of the run):
Dune
Book: Dune Messiah (1965) by Frank Herbert
1
Frank Herbert
Dune Messiah
InvalidOperationException: boom
MissingMemberException: Public member 'Pages' on type 'Book' not found.
MissingMemberException: Public member 'Rewind' on type 'Book' not found.
NullReferenceException
> Integer 7
> Integer 8
InvalidCastException: Conversion from string "nine" to type 'Integer' is not valid.
.....x
2
10,20
Exception: Late-bound assignment to a field of value type 'Point2D' is not valid when 'Point2D' is the result of a late-bound expression.
12.5
102.5
True
Double
4 3.5 42
Emma 474
dark
Open(a, 2)
Status()
5
3
COM automation needs Windows; skipped on Ubuntu
Program.vb then calls the typed facade, with a Book and a Magazine, and gets strings back:
Book -> Book: Emma (1815) by Jane Austen
Magazine -> Magazine: Byte (1982)
Source: Early and late binding and NewLateBinding
The helper classes that the file uses live in Settings.vb, which is Option Strict On:
Option Strict On
Imports System.Dynamic
' A dynamic property bag written under Option Strict On. Late-bound callers can read and write
' its members by name because it implements IDynamicMetaObjectProvider through DynamicObject.
Public Class SettingsBag
Inherits DynamicObject
Private ReadOnly _values As New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase)
Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
Return _values.TryGetValue(binder.Name, result)
End Function
Public Overrides Function TrySetMember(binder As SetMemberBinder, value As Object) As Boolean
_values(binder.Name) = value
Return True
End Function
End Class
' A bag that answers every method call; it also answers a plain member read, as the call
' to a method with no arguments.
Public Class Recorder
Inherits DynamicObject
Public Overrides Function TryInvokeMember(binder As InvokeMemberBinder, args() As Object, ByRef result As Object) As Boolean
result = binder.Name & "(" & String.Join(", ", args) & ")"
Return True
End Function
Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
result = "get " & binder.Name
Return True
End Function
End Class
Public Class Cell
Public Property Value As Integer
Public Function Bump() As Integer
Value += 1
Return Value
End Function
End Class
Public Structure Point2D
Public X As Integer
Public Y As Integer
End Structure
Public Class Corner
Public Property Position As Point2D
End Class
The behaviours, in the order of the file:
-
Property get, set and method call.
item.Title,item.Title = "...",item.Describe(),item.Authors.Countanditem.Authors(0).Nameare all resolved at run time. The compiler emits calls intoMicrosoft.VisualBasic.CompilerServices.NewLateBinding(LateGet,LateSet,LateCall); the names of those helpers are visible in the trim warnings further down. Chained accesses are late bound at every link. -
Case-insensitive lookup.
item.TITLEfoundTitle, asCallByNamedid. -
Exceptions from the target arrive as themselves (
InvalidOperationException: boom). -
Missing members raise
MissingMemberExceptionwith the textPublic member 'Pages' on type 'Book' not found., for properties and methods alike; the type name is the run-time type, not the variable’s. -
Nothing receivers. A late-bound call on
Nothingis aNullReferenceException, as in early-bound code. -
Overloads are resolved from the run-time argument types.
Show(Integer)is the only overload ofPrinter.Show, but the call is made with aShort(widened), aStringthat holds a number (converted, so"8"printed> Integer 8) and aStringthat does not (InvalidCastException: Conversion from string "nine" to type 'Integer' is not valid.). Early binding would have refused two of those three calls at compile time. -
Named arguments work (
Pad(width:=6, value:="x")). -
ByRef arguments are copied back.
printer.Increment(counter)changed the caller’scounterfrom 1 to 2, because the binder copies ByRef values back after the call. -
Boxed structures.
corner.X = 10on anObjectthat holds aPoint2Dworks, because the binder modifies the box. Assigning to a field of a structure that is itself the result of a late-bound read is refused, with a plainSystem.Exception:Late-bound assignment to a field of value type 'Point2D' is not valid when 'Point2D' is the result of a late-bound expression.(the Learn page for this message advises to make the assignment early bound). -
Operators on
Objectoperands follow the Visual Basic operator rules at run time:10 + "2.5"is theDouble12.5 (a string that looks like a number is converted for+when the other operand is numeric),&concatenates (102.5), anda > bcompares numerically. C#dynamicgives different answers for the same operands, as the comparison section shows. -
Narrowing conversions are silent:
3.7became4,"3.5"became3.5, and42became"42". -
Dynamic objects. The binder honours
IDynamicMetaObjectProvider: anExpandoObjecttakesbag.Title = "Emma", and aDynamicObjectsubclass seessettings.THEMEthrough itsTryGetMember. The trap is in the last two lines:recorder.StatusprintedStatus(), notget Status. A member read without parentheses first tries the invoke form, so a class that overridesTryInvokeMemberanswers property reads through it. If you write aDynamicObject, override only the hooks you want. -
Type parameters. A value of an unconstrained type parameter
Tcannot be used late bound even in anOption Strict Offfile:value.Lengthon aTstopped witherror BC30456: 'Length' is not a member of 'T'.The file assigns the value to anObjectvariable first. -
COM automation.
CreateObject("Excel.Application")returns a COM object; every.Workbooks.Add(),.Worksheets(1).Cells(1, 1).Valueafter it is late bound throughIDispatch. That block is compiled but built, not run here, since it needs Windows and Excel; theElsebranch prints a note on Linux.
Containing late binding in one file
The structure of the project is the recommendation:
-
The project sets
<OptionStrict>On</OptionStrict>; a file that wants late binding saysOption Strict Offon its first line and says why in a comment above it. -
The late-bound code lives in one
Friendmodule (hereLateBinding). Everything else calls typed functions on it:DescribeAny(item As Object) As Stringtakes the untyped value and returns aString;LateAddLoopreturns anInteger. Strictness then stops at the facade, and a code search forOption Strict Offlists every place that needs review. -
Prefer to remove the need: if the types are yours, give them a common interface or base class (
CatalogItemhasKindandDescribe, soDescribeAnycould take aCatalogItem); if the names come from data, useCallByNameunderOption Strict On; if the types come from a plug-in, load them through an interface defined in a shared assembly. -
Do not use
Option Strict Offto avoid writing a conversion.CInt,CStr,Integer.ParseandDirectCastsay what happens; the implicit versions hide a rounding or a culture dependency.
What a late call costs
Performance.vb makes the same call one million times five ways: a direct call, CallByName, MethodInfo.Invoke, a delegate created once with Delegate.CreateDelegate, and a late-bound call on an Object
from the Option Strict Off file. Each variant is run once to warm up and once timed.
Option Strict On
Imports System.Diagnostics
Imports System.Reflection
Imports Microsoft.VisualBasic
Module Performance
Private Const Iterations As Integer = 1000000
Private Function Measure(label As String, work As Func(Of Integer)) As Double
work() ' warm up
Dim watch = Stopwatch.StartNew()
Dim result = work()
watch.Stop()
Console.WriteLine(label.PadRight(28) & result.ToString().PadLeft(8) & " " & (watch.Elapsed.TotalMilliseconds).ToString("F1").PadLeft(8) & " ms")
Return watch.Elapsed.TotalMilliseconds
End Function
Sub Run()
Dim cell As New Cell()
Dim bump As MethodInfo = GetType(Cell).GetMethod("Bump")
Dim typed As Func(Of Integer) = Function() As Integer
cell.Value = 0
Dim last As Integer
For i = 1 To Iterations
last = cell.Bump()
Next
Return last
End Function
Dim viaCallByName As Func(Of Integer) = Function() As Integer
cell.Value = 0
Dim last As Integer
For i = 1 To Iterations
last = CInt(CallByName(cell, "Bump", CallType.Method))
Next
Return last
End Function
Dim viaReflection As Func(Of Integer) = Function() As Integer
cell.Value = 0
Dim last As Integer
For i = 1 To Iterations
last = CInt(bump.Invoke(cell, Nothing))
Next
Return last
End Function
Dim viaDelegate As Func(Of Integer) = Function() As Integer
cell.Value = 0
Dim call1 = DirectCast(System.Delegate.CreateDelegate(GetType(Func(Of Integer)), cell, bump), Func(Of Integer))
Dim last As Integer
For i = 1 To Iterations
last = call1()
Next
Return last
End Function
Dim viaLate As Func(Of Integer) = Function() As Integer
cell.Value = 0
Return LateBinding.LateAddLoop(cell, Iterations)
End Function
Dim baseline = Measure("early bound call", typed)
Dim t1 = Measure("CallByName", viaCallByName)
Dim t2 = Measure("MethodInfo.Invoke", viaReflection)
Dim t3 = Measure("cached typed delegate", viaDelegate)
Dim t4 = Measure("late bound (Object)", viaLate)
Console.WriteLine("late / early ratio: " & (t4 / baseline).ToString("F0") & "x (varies by machine)")
End Sub
End Module
The output (the relative cost section; absolute times vary by machine and run, the ranking does not):
early bound call 1000000 6.9 ms
CallByName 1000000 329.2 ms
MethodInfo.Invoke 1000000 28.4 ms
cached typed delegate 1000000 9.4 ms
late bound (Object) 1000000 328.2 ms
late / early ratio: 48x (varies by machine)
Source: MethodBase.Invoke
The late-bound call and CallByName cost about the same, because both run through NewLateBinding, and both were between 34 and 48 times slower than the
direct call in the runs made for this page; MethodInfo.Invoke was in the middle, and a cached delegate was as fast as the direct call. These are the numbers of one Linux arm64 container run, not a
benchmark suite: use them for the shape, and measure your own code with a proper harness if it matters. The binder does extra work on each call that a cache cannot remove
for you: it must find the member by name, apply the overload rules to the actual arguments, convert the arguments, and copy ByRef values back.
Trimming and Native AOT
Late binding looks members up by name at run time, and the .NET trimmer removes members that nothing refers to by name at build time. The Visual Basic runtime marks the late-binding
helpers RequiresUnreferencedCode, so publishing with trimming enabled reports them. This is a real publish of the project with PublishTrimmed:
dotnet publish -r linux-arm64 -p:PublishTrimmed=true -p:TreatWarningsAsErrors=false -o out
Representative warnings (the publish printed dozens of IL2026 warnings, most of them in LateBinding.vb, and more for CallByName and CreateObject in CallByNameDemo.vb):
LateBinding.vb(26,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(Object, Type, String, Object[], String[], Type[], Boolean[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(27,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateSet(Object, Type, String, Object[], String[], Type[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(28,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(Object, Type, String, Object[], String[], Type[], Boolean[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Late binding is dynamic and cannot be statically analyzed. The referenced types and members may be trimmed.
LateBinding.vb(99,9): Trim analysis warning IL2026: LateApp.LateBinding.Run(): Using member 'Microsoft.VisualBasic.CompilerServices.Operators.AddObject(Object, Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The object types cannot be statically analyzed so their operators may be trimmed.
CallByNameDemo.vb(74,17): Trim analysis warning IL2026: LateApp.CallByNameDemo.ProbeCreateObject(): Using member 'Microsoft.VisualBasic.Interaction.CreateObject(String, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The COM object to be created cannot be statically analyzed and may be trimmed.
ReflectionDemo.vb(11,13): Trim analysis warning IL2075: LateApp.ReflectionDemo.Run(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
The warnings are not hypothetical. Running the trimmed program (the first section is early bound and works; Printer.Show is reached only by name, so the trimmer removed it):
-- early binding
Dune
Book: Dune Messiah (1965) by Frank Herbert
Book: Dune Messiah (1965) by Frank Herbert
Book
True
Unable to cast object of type 'LateApp.Book' to type 'LateApp.Magazine'.
1965
-- CallByName under Option Strict On
Dune
Book: Dune Messiah (1965) by Frank Herbert
Unhandled exception. System.MissingMemberException: Public member 'Show' on type 'Printer' not found.
The same program passes when it is not trimmed, as the earlier output shows. The remedies, in order of preference:
-
Replace late binding by interfaces, generics or delegates, so that the trimmer sees the call.
-
Keep reflection but tell the trimmer what it needs:
DynamicDependencyorDynamicallyAccessedMembersannotations on the types you reflect on, or a root descriptor file (TrimmerRootDescriptor) for plug-in assemblies. -
Do not trim the application (
PublishTrimmedis off by default).
The same RequiresUnreferencedCode annotations apply to Native AOT, which also rejects code that must be generated at run time. A Native AOT publish was not run for this page (it needs
a native toolchain that the SDK container does not have), so treat late binding in a Native AOT application as unsupported until you have tested the exact calls.
Source: Native AOT deployment
C# dynamic compared
C# dynamic solves the same problem with a different default: it is opt-in per variable, and it uses the C# binder (Microsoft.CSharp.RuntimeBinder, whose assembly is part of the shared framework) rather than the Visual Basic one. The same scenario written
in C# against the same Visual Basic domain (Shelf.Core):
using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using Shelf.Core;
// C# dynamic: the same late binding idea, resolved by the C# run-time binder.
dynamic item = new Book("Dune", 1965, Isbn.Parse("9780306406157"), new Author("Frank Herbert"));
Console.WriteLine(item.Title);
item.Title = "Dune Messiah";
Console.WriteLine(item.Describe());
Console.WriteLine(item.Authors[0].Name);
try
{
Console.WriteLine(item.Pages);
}
catch (RuntimeBinderException ex)
{
Console.WriteLine(ex.GetType().Name + ": " + ex.Message);
}
dynamic bag = new ExpandoObject();
bag.Title = "Emma";
bag.Pages = 474;
Console.WriteLine(bag.Title + " " + bag.Pages);
dynamic a = 10;
dynamic b = "2.5";
try
{
Console.WriteLine(a + b);
}
catch (RuntimeBinderException ex)
{
Console.WriteLine(ex.GetType().Name);
}
Console.WriteLine(a + 2.5);
Console.WriteLine((a + 2.5).GetType().Name);
Dune
Book: Dune Messiah (1965) by Frank Herbert
Frank Herbert
RuntimeBinderException: 'Shelf.Core.Book' does not contain a definition for 'Pages'
Emma 474
102.5
12.5
Double
Source: Using type dynamic
| Behaviour | Visual Basic with Option Strict Off |
C# dynamic |
|---|---|---|
Switch |
per file, by the |
per variable, by the type |
Binder |
|
|
Missing member |
|
|
Name matching |
case-insensitive |
case-sensitive |
|
|
|
Exceptions from the target |
unwrapped (shown above) |
see the C# page; not measured here |
When a C# library hands you a dynamic value, Visual Basic sees Object (the [Dynamic] attribute is ignored), so you need a late-bound call site to use it. That is one more reason to isolate
the call: see Interoperating with C# and Other .NET Languages, and for the C# side
of the same feature Expression Trees and dynamic and Native Interop.
When late binding is still the right tool
-
COM automation without a type library or with several versions of one. Excel, Word and Outlook are the classic case: a late-bound
CreateObject("Excel.Application")works with whatever version is installed, where an early-bound project references one version’s interop assembly. The cost is no IntelliSense and no compile-time check of member names. Details, and the Windows-only build settings, are on COM Interop and Office Automation; for server-side document generation use the libraries described in Apache POI: introduction and architecture and exporting reports from a server on this site’s backend pages, or the Open XML SDK, not Office automation. -
Dynamic data with
ExpandoObject,DynamicObjector a C# library that returnsdynamic. -
Plug-ins and scripting hosts that expose objects by name, when you control neither the types nor a shared interface.
-
Test and tooling code where the cost does not matter and the type is genuinely unknown.
For everything else, the early-bound version is shorter to read, faster, trimmable and checked by the compiler.
Where older material is outdated
-
Older Visual Basic books and articles often recommend
Option Strict Offfor convenience, or leave it off by default. The .NET SDK still defaults it to Off (OptionStrictis Off unless the project sets it, as I confirmed from the SDK properties), so the recommendation of this section is to setOnin the project or in aDirectory.Build.props. -
Material written for Visual Basic 6 and VBA treats
VariantandObjectas freely late bound, and has no equivalent ofOption Strict. In Visual Basic .NET the difference is a switch, and the default for new code should be strict. -
Advice that Office automation needs
Option Strict Offis out of date: early binding to the interop assemblies is the norm when the Office version is fixed, andCallByNameunderOption Strict Oncovers the occasional late call. -
Older guidance about "late-bound `CreateObject`" does not mention trimming: late-bound code is not trim-safe, and the SDK now tells you so when you publish with trimming.
-
Windows-only advice such as
CreateObjectandGetObjectis not available on Linux and macOS, where the call fails withCannot create ActiveX component.
Rules of thumb
-
Set
OptionStricttoOnin the project; treatOption Strict Offas a per-file exception that needs a comment. -
Use
DirectCastandTryCastfor cast needs,CTypeandParsemethods for conversions. -
Use
CallByNameor a cached delegate when the member name is data. -
Keep late-bound code in a
Friendmodule with typed signatures. -
Remember that a late bind is case-insensitive, allows narrowing, copies ByRef values back and can silently convert strings.
-
Do not publish a late-bound application trimmed or with Native AOT unless you have tested every late call after publishing.
-
Prefer interfaces: the compiler can check a call to an interface and the trimmer can see it.
References
-
Early and late binding (Visual Basic programming guide), with Determining object type and Working with dynamic objects
-
Interaction.CallByName, Interaction.CreateObject and NewLateBinding
-
Introduction to trim warnings, Known trimming incompatibilities and Native AOT deployment
-
Using type dynamic (C#)
-
Option Statements, Type Conversions, Attributes, Caller Information and Reflection, COM Interop and Office Automation and Libraries, Packaging and Deployment in this section