Выбор метода C # дженериков

9

Я пытаюсь написать универсальные алгоритмы на C #, которые могут работать с геометрическими объектами различной размерности.

В следующем надуманном примере я Point2и Point3оба реализую простой IPointинтерфейс.

Теперь у меня есть функция, GenericAlgorithmкоторая вызывает функцию GetDim. Существует несколько определений этой функции в зависимости от типа. Существует также резервная функция, которая определена для всего, что реализует IPoint.

Первоначально я ожидал, что результат следующей программы будет 2, 3. Однако это 0, 0.

interface IPoint {
    public int NumDims { get; } 
}

public struct Point2 : IPoint {
    public int NumDims => 2;
}

public struct Point3 : IPoint {
    public int NumDims => 3;
}

class Program
{
    static int GetDim<T>(T point) where T: IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 0 !!
        Console.WriteLine("{0:d}", d2);        // returns 0 !!
    }
}

Итак, по какой-то причине конкретная информация о типе теряется в GenericAlgorithm. Я не совсем понимаю, почему это происходит, но хорошо. Если я не могу сделать это таким образом, какие у меня есть альтернативы?

mohamedmoussa
источник
2
«Существует также резервная функция» Какова цель этого, точно? Весь смысл реализации интерфейса состоит в том, чтобы гарантировать, что NumDimsсвойство доступно. Почему вы игнорируете это в некоторых случаях?
Джон Ву
Так что в основном это компилируется. Изначально я думал, что резервная функция необходима, если во время выполнения JIT-компилятор не может найти специализированную реализацию для GetDim(т.е. я передаю, Point4но GetDim<Point4>не существует). Однако, похоже, компилятору не стоит искать специализированную реализацию.
Мохамедмусса
1
@woggy: Вы говорите, что «кажется, что компилятору не надо искать специализированную реализацию», как будто это было делом лени со стороны дизайнеров и разработчиков. Это не. Вопрос в том, как дженерики представлены в .NET. Это просто не та же специализация, что у шаблонов в C ++. Универсальный метод не компилируется отдельно для каждого аргумента типа - он компилируется один раз. Конечно, в этом есть свои плюсы и минусы, но дело не в том, чтобы «беспокоиться».
Джон Скит
@jonskeet Извиняюсь, если мой выбор языка был плохим, я уверен, что здесь есть сложности, которые я не рассмотрел. Насколько я понимаю, компилятор не компилирует отдельные функции для ссылочных типов, но делает это для типов / структур значений, это правильно?
Мохамедмусса
@woggy: Это JIT- компилятор, который совершенно не похож на компилятор C #, а компилятор C # выполняет разрешение перегрузки. IL для универсального метода генерируется только один раз, а не один раз для каждой специализации.
Джон Скит

Ответы:

10

Этот способ:

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

... всегда буду звонить GetDim<T>(T point). Разрешение перегрузки выполняется во время компиляции , и на этом этапе нет другого применимого метода.

Если вы хотите, чтобы разрешение перегрузки вызывалось во время выполнения , вам нужно использовать динамическую типизацию, например

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim((dynamic) point);

Но обычно для этого лучше использовать наследование - в вашем примере, очевидно, вы могли бы просто иметь один метод и возвращать point.NumDims. Я предполагаю, что в вашем реальном коде есть какая-то причина, по которой этот эквивалент сложнее сделать, но без большего контекста мы не можем посоветовать, как использовать наследование для выполнения специализации. Это ваши варианты, хотя:

  • Наследование (предпочтительно) для специализации на основе типа времени выполнения цели
  • Динамическая типизация для разрешения перегрузки во время выполнения
Джон Скит
источник
Реальная ситуация у меня есть AxisAlignedBoundingBox2и AxisAlignedBoundingBox3. У меня есть Containsстатический метод, который используется для определения, содержит ли набор ящиков Line2или Line3(который зависит от типа ящиков). Логика алгоритма между двумя типами абсолютно одинакова, за исключением того, что количество измерений различно. Есть также вызовы Intersectвнутри, которые должны быть специализированы для правильного типа. Я хочу избежать виртуальных вызовов функций / динамических, поэтому я использую дженерики ... конечно, я могу просто скопировать / вставить код и двигаться дальше.
Мохамедмусса
1
@woggy: Это довольно сложно представить из описания. Если вам нужна помощь в попытке сделать это с помощью наследования, я предлагаю вам создать новый вопрос с минимальным, но полным примером.
Джон Скит
Хорошо, сделаю, сейчас я приму этот ответ, потому что, кажется, я не привел хороший пример.
Мохамедмусса
6

Начиная с C # 8.0, вы должны иметь возможность предоставлять реализацию по умолчанию для вашего интерфейса, а не требовать универсального метода.

interface IPoint {
    int NumDims { get => 0; }
}

Реализация универсального метода и перегрузок для каждой IPointреализации также нарушает принцип подстановки Лискова (L в SOLID). Было бы лучше внедрить алгоритм в каждую IPointреализацию, а это значит, что вам нужен только один вызов метода:

static int GetDim(IPoint point) => point.NumDims;
Мэтью Лейтон
источник
3

Шаблон посетителя

в качестве альтернативы dynamicиспользованию вы можете использовать шаблон посетителя, как показано ниже:

interface IPoint
{
    public int NumDims { get; }
    public int Accept(IVisitor visitor);
}

public struct Point2 : IPoint
{
    public int NumDims => 2;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public struct Point3 : IPoint
{
    public int NumDims => 3;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public class Visitor : IVisitor
{
    public int Visit(Point2 toVisit)
    {
        return toVisit.NumDims;
    }

    public int Visit(Point3 toVisit)
    {
        return toVisit.NumDims;
    }
}

public interface IVisitor<T>
{
    int Visit(T toVisit);
}

public interface IVisitor : IVisitor<Point2>, IVisitor<Point3> { }

class Program
{
    static int GetDim<T>(T point) where T : IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => point.Accept(new Visitor());

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 2
        Console.WriteLine("{0:d}", d2);        // returns 3
    }
}
потрясающий
источник
1

Почему бы вам не определить функцию GetDim в классе и интерфейсе? На самом деле вам не нужно определять функцию GetDim, просто используйте свойство NumDims.

player2135
источник