简体   繁体   中英

Generic Extension Method Ambiguity

I have two interfaces defined:

// IVector.cs
public interface IVector
{
    int Size { get; }

    float this[int index] { get; set; }
}

// IMatrix.cs
public interface IMatrix
{
    int Size { get; }

    float this[int row, int column] { get; set; }
}

As well as extension methods for those interfaces

// VectorExtensions.cs
public static T Add<T>(this T vector, T value) where T : struct, IVector
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        output[i] = vector[i] + value[i];

    return output;
}

// MatrixExtensions.cs
public static T Add<T>(this T matrix, T value) where T : struct, IMatrix
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        for (int j = 0; j < output.Size; j++)
            output[i, j] = vector[i, j] + value[i, j];

    return output;
}

All the types are in the same namespace.

For some reason, when calling Add() on something derived from IVector, the compiler can't determine whether to use the definition in the MatrixExtensions class or the VectorExtensions class. Moving one of the extension classes to a different namespace stops the errors... but I kinda want them in the same namespace :D

Why is this happening?

EDIT: (I can't believe I forgot to add this)
What should I do to work around this?

I just found a curious way that works in .NET 4.5 using a trick with Default Parameters.

/// <summary>Simple base class. Can also be an interface, for example.</summary>
public abstract class MyBase1
{
}

/// <summary>Simple base class. Can also be an interface, for example.</summary>
public abstract class MyBase2
{
}

/// <summary>Concrete class 1.</summary>
public class MyClass1 :
    MyBase1
{
}

/// <summary>Concrete class 2.</summary>
public class MyClass2 :
    MyBase2
{
}

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary>
public class Magic<TBase, TInherited>
    where TInherited : TBase
{
    private Magic()
    {
    }
}

// Extensions
public static class Extensions
{
    // Rainbows and pink unicorns happens here.
    public static T Test<T>(this T t, Magic<MyBase1, T> x = null)
        where T : MyBase1
    {
        Console.Write("1:" + t.ToString() + " ");
        return t;
    }

    // More magic, other pink unicorns and rainbows.
    public static T Test<T>(this T t, Magic<MyBase2, T> x = null)
        where T : MyBase2
    {
        Console.Write("2:" + t.ToString() + " ");
        return t;
    }
}

class Program
{
    static void Main(string[] args)
    {

        MyClass1 t1 = new MyClass1();
        MyClass2 t2 = new MyClass2();

        MyClass1 t1result = t1.Test();
        Console.WriteLine(t1result.ToString());

        MyClass2 t2result = t2.Test();
        Console.WriteLine(t2result.ToString());
    }
}

I am curious to see if this works on MONO compiler (Mcs) Someone wanna try? :)

You have two extension method that each have the same signature.

// VectorExtensions.cs
public static T Add<T>(this T vector, T value)

// MatrixExtensions.cs 
public static T Add<T>(this T matrix, T value)

Yes, you have provided constraints in your code, but constraints are not part of the signature. So you have two method with the same signature, neither method is therefore better than the other, and you have an ambiguity problem.

The reason why moving one of the static extension method classes into a different namespace has a different result is that the compiler will look for extension method matches first within the closest containing namespace before expanding the search outward. (See: Section 7.5.5.2 [below] of the C# language specification.) If you move MatrixExtensions , for example, into a different namespace, now extension method invocations inside the original namespace will unambiguously resolve to the VectorExtensions method, as it is the closest in terms of namespaces. However, this doesn't completely resolve your problem. Because you could still have IMatrix trying to use the VectorExtensions implementation if it's the closest extension method because, again, constraints are not part of the signature.

For your convenience, the language specification.

7.5.5.2 Extension method invocations

In a method invocation (§7.5.5.1) of one of the forms

expr . identifier ( )

expr . identifier ( args )

expr . identifier < typeargs > ( )

expr . identifier < typeargs > ( args )

if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation. The objective is to find the best type-name C, so that the corresponding static method invocation can take place:

C . identifier ( expr )

C . identifier ( expr , args )

C . identifier < typeargs > ( expr )

C . identifier < typeargs > ( expr , args )

The search for C proceeds as follows:

  • Starting with the closest enclosing namespace declaration, continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made to find a candidate set of extension methods:
    • If the given namespace or compilation unit directly contains non-generic type declarations Ci with extension methods Mj that have the name identifier and are accessible and applicable with respect to the desired static method invocation above, then the set of those extension methods is the candidate set.
    • If namespaces imported by using namespace directives in the given namespace or compilation unit directly contain non-generic type declarations Ci with extension methods Mj that have the name identifier and are accessible and applicable with respect to the desired static method invocation above, then the set of those extension methods is the candidate set.
  • If no candidate set is found in any enclosing namespace declaration or compilation unit, a compile-time error occurs.
  • Otherwise, overload resolution is applied to the candidate set as described in (§7.4.3). If no single best method is found, a compile-time error occurs.
  • C is the type within which the best method is declared as an extension method. Using C as a target, the method call is then processed as a static method invocation (§7.4.4). The preceding rules mean that instance methods take precedence over extension methods, that extension methods available in inner namespace declarations take precedence over extension methods available in outer namespace declarations, and that extension methods declared directly in a namespace take precedence over extension methods imported into that same namespace with a using namespace directive

It is happening because generic constraints are not considered when evaluating whether two methods have the same signature. You are effectively defining two identical add methods.

Try this approach:

// VectorExtensions.cs
public static T Add<T>(this T vector, IVector value) where T : struct, IVector
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        output[i] = vector[i] + value[i];

    return output;
}

// MatrixExtensions.cs
public static T Add<T>(this T matrix, IMatrix value) where T : struct, IMatrix
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        for (int j = 0; j < output.Size; j++)
            output[i, j] = vector[i, j] + value[i, j];

    return output;
}

Constraints are not part of the signature, which is what's used to determine which overload to use. Without considering the constraints, you can see that both your methods have the same signature, hence the ambiguity. See Eric Lippert's article here: Constraints are not part of the signature .

Why is this happening?

Overload resolution does not take constraints ( IVector , IMatrix ) into account, since this is the only thing that differs between your extension methods both of them are ambiguous - they have the same name and same generic parameters.

Found my own answer again (and it's a bit of a hack):

// IVector.cs
public interface IVector<T>
    where T : IVector<T>
{
    int Size { get; }

    float this[int index] { get; set; }
}

// IMatrix.cs
public interface IMatrix<T>
    where T : IMatrix<T>
{
    int Size { get; }

    float this[int row, int column] { get; set; }
}

// VectorExtensions.cs
public T Add<T>(this IVector<T> vector, T value)
    where T : struct, IVector<T>
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        output[i] = vector[i] + value[i];

    return output;
}

// MatrixExtensions.cs
public static T Add<T>(this IMatrix<T> matrix, T value)
    where T : struct, IMatrix<T>
{
    var output = default(T);

    for (int i = 0; i < output.Size; i++)
        for (int j = 0; j < output.Size; j++)
            output[i, j] = vector[i, j] + value[i, j];

    return output;
}

It works beautifully. Hooray for CRTP :D

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