Як зробити спеціалізацію на шаблонах на C #


84

Як би ви пройшли спеціалізацію на C #?

Я буду створювати проблеми. У вас тип шаблону, ви не уявляєте, що це. Але ви знаєте, чи це походить від того, що XYZви хочете зателефонувати .alternativeFunc(). Чудовим способом є виклик спеціалізованої функції або класу і normalCallповернення .normalFunc(), маючи іншу спеціалізацію на будь-якому похідному типі XYZдзвінка .alternativeFunc(). Як це можна зробити в C #?

Відповіді:


86

У C # найближче до спеціалізації - використання більш конкретного перевантаження; однак це крихко і не охоплює всі можливі способи використання. Наприклад:

void Foo<T>(T value) {Console.WriteLine("General method");}
void Foo(Bar value) {Console.WriteLine("Specialized method");}

Тут, якщо компілятор знає типи під час компіляції, він вибере найбільш конкретні:

Bar bar = new Bar();
Foo(bar); // uses the specialized method

Однак ....

void Test<TSomething>(TSomething value) {
    Foo(value);
}

буде використовувати Foo<T>навіть для TSomething=Bar, оскільки це спалюється під час компіляції.

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

В основному, C # просто не хоче, щоб ви працювали зі спеціалізаціями, за винятком поліморфізму:

class SomeBase { public virtual void Foo() {...}}
class Bar : SomeBase { public override void Foo() {...}}

Тут Bar.Fooзавжди буде вирішено правильне заміщення.


63

Якщо припустити, що ви говорите про спеціалізацію шаблонів, як це можна зробити за допомогою шаблонів C ++ - така функція насправді недоступна в C #. Це пов’язано з тим, що узагальнені засоби C # не обробляються під час компіляції, а скоріше є особливістю виконання.

Однак ви можете досягти подібного ефекту, використовуючи методи розширення C # 3.0. Ось приклад, який показує, як додати метод розширення лише для MyClass<int>типу, що подібно до спеціалізації на шаблонах. Однак зауважте, що ви не можете використовувати це, щоб приховати реалізацію методу за замовчуванням, оскільки компілятор C # завжди віддає перевагу стандартним методам перед методами розширення:

class MyClass<T> {
  public int Foo { get { return 10; } }
}
static class MyClassSpecialization {
  public static int Bar(this MyClass<int> cls) {
    return cls.Foo + 20;
  }
}

Тепер ви можете написати це:

var cls = new MyClass<int>();
cls.Bar();

Якщо ви хочете мати варіант за замовчуванням для методу, який би використовувався, коли спеціалізація не передбачена, тоді я вважаю, що написання одного загального Barметоду розширення повинно зробити трюк:

  public static int Bar<T>(this MyClass<T> cls) {
    return cls.Foo + 42;
  }

Властивість Foo проти методу Bar ... насправді не здається типовою спеціалізацією ...
Марк Гравелл

2
Ні, це не типова специфікація, але це єдина легка річ, яку ви можете зробити ... (AFAIK)
Томаш Петрічек 02.03.09

3
Здається, це також добре працює, не використовуючи staticметоди розширення - лише методи, що приймають загальний тип. Тобто проблему, на яку вказує відповідь @MarcGravell, схоже, можна обійти шляхом «шаблонування» методу, заснованого на аргументі, подібному MyClass<T>/ MyClass<int>, а не шаблону методу до конкретного типу «даних» ( T/ int).
Сліпп Д. Томпсон,

4
Додатковим обмеженням є те, що він не буде працювати для непрямих загальних викликів, наприклад, всередині методу void CallAppropriateBar<T>() { (new MyClass<T>()).Bar(); }.
BartoszKP

18

Додавши проміжний клас та словник, можлива спеціалізація .

Для спеціалізації на T ми створюємо загальний інтерфейс, який має метод, що називається (наприклад, Apply). Для конкретних класів, що реалізований інтерфейс, визначається метод Apply, характерний для цього класу. Цей проміжний клас називається класом ознак.

Цей клас ознак можна вказати як параметр у виклику загального методу, який тоді (звичайно) завжди приймає правильну реалізацію.

Замість того, щоб вказувати його вручну, клас ознак можна також зберігати в глобальному IDictionary<System.Type, object> . Потім його можна переглянути і вуаля, у вас там справжня спеціалізація.

Якщо зручно, ви можете виставити це методом розширення.

class MyClass<T>
{
    public string Foo() { return "MyClass"; }
}

interface BaseTraits<T>
{
    string Apply(T cls);
}

class IntTraits : BaseTraits<MyClass<int>>
{
    public string Apply(MyClass<int> cls)
    {
        return cls.Foo() + " i";
    }
}

class DoubleTraits : BaseTraits<MyClass<double>>
{
    public string Apply(MyClass<double> cls)
    {
        return cls.Foo() + " d";
    }
}

// Somewhere in a (static) class:
public static IDictionary<Type, object> register;
register = new Dictionary<Type, object>();
register[typeof(MyClass<int>)] = new IntTraits();
register[typeof(MyClass<double>)] = new DoubleTraits();

public static string Bar<T>(this T obj)
{
    BaseTraits<T> traits = register[typeof(T)] as BaseTraits<T>;
    return traits.Apply(obj);
}

var cls1 = new MyClass<int>();
var cls2 = new MyClass<double>();

string id = cls1.Bar();
string dd = cls2.Bar();

Перегляньте це посилання на мій нещодавній блог та подальші кроки для детального опису та зразків.


Це заводський шаблон, і це гідний спосіб
усунути

1
@Yaur Я для мене схожий на зразок підручника.
Сліпп Д. Томпсон,

13

Я також шукав шаблон для імітації спеціалізації на шаблонах. Є деякі підходи, які можуть спрацювати за певних обставин. Однак як щодо справи

static void Add<T>(T value1, T value2)
{
    //add the 2 numeric values
}

Можна було б вибрати дію, використовуючи твердження, наприклад if (typeof(T) == typeof(int)). Але є кращий спосіб імітувати реальну спеціалізацію шаблону із накладними витратами на один виклик віртуальної функції:

public interface IMath<T>
{
    T Add(T value1, T value2);
}

public class Math<T> : IMath<T>
{
    public static readonly IMath<T> P = Math.P as IMath<T> ?? new Math<T>();

    //default implementation
    T IMath<T>.Add(T value1, T value2)
    {
        throw new NotSupportedException();    
    }
}

class Math : IMath<int>, IMath<double>
{
    public static Math P = new Math();

    //specialized for int
    int IMath<int>.Add(int value1, int value2)
    {
        return value1 + value2;
    }

    //specialized for double
    double IMath<double>.Add(double value1, double value2)
    {
        return value1 + value2;
    }
}

Тепер ми можемо писати, не знаючи тип заздалегідь:

static T Add<T>(T value1, T value2)
{
    return Math<T>.P.Add(value1, value2);
}

private static void Main(string[] args)
{
    var result1 = Add(1, 2);
    var result2 = Add(1.5, 2.5);

    return;
}

Якщо спеціалізацію слід викликати не лише для реалізованих типів, а й похідних типів, можна використовувати Inпараметр для інтерфейсу. Однак у цьому випадку типи повернення методів більше не можуть бути загального типу T.


Це дуже дивно, дякую. Це дозволило мені створити загальний інтерфейс для виклику в купу вже існуючих методів, кожен із яких написаний для певних типів, які не можуть бути (або, принаймні, з великими труднощами) переписані загально. Починало здаватися, що мені доведеться зробити щось жахливе, if (type == typeof(int))а потім повернутися до загального типу з додатковим боксом / розпаковкою return (T)(object)result;(оскільки тип відомий лише логічно, а не статистично відомий)
Ендрю Райт,

6

Деякі із запропонованих відповідей використовують інформацію про тип виконання: за своєю суттю повільніше, ніж виклики методу під час компіляції.

Компілятор не застосовує спеціалізацію так само, як це робиться на C ++.

Я б порадив шукати PostSharp для способу введення коду після того, як звичайний компілятор буде зроблений, щоб досягти ефекту, подібного до C ++.


4

Я думаю, що є спосіб досягти цього за допомогою .NET 4+ за допомогою динамічного дозволу:

static class Converter<T>
{
    public static string Convert(T data)
    {
        return Convert((dynamic)data);
    }

    private static string Convert(Int16 data) => $"Int16 {data}";
    private static string Convert(UInt16 data) => $"UInt16 {data}";
    private static string Convert(Int32 data) => $"Int32 {data}";
    private static string Convert(UInt32 data) => $"UInt32 {data}";
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(Converter<Int16>.Convert(-1));
        Console.WriteLine(Converter<UInt16>.Convert(1));
        Console.WriteLine(Converter<Int32>.Convert(-1));
        Console.WriteLine(Converter<UInt32>.Convert(1));
    }
}

Вихід:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Що показує, що для різних типів потрібна інша реалізація.


2
Хочеться трохи поплакати.
user2864740

0

Якщо ви просто хочете перевірити, чи тип походить від XYZ, ви можете використовувати:

theunknownobject.GetType().IsAssignableFrom(typeof(XYZ));

Якщо так, ви можете передати "theunknownobject" в XYZ і викликати alternativeFunc () так:

XYZ xyzObject = (XYZ)theunknownobject; 
xyzObject.alternativeFunc();

Сподіваюся, це допомагає.


1
Я не знаю багато C #, але той, хто проголосував за вас, повинен сказати, чому. Я не уявляю, що не так, коли ваша відповідь чи щось із нею не так.

Не впевнений також. Мені це здається достатньо дійсним. Хоча трохи більш багатослівний, ніж потрібно.
jalf 02.03.09

3
Це був не я, але це тому, що відповідь абсолютно не має значення для питання. Пошук"c++ template specialization"
georgiosd

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