您的位置:首页 > 编程语言 > C#

.NET Core CSharp初级篇 1-8泛型、逆变与协变

2019-07-28 22:00 2056 查看

.NET Core CSharp初级篇 1-8

本节内容为泛型

为什么需要泛型

泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助。比如说遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?

泛型简介

在我们的C#中,使用泛型对允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。泛型的定义非常简单,在类或函数名后使用作为占位符即可,这个T也可以换成其他的字母代替。

注意:属性和索引器不能指定自己的泛型参数,它们只能使用所属类中定义的泛型参数进行操作。
你可以通过下面这个例子得到一些关于泛型定义的方法。

值得注意的是泛型是在运行时进行动态变化,并不是在编译时发生。

泛型类与泛型函数

泛型类和泛型函数在使用上基本上是一样的,只不过定义后的范围不一样。对于泛型类,泛型的范围是整个类,泛型函数则是在函数内部。

例如这个例子

class A<T>
{
public T getSomething<X>(X m,T n)
{
return n;
}
public static U test<U>(U x)
{
return x;
}
}
// 实例泛型类必须指定类型
A<string> a = new A<string>()
//泛型推断
A.test<int>(1);//原式
A.test(1);//推断

在泛型函数的调用中,有一个语法糖,它就是泛型类型推断。这非常好理解,C#的编译器足够聪明,它可以根据你传入的参数类型,调用gettype方法进行类型的推断。因此你可以在泛型函数中不显式的指定类型。
类型推理的相同规则适用于静态方法和实例方法。 编译器可基于传入的方法参数推断类型参数;而无法仅根据约束或返回值推断类型参数。 因此,类型推理不适用于不具有参数的方法。 类型推理发生在编译时,之后编译器尝试解析重载的方法签名。 编译器将类型推理逻辑应用于共用同一名称的所有泛型方法。 在重载解决方案步骤中,编译器仅包含在其上类型推理成功的泛型方法。

泛型的范围则是包含关系。包含在泛型类中的泛型函数可以自由的访问泛型类中的泛型,但是类不可以访问泛型函数中指定的泛型。

泛型约束

如果我们使用了泛型,那么必定面临的一个问题就是权限问题。例如class A,假定我希望某些类型不可以作为泛型传入,那么我们就应当使用我们的泛型约束。
泛型约束的使用如下例:

class A : T where T:class
{

}

泛型约束通常有下面几类:

  • where T : struct:类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。
  • where T : class 类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。
  • where T : unmanaged 类型参数不能是引用类型,并且任何嵌套级别均不能包含任何引用类型成员。
  • where T : new() 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。
  • where T : 类型参数必须是指定的基类或派生自指定的基类。
  • where T : 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。
  • where T : U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。

某些约束是互斥的。 所有值类型必须具有可访问的无参数构造函数。 struct 约束包含 new() 约束,且 new() 约束不能与 struct 约束结合使用。 unmanaged 约束包含 struct 约束。 unmanaged 约束不能与 struct 或 new() 约束结合使用。使用的时候稍加注意即可。

你也可以指定多个类型占位符,并且单独为他们进行约束,如:

class A<T,U>
where T:struct
where U:class

甚至你可以进行泛型自我约束,例如:

class A<T,U,K>
where T:struct
where U:K

协变和逆变

这三个名词来自于数学和物理,很多初学者都难以理解这些名词。但事实上在C#上,这些词是用于标示类型与类型之间的绑定。可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变。

协变

如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的。直白的说,协变就是合理的变化,例如猫->动物,这个看上去丝毫没有问题。这就是协变,从小变大。

例如:

// Cat:Animal
//这种变化毫无问题
Cat c = new Cat();
Animal a = c;
//报错,因为List<Cat>不继承于List<Animal>
List<Cat> d = new List<Cat>();
List<Animal> = d;

对于泛型的参数,我们可以使用到我们之前讲函数参数的时候所遇到的 in,out 关键字。In代表输入,体现的就是逆变,Out代表输出,代表的是协变。对于Out输出的东西,自然不可以对他进行输入操作,他只能作为结果返回,因此它不会被修改。因此进行隐式转换的时候,编译器认为该转换是安全的(返回值不变)。

例如

IEnumerable<Cat> c = new List<Cat>();

IEnumerable<Animal> a = c;

很多人可能不不能很好地理解这些来自于物理和数学的名词。我们无需去了解他们的数学定义,但是至少应该能分清协变和逆变。实际上这个词来源于类型和类型之间的绑定。我们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个原本的类型。任何类型T都有其对应的数组类型T[]。那么我们的问题就来了,如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET世界中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称作协变

逆变

逆变则恰恰与协变完全相反,逆变是指代类型往更小的派生类中进行转换,显然这有可能是不安全的,因为有可能会导致数据的丢失。在C#中使用逆变式的方法是使用In关键字,这意味着这个参数只能作为返回值返回,那么我们就有可能对传入的参数进行修改,因此使用强制转换有可能是不合法的。
例如:

public interface IMyList<in T>
{
T GetElement();
void ChangeT(T t);
}

public class MyList<T> : IMyList<T>
{
public T GetElement()
{
return default(T);
}
public void ChangeT(T t)
{
//Change T
}
}

这段代码无法通过编译,因为GetElement是将T返回,这显然不符合逆变的定义。逆变的参数只允许输入而不允许输出。

对于逆变的实践,各位可以去参阅下IList接口与IEnumerable接口的实现。这两个接口很好的体现了在集合中的逆变与协变。

(缺少举例说明,后续补上)

总结

对于泛型,并没有太多的奇技淫巧可言,因为泛型的出现已经就是一个奇技淫巧了。泛型最常用的地方是泛型数组。并且C#对于不确定类型和大小的数组会使用一个非常好用的类,叫做List类,我们将会在中级篇中进行详细的介绍。

如果我的文章帮助了您,请您在github .NET Core Guide项目帮我点一个star,在博客园中点一个关注和推荐。

Github

BiliBili主页

WarrenRyan's Blog

博客园

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: