如何在 C # 中进行模板专门化

如何在 C # 中进行专门化?

我会提出一个问题。你有一个模板类型,你不知道它是什么。但是你知道如果它是从 XYZ派生出来的,你要调用 .alternativeFunc()。一个很好的方法是调用一个专门的函数或类,让 normalCall返回 .normalFunc(),同时让任何派生类型的 XYZ上的其他专门化调用 .alternativeFunc()。如何在 C # 中实现这一点?

74790 次浏览

Assuming you're talking about template specialization as it can be done with C++ templates - a feature like this isn't really available in C#. This is because C# generics aren't processed during the compilation and are more a feature of the runtime.

However, you can achieve similar effect using C# 3.0 extension methods. Here is an example that shows how to add extension method only for MyClass<int> type, which is just like template specialization. Note however, that you can't use this to hide default implementation of the method, because C# compiler always prefers standard methods to extension methods:

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

Now you can write this:

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

If you want to have a default case for the method that would be used when no specialization is provided, than I believe writing one generic Bar extension method should do the trick:

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

If you just want to test if a type is derrived from XYZ, then you can use:

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

If so, you can cast "theunknownobject" to XYZ and invoke alternativeFunc() like this:

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

Hope this helps.

In C#, the closest to specialization is to use a more-specific overload; however, this is brittle, and doesn't cover every possible usage. For example:

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

Here, if the compiler knows the types at compile, it will pick the most specific:

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

However....

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

will use Foo<T> even for TSomething=Bar, as this is burned in at compile-time.

One other approach is to use type-testing within a generic method - however, this is usually a poor idea, and isn't recommended.

Basically, C# just doesn't want you to work with specializations, except for polymorphism:

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

Here Bar.Foo will always resolve to the correct override.

By adding an intermediate class and a dictionary, specialization is possible.

To specialize on T, we create an generic interface, having a method called (e.g.) Apply. For the specific classes that interface is implemented, defining the method Apply specific for that class. This intermediate class is called the traits class.

That traits class can be specified as a parameter in the call of the generic method, which then (of course) always takes the right implementation.

Instead of specifying it manually, the traits class can also be stored in a global IDictionary<System.Type, object>. It can then be looked up and voila, you have real specialization there.

If convenient you can expose it in an extension method.

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

See this link to my recent blog and the follow ups for an extensive description and samples.

Some of the proposed answers are using runtime type info: inherently slower than compile-time bound method calls.

Compiler does not enforce specialization as well as it does in C++.

I would recommend looking at PostSharp for a way to inject code after the usual compiler is done to achieve an effect similar to C++.

I was searching for a pattern to simulate template specialization, too. There are some approaches which may work in some circumstances. However what about the case

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

It would be possible to choose the action using statements e.g. if (typeof(T) == typeof(int)). But there is a better way to simulate real template specialization with the overhead of a single virtual function call:

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

Now we can write, without having to know the type in advance:

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

If the specialization should not only be called for the implemented types, but also derived types, one could use an In parameter for the interface. However, in this case the return types of the methods cannot be of the generic type T any more.

I think there is a way to achieve it with .NET 4+ using dynamic resolution:

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

Output:

Int16 -1
UInt16 1
Int32 -1
UInt32 1

Which shows that a different implementation is called for different types.

A simpler, shorter and more readable version of what @LionAM proposed (about half of the code size), shown for lerp since this was my actual use case:

public interface ILerp<T> {
T Lerp( T a, T b, float t );
}


public class Lerp : ILerp<float>, ILerp<double> {
private static readonly Lerp instance = new();


public static T Lerp<T>( T a, T b, float t )
=> ( instance as ILerp<T> ?? throw new NotSupportedException() ).Lerp( a, b, t );


float ILerp<float>.Lerp( float a, float b, float t ) => Mathf.Lerp( a, b, t );
double ILerp<double>.Lerp( double a, double b, float t ) => Mathd.Lerp( a, b, t );
}

You can then just e.g.

Lerp.Lerp(a, b, t);

in any generic context, or provide the method as a grouped Lerp.lerp method reference matching T(T,T,float) signature.

If ClassCastException is good enough for you, you can of course just use

 => ( (ILerp<T>) instance ).Lerp( a, b, t );

to make the code even shorter/simpler.