Szer
Szer

Reputation: 3476

Ambiguous extension method call

This code won't compile:

using System;
using System.Runtime.CompilerServices;

static class Extensions {
    public static void Foo(this A a, Exception e = null, string memberName = "") {
    }

    public static void Foo<T>(this A a, T t, Exception e = null, string memberName = "")
        where T : class, IB {
    }
}

interface IB { }

class A { }

class Program {
    public static void Main() {
        var a = new A();
        var e = new Exception();

        a.Foo(e); //<- Compile error "ambiguous call"
    }
}

But if I delete last string arguments everything is fine:

    public static void Foo(this A a, Exception e = null) {
    }

    public static void Foo<T>(this A a, T t, Exception e = null)
        where T : class, IB {
    }

Question is - why these optional string arguments break compiler's choice of method call?

Added: Clarified question: I dont get why compiler can't choose right overload in the first case but could do it in second one?

Edited: [CallerMemberName] attribute is not a cause of a problem here so I've deleted it from question.

Upvotes: 10

Views: 1315

Answers (1)

user743382
user743382

Reputation:

@PetSerAl pointed to the spec in the comments already, but let me translate that to plain English:

The C# language has a rule that says an overload without omitted defaulted arguments is preferred over an overload with omitted defaulted arguments. This rule makes Foo(this A a, Exception e = null) a better match than Foo(this A a, T t, Exception e = null).

The C# language does not have a rule saying that an overload with one omitted defaulted argument is preferred over an overload with two omitted defaulted arguments. Because it does not have such a rule, Foo(this A a, Exception e = null, string s = "") is not preferred over Foo<T>(this A a, T t, Exception e = null, string s = "").

The easiest way to avoid this problem would normally be by providing additional overloads, instead of using default parameter values. You need default parameter values for CallerMemberName to work, but you can provide additional overloads that omit the Exception, and forward to the real implementation by passing null for that.

Note: ensuring that Foo<T>(this A a, T t, string s = "") doesn't get picked when Foo(this A a, Exception e, string s = "") is available is going to be a tricky problem regardless. If your variable is statically typed as Exception, then the non-generic method will be preferred, but if it's statically typed as e.g. ArgumentException, then T=ArgumentException is a better match than the base class Exception, and the error in T=ArgumentException will be detected too late to pick the method you want to call. Perhaps it would be safest to place T after Exception, and always require an exception being passed in (possibly null) when the generic method is intended.

Upvotes: 7

Related Questions