Вибір методу дженерики 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. Я не повністю розумію, чому це відбувається, але добре. Якщо я не можу зробити це таким чином, які інші альтернативи у мене є?


2
"Існує також функція запасного" Яка мета цього саме? Весь сенс впровадження інтерфейсу полягає у гарантуванні наявності NumDimsвластивості. Чому ви її ігноруєте в деяких випадках?
Джон Ву

Так воно складено, в основному. Спочатку я вважав, що функція "back back" потрібна, якщо під час виконання компілятор JIT не може знайти спеціалізовану реалізацію для GetDim(тобто я передаю, Point4але GetDim<Point4>не існує). Однак, здається, компілятор не турбується шукати спеціалізовану реалізацію.
mohamedmoussa

1
@woggy: Ви говорите: "Схоже, компілятор не турбується шукати спеціалізовану реалізацію", як ніби це було проблемою лінь з боку дизайнерів та реалізаторів. Це не. Справа в тому, як представлені дженерики в .NET. Це просто не та сама спеціалізація, як шаблонування на C ++. Загальний метод не компілюється окремо для кожного аргументу типу - він складається один раз. У цьому, звичайно, є плюси і мінуси, але справа не в тому, щоб "турбувати".
Джон Скіт

@jonskeet Вибачте, якщо мій вибір мови був поганим, я впевнений, що тут є складності, які я не вважав. Як я розумію, компілятор не компілює окремі функції для посилальних типів, але це стосується значень типів / структур, чи правильно це?
mohamedmoussa

@woggy: Це JIT -компілятор, який є абсолютно окремим питанням від компілятора C # - і це компілятор C #, який виконує роздільну здатність перевантаження. ІЛ для загального методу генерується лише один раз - не один раз за спеціалізацією.
Джон Скіт

Відповіді:


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внутрішні дзвінки, які потрібно спеціалізувати на правильний тип. Я хочу уникати віртуальних викликів функцій / динамічних, саме тому я використовую дженерики ... Звичайно, я можу просто скопіювати / вставити код і рухатися далі.
mohamedmoussa

1
@woggy: Це досить важко уявити це лише з опису. Якщо ви хочете допомогти спробувати це за допомогою спадкування, пропоную вам створити нове запитання з мінімальним, але повним прикладом.
Джон Скіт

Гаразд, я зроблю, я зараз прийму цю відповідь, оскільки, здається, я не подав хорошого прикладу.
mohamedmoussa

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.