C# Advanced Tutorial 1-1 Generic


主要内容概要

  • 引入泛型:延迟声明
  • 如何声明和使用泛型
  • 泛型的好处和原理
  • 泛型类、泛型方法、泛型接口、泛型委托
  • 泛型约束
  • 协变 逆变(选修)
  • 泛型缓存(选修)

为什么要有泛型

很常见的比如List, List可以用List来表示。
List就是泛型,为什么要有泛型?
List是一个集合,可能是一组int 也可能是一组string。
泛型就是用一个东西来满足多种不同类型的需求的。
下面举一个例子来说明一下,为何泛型会被引入。
在CommonMethod里面有三个方法,分别打印三种不同的类型,如下:

public class CommonMethod
{
  // 打印个int值 
  public static void ShowInt(int iParameter)
  {
    Console.WriteLine("This is {0},parameter={1},type={2}", 
      typeof(CommonMethod).Name, iParameter.GetType().Name, iparameter);
  }
  //打印个string值
  public static void ShowString(string sParameter)
  {
    Console.WriteLine("This is {0},parameter={1},type={2}",
      typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);
  }
  //打印个DateTime值
  public static void ShowDateTime(DateTime dtParameter)
  {
    Console.WriteLine("This is {0},parameter={1},type={2}",
      typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);
  }
}

那么在program中我们就可以这么来调用这三种方法:

int iValue = 123;
string sValue = "456";
DateTime dtValue = DateTime.Now;
object oValue = "789";

CommonMethod.ShowInt(iValue);
CommonMethod.ShowString(sValue);
CommonMethod.ShowDateTime(dtValue);

这个写法是不是感觉有点啰嗦呢,三个方法其实只有传入的参数不一样而已。此时,我们可能会考虑在common中这么来写一个函数,将object对象作为传入参数:

public static void ShowObject(object oParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
        typeof(CommonMethod), oParameter.GetType().Name, oParameter);
 }

然后在program中可以这么来调用:

CommonMethod.ShowObject(oValue);
CommonMethod.ShowObject(iValue);
CommonMethod.ShowObject(sValue);
CommonMethod.ShowObject(dtValue);

为什么用object 作为参数类型,调用的时候,可以把任何类型都传进来?

  • C#: 任何父类出现的地方,都可以用子类代替
  • Object类型是一切类型的父类;

这里使用object会有两个问题:

  • 第一是装箱拆箱,传入一个int值(栈)
    object又在堆里面,如果把int传递进来,就会把值从栈里面copy到堆里,使用的时候,又需要用对象值,又会copy到栈(拆箱);
  • 类型安全问题,可能会有,因为传递的对象是没有限制的;

泛型简介

泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性。泛型为.Net框架引入了类型参数(type parameters)的概念。类型阐述是的设计类和方法时,不必确定一个或多个具体参数,其具体参数可以延迟到客户代码中声明、实现。这意味着使用泛型的类型参数T,写一个类MyList,客户代码可以这样调用,MyList,MyList或MyList。这避免了运行时类型转换或装箱操作的代价和风险。

*泛型只有:泛型方法、泛型类、泛型接口、泛型委托 *
泛型方法调用的时候,需要加上<>,而且需要指定具体的类型、指定的类型和传入的参数类型保持一致。
定义一个泛型方法:

public static void Show<T,S,ZCQ>(T tParameter)
{
  Console.WriteLine("This is {0},parameter={1},type={2}",
    typeof(CommonMethod), tParameter.GetType().Name, tParameter);
}

然后调用泛型方法:

CommonMethod.Show<int>(iValue);
CommonMethod.Show(iValue); //如果类型参数,可以通过参数类型推导出来,那么就可以省略,VS中会变灰
// 下面的例子中类型错了 
//CommonMethod.Show<int>(sValue);
CommonMethod.Show(sValue);
CommonMethod.Show<DateTime>(dtValue);
CommonMethod.Show<object>(oValue);

思考一下,为什么泛型可以支持不同类型的参数?
声明一般方法时,指定了参数类型,确定了只能传递某个类型;
泛型声明方法时,并没有写死类型,是什么类型,不知道;
只有在调用的时候才指定;
泛型这里的设计思想是延迟声明,这是架构设计推崇的思想:推迟一切可以推迟的

泛型原理

编译器编译(例如VS),得到一些dll exe这种文件,这些文件要运行需要CLR/JIT进行转换(这个CLR是个环境),这个CLR会把中间语言转换成机器语言。
那么,泛型在编译时究竟编译成什么呢?
机器码的时候,类型必须是确定的,因为要分配内存;
编译时确实不知道是什么类型。
CompilingPrinciple

那么泛型在编译时,延迟声明究竟是怎么实现的呢?

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));

上面的输出结果如下:
System.Collections.Generic.List1[T] System.Collections.Generic.Dictionary2[TKey,TValue]

通过结果可以发现,所谓延迟声明实际上是通过占位符来实现的。
泛型是.NetFramework2.0出来
包含了以下升级:

  • 编译器升级,能够支持类型参数,用占位符来表达
    `1、`2来表示
  • CLR升级才能支持占位符
    运行的时候,类型会确定,会把占位符给替换成具体的类型
  • 泛型不是语法糖* (语法糖是指编译器提供的便捷功能,例如 var i=2,编译器可以推断出来是int类型。)

泛型性能

下面比较一下泛型方法,object方法以及一般方法的性能:

public class Monitor
{
  public static void Show()
  {
    Console.WriteLine("****************Monitor******************");
    {
      int iValue = 12345;
      long commonSecond = 0;
      long objectSecond = 0;
      long genericSecond = 0;
    {
       Stopwatch watch = new Stopwatch();
       watch.Start();
       for (int i = 0; i < 100_000_000; i++)
       {
         ShowInt(iValue);
       }
       watch.Stop();
       commonSecond = watch.ElapsedMilliseconds;
     }
     {
       Stopwatch watch = new Stopwatch();
       watch.Start();
       for (int i = 0; i < 100_000_000; i++)
       {
         ShowObject(iValue);
       }
       watch.Stop();
       objectSecond = watch.ElapsedMilliseconds;
      }
      {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 100_000_000; i++)
        {
          Show<int>(iValue);
        }
      watch.Stop();
      genericSecond = watch.ElapsedMilliseconds;
      }
      Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
        , commonSecond, objectSecond, genericSecond);
    }
  }

  #region PrivateMethod
  private static void ShowInt(int iParameter)
  {
     //do nothing
  }
  private static void ShowObject(object oParameter)
  {
     //do nothing
  }
  private static void Show<T>(T tParameter)
  {
     //do nothing
  }
  #endregion
}

得到的结果是:commonSecond=363,objectSecond=696,genericSecond=362
可以发现泛型方法的性能和普通方法的性能是一致的,在一个数量级上,而object需要在内存中装箱,因此消耗的时间比较多。

泛型缓存

  • 字典缓存
    字典缓存:静态属性常驻内存

    public class DictionaryCache
    {
    private static Dictionary<Type, string> _TypeTimeDictionary = null;
    static DictionaryCache()
    {
      Console.WriteLine("This is DictionaryCache 静态构造函数");
      _TypeTimeDictionary = new Dictionary<Type, string>();
    }
    public static string GetCache<T>()
    {
      Type type = typeof(Type);
      if (!_TypeTimeDictionary.ContainsKey(type))
      {
        _TypeTimeDictionary[type] = string.Format("{0}_{1}", typeof(T).FullName,           DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
      }
      return _TypeTimeDictionary[type];
    }
    }
  • 泛型缓存
    会根据传入的不同类型,分别生成不同的副本。虽然是静态字段,但是遇到泛型类,也是不同的。
    可以接受任何类型,需要根据不同类型缓存一部分数据就可以使用,效率更好。

    public class GenericCache<T>
    {
    static GenericCache()
    {
      Console.WriteLine("This is GenericCache 静态构造函数");
      _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
    }
    private static string _TypeTime = "";
    public static string GetCache()
    {
      return _TypeTime;
    }
    }

验证一下:第一次调用int string long等类型的时候会产生一个类,第二次的时候就不会进入构造函数了,这样就达到了类型缓存的目的。

public class GenericCacheTest
{
  public static void Show()
  {
    for (int i = 0; i < 5; i++)
    {
      Console.WriteLine(GenericCache<int>.GetCache());
      Thread.Sleep(10);
      Console.WriteLine(GenericCache<long>.GetCache());
      Thread.Sleep(10);
      Console.WriteLine(GenericCache<DateTime>.GetCache());
      Thread.Sleep(10);
      Console.WriteLine(GenericCache<string>.GetCache());
      Thread.Sleep(10);
      Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
      Thread.Sleep(10);
    }
  }
}

泛型缓存比字典缓存效率要高,字典缓存要用哈希,转换效率低。但是这种泛型缓存只能是为某一个类型进行缓存,有一定的局限性。

泛型的应用

  1. 泛型方法
    泛型方法:为了一个方法满足不同的类型的需求
  • 一个方法完成多实体的查询
  • 一个方法完成不同的类型的数据展示
  • 任意一个实体,转换成一个Json字符串
  1. 泛型类
    泛型类:一个类型,满足不同类型的需求;List Dictionary

    public class GenericClass<T>
    { 
    }
  2. 泛型接口
    泛型接口:一个接口满足不同类型的需求

    public interface GenericInterface<T>
    {
    }
  3. 泛型委托
    泛型委托:就是一个委托 满足多个多个类型的需求

    public delegate void Do<T>();

泛型类、接口的继承,也要遵从相应的规则,例如:

public class ChildClass<S, T> : GenericClass/<S/>, GenericInterface<T>

泛型约束

先看一个例子用object有什么样的弊端?任何类型都能传递进来可能不安全。

public static void ShowObject(object oParameter)
{
  //Console.WriteLine(oParameter.Id);// 编译器就报错,因为C# 是强类型的语言,在编译的时候就要确定类型
  People people = (People)oParameter;
  Console.WriteLine(people.Id);
  Console.WriteLine("This is {0},parameter={1},type={2}",
    typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}

下面再看看泛型约束:
先定义一些类,接口,有人,日本人,中国人,湖北人:

public interface ISports
{
  void Pingpang();
}

public interface IWork
{
  void Work();
}

public class People
{
  public int Id { get; set; }
  public string Name { get; set; }
  public void Hi()
  { }
}

public class Chinese : People, ISports, IWork
{
  //...
}

public class Hubei : Chinese
{
  //...
}

public class Japanese : ISports
{
  //...
}

可以定义一些泛型约束:
where T: BaseModel
基类约束;

  • 可以把T当成基类——权利
  • T必须是People 或者 其子类——约束
    public static void GenericShow<T>(T tParameter)
    where T : People
    {
    Console.WriteLine("This is {0},parameter={1},type={2}",
      typeof(CommonMethod), tParameter.GetType().Name, tParameter);
    }
    GenericConstraint.Show(people);
    GenericConstraint.Show(chinese);
    GenericConstraint.Show(hubei);
    GenericConstraint.Show(japanese);//这里就会报错, Japanese传不进来

泛型约束类型有:

  1. 基类约束 where T : People

  2. 接口约束 where T : ISports

  3. 引用类型约束,值类型约束

    public T GenericShow<T>() where T: class
    {
    return null;//引用类型
    }
    public T GenericShow<T>() where T: struct
    {
    return default(T);//值类型,不约束也可以这么用
    }
  4. 无参数构造函数约束

    public static void GenericShow<T>(T tParameter)
    where T : new()// 无参数构造偶函数约束
    {
     return new T();
    }
  • 约束中,密封的是不行的,没有子类就谈不上约束了,例如where T: string 是不对的!
  • 约束的时候,父类只有一个,接口可以多个
  • 泛型约束可以结合使用 ,用逗号分隔就OK
    public static void GenericShow1<T>(T tParameter)
    where T : class, new() // 泛型约束可以结合使用 ,用逗号分隔就OK
    {
    //...
    }
    public static void GenericShow1<T,S>(T tParameter)
    where T : class, new() 
    where S :People
    {
    //...
    }
  1. 用泛型类型的参数来约束
    public static void ShowTS<T,S>(T tParameter,S sParameter)
    where T: People
    where S: T//用类型参数约束
    {
    //...
    }

泛型的协变与逆变

Func<int,string>这是一个泛型委托,来看看这个定义:
public delegate TResult Func<in T, out TResult>(T arg)简单理解,这个里面的 in 和out 就是协变与逆变。
下面举一个例子来具体阐述一下:

public class Bird
{
  public int Id { get; set; }
}
public class Sparrow : Bird
{
  public string Name { get; set; }
}
Bird bird1 = new Bird();//子类实例化 麻雀当然是个bird
Bird bird2 = new Sparrow();
Sparrow sparrow1 = new Sparrow();
/* Sparrow sparrow2 = new Bird()*/ //子类变量 不能用父类实例化

思考: 一个麻雀是一个bird,难道一组麻雀不是一组鸟吗?语义应该是可以的。
但是语法上是通不过的,List 是一个类, List 是另外一个类,二者没有继承 没有父子关系。
如果非要这么实现一下,可以这么写:List<Bird> birdList3 = new List<Sparrow>().Select(c => (Bird)c).ToList();
至此,会感觉泛型在使用的时候,会存在不和谐的地方。
更进一步,我们看一下,不用List声明,用IEnumerable来实现上例。可以这样写:

IEnumerable<Bird> birdList1 = new List<Bird>();
IEnumerable<Bird> birdList2 = new List<Sparrow>(); 

为什么可以这么写呢?探究一下IEnumerable和List:
IEnumerable
这里面IEnumerable的参数就是个out类型,修饰返回值,就是协变covariant。

先给出协变逆变的几个结论:

  • 协变逆变只有在接口或者委托的泛型参数前面用 out in来实现
  • out 协变covariant 修饰返回值
  • in 逆变contravariant 修饰传入参数
  1. 下面自己来写一个协变的实例:
public interface ICustomerListOut<out T>
{
  T Get();
  //void Show(T t);
}

public class CustomerListOut<T> : ICustomerListOut<T>
{
  public T Get()
  {
    return default(T);
  }
  //public void Show(T t) { }
}

上述ICustomerListOut这个接口,用out修饰,就是协变,意味着T就只能做返回值 ,不能做参数。也就是说,不能再接口中定义void Show(T t);这样的方法,同样也不能在实现类中把T作为参数使用。
这样一来,通过协变,就可以像下面这样,可以把子类放在右边,这才是泛型该有的样子。

ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();
ICustomerListOut<Bird> customerList2 = new CustomerListOut<Sparrow>();

我们再来想一下这个过程,bird在ICustomerListOut这个接口中是作为返回值的,也就是返回结果要是一个bird;再看CustomerListOut()和CustomerListOut()这两个实现,当传入Bird时,返回Bird,当传入Sparrow时,返回Sparrow,Sparrow也是bird,这是没问题的。
如果在上述过程中,这个T可以作参数来用,例如我们有这么一个方法:void Show(T t);,这样,在接口ICustomerListOut中,T是作bird来用,但是在CustomerListOut()实现中,T又是作为Sparrow来用,肯定会出问题的。

  1. 下面再看一下逆变的实例:
public interface ICustomerListIn<in T>
{
  //T Get();
  void Show(T t);
}

public class CustomerListIn<T> : ICustomerListIn<T>
{
  //public T Get()
  //{
  //    return default(T);
  //}

  public void Show(T t)
  {
  }
}

与协变相反,逆变是用in作修饰符,泛型T只能作输入参数,不能作返回值,可以让右边使用父类

ICustomerListIn<Sparrow> customerList2 = new CustomerListIn<Sparrow>();
ICustomerListIn<Sparrow> customerList1 = new CustomerListIn<Bird>();
customerList1.Show(new Sparrow());
ICustomerListIn<Bird> birdList1 = new CustomerListIn<Bird>();
birdList1.Show(new Sparrow());
birdList1.Show(new Bird());

再来想一下上面这个过程:对customerList1来说,其实现类CustomerListIn<Bird>()会把Bird作为参数在方法public void Show(T t)中使用,所以这个时候传入Sparrow没问题,因为Sparrow也是bird。如果T作为返回值使用,那么接口中的T范围比实现类中的T范围大,就无法正常工作了。
对于birdList1来说,实现类和接口中T都是Bird,只会作参数使用,所以birdList1.Show(new Sparrow())中,传入Sparrow没有问题。

  1. 最后再看一个协变逆变一起的实例:
public interface IMyList<in inT, out outT>
{
  void Show(inT t);
  outT Get();
  outT Do(inT t);
  ////out 只能是返回值   in只能是参数
  //void Show1(outT t);
  //inT Get1();
}

实现类如下:

public class MyList<T1, T2> : IMyList<T1, T2>
{
  public void Show(T1 t)
  {
    Console.WriteLine(t.GetType().Name);
  }

  public T2 Get()
  {
    Console.WriteLine(typeof(T2).Name);
    return default(T2);
  }

  public T2 Do(T1 t)
  {
    Console.WriteLine(t.GetType().Name);
    Console.WriteLine(typeof(T2).Name);
    return default(T2);
  }
}

在使用的时候,可以这么用:

IMyList<Sparrow, Bird> myList1 = new MyList<Sparrow, Bird>();
myList1.Show(new Sparrow());
IMyList<Sparrow, Bird> myList2 = new MyList<Sparrow, Sparrow>();//协变
myList2.Show(new Sparrow());
IMyList<Sparrow, Bird> myList3 = new MyList<Bird, Bird>();//逆变
myList3.Show(new Sparrow());
IMyList<Sparrow, Bird> myList4 = new MyList<Bird, Sparrow>();//协变+逆变
myList4.Show(new Sparrow());

上述过程这里不再赘述,就是交叉着使用协变逆变,应该比较好理解了。


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
C# Advanced Tutorial 1-2-Reflection C# Advanced Tutorial 1-2-Reflection
主要内容概要1 反射调用实例方法、静态方法、重载方法 选修:调用私有方法 调用泛型方法2 反射字段和属性,分别获取值和设置值3 反射的好处和局限 反射反射 程序员的快乐。反射无处不在,MVC ASP.Net ORM IOC AOP几乎所有的
下一篇 
我的.Net Core技术路线 我的.Net Core技术路线
为什么写就像自己在2020年的计划书中描述的那样,自己的重要目标之一就是夯实基础,形成自己的知识体系,输出点什么。.Net是其中一个主要的技术栈维度,虽然自己此前了解一些C#,了解一些.Net,但是对于其中的细节深究的很少,距离掌握这个程度
2020-01-04
  目录