Method resolution issue with default parameters and generics
Asked Answered
I

1

12

Using .NET 4, I am confused by the inability of the compiler to resolve the first method call in the sample below.

using System;

namespace MethodResolutionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            NonGeneric foo = null;

            // ambiguous
            foo.Ext1(x => new NonGeneric());

            // resolves to first Ext1
            foo.Ext1(x => new NonGeneric(), 1);


            // resolves to first Ext2
            foo.Ext2(x => new NonGeneric());

            // resolves to first Ext2
            foo.Ext2(x => new NonGeneric(), 1);

            // resolves to second Ext2
            foo.Ext2(x => "foo");

            // resolves to second Ext2
            foo.Ext2(x => "foo", 1);


            // resolves to first Ext3
            foo.Ext3(x => new NonGeneric());

            // resolves to first Ext3
            foo.Ext3(x => new NonGeneric(), 1);

            // resolves to second Ext3
            foo.Ext3(x => "foo");

            // resolves to second Ext3
            foo.Ext3(x => "foo", 1);
        }
    }

    public class NonGeneric
    {
    }

    public class Generic<T> : NonGeneric
    {
    }

    public static class Extensions1
    {
        public static NonGeneric Ext1(this NonGeneric first, Func<NonGeneric, NonGeneric> getNext, int i = 0)
        {
            return null;
        }

        public static Generic<TNext> Ext1<TNext>(this NonGeneric first, Func<NonGeneric, TNext> getNext, int i = 0, string s = null)
        {
            return null;
        }
    }

    // only difference between Extensions2 and Extensions1 is that the second overload no longer has a default string parameter
    public static class Extensions2
    {
        public static NonGeneric Ext2(this NonGeneric first, Func<NonGeneric, NonGeneric> getNext, int i = 0)
        {
            return null;
        }

        public static Generic<TNext> Ext2<TNext>(this NonGeneric first, Func<NonGeneric, TNext> getNext, int i = 0)
        {
            return null;
        }
    }

    // Extensions3 explicitly defines an overload that does not default the int parameter
    public static class Extensions3
    {
        public static NonGeneric Ext3(this NonGeneric first, Func<NonGeneric, NonGeneric> getNext)
        {
            return Ext3(first, getNext, default(int));
        }

        public static NonGeneric Ext3(this NonGeneric first, Func<NonGeneric, NonGeneric> getNext, int i = 0)
        {
            return null;
        }

        public static Generic<TNext> Ext3<TNext>(this NonGeneric first, Func<NonGeneric, TNext> getNext, int i = 0)
        {
            return null;
        }
    }
}

Can anyone shed some light on this? I suspect I don't really have a way forward here other than modifying my APIs to help the compiler (as per Extensions3 above), but if there is an easier/better way then I'd love to hear it.

Intimidate answered 13/11, 2012 at 10:52 Comment(0)
A
1

It is ambiguous because you have two optional parameters in the second Ext1 extension method. Because both parameters are omitted in the first call, the compiler doesn't know which one you want to use.

From C# 4.0 Language Specification:

§7.5.3 Overload resolution:

Given the set of applicable candidate function members, the best function member in that set is located. If the set contains only one function member, then that function member is the best function member. Otherwise, the best function member is the one function member that is better than all other function members with respect to the given argument list, provided that each function member is compared to all other function members using the rules in §7.5.3.2. If there is not exactly one function member that is better than all other function members, then the function member invocation is ambiguous and a binding-time error occurs.

Further, under §7.5.3.2 Better function member:

Optional parameters with no corresponding arguments are removed from the parameter list

What this means is that when you omit the two last arguments in the method call and the NonGeneric type is inferred (read about type inference under §7.5.2), both methods would look like this:

Ext1(this NonGeneric first, Func<NonGeneric, NonGeneric> getNext)

Thus, they would be ambiguous...

I would recommend reading §7.5.3.2 or even the whole §7.5.3 of the specification for further info.

The solution is to either change your method declarations or remove the first overload altogether and let the second do the work :)

Aikoail answered 13/11, 2012 at 11:8 Comment(7)
The compiler chooses fine in both the Extensions2 and Extensions3 scenarios, so it's not that simple. Also, if I didn't want the default int parameter, then I obviously wouldn't have declared it that way in the first place!Intimidate
But why do you have two methods with optional parameters which effectively are ambiguous if the optional parameters are omitted? If you absolutely need both methods, you would need to use either your Extensions2 or Extensions3 solutions.Aikoail
@khellang: can you point out C# language specification sections leading to such behaviour (overload resolution ambiguity)?Gerber
@KonstantinOznobihin Check the revised answer :)Aikoail
@khellang: what makes me doubt is '§7.5.3.2 Better function member', the first tie-breaking rule there is 'If MP is a non-generic method and MQ is a generic method, then MP is better than MQ.' I've read through several spec sections, but still unable to find explanation why generic method causes ambiguity (that's why I've asked you a question).Gerber
@khellang: your reasoning also does not explain why we don't have ambiguity with Extensions2 methods.Gerber
You are right. After type inference and removal of the optional parameters, both methods in Extensions1 and Extensions2 should have the same arguments. The generic vs non-generic tie-break rule should therefore pick the first method in both cases.Aikoail

© 2022 - 2024 — McMap. All rights reserved.