Универсальный метод расширения Неоднозначность

У меня есть два интерфейса:

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

Все типы находятся в одном и том же пространстве имен.

По какой-то причине при вызове Add() на чем-то, полученном из IVector, компилятор не может определить, следует ли использовать определение в классе MatrixExtensions или в классе VectorExtensions. Перемещение одного из классов расширения в другое пространство имен останавливает ошибки... но я хочу, чтобы они были в одном пространстве имен: D

Почему это происходит?

EDIT: (я не могу поверить, что забыл добавить это)
Что мне делать, чтобы обойти это?

Ответ 1

Нашел снова мой собственный ответ (и это немного взломало):

// 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 для CRTP: D

Ответ 2

У вас есть два метода расширения, каждый из которых имеет одну и ту же подпись.

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

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

Да, вы предоставили ограничения в своем коде, но ограничения не являются частью подписи. Итак, у вас есть два метода с одной и той же сигнатурой, поэтому ни один метод не лучше другого, и у вас есть проблема двусмысленности.

Причина, по которой перемещение одного из классов статического класса расширения в другое пространство имен имеет другой результат, заключается в том, что компилятор будет искать метод расширения, совпадающий сначала с ближайшим, содержащим пространство имен, перед расширением поиска наружу. (См. Раздел 7.5.5.2 [ниже] спецификации языка С#.) Если вы перемещаете MatrixExtensions, например, в другое пространство имен, теперь вызовы метода расширения внутри исходного пространства имен будут однозначно разрешать метод VectorExtensions поскольку он является самым близким по пространству имен. Однако это не полностью решает вашу проблему. Поскольку вы все еще можете использовать IMatrix для использования реализации VectorExtensions, если это ближайший метод расширения, потому что опять же ограничения не являются частью сигнатуры.

Для вашего удобства, спецификация языка.

7.5.5.2 Вызов метода расширения

В вызове метода (п. 7.5.5.1) одна из форм

выражение. Идентификатор()

выражение. идентификатор (args)

выражение. идентификатор <typeargs> ()

выражение. идентификатор <typeargs> (args)

если нормальная обработка вызов не применим методов, делается попытка обрабатывать конструкция как метод расширения призывание. Цель состоит в том, чтобы найти лучшее имя типа C, так что соответствующий вызов статического метода может иметь место:

C. идентификатор (expr)

C. идентификатор (expr, args)

C. идентификатор <typeargs> (expr)

C. идентификатор <typeargs> (expr, args)

Поиск C продолжается следующим образом:

  • Начиная с ближайшего объявляющего объявления пространства имен, продолжая каждая включающая декларация пространства имен, и заканчивая содержащим блок компиляции, последовательные попытки для поиска кандидата методы расширения:
    • Если данное пространство имен или блок компиляции непосредственно содержит объявления не общего типа Ci с методы расширения Mj, которые имеют Идентификатор имени и доступны и применимым в отношении желаемого вызов статического метода выше, затем набор этих методов расширения набор кандидатов.
    • Если пространства имён импортируются с помощью директив namespace в заданный пространство имен или блок компиляции напрямую содержат объявления не общего типа Ci с методами расширения Mj, которые имеют идентификатор имени и доступны и применимы в отношении желаемый вызов статического метода выше, то набор этих расширений методы - это набор кандидатов.
  • Если набор кандидатов не найден в любом объявлении пространства имен или блок компиляции, ошибка времени компиляции имеет место.
  • В противном случае разрешение перегрузки применяется к кандидату, установленному как описанных в (§7.4.3). Если ни один найден лучший метод, время компиляции возникает ошибка.
  • C - тип, в котором наилучший метод объявлен как расширение метод. Используя C в качестве цели, вызов метода затем обрабатывается как вызов статического метода (§7.4.4). предшествующие правила означают, что экземпляр методы имеют приоритет над расширением методы, методы расширения доступно во внутреннем пространстве имен декларации имеют приоритет над методы расширения, доступные во внешнем объявления пространства имен и методы расширения, объявленные непосредственно в пространство имен имеет приоритет над методы расширения, импортированные в этот такое же пространство имен с использованием пространства имен Директива

Ответ 3

Я просто нашел любопытный способ, который работает в .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) Кто-то хочет попробовать?:)

Ответ 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;
}

Ответ 5

Ограничения не являются частью подписи, которая используется для определения того, какую перегрузку использовать. Не рассматривая ограничения, вы можете видеть, что оба метода имеют одну и ту же подпись, а значит и двусмысленность. См. Статью Эрика Липперта здесь: Ограничения не входят в подпись.

Ответ 6

Почему это происходит?

Разрешение перегрузки не учитывает ограничений (IVector, IMatrix), так как это единственное, что отличается между вашими методами расширения, оба они неоднозначны - они имеют одно и то же имя и одни и те же общие параметры.