Cómo hacer la especialización de plantillas en C #

84

¿Cómo harías la especialización en C #?

Plantearé un problema. Tienes un tipo de plantilla, no tienes idea de qué es. Pero sí sabe si se deriva de XYZsu deseo de llamar .alternativeFunc(). Una excelente manera es llamar a una función o clase especializada y tener normalCallretorno .normalFunc()mientras se tiene la otra especialización en cualquier tipo derivado de XYZllamar .alternativeFunc(). ¿Cómo se haría esto en C #?

BartoszKP
fuente

Respuestas:

86

En C #, lo más cercano a la especialización es usar una sobrecarga más específica; sin embargo, es frágil y no cubre todos los usos posibles. Por ejemplo:

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

Aquí, si el compilador conoce los tipos en la compilación, elegirá el más específico:

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

Sin embargo....

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

se utilizará Foo<T>incluso para TSomething=Bar, ya que se graba en tiempo de compilación.

Otro enfoque es utilizar la prueba de tipo dentro de un método genérico; sin embargo, esto suele ser una mala idea y no se recomienda.

Básicamente, C # simplemente no quiere que trabajes con especializaciones, excepto por el polimorfismo:

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

Aquí Bar.Foosiempre se resolverá la anulación correcta.

Marc Gravell
fuente
63

Suponiendo que está hablando de la especialización de plantillas, ya que se puede hacer con plantillas de C ++, una característica como esta no está realmente disponible en C #. Esto se debe a que los genéricos de C # no se procesan durante la compilación y son más una característica del tiempo de ejecución.

Sin embargo, puede lograr un efecto similar con los métodos de extensión de C # 3.0. Aquí hay un ejemplo que muestra cómo agregar un método de extensión solo para el MyClass<int>tipo, que es como la especialización de plantilla. Sin embargo, tenga en cuenta que no puede usar esto para ocultar la implementación predeterminada del método, porque el compilador de C # siempre prefiere los métodos estándar a los métodos de extensión:

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

Ahora puedes escribir esto:

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

Si desea tener un caso predeterminado para el método que se usaría cuando no se proporciona ninguna especialización, creo que escribir un Barmétodo de extensión genérico debería ser suficiente:

  public static int Bar<T>(this MyClass<T> cls) {
    return cls.Foo + 42;
  }
Tomas Petricek
fuente
Propiedad Foo vs método Bar ... realmente no parece una especialización típica ...
Marc Gravell
2
No, no es una especificación típica, pero es lo único fácil que puedes hacer ... (AFAIK)
Tomas Petricek
3
Parece que esto también funciona bien sin usar métodos de extensión, solo staticmétodos que toman un tipo genérico. Es decir, el problema señalado en la respuesta de @MarcGravell parece ser eludido mediante una "plantilla" del método basado en un argumento como MyClass<T>/ MyClass<int>, en lugar de una plantilla del método para el tipo de "datos" específico ( T/ int).
Slipp D. Thompson
4
La limitación adicional es que no funcionará para llamadas genéricas indirectas, por ejemplo, desde dentro de un método void CallAppropriateBar<T>() { (new MyClass<T>()).Bar(); }.
BartoszKP
18

Añadiendo una clase intermedia y un diccionario, es posible la especialización .

Para especializarnos en T, creamos una interfaz genérica, que tiene un método llamado (por ejemplo) Aplicar. Para las clases específicas que se implementa la interfaz, la definición del método Apply específico para esa clase. Esta clase intermedia se llama clase de rasgos.

Esa clase de rasgos se puede especificar como un parámetro en la llamada del método genérico, que luego (por supuesto) siempre toma la implementación correcta.

En lugar de especificarlo manualmente, la clase de rasgos también se puede almacenar en un archivo global IDictionary<System.Type, object>. Luego se puede buscar y listo, tienes una especialización real allí.

Si es conveniente, puede exponerlo en un método de extensión.

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

Vea este enlace a mi blog reciente y los seguimientos para obtener una descripción detallada y ejemplos.

Barend Gehrels
fuente
Este es el patrón de fábrica y es una forma decente de lidiar con algunas de las deficiencias de los genéricos
Yaur
1
@Yaur Me parece un patrón de Decorador de libros de texto.
Slipp D. Thompson
13

También estaba buscando un patrón para simular la especialización de plantillas. Hay algunos enfoques que pueden funcionar en algunas circunstancias. Sin embargo, ¿qué pasa con el caso?

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

Sería posible elegir la acción utilizando declaraciones, por ejemplo if (typeof(T) == typeof(int)). Pero hay una mejor manera de simular la especialización de plantillas reales con la sobrecarga de una sola llamada de función virtual:

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

Ahora podemos escribir, sin tener que conocer el tipo de antemano:

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

Si la especialización no solo debe llamarse para los tipos implementados, sino también para los tipos derivados, se podría usar un Inparámetro para la interfaz. Sin embargo, en este caso, los tipos de retorno de los métodos ya no pueden ser del tipo genérico T.

LionAM
fuente
Esto es bastante sorprendente, gracias. Me permitió crear una interfaz genérica para llamar a un montón de métodos preexistentes, cada uno escrito para tipos específicos, que no podían (o con gran dificultad al menos) ser reescritos genéricamente. Empezaba a parecer que tendría que hacer algo horrible if (type == typeof(int))y luego volver al tipo genérico con boxeo / unboxing adicional return (T)(object)result;(porque el tipo solo se conoce lógicamente, no se conoce estáticamente)
Andrew Wright
6

Algunas de las respuestas propuestas usan información de tipo de tiempo de ejecución: inherentemente más lento que las llamadas a métodos vinculados en tiempo de compilación.

El compilador no aplica la especialización tan bien como lo hace en C ++.

Recomendaría buscar en PostSharp una forma de inyectar código después de que se realiza el compilador habitual para lograr un efecto similar a C ++.

GregC
fuente
4

Creo que hay una manera de lograrlo con .NET 4+ usando resolución dinámica:

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

Salida:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Lo que muestra que se requiere una implementación diferente para diferentes tipos.

Drolevar
fuente
2
Quiero llorar un poquito.
user2864740
0

Si solo desea probar si un tipo se deriva de XYZ, puede usar:

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

Si es así, puede lanzar "theunknownobject" a XYZ e invocar AlternativeFunc () de esta manera:

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

Espero que esto ayude.

Jeroen Landheer
fuente
1
No sé mucho de C #, pero quien lo haya rechazado debería haber dicho por qué. No tengo idea de qué está mal cuando su respuesta o si algo está mal en ella.
Tampoco estoy seguro. Me parece bastante válido. Aunque un poco más detallado de lo necesario.
jalf
3
No fui yo, pero es porque la respuesta es completamente irrelevante para la pregunta. Búsqueda"c++ template specialization"
georgiosd
Esto no siempre funciona. Por ejemplo, no puede ver si T es un bool y luego lanzarlo a un bool.
Kos