在C#中,泛型无处不在,因为它可以避免成员膨胀或者类型膨胀,又因为其具有良好的正交性继而产生了泛型类型(类/接口/委托/...)和泛型成员(属性/方法/字段/...),泛型也可以做到类型方法的参数推断,我们会通过例子一一了解
类型膨胀 和 成员膨胀
先来看看什么是类型膨胀
请按照注释顺序查看代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
//1.2 卖苹果
Apple apple = new Apple();
apple.Color = "red";
AppleBox box = new AppleBox()
{
Cargo = apple
};//这是两种属性赋值方式
Console.WriteLine(box.Cargo.Color);
Book book = new Book() { Name = "New Book"};
BookBox bookBox = new BookBox(){ Cargo = book };
Console.WriteLine(bookBox.Cargo.Name);
//1.5 目前为止看着没什么问题是吧,实际上已经出问题了,叫做类型膨胀,目前我就两种商品,还需要创建两种盒子,那以后商品越来越多,盒子也得越来越多,就造成了类型膨胀
Console.ReadKey();
}
}
//1. 假设我是一个苹果商,准备在圣诞卖苹果
class Apple
{
public string Color { get; set; }
}
//1.3 因为我圣诞赚了一笔我决定顺手再卖点书
class Book
{
public string Name { get; set; }
}
//1.1 为了更贵点我要准备一个好看的盒子把苹果装起来
class AppleBox
{
//我这个盒子的用途非常明确,就是为了装苹果,所以可以给他准备一个苹果类型的属性Cargo表示我的货物
public Apple Cargo { get; set; }
}
//1.4 我也得把书包装一下卖的更贵
class BookBox
{
public Book Cargo { get; set; }
}
}
再来看看什么是成员膨胀
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "red" };
Book book = new Book() { Name = "New Book" };
Box box1 = new Box()
{ Apple = apple };//在这里 book属性是闲置得
Box box2 = new Box()
{ Book = book };//在这里 apple属性是闲置得
//这就产生了第二个问题,成员膨胀,如果有一千个商品就会有一千个属性,那么同时就会有999个属性闲置
Console.ReadKey();
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
//1. 那我们试一下用一个Box来装所有的东西
class Box
{
public Apple Apple { get; set; }
public Book Book { get; set; }
//......
}
}
泛型类
来看看怎么用泛型解决上面的问题
注意按照注释顺序查看:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "red" };
Book book = new Book() { Name = "New Book" };
//1.2 注意!所有泛型实体都不能直接拿来编程,使用前要对其进行特化,如下操作
Box<Apple> box1 = new Box<Apple>() { Cargo = apple }; //1.3 这里先把一个泛型Box特化成一个装苹果的Box,相当于我们用Apple类型替换了我们的TCargo类型,所以原来Box中的Cargo属性的类型也就变成了apple类型
//1.4 我们再特化一个类型
Box<Book> box2 = new Box<Book>() { Cargo = book };
//1.5 此处注意!我们的泛型类在经过特化之后,里面凡是使用到类型参数的地方,都是强类型的
//Console.WriteLine(box1.Cargo.); 1.6 在Cargo后面打.的时候就能清楚看到Color的属性
Console.WriteLine(box1.Cargo.Color);
Console.WriteLine(box2.Cargo.Name);
Console.ReadKey();
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
//1. 首先把Box类改成泛型类,方法就是在类名后面加一对尖括号,写在这对尖括号里面的内容叫做类型参数,类型参数一定是类名!!!!!
//这个类型参数其实就是一个标识符,这个标识符代表一个泛化的类型,比如我喜欢听音乐,这个音乐就是一个泛指
class Box<TCargo>//这里代表类型是一个商品类型
{
//属性的声明也会发生改变
public TCargo Cargo { get; set; }
//1.1 现在我们回到Main函数中使用它
}
}
泛型接口
再来看看泛型接口如何使用
同样是按照注释顺序查看:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
//1,4 现在来使用一下,比如在我的系统中我希望学生的ID是一个整数
//Student<int> stu = new Student<int>();
//stu.ID = 101;//1.5 现在把鼠标放在ID上会发现它变成了int属性,神不神奇??!!
//stu.Name = "DiaoMao";
//1.6 因为我学校人越来越多,整数类型已经不够用了,我现在想换成无符号长整型作为学生id
Student<ulong> stu = new Student<ulong>();//所以我们再特化一次
stu.ID = 10000000000000001;
stu.Name = "ShaDiao";
//1.7 以上是踏出泛型学习的第一步
Console.ReadKey();
}
}
//1. 声明一个接口IUnique,我怎么保证对象的唯一呢,就需要这个对象有ID这个属性,那这个属性是什么类型我不确定,所以我要把这个接口改成泛型接口
//interface IUnique
interface IUnique<TId>//1.1 类型参数给一个T就可以,但是如果你怕别人不知道这个T是ID属性类型的话,可以写成TId,表示这个类型是为了声明ID这个属性的
{
//public TId ID { get; set; } 注意接口里不能有public ,因为他默认全是public
TId ID { get; set; }
}
//1.2 创建一个学生类,学生是有ID的,但是注意,如果一个类继承了泛型接口,那么这个类也必须变成泛型的
class Student<TId> : IUnique<TId>//Alt + Enter自动实现接口
{
public TId ID { get; set; }//1.3 删除多余的代码,并注意,Student<TId>的TId这个类型参数只会影响到这一行的TId属性,不会影响其他的
public string Name { get; set; }//这里不受TId影响
}
}
这个例子在声明Student类的时候,我们实现了泛型接口IUnique,然后Student这个类就自然而然地变成了泛型类,但是还有一种实现泛型接口的方法,就是当我们实现泛型接口的时候,我们实现的是一个特化之后的泛型接口,这个时候我们的类就不是泛型类了
来看看这种情况:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
Student student = new Student();
student.ID = 1000000000000000001;
student.Name = "ShaDiao";
//1.3 这就是泛型接口的第二种情况,我们在使用一个类实现这个泛型接口的时候,我们在实现这个接口的时候,就把这个接口特化了,此时类实现的就是一个特化以后的泛型接口
Console.ReadKey();
}
}
//1. 上个例子在声明Student类的时候,我们实现了泛型接口IUnique,然后Student这个类就自然而然地变成了泛型类
//但是还有一种实现泛型接口的方法,就是当我们实现泛型接口的时候,我们实现的是一个特化之后的泛型接口,这个时候我们的类就不是泛型类了
interface IUnique<TId>
{
TId ID { get; set; }
}
//1.1 如果我确定以后学生的id都是无符号长整型,那我就不用TId了,直接在实现接口的时候给他特化
class Student : IUnique<ulong>//1.2 在继承接口的时候就特化,此时类就不需要用泛型
{
public ulong ID { get; set; } //这行需要删掉重新实现,就会从Tid类型变成ulong类型
public string Name { get; set; }
}
}
在我们.Net Framework框架中,几乎常用的数据结构都是泛型的,我们平时处理的数据大部分都存储在各种各样的集合中,比如数组、链表、列表、字典等等这些集合或者说这些数据结构都是泛型的,而且他们的基接口和基类也都是泛型的,泛型的集合或者泛型的数据结构以及他们的泛型基接口基类都集中在这个命名空间里:using System.Collections.Generic;
常见的泛型结构——List(动态数组)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
IList<int> list = new List<int>();//1. List背后其实维护者一个数组,我们可以不停的往里面放东西,当我们放的东西太多了,超过其存储能力后,他会生成一个更长的空间,然后把旧的数组复制过来继续存储
//1.1 数组的长度不能改变,但是我们的List和ArrayList可以
for (int i = 0; i < 100; i++)
{
list.Add(i);
}
foreach (var item in list)
{
Console.WriteLine(item);
}
//1.2 这个就是带有一个类型参数的IList泛型接口,和带有一个类型参数的list泛型类
//1.3 我们把鼠标点击在List上按F12可以查看定义,在定义的最前面显式List继承了一系列基接口,包括IEnumerab,这代表他可以被迭代;还有ICollection基接口,代表List也是一个集合,可以向集合添加和移除元素
Console.ReadKey();
}
}
}
双类型参数的泛型——IDictionary(字典)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
IDictionary<int, string> dict = new Dictionary<int, string>();//1. IDictionary被特化成了以int类型为Key,string类型为Value的类型
//1.1 由多态可知:dict是一个接口类型的变量,引用了一个Dictionary类型的实例
//1.2 注意这里IDictionary是接口,Dictionary是一个类,后者继承了前者;而且现在两者都被特化了,所以1.1的引用是没有问题的
dict[1] = "tim";
dict[2] = "tom";
Console.WriteLine($"Student #1 is {dict[1]}");
Console.WriteLine($"Student #2 is {dict[2]}");
//1.3 以上就是给大家展示一下带有不止一个类型参数的泛型接口和泛型类
Console.ReadKey();
}
}
}
泛型算法以及类型推断
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2 = { 1, 2, 3, 4, 5, 6 };
double[] a3 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
double[] a4 = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
//int[] result = Zip(a1, a2);//输出为1,1,2,2,3,3,4,4,5,5,6
//1.那么我想对a3,a4进行一样的操作可以吗,答案是不行
//double[] result = Zip(a3, a4);//这里会警告你参数无法从double转换成int
//1.3 那么现在更完蛋了,因为出现了更麻烦的方法膨胀,接下来我们尝试把数组改成泛型的
//1.6 此时两种类型的数组都可以使用一个方法了,这就是泛型方法的特点,我们只需要写一个方法,就可以在不同的类型之间反复特化以及调用,就不会出现成员膨胀和逻辑重复的问题
double[] result = Zip(a3, a4);//1.7 此时应该注意Zip并没有特化,也就是没有写成Zip<T>(a3, a4)的形式,这是为什么?
//1.8 因为当你传进去a1,a2或者a3,a4的时候,编译器可以判断出这是整型数组还是double数组,然后编译器就能推断出来这个<T>是什么类型的,就不用显示的去写
//double[] result = Zip<double>(a3, a4); 1.9 这行代码是等效于double[] result = Zip(a3, a4);的
//2. 以上就是泛型方法在调用的时候的类型自动推断
Console.WriteLine(string.Join(",", result));//串联集合的成员,并且在每个成员之间使用指定的分隔符
Console.ReadKey();
}
//1.1 一个解决方法就是把下面所有的int数组改成double的
static int[] Zip(int[] a, int[] b)
{
int[] zipped = new int[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai < a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai < a.Length || bi < b.Length);
return zipped;
}
//1.2 改成这样上面的就可以正常输出了
static double[] Zip(double[] a, double[] b)
{
double[] zipped = new double[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai < a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai < a.Length || bi < b.Length);
return zipped;
}
//1.4 Ctrl + H替换所有的int[]数组为T[],此时报警找不到T的来源
static T[] Zip<T>(T[] a, T[] b)//1.5 只需要在方法后面加上:<T>即可,现在这个方法就变成了一个泛型方法
{
T[] zipped = new T[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai < a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai < a.Length || bi < b.Length);
return zipped;
}
}
}
泛型委托——Action
.Net Framework为我们准备了很多泛型委托,他们经常会和Lambda表达式一起使用来构成Linq查询,泛型委托最常用的就是Function和Action这两种
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
//1. 接下来我们用泛型委托Action调用下面两个参数类型完全不同的方法
Action<string> a1 = Say;
a1.Invoke("tim");
Action<int> a2 = Mul;
a2.Invoke(1);
//1.1 注意Action委托是间接调用,而且只能调用没有返回值的方法
Console.ReadKey();
}
static void Say(string str)
{
Console.WriteLine($"Hello {str}!");
}
static void Mul(int x)
{
Console.WriteLine(x * 100);
}
}
}
泛型委托——Function
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
static void Main(string[] args)
{
Func<int, int, int> f1 = Add;//前两个int 是参数类型以及参数个数,最后一个int是返回值类型
Console.WriteLine(f1.Invoke(3, 6));
Func<double, double, double> f2 = Add;
Console.WriteLine(f2(2.4, 4.1));
//以上就是Function的泛型委托
Console.ReadKey();
}
static int Add(int a, int b)
{
return a + b;
}
static double Add(double a, double b)
{
return a + b;
}
}
}
泛型委托与Lambda表达式
Lambda表达式:对于一些逻辑非常简单的方法,我懒得声明他,而是在调用的地方随调用而声明,而且是匿名的声明比如:Function<double,double,double> func1 = (double a,double b) => {return a+b;};,因为Function里面说明了参数类型是double可以简化为:Function<double,double,double> func1 = (a,b) => {return a+b;}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _16.泛型_partial类_枚举_结构
{
internal class Program
{
/*
Lambda表达式:对于一些逻辑非常简单的方法,我懒得声明他,而是在调用的地方随调用而声明,而且是匿名的声明
比如:Function<double,double,double> func1 = (double a,double b) => {return a+b;};,因为Function里面说明了参数类型是double
可以简化为:Function<double,double,double> func1 = (a,b) => {return a+b;}
*/
static void Main(string[] args)
{
Func<double, double, double> func1 = (a, b) => { return a + b; };
var result = func1(100.1, 100.2);
Console.WriteLine(result);
Console.ReadKey();
}
}
}