问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

c#怎样把泛型类的泛型模板实例化

发布网友 发布时间:2022-04-27 08:21

我来回答

3个回答

懂视网 时间:2022-04-27 12:43

前言

当你想写一个泛型 <T> 的类型的时候,是否想过两个泛型参数、三个泛型参数、四个泛型参数或更多泛型参数的版本如何编写呢?是一个个编写?类小还好,类大了就杯具!

事实上,在 Visual Studio 中生成代码的手段很多,本文采用最笨的方式生成,但效果也很明显——代码写得轻松写得爽!

本文主要给大家介绍了关于从T到T1、T2、Tn自动生成多个类型的泛型的方法,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧

我们想要的效果

我们现在有一个泛型的版本:

public class Demo<T>
{
 public Demo(Action<T> demo)
 {
 _demo = demo ?? throw new ArgumentNullException(nameof(action));
 }

 private Action<T> _demo;

 public async Task<T> DoAsync(T t)
 {
 // 做某些事情。
 }

 // 做其他事情。
}

希望生成多个泛型的版本:

public class Demo<T1, T2>
{
 public Demo(Action<T1, T2> demo)
 {
 _demo = demo ?? throw new ArgumentNullException(nameof(action));
 }

 private Action<T1, T2> _demo;

 public async Task<(T1, T2)> DoAsync(T1 t1, T2 t2)
 {
 // 做某些事情。
 }

 // 做其他事情。
}

注意到类型的泛型变成了多个,参数从一个变成了多个,返回值从单个值变成了元组。

于是,怎么生成呢?

回顾 Visual Studio 那些生成代码的方式

Visual Studio 原生自带两种代码生成方式。

第一种:T4 文本模板

事实上 T4 模板算是 Visual Studio 最推荐的方式了,因为你只需要编写一个包含占位符的模板文件,Visual Studio 就会自动为你填充那些占位符。

那么 Visual Studio 用什么填充?是的,可以在模板文件中写 C# 代码!比如官方 DEMO:

<#@ output extension=".txt" #> 
<#@ assembly name="System.Xml" #> 
<# 
 System.Xml.XmlDocument configurationData = ...; // Read a data file here. 
#> 
namespace Fabrikam.<#= configurationData.SelectSingleNode("jobName").Value #> 
{ 
 ... // More code here. 
} 

这代码写哪儿呢?在项目上右键新建项,然后选择“运行时文本模板”。

T4 模板编辑后一旦保存(Ctrl+S),代码立刻生成。

有没有觉得这代码着色很恐怖?呃……根本就没有代码着色好吗!即便如此,T4 本身也是非常强悍的代码生成方式。

这不是本文的重点,于是感兴趣请阅读官方文档 Code Generation and T4 Text Templates - Microsoft Docs 学习。

第二种:文件属性中的自定义工具

右键选择项目中的一个代码文件,然后选择“属性”,你将看到以下内容:

就是这里的自定义工具。在这里填写工具的 Key,那么一旦这个文件保存,就会运行自定义工具生成代码。

那么 Key 从哪里来?这货居然是从注册表拿的!也就是说,如果要在团队使用,还需要写一个注册表项!即便如此,自定义工具本身也是非常强悍的代码生成方式。

这也不是本文的重点,于是感兴趣请阅读官方文档 Custom Tools - Microsoft Docs 学习。

第三种:笨笨的编译生成事件

这算是通常项目用得最多的方式了,因为它可以在不修改用户开发环境的情况下执行几乎任何任务。

右键项目,选择属性,进入“生成事件”标签:

在“预先生成事件命令行”中填入工具的名字和参数,便可以生成代码。

制作生成泛型代码的工具

我们新建一个控制台项目,取名为 CodeGenerator,然后把我写好的生成代码粘贴到新的类文件中。

using System;
using System.Linq;
using static System.Environment;

namespace Walterlv.BuildTools
{
 public class GenericTypeGenerator
 {
 private static readonly string GeneratedHeader =
$@"//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:{Environment.Version.ToString(4)}
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------

#define GENERATED_CODE
";

 private static readonly string GeneratedFooter =
 $@"";

 private readonly string _genericTemplate;
 private readonly string _toolName;

 public GenericTypeGenerator(string toolName, string genericTemplate)
 {
 _toolName = toolName ?? throw new ArgumentNullException(nameof(toolName));
 _genericTemplate = genericTemplate ?? throw new ArgumentNullException(nameof(toolName));
 }

 public string Generate(int genericCount)
 {
 var toolName = _toolName;
 var toolVersion = "1.0";
 var GeneratedAttribute = $"[System.CodeDom.Compiler.GeneratedCode("{toolName}", "{toolVersion}")]";

 var content = _genericTemplate
 // 替换泛型。
 .Replace("<out T>", FromTemplate("<{0}>", "out T{n}", ", ", genericCount))
 .Replace("Task<T>", FromTemplate("Task<({0})>", "T{n}", ", ", genericCount))
 .Replace("Func<T, Task>", FromTemplate("Func<{0}, Task>", "T{n}", ", ", genericCount))
 .Replace(" T, Task>", FromTemplate(" {0}, Task>", "T{n}", ", ", genericCount))
 .Replace("(T, bool", FromTemplate("({0}, bool", "T{n}", ", ", genericCount))
 .Replace("var (t, ", FromTemplate("var ({0}, ", "t{n}", ", ", genericCount))
 .Replace(", t)", FromTemplate(", {0})", "t{n}", ", ", genericCount))
 .Replace("return (t, ", FromTemplate("return ({0}, ", "t{n}", ", ", genericCount))
 .Replace("<T>", FromTemplate("<{0}>", "T{n}", ", ", genericCount))
 .Replace("(T value)", FromTemplate("(({0}) value)", "T{n}", ", ", genericCount))
 .Replace("(T t)", FromTemplate("({0})", "T{n} t{n}", ", ", genericCount))
 .Replace("(t)", FromTemplate("({0})", "t{n}", ", ", genericCount))
 .Replace("var t =", FromTemplate("var ({0}) =", "t{n}", ", ", genericCount))
 .Replace(" T ", FromTemplate(" ({0}) ", "T{n}", ", ", genericCount))
 .Replace(" t;", FromTemplate(" ({0});", "t{n}", ", ", genericCount))
 // 生成 [GeneratedCode]。
 .Replace(" public interface ", $" {GeneratedAttribute}{NewLine} public interface ")
 .Replace(" public class ", $" {GeneratedAttribute}{NewLine} public class ")
 .Replace(" public sealed class ", $" {GeneratedAttribute}{NewLine} public sealed class ");
 return GeneratedHeader + NewLine + content.Trim() + NewLine + GeneratedFooter;
 }

 private static string FromTemplate(string template, string part, string separator, int count)
 {
 return string.Format(template,
 string.Join(separator, Enumerable.Range(1, count).Select(x => part.Replace("{n}", x.ToString()))));
 }
 }
}

这个类中加入了非常多种常见的泛型字符串特征,当然是采用最笨的字符串替换方法。如果感兴趣优化优化,可以用正则表达式,或者使用 Roslyn 扩展直接拿语法树。

于是,在 Program.cs 中调用以上代码即可完成泛型生成。我写了一个简单的版本,可以将每一个命令行参数解析为一个需要进行转换的泛型类文件。

using System.IO;
using System.Linq;
using System.Text;
using Walterlv.BuildTools;

class Program
{
 static void Main(string[] args)
 {
 foreach (var argument in args)
 {
 GenerateGenericTypes(argument, 4);
 }
 }

 private static void GenerateGenericTypes(string file, int count)
 {
 // 读取原始文件并创建泛型代码生成器。
 var template = File.ReadAllText(file, Encoding.UTF8);
 var generator = new GenericTypeGenerator(template);

 // 根据泛型个数生成目标文件路径和文件内容。
 var format = GetIndexedFileNameFormat(file);
 (string targetFileName, string targetFileContent)[] contents = Enumerable.Range(2, count - 1).Select(i =>
 (string.Format(format, i), generator.Generate(i))
 ).ToArray();

 // 写入目标文件。
 foreach (var writer in contents)
 {
 File.WriteAllText(writer.targetFileName, writer.targetFileContent);
 }
 }

 private static string GetIndexedFileNameFormat(string fileName)
 {
 var directory = Path.GetDirectoryName(fileName);
 var name = Path.GetFileNameWithoutExtension(fileName);
 if (name.EndsWith("1"))
 {
 name = name.Substring(0, name.Length - 1);
 }

 return Path.Combine(directory, name + "{0}.cs");
 }
}

考虑到这是 Demo 级别的代码,我将生成的泛型个数直接写到了代码当中。这段代码的意思是按文件名递增生成多个泛型类。

例如,有一个泛型类文件 Demo.cs,则会在同目录生成 Demo2.cs,Demo3.cs,Demo4.cs。当然,Demo.cs 命名为 Demo1.cs 结果也是一样的。

在要生成代码的项目中添加“预先生成事件命令行”:

"$(ProjectDir)..CodeGenerator$(OutDir)net47CodeGenerator.exe" "$(ProjectDir)..Walterlv.DemoGenericIDemoFile.cs" "$(ProjectDir)....Walterlv.DemoGenericDemoFile.cs" 

现在,编译此项目,即可生成多个泛型类了。

彩蛋

如果你仔细阅读了 GenericTypeGenerator 类的代码,你将注意到我为生成的文件加上了条件编译符“GENERATED_CODE”。这样,你便可以使用 #ifdef GENERATED_CODE 来处理部分不需要进行转换或转换有差异的代码了。

这时写代码,是不是完全感受不到正在写模板呢?既有代码着色,又适用于团队其他开发者的开发环境。是的,个人认为如果带来便捷的同时注意不到工具的存在,那么这个工具便是好的。

如果将传参改为自动寻找代码文件,将此工具发布到 NuGet,那么可以通过 NuGet 安装脚本将以上过程全自动化完成。

参考资料

  • Code Generation and T4 Text Templates - Microsoft Docs
  • Custom Tools - Microsoft Docs
  • 总结

    热心网友 时间:2022-04-27 09:51

    C#泛型编程
    泛型:通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用。例子代码:
    class Program
    {
    static void Main(string[] args)
    {
    int obj = 2;
    Test<int> test = new Test<int>(obj);
    Console.WriteLine("int:" + test.obj);
    string obj2 = "hello world";
    Test<string> test1 = new Test<string>(obj2);
    Console.WriteLine("String:" + test1.obj);
    Console.Read();
    }
    }

    class Test<T>
    {
    public T obj;
    public Test(T obj)
    {
    this.obj = obj;
    }
    }
    输出结果是:
    int:2
    String:hello world
    程序分析:
    1、 Test是一个泛型类。T是要实例化的范型类型。如果T被实例化为int型,那么成员变量obj就是int型的,如果T被实例化为string型,那么obj就是string类型的。
    2、 根据不同的类型,上面的程序显示出不同的值。

    C#泛型特点:
    1、如果实例化泛型类型的参数相同,那么JIT编辑器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模板可能导致的代码膨胀的问题。
    2、C#泛型类型携带有丰富的元数据,因此C#的泛型类型可以应用于强大的反射技术。
    3、C#的泛型采用“基类、接口、构造器,值类型/引用类型”的约束方式来实现对类型参数的“显示约束”,提高了类型安全的同时,也丧失了C++模板基于“签名”的隐式约束所具有的高灵活性

    C#泛型继承:
    C#除了可以单独声明泛型类型(包括类与结构)外,也可以在基类中包含泛型类型的声明。但基类如果是泛型类,它的类型要么以实例化,要么来源于子类(同样是泛型类型)声明的类型参数,看如下类型
    class C<U,V>
    class D:C<string,int>
    class E<U,V>:C<U,V>
    class F<U,V>:C<string,int>
    class G:C<U,V> //非法
    E类型为C类型提供了U、V,也就是上面说的来源于子类
    F类型继承于C<string,int>,个人认为可以看成F继承一个非泛型的类
    G类型为非法的,因为G类型不是泛型,C是泛型,G无法给C提供泛型的实例化

    泛型类型的成员:
    泛型类型的成员可以使用泛型类型声明中的类型参数。但类型参数如果没有任何约束,则只能在该类型上使用从System.Object继承的公有成员。如下图:

    泛型接口:
    泛型接口的类型参数要么已实例化,要么来源于实现类声明的类型参数

    泛型委托:
    泛型委托支持在委托返回值和参数上应用参数类型,这些参数类型同样可以附带合法的约束
    delegate bool MyDelegate<T>(T value);
    class MyClass
    {
    static bool F(int i){...}
    static bool G(string s){...}
    static void Main()
    {
    MyDelegate<string> p2 = G;
    MyDelegate<int> p1 = new MyDelegate<int>(F);
    }
    }

    泛型方法:
    1、C#泛型机制只支持“在方法声明上包含类型参数”——即泛型方法。
    2、C#泛型机制不支持在除方法外的其他成员(包括属性、事件、索引器、构造器、析构器)的声明上包含类型参数,但这些成员本身可以包含在泛型类型中,并使用泛型类型的类型参数。
    3、泛型方法既可以包含在泛型类型中,也可以包含在非泛型类型中。

    泛型方法声明:如下
    public static int FunctionName<T>(T value){...}

    泛型方法的重载:
    public void Function1<T>(T a);
    public void Function1<U>(U a);
    这样是不能构成泛型方法的重载。因为编译器无法确定泛型类型T和U是否不同,也就无法确定这两个方法是否不同
    public void Function1<T>(int x);
    public void Function1(int x);
    这样可以构成重载
    public void Function1<T>(T t) where T:A;
    public void Function1<T>(T t) where T:B;
    这样不能构成泛型方法的重载。因为编译器无法确定约束条件中的A和B是否不同,也就无法确定这两个方法是否不同

    泛型方法重写:
    在重写的过程中,抽象类中的抽象方法的约束是被默认继承的。如下:
    abstract class Base
    {
    public abstract T F<T,U>(T t,U u) where U:T;
    public abstract T G<T>(T t) where T:IComparable;
    }
    class MyClass:Base
    {
    public override X F<X,Y>(X x,Y y){...}
    public override T G<T>(T t) where T:IComparable{}
    }
    对于MyClass中两个重写的方法来说
    F方法是合法的,约束被默认继承
    G方法是非法的,指定任何约束都是多余的

    热心网友 时间:2022-04-27 11:09

    改一下你的类定义就行了
    class MyClass<T> where T:new()

    {
    .....
    }
    声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
    青海摇什么时候火的 宋庄镇都有哪些村 唐山多地解除封控管理、静态管理,市民仍需做好哪些防护? 拼多多上面的旗舰店是正品吗 ...尺是20:1的图纸上,应画多少厘米,在比例尺是1:200的图纸上测_百度知... 4x2=8,4x20=8,4x200=800,我发现了:一个因数不变,另外一个因数乘以几,积... 小学三年级数学上册4X2=8,4X20=80,4X200=800我发现:一个因数不变,另... 描写雪的段落优选好句60句 程序员最低学历是多少 电脑不识别独显了怎么办? 清末中国人对西方世界的认识经历了怎样的演变过程? 什么是 二元世界观西方哲学上的“二元世界观”具体指 应用使用时间卸载了怎么办? 欧洲中世纪的世界观 双时间移除了就找不到怎么弄回来 唯物主义世界观 相对 西方是怎么看待世界 《权力的游戏》你有没有了解西方人的世界观? 唯物主义 世界观 什么是二元世界观西方哲学上的“二元世界观”具体指 中西方辩证法各自的世界观是什么? 比较东西方早期的世界观和物质观? 国内玩家如何看待西方魔幻世界观? 两种世界观怎么影响西方世界和中国世界 - 信息提示 世界观在西方哲学的哲学体系中处于哪个部分或者分支,辩证和历史唯物主义属于哪个类别或分支 西方的世界观,人生观,宗教观,价值观,道德观的主要观点中哪些实在批判吸收价值观,哪些是必须反对. 中西方世界观人生观价值观有各有几种类型,有什么异同 亚里士多德世界观,为什么能统治西方2000年? 什么是西方古老的世界观 2017公务员工资套改等级标准对照表 公务员工资福利待遇多少一个月 胡均利是颖儿老公吗 颖儿婚戒为什么和别人的不一样? 知情同意权包括哪几个要素 患者知情同意权 如何拦截电脑弹窗广告 中邮成长基金如何分红,是否不从银行卡里把钱取走(赎回)就继续持有呢? 如何拦截电脑网页中的弹窗呢? 电影了不起的盖茨比中,盖茨比的扮演者是谁? 了不起的盖茨比男主是谁扮演的 天津美术学院考研有什么要求,能跨考吗? 天津美术学院考研跨考有什么要求 跨专业考天津财经大学研究生 天津跨考考研好么? 好看的 玄幻小说有哪些呢 用扫描扫到电脑里的文章能修改吗 天津工科考研报哪个考研机构 电影了不起的盖茨比里盖茨比的扮演者是谁 跨专业考研天津大学的建筑学可以吗 天津商业大学可以跨专业考研吗 天津跨考考研怎么样啊?听说他们有个学生考了401 是真的吗?