通用扩展方法歧义

时间:2011-05-07 03:00:51

标签: c# .net generics

我定义了两个接口:

// 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; }
}

以及这些接口的扩展方法

// 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;
}

所有类型都在同一名称空间中。

出于某种原因,在从IVector派生的内容上调用Add()时,编译器无法确定是在MatrixExtensions类还是VectorExtensions类中使用该定义。将其中一个扩展类移动到另一个名称空间会停止错误...但我希望它们位于相同的名称空间中:D

为什么会这样?

编辑:(我不敢相信我忘记添加此内容)
我该怎么做才能解决这个问题?

6 个答案:

答案 0 :(得分:12)

您有两种扩展方法,每种方法都具有相同的签名。

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

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

是的,你在代码中提供了约束,但是constraints are not part of the signature.所以你有两个方法具有相同的签名,因此这两种方法都不比另一种方法好,而且你有一个歧义问题。

将一个静态扩展方法类移动到另一个名称空间的原因有不同的结果是,在向外扩展搜索之前,编译器将首先在最近的包含名称空间中查找扩展方法匹配。 (参见:C#语言规范的第7.5.5.2节[下面]。)例如,如果将MatrixExtensions移动到另一个命名空间,现在原始命名空间内的扩展方法调用将明确地解析为VectorExtensions方法,因为它在命名空间方面最接近。但是,这并不能完全解决您的问题。因为如果它是最接近的扩展方法,您仍然可以让IMatrix尝试使用VectorExtensions实现,因为约束不是签名的一部分。

为方便起见,语言规范。

  

7.5.5.2扩展方法调用

     

在方法调用(第7.5.5.1节)中   其中一种形式

     

expr。 identifier()

     

expr。标识符(args)

     

expr。标识符&lt; typeargs&gt; ()

     

expr。标识符&lt; typeargs&gt; (args)

     

如果正常处理了   调用找不到适用的   方法,尝试处理   构造作为扩展方法   调用。目标是找到   最好的类型名称C,这样   相应的静态方法调用   可以发生:

     

C. identifier(expr)

     

C. identifier(expr,args)

     

C.标识符&lt; typeargs&gt; (expr)

     

C.标识符&lt; typeargs&gt; (expr,args)

     

对C的搜索过程如下:

     
      
  • 从最近的封闭命名空间声明开始,继续   每个封闭的命名空间声明,   并以包含结束   编译单元,连续尝试   是为了找到一个候选集   扩展方法:      
        
    • 如果给定的命名空间或编译单元直接包含   非泛型类型声明Ci with   扩展方法Mj有   名称标识符,可访问和   适用于所需的   上面的静态方法调用,然后   这些扩展方法的集合是   候选人集。
    •   
    • 如果在给定的命名空间中使用命名空间指令导入名称空间   命名空间或编译单元直接   包含非泛型类型声明   Ci有扩展方法Mj有   名称标识符,可访问   适用于   所需的静态方法调用   上面,然后是那些扩展的集合   方法是候选集。
    •   
  •   
  • 如果在任何封闭的命名空间声明中找不到候选集,或者   编译单元,编译时错误   发生。
  •   
  • 否则,将重载决策应用于候选集   (§7.4.3)中描述的。如果没有单身   找到最佳方法,编译时   发生错误。
  •   
  • C是将最佳方法声明为扩展名的类型   方法。使用C作为目标,   然后将方法调用处理为   静态方法调用(第7.4.4节)。该   前面的规则意味着该实例   方法优先于扩展   方法,即扩展方法   内部命名空间中可用   声明优先于   外部可用的扩展方法   名称空间声明,以及   直接声明的扩展方法   命名空间优先于   导入的扩展方法   具有using命名空间的相同命名空间   指令
  •   

答案 1 :(得分:12)

我刚刚发现了一种奇怪的方法,可以在.NET 4.5中使用默认参数技巧。

/// <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());
    }
}

我很想知道这是否适用于MONO编译器(Mcs)有人想尝试吗? :)

答案 2 :(得分:4)

这是因为在评估两个方法是否具有相同签名时不考虑通用约束。您正在有效地定义两个相同的添加方法。

尝试这种方法:

// 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;
}

答案 3 :(得分:3)

约束不是签名的一部分,签名是用于确定要使用哪个重载的内容。在不考虑约束的情况下,您可以看到两种方法都具有相同的签名,因此存在歧义。请参阅Eric Lippert的文章:Constraints are not part of the signature

答案 4 :(得分:3)

  

为什么会这样?

重载决策不考虑约束(IVectorIMatrix),因为这是你的扩展方法之间唯一不同的东西,它们都是不明确的 - 它们具有相同的名称和相同的名称通用参数。

答案 5 :(得分:2)

再次找到我自己的答案(这有点像黑客):

// 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;
}

它很漂亮。 Hooray for CRTP:D