Jak zrobić specjalizację szablonu w C #

84

Jak zrobiłbyś specjalizację w C #?

Będę stanowił problem. Masz typ szablonu, nie masz pojęcia, co to jest. Ale wiesz, czy pochodzi z tego, XYZże chcesz zadzwonić .alternativeFunc(). Świetnym sposobem jest wywołanie wyspecjalizowanej funkcji lub klasy i normalCallzwrócenie jej, .normalFunc()podczas gdy inna specjalizacja na dowolnym pochodnym typie XYZwywołania .alternativeFunc(). Jak by to zrobiono w C #?

BartoszKP
źródło

Odpowiedzi:

86

W C # najbliższa specjalizacji jest użycie bardziej szczegółowego przeciążenia; jest to jednak kruche i nie obejmuje wszystkich możliwych zastosowań. Na przykład:

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

Tutaj, jeśli kompilator zna typy podczas kompilacji, wybierze najbardziej szczegółowe:

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

Jednak....

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

użyje Foo<T>nawet for TSomething=Bar, ponieważ jest to wypalane w czasie kompilacji.

Innym podejściem jest użycie testów typu w ramach metody ogólnej - jest to jednak zwykle kiepski pomysł i nie jest zalecane.

Zasadniczo C # po prostu nie chce, abyś pracował ze specjalizacjami, z wyjątkiem polimorfizmu:

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

Tutaj Bar.Foozawsze zostanie rozwiązany do prawidłowego zastąpienia.

Marc Gravell
źródło
63

Zakładając, że mówisz o specjalizacji szablonów, ponieważ można to zrobić za pomocą szablonów C ++ - taka funkcja nie jest tak naprawdę dostępna w C #. Dzieje się tak, ponieważ C # typy ogólne nie są przetwarzane podczas kompilacji i są bardziej funkcją środowiska uruchomieniowego.

Można jednak osiągnąć podobny efekt przy użyciu metod rozszerzających C # 3.0. Oto przykład, który pokazuje, jak dodać metodę rozszerzenia tylko dla MyClass<int>typu, co jest podobne do specjalizacji szablonu. Należy jednak pamiętać, że nie można tego użyć do ukrycia domyślnej implementacji metody, ponieważ kompilator C # zawsze preferuje metody standardowe od metod rozszerzających:

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

Teraz możesz napisać to:

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

Jeśli chcesz mieć domyślny przypadek dla metody, która byłaby używana, gdy nie podano specjalizacji, to uważam, że napisanie jednej ogólnej Barmetody rozszerzenia powinno załatwić sprawę:

  public static int Bar<T>(this MyClass<T> cls) {
    return cls.Foo + 42;
  }
Tomas Petricek
źródło
Właściwość Foo vs metoda Bar ... tak naprawdę nie wygląda na typową specjalizację ...
Marc Gravell
2
Nie, to nie jest typowa specjalizacja, ale to jedyna łatwa rzecz, jaką możesz zrobić ... (AFAIK)
Tomas Petricek
3
Wygląda na to, że działa to dobrze bez używania staticmetod rozszerzających - tylko metody, które przyjmują typ ogólny. Oznacza to, że problem wskazany w odpowiedzi @MarcGravell wydaje się być ominięty przez „tworzenie szablonów” metody opartej na argumencie typu MyClass<T>/ MyClass<int>, zamiast tworzenia szablonów metody do określonego typu „danych” ( T/ int).
Slipp D. Thompson
4
Dodatkowym ograniczeniem jest to, że nie będzie działać w przypadku pośrednich wywołań ogólnych, np. Z poziomu metody void CallAppropriateBar<T>() { (new MyClass<T>()).Bar(); }.
BartoszKP
18

Dodanie klasy pośredniej i słownika umożliwia specjalizację .

Aby specjalizować się w T, tworzymy ogólny interfejs, z metodą o nazwie (np.) Apply. Dla określonych klas ten interfejs jest zaimplementowany, definiując metodę Zastosuj specyficzną dla tej klasy. Ta klasa pośrednia nazywana jest klasą cech.

Ta klasa cech może być określona jako parametr w wywołaniu metody generycznej, która wtedy (oczywiście) zawsze przyjmuje właściwą implementację.

Zamiast określać ją ręcznie, klasa cech może być również przechowywana w pliku globalnym IDictionary<System.Type, object>. Następnie można go sprawdzić i voila, masz tam prawdziwą specjalizację.

Jeśli jest to wygodne, możesz je wyeksponować metodą rozszerzania.

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();

Zobacz ten link do mojego ostatniego bloga i dalsze informacje, aby uzyskać obszerny opis i próbki.

Barend Gehrels
źródło
To jest wzór fabryczny i jest to przyzwoity sposób radzenia sobie z niektórymi niedociągnięciami leków generycznych
Yaur
1
@Yaur Wyglądam dla mnie jak podręcznikowy wzór Dekoratora.
Slipp D. Thompson
13

Szukałem też wzoru do symulacji specjalizacji szablonów. Istnieje kilka podejść, które mogą działać w pewnych okolicznościach. Jednak co ze sprawą

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

Możliwe byłoby wybranie akcji za pomocą instrukcji np if (typeof(T) == typeof(int)). Ale jest lepszy sposób na symulację prawdziwej specjalizacji szablonów z narzutem pojedynczego wywołania funkcji wirtualnej:

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

Teraz możemy pisać bez konieczności wcześniejszej znajomości typu:

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

Gdyby specjalizacja była wywoływana nie tylko dla implementowanych typów, ale także typów pochodnych, można by użyć Inparametru dla interfejsu. Jednak w tym przypadku zwracane typy metod nie mogą być już typu ogólnego T.

LionAM
źródło
To niesamowite, dziękuję. Pozwoliło mi to stworzyć ogólny interfejs do wywoływania wielu wcześniej istniejących metod, z których każda została napisana dla określonego typu, których nie można (lub przynajmniej z wielką trudnością) przepisać ogólnie. if (type == typeof(int))Zaczynało wyglądać, że będę musiał zrobić coś okropnego, a następnie wrzucić z powrotem do ogólnego typu z dodatkowym pudełkiem / rozpakowaniem return (T)(object)result;(ponieważ typ jest znany tylko logicznie, a nie statycznie)
Andrew Wright,
6

Niektóre z proponowanych odpowiedzi używają informacji o typie środowiska uruchomieniowego: z natury wolniej niż wywołania metod związane z czasem kompilacji.

Kompilator nie wymusza specjalizacji tak dobrze, jak robi to w C ++.

Poleciłbym przyjrzeć się PostSharp, aby znaleźć sposób na wstrzyknięcie kodu po zakończeniu zwykłego kompilatora, aby uzyskać efekt podobny do C ++.

GregC
źródło
4

Myślę, że jest sposób, aby to osiągnąć dzięki .NET 4+ przy użyciu dynamicznej rozdzielczości:

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

Wynik:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Co pokazuje, że dla różnych typów wymagana jest inna implementacja.

Drolevar
źródło
2
Chcę trochę popłakać.
user2864740
0

Jeśli chcesz tylko sprawdzić, czy typ pochodzi od XYZ, możesz użyć:

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

Jeśli tak, możesz rzucić „theunknownobject” na XYZ i wywołać alternatywnąFunc () w następujący sposób:

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

Mam nadzieję że to pomoże.

Jeroen Landheer
źródło
1
Nie znam zbyt wiele języka C #, ale ktokolwiek głosował na ciebie, powinien powiedzieć dlaczego. Nie mam pojęcia, co jest nie tak, gdy twoja odpowiedź lub coś jest z nią nie tak.
Nie jestem też pewien. Wydaje mi się to wystarczająco ważne. Chociaż trochę bardziej szczegółowe niż to konieczne.
jalf
3
To nie byłem ja, ale to dlatego, że odpowiedź jest zupełnie nieistotna na pytanie. Lookup"c++ template specialization"
georgiosd
To nie zawsze działa. Na przykład nie możesz sprawdzić, czy T jest wartością bool, a następnie rzutem na bool.
Kos