撰写了文章 发布于 2020-03-12 15:59:08
从C#开始的编程入门——范型
莫名想到了,汎用人型決戦兵器。大家注意身体。
范型
如果说接口是为了要求各种类型能够满足某种需求,那么范型(Generic programming,generic在这里应该理解为通用之意)就是为了打破类型的隔阂,专注于操作本身。
回想我们之前讲解按引用传递时的交换变量值的Swap方法。
static void Swap(ref int left, ref int right)
{
var temp = left;
left = right;
right = temp;
}
如果我们写一个这样的方法,那么只能传入int类型的参数。当然你可能会说还能传递可以转换成int类型的参数。
不过如果要交换的是string呢。我们就得再写一个重载版本。
static void Swap(ref string left, ref string right)
{
var temp = left;
left = right;
right = temp;
}
很显然,逻辑上和数值版本一模一样。如果还有其他类型需要交换,那么我们可能需要重载更多版本。并且一直重复相同的代码。
我们意识到,Swap的逻辑和类型本身是无关的。这样的赋值操作几乎所有类型都是可以做到的。面对这种情况,我们应该考虑使用范型。如果你来自C++宇宙,这种东西和模版类似,但是有区别。
范型方法
定义范型方法时,我们不会指定方法中会用到的某些变量的具体类型,而是使用一个范型参数。即在调用时把具体类型也当成方法的参数传递给范型方法。
定义范型方法时,在方法名称后面加一对中括号,其中是范型参数名称。这个名称没有确切含义,通常使用T,主要是方便。T又好写,又象征着type。
void Swap<T>(ref T left, ref T right)
{
T temp = left;
left = right;
right = temp;
}
在我们真正调用范型方法时,我们需要给它传递类型参数,届时运行时会调用对应类型的版本。
Swap<int>(ref a, ref b);
对于这种比较简单的情况,在调用时可以省略这个方括号和类型,编译器会自动推断出类型。
再看ref
Swap方法已经被我们范型化,但是参数前面的ref我们依然保留了。我们第一次介绍ref的时候提到,ref用在值类型上可以让值类型参数按引用传递。而这里它作为范型方法,也可以接受引用类型的参数。那么,对于本身就是按引用传递的引用类型参数来说,ref又意味着什么呢?
首先我们考虑一个简单的接受引用类型参数的方法。
static void Bar(Foo foo)
{
foo = new Foo();
foo.SomeProperty++;
}
我们要思考的问题是,这个时候被传进来的引用类型变量难道引用了一个新的对象?
事实上并不是。如果我们回忆前面讲的,引用类型按引用传递,实际上真正的含义是,方法创建了一个新的引用,而这个引用和传递给方法的参数引用了同一个对象。而函数中的本地变量并不会影响作为参数传递的引用类型变量。如果我们在方法中保持其原本的引用,那么我可以操作原本参数所引用的对象,我们当然也可以更改引用的对象,只不过我们不会引用原本的对象进而进行操作了。
我们回过来思考Swap方法和ref。这个方法的最终目的是交换两个变量的值(引用也算是变量的“值”),对于值类型的变量似乎是直观的,我们不是交换值而是直接引用变量。
而对于本身就是引用的引用类型变量,现在我们想做的不是直接拿到引用所引用的对象,而是要直接修改它的引用,使得引用指向了另一个对象从而达到修改引用类型变量原本的引用的目的。这个时候ref关键字仍然发挥着它在值类型上的作用,引用变量,只不过这次他不是引用值类型的变量,而是引用了一个引用。如果你更熟悉C/C++的话,用指向指针的指针来解释是更加简单的。
我们给“引用的引用”复制时直接修改了引用的目标,达到了修改引用类型变量”值“的效果。因而Swap方法的定义对于引用类型来说依然是同样的效果。
范型约束
比如我要简单写一个支持各种类型的确认两者谁大的方法。显然我们需要范型。 我们首先能够轻松想到如何比较各种数值类型。然后把它改写成范型方法。
static bool IsLessThan<T>(T left, T right)
{
return left < right;
}
然后你马上就会发现你的编辑器会提示你这是不行的。因为我们无法保证运行时传递进来T类型重载了小于号。
另一方面,打个比方来说,两台冰箱可能第一时间并不是可以比较的,因为我们没有给冰箱的大小作一个明确定义。比如究竟是按功率比较,还是容量比较?因此编译器也无法假定某个类型的两个对象如何比较。
这个时候我们需要对类型进行约束。范型约束的语法是在范型方法或者范型类的声明后面加上where关键字,然后以要进行约束的类型参数名开始,冒号后面跟上约束的方式。
ReturnType Method<T>() where T:Constraint
还记得讲接口的时候我说过接口是对类型的要求吗,自然而然地,范型约束可以指定作为类型参数的类型必须实现某些接口。
对于比较大小,.NET提供了一个IComparable接口。所有基本数值类型都实现了这个接口。这个接口只需要实现一个方法,CompareTo。返回负数时代表小于,为0代表等于,大于零代表大于(不用布尔类型显然是因为无法表达相等的情况)。需要对某些类型的对象进行大小比较(比如说排序)的的方法就可以要求类型必须实现这个接口以便可以比较大小。此时,我们的小于方法就可以这样写:
bool IsLessThan<T>(T left, T right) where T:IComparable
{
return left.CompareTo(right) < 0;
}
除了约束接口,还可以约束类型的基类、提供默认构造函数等等,更详细的内容,参见这里。
多个类型参数
类型参数可以有多个。
T Foo<T,U>(T a, U b);
现在你可能一时想象不出什么时候会用到多个类型参数,但是随着后面的讲解你就会接触到有多个类型参数的范型。
范型类
类也可以是范型的。比如我们有一个可以装任何东西的盒子。
public class Box<T>
{
public Box(T item)
{
Item = item;
}
public T Item { set; get; }
}
创建实例时我们就像这样:
Box<int> box = new Box<int>(1);
除了类,结构、接口也可以是范型的。
和范型方法一样,对于某些类来说,其行为可以在不同数据类型上发生,因此也就可以把某些成员也范型化。实际上基础类库提供的范型集合类就是利用范型来提供支持不同数据类型的数据结构,这部分内容我们会在后续文章中介绍。
目录