Why are late bound method calls designed to only w

2019-07-10 05:02发布

问题:

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)

  1. 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).

  2. 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)

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