简体   繁体   中英

Overloading, generics and type constraints : method resolution

Consider this code snippet, featuring generics and overloaded functions :

using System;

namespace Test_Project
{
    public interface Interface
    {
        void f();
    }

    public class U : Interface
    {
        public void f() {}
    }

    public class Class<T> where T: Interface
    {
        public static void OverloadedFunction(T a)
        {
            Console.WriteLine("T");
            a.f();
        }

        public static void OverloadedFunction(U a)
        {
            Console.WriteLine("U");
            a.f();
        }
    }

    class Program
    {
        public static void Invoke(U instance)
        {
            Class<U>.OverloadedFunction(instance);
        }

        static void Main(string[] args)
        {
            Invoke(new U());
        }
    }
}

I would say it doesn't compile, as I have two suitable candidates methods for OverloadedFunction. However it does and prints "U".

In the generated IL, I can see that:

.method public hidebysig static 
    void Invoke (
        class Test_Project.U 'instance'
    ) cil managed 
{
    // Method begins at RVA 0x2085
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call void class Test_Project.Class`1<class Test_Project.U>::OverloadedFunction(class Test_Project.U)
    IL_0006: ret
} // end of method Program::Invoke

meaning that the C# compiler resolved the call to OverloadedFunction to a call instead of the callvirt which would have been required by the "generic" function. I can guess that the 'U' method is a better candidate from a compiler perspective, but I can't definitely explain why...

I'd really like to understand what happened here and I have no clue.

But it gets even weirder if you consider this modified version of the snippet, where we introduce another level of indirection :

using System;

namespace Test_Project
{
    public interface Interface
    {
        void f();
    }

    public class U : Interface
    {
        public void f() {}
    }

    public class V : U { }

    public class Class<T> where T: Interface
    {
        public static void OverloadedFunction(T a)
        {
            Console.WriteLine("T");
            a.f();
        }

        public static void OverloadedFunction(U a)
        {
            Console.WriteLine("U");
            a.f();
        }
    }

    class Program
    {
        public static void Invoke(V instance)
        {
            Class<V>.OverloadedFunction(instance);
        }

        static void Main(string[] args)
        {
            Invoke(new V());
        }
    }
}

I would expect this program to still print 'U', as 'V' are 'U' by inheritance. But it prints 'T', as show by the MSIL :

.method public hidebysig static 
    void Invoke (
        class Test_Project.V 'instance'
    ) cil managed 
{
    // Method begins at RVA 0x208d
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call void class Test_Project.Class`1<class Test_Project.V>::OverloadedFunction(!0)
    IL_0006: ret
} // end of method Program::Invoke

meaning that the generic version has been preferred by the c# compiler.

Could please someone explain what are the rules of overloaded methods resolution when it comes to generic parameters and inheritance?

This is specified in the C# spec .


In the first case, we have two candidates:

1. public static void OverloadedFunction(T (= U) a)
2. public static void OverloadedFunction(U a)

§ 7.5.3.6 of the spec says (emphasis mine):

While signatures as declared must be unique, it is possible that substitution of type arguments results in identical signatures. In such cases, the tie-breaking rules of overload resolution above will pick the most specific member .

And the tie-breaking rules (§ 7.5.3.2) say:

A type parameter is less specific than a non-type parameter

T is a type parameter; U isn't. Thus, U is more specific and overload 2 is chosen.


In the second case, we have the following two candidates:

1. public static void OverloadedFunction(T (= V) a)
2. public static void OverloadedFunction(U a)

Here, overload 1 T (= V) is a better match:

  • According to § 7.5.3.3, an identity conversion ( V to V ) is a better conversion than any other kind of conversion (such as widening V to U ).
  • Thus, according to § 7.5.3.2, overload 1 is a "better function member" than overload 2 for this invocation.

It makes sense to me.

It will pick the best match, preferring the overload where no casting is needed. Since you're using Class<V> , then this method:

public static void OverloadedFunction(T a)

effectively becomes:

public static void OverloadedFunction(V a)

which is a better match when accepting a parameter of type V , since no casting is needed.

Your first example is more unpredictable in my opinion, since either one could work. But it does seem like it prefers the strongly-typed method over the generic, which I guess makes sense too.

Reading the specifications, it does look like non-generic methods are preferred: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#method-invocations

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM