C#模擬C++模板特化對型別的值的支援

2023-04-17 18:03:11

概述

C++的模板相比於C#的泛型,有很多地方都更加的靈活(雖然代價是降低了編譯速度),比如C++支援變長引數模板、支援列舉、int等型別的值作為模板引數。
C++支援列舉、int等型別的值作為模板引數,為C++的靜態多型程式設計提供了很好的幫助,比如根據列舉值編譯期確定某個物件的行為策略等(下文舉例)。但是C#對這些都是不支援,但是C#天然支援反射,這種需求可以使用反射特性來實現。

需求範例

定義列舉 enum EPlant {Tree, Flower},根據列舉的值列印Tree,Flower字串。注意,這裡的應用場景是編譯器時的多型,即編碼時便確定使用的物件的型別。

C++的實現

上述的例子,C++的語法支援可以天然的實現,如下:

#include <iostream>

enum class EPlant
{
    Tree = 0,
    Flower,
};

template<EPlant ...Args>
class PrintPlant
{
    
};

template<>
class PrintPlant<>
{
public:
    void Print()
    {
        std::cout << "Plant" << std::endl;;
    }
};

template<>
class PrintPlant<EPlant::Tree>
{
public: 
    void Print()
    {
        std::cout << "Tree" << std::endl;;
    }
};

template<>
class PrintPlant<EPlant::Flower>
{
public:
    void Print()
    {
        std::cout << "Flower" << std::endl;
    }
};

int main()
{
    auto plant = new PrintPlant<>();
    plant->Print();
    auto flower = new PrintPlant<EPlant::Flower>();
    flower->Print();
    auto tree = new PrintPlant<EPlant::Tree>();
    tree->Print();
}

輸出:

  • template<EPlant ...Args> 這裡使用變長引數模板,來支援沒有傳入模板引數的情況,特化型別Print函數列印"plant"
  • template<> class PrintPlant<EPlant::Tree> 模板特化的型別,在main裡使用了new PrintPlant<EPlant::Tree>();語句建立該型別的物件。該物件列印"Tree"。

C# 實現

C#的模板不支援列舉的值作為模板引數,使用反射進行模擬。

using System;
using System.Reflection;
using System.Collections.Generic;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ABTEX : Attribute
{
    public object key;

    public ABTEX(object k)
    {
        key = k;
    }
}

public class TEX
{
    static Dictionary<Type, Dictionary<Type, Dictionary<string, object>>> dict;
    public static void Init(Type[] types)
    {
        dict = new();
        foreach (var t in types)
        {
            var ABTEX = t.GetCustomAttribute<ABTEX>();
            var bt = t.BaseType;
            if (ABTEX != null && bt != null)
            {
                AddInst(t, bt, ABTEX.key);
            }
        }
    }

    static string FmtKey(object key)
    {
        return $"{key}";
    }

    static void AddInst(Type ty, Type bt, object key)
    {
        if (!dict.ContainsKey(bt))
        {
            dict[bt] = new();
        }

        var kt = key.GetType();
        string k = FmtKey(key);

        if (!dict[bt].ContainsKey(kt))
        {
            dict[bt][kt] = new();
        }

        dict[bt][kt][k] = Activator.CreateInstance(ty);
    }

    static public R T<R>(object key)
    {
        if (dict.TryGetValue(typeof(R), out Dictionary<Type, Dictionary<string, object>> dbt))
        {
            var kt = key.GetType();
            string k = FmtKey(key);
            if (dbt.TryGetValue(kt, out Dictionary<string, object> kbt))
            {
                if (kbt.TryGetValue(k, out object ins))
                {
                    return (R)ins;
                }
            }
        }

        return default(R);
    }
}

public enum EPlant : int
{
    None = 0,
    Tree,
    Flower,
}

public class APrintPlant
{
    public virtual void Print()
    {
        Console.WriteLine("Plant");
    }
}

[ABTEX(EPlant.Tree)]
public class PrintTree : APrintPlant
{
    public override void Print()
    {
        Console.WriteLine("Tree");
    }
}

[ABTEX(EPlant.Flower)]
public class PrintFlower : APrintPlant
{
    public override void Print()
    {
        Console.WriteLine("Flower");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var all = Assembly.GetExecutingAssembly().GetTypes();
        TEX.Init(all);
        TEX.T<APrintPlant>(EPlant.Tree).Print();
        TEX.T<APrintPlant>(EPlant.Flower).Print();
    }
}

輸出:

C#可以儲存型別資訊到執行期,通過執行期分析型別資訊建立物件實現靜態多型。

  • TEX類分析傳入的所有型別,篩選父類別和ABTEX特性,使用父類別型,ABTEX的key的型別和值來索引該型別。(這裡索引是範例物件,有需求的話可以儲存型別Type,使用型別通過反射建立物件)
  • ABTEX標記需要反射分析的型別,並且標記key。
  • Main入口獲取當前程式集下所有的型別資訊,初始化TEX
  • 通過TEX.T<抽象類>(key).Func 呼叫方法(注意: 這裡使用這些類作為純函數的類,故使用類似單例的用法。也可以在初始化記錄型別,通過反射建立多個範例。)