Come eseguire la specializzazione dei modelli in C #


84

Come faresti la specializzazione in C #?

Porrò un problema. Hai un tipo di modello, non hai idea di cosa sia. Ma sai se deriva da XYZte che vuoi chiamare .alternativeFunc(). Un ottimo modo è chiamare una funzione o una classe specializzata e avere il normalCallritorno .normalFunc()mentre avere l'altra specializzazione su qualsiasi tipo derivato XYZda chiamare .alternativeFunc(). Come sarebbe fatto in C #?

Risposte:


86

In C #, il più vicino alla specializzazione consiste nell'usare un overload più specifico; tuttavia, questo è fragile e non copre ogni possibile utilizzo. Per esempio:

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

Qui, se il compilatore conosce i tipi in fase di compilazione, sceglierà il più specifico:

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

Però....

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

userà Foo<T>anche per TSomething=Bar, poiché viene masterizzato in fase di compilazione.

Un altro approccio consiste nell'utilizzare il test del tipo all'interno di un metodo generico, tuttavia, di solito questa è una cattiva idea e non è consigliata.

Fondamentalmente, C # semplicemente non vuole che tu lavori con le specializzazioni, ad eccezione del polimorfismo:

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

Qui Bar.Foorisolverà sempre l'override corretto.


63

Supponendo che tu stia parlando della specializzazione dei modelli in quanto può essere eseguita con i modelli C ++, una funzionalità come questa non è realmente disponibile in C #. Questo perché i generici C # non vengono elaborati durante la compilazione e sono più una funzionalità del runtime.

Tuttavia, è possibile ottenere un effetto simile utilizzando i metodi di estensione C # 3.0. Ecco un esempio che mostra come aggiungere il metodo di estensione solo per il MyClass<int>tipo, che è proprio come la specializzazione del modello. Si noti tuttavia che non è possibile utilizzarlo per nascondere l'implementazione predefinita del metodo, poiché il compilatore C # preferisce sempre i metodi standard ai metodi di estensione:

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

Ora puoi scrivere questo:

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

Se vuoi avere un caso predefinito per il metodo che verrebbe utilizzato quando non viene fornita alcuna specializzazione, credo che scrivere un Barmetodo di estensione generico dovrebbe fare il trucco:

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

Proprietà Foo vs metodo Bar ... non sembra proprio una tipica specializzazione ...
Marc Gravell

2
No, non è una speculazione tipica, ma è l'unica cosa facile che puoi fare ... (AFAIK)
Tomas Petricek

3
Questo sembra funzionare bene anche senza usare staticmetodi di estensione, solo metodi che accettano un tipo generico. Cioè, il problema sottolineato nella risposta di @MarcGravell sembra essere aggirato "modellando" il metodo basato su un argomento come MyClass<T>/ MyClass<int>, piuttosto che modellando il metodo sul tipo di "dati" specifico ( T/ int).
Slipp D. Thompson

4
Una limitazione aggiuntiva è che non funzionerà per chiamate generiche indirette, ad esempio dall'interno di un metodo void CallAppropriateBar<T>() { (new MyClass<T>()).Bar(); }.
BartoszKP

18

Aggiungendo una classe intermedia e un dizionario, è possibile la specializzazione .

Per specializzarci su T, creiamo un'interfaccia generica, con un metodo chiamato (es.) Apply. Per le classi specifiche viene implementata l'interfaccia, definendo il metodo Apply specifico per quella classe. Questa classe intermedia è chiamata classe dei tratti.

Quella classe di tratti può essere specificata come parametro nella chiamata del metodo generico, che poi (ovviamente) prende sempre la giusta implementazione.

Invece di specificarlo manualmente, la classe traits può anche essere memorizzata in un file globale IDictionary<System.Type, object>. Può quindi essere cercato e voilà, hai una vera specializzazione lì.

Se conveniente puoi esporlo in un metodo di estensione.

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

Vedi questo collegamento al mio blog recente e i follow-up per un'ampia descrizione e esempi.


Questo è il modello di fabbrica ed è un modo decente per affrontare alcune delle carenze dei generici
Yaur

1
@ Yaur mi sembra un modello Decorator da manuale.
Slipp D. Thompson

14

Stavo anche cercando un modello per simulare la specializzazione del modello. Ci sono alcuni approcci che possono funzionare in alcune circostanze. Tuttavia per quanto riguarda il caso

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

Sarebbe possibile scegliere l'azione utilizzando dichiarazioni es if (typeof(T) == typeof(int)). Ma esiste un modo migliore per simulare la specializzazione del modello reale con l'overhead di una singola chiamata di funzione virtuale:

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

Ora possiamo scrivere, senza dover conoscere in anticipo il tipo:

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

Se la specializzazione non dovrebbe essere chiamata solo per i tipi implementati, ma anche per i tipi derivati, si potrebbe utilizzare un Inparametro per l'interfaccia. Tuttavia, in questo caso i tipi di ritorno dei metodi non possono più essere di tipo generico T.


È davvero sorprendente, grazie. Mi ha permesso di creare un'interfaccia generica per chiamare un gruppo di metodi preesistenti, ognuno scritto per tipi specifici, che non potevano (o almeno con grande difficoltà) essere riscritti genericamente. Cominciava a sembrare che avrei dovuto fare qualcosa di orribile if (type == typeof(int))e poi tornare al tipo generico con boxe / unboxing extra return (T)(object)result;(perché il tipo è solo logicamente noto, non staticamente noto)
Andrew Wright

6

Alcune delle risposte proposte utilizzano le informazioni sul tipo di runtime: intrinsecamente più lente delle chiamate al metodo vincolate in fase di compilazione.

Il compilatore non applica la specializzazione come in C ++.

Consiglierei di guardare PostSharp per un modo per iniettare il codice dopo che il solito compilatore è stato fatto per ottenere un effetto simile a C ++.


4

Penso che ci sia un modo per ottenerlo con .NET 4+ utilizzando la risoluzione dinamica:

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

Produzione:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Il che dimostra che è necessaria un'implementazione diversa per tipi diversi.


2
Ho voglia di piangere un po '.
user2864740

-1

Se vuoi solo testare se un tipo è deragliato da XYZ, puoi usare:

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

In tal caso, puoi lanciare "theunknownobject" su XYZ e invocare alternativeFunc () in questo modo:

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

Spero che sia di aiuto.


1
Non so molto C #, ma chiunque ti abbia votato dovrebbe dire perché. Non ho idea di cosa ci sia di sbagliato quando la tua risposta o se qualcosa non va.

Non ne sono sicuro neanche. Mi sembra abbastanza valido. Anche se un po 'più prolisso del necessario.
jalf

3
Non sono stato io, ma è perché la risposta è completamente irrilevante per la domanda. Consultare"c++ template specialization"
georgiosd

Questo non funziona sempre. Ad esempio, non puoi vedere se T è un bool e poi cast a bool.
Kos
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.