C# 泛型(Generic)

泛型(Generic)

允许延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候

在泛型类型的定义中,出现的每个T(一个展位变量而已叫别的名字也行)在运行时都会被替换成实际的类型参数。

泛型方法

现在有一个需求,需要写一个方法,这个方法传入的参数可能是int型的,也可能是string型的。首先我们可以用方法的重载方案解决这个问题,比如下面两个重载方法:

public void test(int param)
{ }
public void test(string param)
{ }

但是这样的话如果支持的类型变多了,那么你需要写很多重载方法。如果只写一个重载方法,则可以写成

 public void test(object param) { }

但是这样写又出现object转成其他类型的问题,会带来效率损失。同时不检查类型,一旦传入了不支持的类型,可能会出问题。

现在把test方法改造一下,这样写:

 public void test<T>(T param){ }

这样写之后,使用的时候要求提前通知这个方法,你传入的是什么类型,即:

test<int>(10);

如果写成 test<int>("10");编译器就会报错。

这就是泛型方法。这里面我们省略了方法内部的实现,其实仔细想一下,如果要在这样的方法里面添加业务代码,似乎除了用于存放数据的集合之外,并没有多少场景需要这么写方法。没错,泛型这个东西最常用的应用场景就是数据集合。而List<T>就是一个存放各种数据的泛型类。

泛型类

上面的方法:public void test<T>(T param){ },我们可以尝试一下把<T>去掉,只写成public void test(T param){ }看看会发生什么。你会发现编译器会报错,那么我们再尝试一下在这个方法的类名上加上<T>,即写成:

class TClass<T>
{
    public void test(T param)
    { }
}

你会发现,如果把<T>放到类名上,里面的方法就不需要加 <T>了,同时编译器也不会报错。这是一种比较简洁的写法。这个时候,TClass这个类就是泛型类,而它的构造方法,则和普通的类的构造方法的写法是一样的。当你要实例化这个类型的时候,必须告诉这个类型T代表哪个类型,之后,所有这个类里面被标识了T的地方,都是指你开始实例化指明的类型。比如test这个方法里面传入的param,一定要和你开始实例化这个类的时候指明的类型一致。再比如你写一个返回T的方法: public T returnTest() { },这个方法的返回值也必须是你实例化类时指明的类型。如果我们TClass改成List,把test改成Add,则方法变成了下面这样

 class List<T>
 {
     public void Add(T param)
     { }
 }

这不就是我们经常用的List<T>这个泛型集合吗。当然它的内部实现还有很多东西,这里我们不去关注。

参考链接:https://www.cnblogs.com/ypa-yap-yap/p/11523736.html

泛型约束

为什么要泛型约束,其主要问题还是解决安全问题,规范开发人员写代码的规范性,避免一些在运行时期才能检查到的错误。比如下面的代码,在编译器是不会报错的,但是在运行期会出现转换异常。

public class 动物
{ 
}
 public class 狗 :动物
{
} 
public class 猫
{ 
}
  
 public class Generic
    {
        public void method(object cat)
        {
            动物 animal= (动物)cat;
        }
    }

所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。

序号约束说明
1 T:struct 类型参数必须是值类型
2 T:class 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
3 T:new() 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
4 T:基类名 类型参数必须是指定的基类或派生自指定的基类。
5 T:接口名称 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。
6 T:基类名,接口名称,new() 泛型约束也可以同时约束多个,但是new()必须放在最后

泛型的协变和逆变

  • 引入时间:.net framework4.0

  • 引入目的:为了解决泛型父子类型的转换问题。

  • 使用规则:

    1. 只能放在接口或者委托的泛型参数前面。

    2. out 协变covariant,用来修饰返回值,儿子可以赋值给老父亲。

    3. in:逆变contravariant,用来修饰传入参数,老父亲可以赋值给儿子。

在OO的世界里,可以安全地把子类的引用赋给父类引用。但是在T的世界里,就不一定了。有的能变,有的不能变,先了解以下几点:

  • 以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。

  • 当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。

  • 值类型不参与逆变与协变。

如果不能理解以上几句话,就先看下面的知识点

协变

所谓协变,就是为了解决子类泛型接口或委托能回到父类泛型接口或委托上来。(Foo<父类> = Foo<子类> )

//泛型委托:
public delegate T MyFuncA<T>();//不支持逆变与协变
public delegate T MyFuncB<out T>();//支持协变
 
MyFuncA<object> funcAObject = null;
MyFuncA<string> funcAString = null;
MyFuncB<object> funcBObject = null;
MyFuncB<string> funcBString = null;
MyFuncB<int> funcBInt = null;
 
funcAObject = funcAString;//编译失败,MyFuncA不支持逆变与协变
funcBObject = funcBString;//变了,协变
funcBObject = funcBInt;//编译失败,值类型不参与协变或逆变
 
//泛型接口
public interface IFlyA<T> { }//不支持逆变与协变
public interface IFlyB<out T> { }//支持协变
 
IFlyA<object> flyAObject = null;
IFlyA<string> flyAString = null;
IFlyB<object> flyBObject = null;
IFlyB<string> flyBString = null;
IFlyB<int> flyBInt = null;
 
flyAObject = flyAString;//编译失败,IFlyA不支持逆变与协变
flyBObject = flyBString;//变了,协变
flyBObject = flyBInt;//编译失败,值类型不参与协变或逆变
 
//数组:
string[] strings = new string[] { "string" };
object[] objects = strings;

逆变

所谓协变,就是为了解决父类泛型接口或委托能回到子类泛型接口或委托上来。(Foo<子类> = Foo<父类>)

public delegate void MyActionA<T>(T param);//不支持逆变与协变
public delegate void MyActionB<in T>(T param);//支持逆变
 
public interface IPlayA<T> { }//不支持逆变与协变
public interface IPlayB<in T> { }//支持逆变
 
MyActionA<object> actionAObject = null;
MyActionA<string> actionAString = null;
MyActionB<object> actionBObject = null;
MyActionB<string> actionBString = null;
actionAString = actionAObject;//MyActionA不支持逆变与协变,编译失败
actionBString = actionBObject;//变了,逆变
 
IPlayA<object> playAObject = null;
IPlayA<string> playAString = null;
IPlayB<object> playBObject = null;
IPlayB<string> playBString = null;
playAString = playAObject;//IPlayA不支持逆变与协变,编译失败
playBString = playBObject;//变了,逆变

注意

in/out是什么意思呢?为什么加了它们就有了“变”的能力,是不是我们定义泛型委托或者接口都应该添加它们呢?

原来,在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。

使用注意点(重点)

注意点1

泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型;

如果子类也是泛型的,那么继承的时候可以不指定具体类型

namespace MyGeneric
{
    /// <summary>
    /// 使用泛型的时候必须指定具体类型,
    /// 这里的具体类型是int
    /// </summary>
    public class CommonClass :GenericClass<int>
    {
    }

    /// <summary>
    /// 子类也是泛型的,继承的时候可以不指定具体类型
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CommonClassChild<T>:GenericClass<T>
    {

    }
}

注意点2

类实现泛型接口也是这种情况

namespace MyGeneric
{
    /// <summary>
    /// 必须指定具体类型
    /// </summary>
    public class Common : IGenericInterface<string>
    {
        public string GetT(string t)
        {
            throw new NotImplementedException();
        }
    }

    /// <summary>
    /// 可以不知道具体类型,但是子类也必须是泛型的
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CommonChild<T> : IGenericInterface<T>
    {
        public T GetT(T t)
        {
            throw new NotImplementedException();
        }
    }
}

 

 

参考链接:https://www.cnblogs.com/zhan520g/p/10397117.html#idx_10

参考链接:https://www.cnblogs.com/dotnet261010/p/9034594.html

posted @ 2020-01-13 17:39  智者见智  阅读(765)  评论(0编辑  收藏  举报