So führen Sie eine Vorlagenspezialisierung in C # durch

81

Wie würden Sie sich auf C # spezialisieren?

Ich werde ein Problem aufwerfen. Sie haben einen Vorlagentyp, Sie haben keine Ahnung, was es ist. Aber Sie wissen, ob es von XYZIhnen abgeleitet ist, die Sie anrufen möchten .alternativeFunc(). Eine gute Möglichkeit besteht darin, eine spezialisierte Funktion oder Klasse aufzurufen und normalCallzurückzukehren, .normalFunc()während die andere Spezialisierung auf einen abgeleiteten XYZAufruftyp erfolgt .alternativeFunc(). Wie würde das in C # gemacht werden?

BartoszKP
quelle

Antworten:

84

In C # ist es am nächsten an der Spezialisierung, eine spezifischere Überladung zu verwenden. Dies ist jedoch spröde und deckt nicht jede mögliche Verwendung ab. Beispielsweise:

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

Wenn der Compiler die Typen beim Kompilieren kennt, wählt er die spezifischsten aus:

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

Jedoch....

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

wird Foo<T>auch für verwendet TSomething=Bar, da dies zur Kompilierungszeit eingebrannt wird.

Ein anderer Ansatz ist die Verwendung von Typprüfungen innerhalb einer generischen Methode. Dies ist jedoch normalerweise eine schlechte Idee und wird nicht empfohlen.

Grundsätzlich möchte C # nicht, dass Sie mit Spezialisierungen arbeiten, außer mit Polymorphismus:

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

Hier Bar.Foowird immer die richtige Überschreibung aufgelöst.

Marc Gravell
quelle
62

Angenommen, Sie sprechen von einer Vorlagenspezialisierung, wie sie mit C ++ - Vorlagen möglich ist - eine solche Funktion ist in C # nicht wirklich verfügbar. Dies liegt daran, dass C # -Generika während der Kompilierung nicht verarbeitet werden und eher ein Merkmal der Laufzeit sind.

Mit C # 3.0-Erweiterungsmethoden können Sie jedoch einen ähnlichen Effekt erzielen. Hier ist ein Beispiel, das zeigt, wie eine Erweiterungsmethode nur für den MyClass<int>Typ hinzugefügt wird, ähnlich wie bei der Vorlagenspezialisierung. Beachten Sie jedoch, dass Sie dies nicht verwenden können, um die Standardimplementierung der Methode auszublenden, da der C # -Compiler immer Standardmethoden gegenüber Erweiterungsmethoden bevorzugt:

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

Jetzt können Sie Folgendes schreiben:

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

Wenn Sie einen Standardfall für die Methode haben möchten, der verwendet wird, wenn keine Spezialisierung angegeben ist, sollte meiner Meinung nach das Schreiben einer generischen BarErweiterungsmethode den Trick tun:

  public static int Bar<T>(this MyClass<T> cls) {
    return cls.Foo + 42;
  }
Tomas Petricek
quelle
Foo Property vs Bar Methode ... scheint nicht wirklich eine typische Spezialisierung zu sein ...
Marc Gravell
2
Nein, es ist keine typische Spekilaziation, aber es ist die einzige einfache Sache, die Sie tun können ... (AFAIK)
Tomas Petricek
3
Dies scheint auch ohne Erweiterungsmethoden gut zu funktionieren - nur staticMethoden, die einen generischen Typ annehmen. Das heißt, das Problem, auf das in der Antwort von @MarcGravell hingewiesen wird, scheint umgangen zu werden, indem die Methode basierend auf einem Argument wie MyClass<T>/ "templiert" wird MyClass<int>, anstatt die Methode auf den spezifischen "Datentyp" ( T/ int) zu templieren .
Slipp D. Thompson
4
Eine zusätzliche Einschränkung besteht darin, dass es für indirekte generische Aufrufe nicht funktioniert, z. B. innerhalb einer Methode void CallAppropriateBar<T>() { (new MyClass<T>()).Bar(); }.
BartoszKP
17

Durch Hinzufügen einer Zwischenklasse und eines Wörterbuchs ist eine Spezialisierung möglich .

Um uns auf T zu spezialisieren, erstellen wir eine generische Schnittstelle mit einer Methode namens (zB) Apply. Definieren Sie für die spezifischen Klassen, für die diese Schnittstelle implementiert ist, die für diese Klasse spezifische Methode. Diese Zwischenklasse wird als Merkmalsklasse bezeichnet.

Diese Merkmalsklasse kann als Parameter im Aufruf der generischen Methode angegeben werden, die dann (natürlich) immer die richtige Implementierung benötigt.

Anstatt es manuell anzugeben, kann die Merkmalsklasse auch in einer globalen Klasse gespeichert werden IDictionary<System.Type, object>. Es kann dann nachgeschlagen werden und voila, Sie haben dort echte Spezialisierung.

Wenn es Ihnen passt, können Sie es in einer Erweiterungsmethode verfügbar machen.

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

Unter diesem Link zu meinem letzten Blog und den Folgemaßnahmen finden Sie eine ausführliche Beschreibung und Beispiele.

Barend Gehrels
quelle
Dies ist das Fabrikmuster und es ist eine anständige Möglichkeit, einige der Mängel von Generika zu
beheben
1
@ Yaur Ich sehe aus wie ein Lehrbuch Dekorateur Muster für mich.
Slipp D. Thompson
13

Ich suchte nach einem Muster, um auch die Spezialisierung von Vorlagen zu simulieren. Es gibt einige Ansätze, die unter bestimmten Umständen funktionieren können. Was ist jedoch mit dem Fall?

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

Es wäre möglich, die Aktion mit Anweisungen zu wählen, z if (typeof(T) == typeof(int)). Es gibt jedoch eine bessere Möglichkeit, die Spezialisierung realer Vorlagen mit dem Aufwand eines einzelnen virtuellen Funktionsaufrufs zu simulieren:

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

Jetzt können wir schreiben, ohne den Typ vorher kennen zu müssen:

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

Wenn die Spezialisierung nicht nur für die implementierten Typen, sondern auch für abgeleitete Typen aufgerufen werden soll, könnte man einen InParameter für die Schnittstelle verwenden. In diesem Fall können die Rückgabetypen der Methoden jedoch nicht mehr vom generischen Typ Tsein.

LionAM
quelle
Das ist ziemlich erstaunlich, danke. Es ermöglichte mir, eine generische Schnittstelle zum Aufrufen einer Reihe bereits vorhandener Methoden zu erstellen, die jeweils für bestimmte Typen geschrieben wurden und die nicht (oder zumindest mit großen Schwierigkeiten) generisch neu geschrieben werden konnten. Es fing an, so auszusehen, als müsste ich etwas Schreckliches tun if (type == typeof(int))und dann zum generischen Typ mit zusätzlichem Boxen / Unboxing zurückkehren return (T)(object)result;(weil der Typ nur logisch bekannt ist, nicht statisch bekannt)
Andrew Wright
6

Einige der vorgeschlagenen Antworten verwenden Laufzeittypinformationen: von Natur aus langsamer als Methodenaufrufe, die an die Kompilierungszeit gebunden sind.

Der Compiler erzwingt die Spezialisierung nicht so gut wie in C ++.

Ich würde empfehlen, in PostSharp nach einer Möglichkeit zu suchen, Code einzufügen, nachdem der übliche Compiler ausgeführt wurde, um einen ähnlichen Effekt wie C ++ zu erzielen.

GregC
quelle
4

Ich denke, es gibt eine Möglichkeit, dies mit .NET 4+ mithilfe der dynamischen Auflösung zu erreichen:

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

Ausgabe:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Dies zeigt, dass für verschiedene Typen eine andere Implementierung erforderlich ist.

Drolevar
quelle
2
Ich möchte ein bisschen weinen.
user2864740
1

Wenn Sie nur testen möchten, ob ein Typ von XYZ abgeleitet ist, können Sie Folgendes verwenden:

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

In diesem Fall können Sie "theunknownobject" in XYZ umwandeln und alternativeFunc () wie folgt aufrufen:

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

Hoffe das hilft.

Jeroen Landheer
quelle
1
Ich weiß nicht viel C #, aber wer auch immer Sie abgewählt hat, sollte sagen warum. Ich habe keine Ahnung, was falsch ist, wenn Ihre Antwort oder wenn etwas falsch ist.
Ich bin mir auch nicht sicher. Es scheint mir gültig genug zu sein. Obwohl etwas ausführlicher als nötig.
Jalf
3
Ich war es nicht, aber es liegt daran, dass die Antwort für die Frage völlig irrelevant ist. Lookup"c++ template specialization"
Georgiosd
Das funktioniert nicht immer. Zum Beispiel können Sie nicht sehen, ob T ein Bool ist, und dann in einen Bool umwandeln.
Kos