About the limitation
As per Microsoft's official docs, late binding only works on public methods.
Late binding can only be used to access type members that are declared as Public. Accessing members declared as Friend or Protected Friend results in a run-time error.
I've been curious as to why this restriction was put in.
It seems a bit strange from a language design perspective that early binding respects access rights for methods, but late binding only supports public
methods.
Example showing the limitation
Here's a VB.NET MVCE, it demonstrates the limitation.
Option Explicit On
Option Strict Off
Class SomeClass
Public Sub PublicMethod()
Console.WriteLine("SomePublicMethod")
End Sub
Friend Sub InternalMethod()
Console.WriteLine("InternalMethod")
End Sub
Private Sub PrivateMethod()
Console.WriteLine("PrivateMethod")
End Sub
'Note Late binding can only be used to access type members that are declared as Public. Accessing members
'declared as Friend Or Protected Friend results in a runtime error.
Public Sub Test()
Dim classInstance As New SomeClass
Dim classInstanceObj As Object = classInstance
'Early binding. All of these work.
classInstance.PublicMethod()
classInstance.InternalMethod()
classInstance.PrivateMethod()
'Now for late binding
'Runs okay.
classInstanceObj.PublicMethod()
'Both of these fail (MissingMemberException), even though technically it turns out that
'this class has access to them.
classInstanceObj.InternalMethod()
classInstanceObj.PrivateMethod()
End Sub
End Class
Module Module1
Sub Main()
Dim testInstance As New SomeClass()
testInstance.Test()
End Sub
End Module
For fun, let's look at the implementation.
The call stack of the resulting exception looks like this:
at Microsoft.VisualBasic.CompilerServices.Symbols.Container.GetMembers(String& MemberName, Boolean ReportErrors)
at Microsoft.VisualBasic.CompilerServices.NewLateBinding.CallMethod(Container BaseReference, String MethodName, Object[] Arguments, String[] ArgumentNames, Type[] TypeArguments, Boolean[] CopyBack, BindingFlags InvocationFlags, Boolean ReportErrors, ResolutionFailure& Failure)
at Microsoft.VisualBasic.CompilerServices.NewLateBinding.ObjectLateCall(Object Instance, Type Type, String MemberName, Object[] Arguments, String[] ArgumentNames, Type[] TypeArguments, Boolean[] CopyBack, Boolean IgnoreReturn)
at Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateCall(Object Instance, Type Type, String MemberName, Object[] Arguments, String[] ArgumentNames, Type[] TypeArguments, Boolean[] CopyBack, Boolean IgnoreReturn)
at PublicLateBinding.SomeClass.Test() in <PathToModule1.vb>\Module1.vb:line 39
Unfortunately, Microsoft.VisualBasic.dll is still mostly closed source, so I'll have to rely on DotPeek for this one.
I'm going to take a guess and say that LookupDefaultMembers
in Microsoft.VisualBasic.CompilerServices.Symbols.Container
is responsible for collecting runnable member methods. Its decompiled code looks like this (sorry, DotPeek can only decompile to C#).
Decompiled implementation code from Microsoft.VisualBasic.dll:
private MemberInfo[] LookupDefaultMembers(ref string DefaultMemberName, Type SearchType)
{
string name = (string) null;
Type Type = SearchType;
do
{
object[] customAttributes = Type.GetCustomAttributes(typeof (DefaultMemberAttribute), false);
if (customAttributes != null && customAttributes.Length > 0)
{
name = ((DefaultMemberAttribute) customAttributes[0]).MemberName;
break;
}
Type = Type.BaseType;
}
while ((object) Type != null && !Symbols.IsRootObjectType(Type));
MemberInfo[] memberInfoArray1;
if (name != null)
{
MemberInfo[] memberInfoArray2 = Symbols.Container.FilterInvalidMembers(Type.GetMember(name, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy));
if (memberInfoArray2 != null)
{
DefaultMemberName = name;
if (memberInfoArray2.Length > 1)
{
Array.Sort((Array) memberInfoArray2, (IComparer) Symbols.Container.InheritanceSorter.Instance);
}
memberInfoArray1 = memberInfoArray2;
goto label_10;
}
}
memberInfoArray1 = Symbols.Container.NoMembers;
label_10:
return memberInfoArray1;
}
I'd like to specifically emphasize this part:
Type.GetMember(name, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
It appears that this was a deliberate move, to not pass GetMember
BindingFlags.NonPublic
, among others. Though, the fact that BindingFlags.Static
is included is interesting...
I'll take a couple guesses as to why it's like this:
(to the best of my knowledge, there isn't any official statement from Microsoft on this)
It was not worth the effort to make a version of the reflection methods that support enforcing access rights (i.e., something that would check the method flags / assemblies, and throw an exception if and only if the late bound object really can't run the method, based on the access level of the method).
This is some kind of backwards compatibility thing, e.g,. VB6 only allowed late binding on
Public
methods and this was done to make automatic code conversion easier? (I haven't done any VB6/VBA/VBScript/anything else VB)Having late binding only on
Public
methods was easier to explain and document, instead of writing "Late binding only works if you are following the access level of the method".
I figured it was alright to post this because C# gets questions like "What was the design rationale for making void not a primitive type?", unfortunately there seems to be a lack of questions like this for VB.NET, hopefully this is well received.