Как сделать специализацию шаблона на 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)
Томас Петричек
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();

См. Эту ссылку на мой недавний блог и последующие действия для подробного описания и примеров.

Баренд Герельс
источник
Это заводской шаблон, и это достойный способ справиться с некоторыми недостатками дженериков
Yaur
1
@Yaur Мне кажется, я похож на шаблон декоратора из учебника.
Слипп Д. Томпсон
14

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

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.

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

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

Компилятор не требует специализации так же хорошо, как в C ++.

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

GregC
источник
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
-1

Если вы просто хотите проверить, является ли тип производным от XYZ, вы можете использовать:

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

Если это так, вы можете преобразовать «theunknownobject» в XYZ и вызвать альтернативуFunc () следующим образом:

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

Надеюсь это поможет.

Йерун Ландхир
источник
1
Я не очень хорошо разбираюсь в C #, но тот, кто проголосовал против, должен сказать, почему. Я понятия не имею, что не так, когда вы отвечаете, или что-то не так.
Тоже не уверен. Мне это кажется достаточным. Хотя немного более многословно, чем необходимо.
jalf 02
3
Это был не я, но это потому, что ответ совершенно не имеет отношения к вопросу. Lookup"c++ template specialization"
georgiosd
Это не всегда работает. Например, вы не можете увидеть, является ли T логическим значением, а затем преобразовать его в логическое значение.
Кос,