泛型
- 为什么需要泛型:避免成员膨胀或类型膨胀
- 正交性:泛型类型(类、接口、委托……) 泛型成员(属性、方法、字段……)
- 类型方法的参数推断
- 泛型与委托,Lambda 表达式
泛型在面向对象中的地位与接口相当。其内容很多,今天只介绍最常用最重要的部分。
基本介绍

正交性:泛型和其它的编程实体都有正交点,导致泛型对编程的影响广泛而深刻。
泛化 <-> 具体化
泛型类示例
示例背景:开了个小商店,一开始只卖苹果,卖的苹果用小盒子装上给顾客。顾客买到后可以打开盒子看苹果颜色。
class Program{static void Main(string[] args){var apple = new Apple { Color = "Red" };var box = new Box { Cargo = apple };Console.WriteLine(box.Cargo.Color);}}class Apple{public string Color { get; set; }}class Box{public Apple Cargo { get; set; }}
后来小商店要增加商品(卖书),有下面几种处理方法。
一:我们专门为 Book 类添加一个 BookBox 类的盒子。
static void Main(string[] args){var apple = new Apple { Color = "Red" };var box = new AppleBox { Cargo = apple };Console.WriteLine(box.Cargo.Color);var book = new Book { Name = "New Book" };var bookBox = new BookBox { Cargo = book };Console.WriteLine(bookBox.Cargo.Name);}

*
现在代码就出现了“类型膨胀”的问题。未来随着商品种类的增多,盒子种类也须越来越多,类型膨胀,不好维护。
二:用同一个 Box 类,每增加一个商品时就给 Box 类添加一个属性。
class Program{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box { Apple = apple };var box2 = new Box { Book = book };}}...class Book{public string Name { get; set; }}class Box{public Apple Apple { get; set; }public Book Book { get; set; }}
这会导致每个 box 变量只有一个属性被使用,也就是“成员膨胀”(类中的很多成员都是用不到的)。
三:Box 类里面的 Cargo 改为 Object 类型。
class Program{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box { Cargo = apple };var box2 = new Box { Cargo = book };Console.WriteLine((box1.Cargo as Apple)?.Color);}}...class Box{public Object Cargo{ get; set; }}
使用时必须进行强制类型转换或 as,即向盒子里面装东西省事了,但取东西时很麻烦。
四:泛型登场。
class Program{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box<Apple> { Cargo = apple };var box2 = new Box<Book> { Cargo = book };Console.WriteLine(box1.Cargo.Color);Console.WriteLine(box2.Cargo.Name);}}...class Box<TCargo> //此处TCargo为类型参数{public TCargo Cargo { get; set; }}
泛型接口示例
class Program{static void Main(string[] args){//var stu = new Student<int>();//stu.Id = 101;//stu.Name = "Timothy";var stu = new Student<ulong>();stu.Id = 1000000000000001;stu.Name = "Timothy";var stu2 = new Student();stu2.Id = 100000000001;stu2.Name = "Elizabeth";}}interface IUnique<T>{T Id { get; set; }}// 泛型类实现泛型接口class Student<T> : IUnique<T>{public T Id { get; set; }public string Name { get; set; }}// 具体类实现特化化后的泛型接口class Student : IUnique<ulong>{public ulong Id { get; set; }public string Name { get; set; }}
泛型集合
.NET Framework 中常用的数据结构基本都是泛型的。
编程中处理的数据很多都存储在如数组、列表、字典、链表等集合中,这些集合都是泛型的,它们的基接口和基类都是泛型的。
这些泛型集合都集中在 System.Collections.Generic 命名空间中。
static void Main(string[] args){IList<int> list = new List<int>();for (var i = 0; i < 100; i++){list.Add(i);}foreach (var item in list){Console.WriteLine(item);}}
List 的定义:
public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList{...}
- IEnumerable
:可迭代 - ICollection
:集合,可以添加和移除元素
注:很多泛型类型带有不止一个类型参数,例如 IDictionary
IDictionary<int, string> dict = new Dictionary<int, string>();dict[1] = "Timothy";dict[2] = "Michael";Console.WriteLine($"Student #1 is {dict[1]}");Console.WriteLine($"Student #2 is {dict[2]}");
泛型算法示例
泛型不仅与面向对象和数据结构有关系,它也和算法密不可分。
Zip 方法:像拉拉链一样合并整型数组。
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 };var result = Zip(a1, a2);Console.WriteLine(string.Join(",", result));}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;}
现在的问题是:当前的 Zip 仅对 int 类型数组有效,无法合并两个 double 数组。
使用泛型后:
static void Main(string[] args){int[] a1 = { 1, 2, 3, 4, 5 };int[] a2 = { 1, 2, 3, 4, 5, 6 };var result = Zip(a3, a4);Console.WriteLine(string.Join(",", result));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 };var result2 = Zip(a3, a4);Console.WriteLine(string.Join(",", result2));}static T[] Zip<T>(T[] a, T[] b){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;}
泛型委托
● C# 内置了很多泛型委托,它们经常会和 Lambda 表达式配合使用,构成 LINQ 查询。
Action 泛型委托:
static void Main(string[] args){Action<string> a1 = Say;a1("Timothy"); //输出 "Hello, Timothy!"Action<int> a2 = Mul;a2(1); //输出100}static void Say(string str){Console.WriteLine($"Hello, {str}!");}static void Mul(int x){Console.WriteLine(x * 100);}
Func 泛型委托:
static void Main(string[] args){Func<int, int, int> f1 = Add;Console.WriteLine(f1(1, 2));Func<double, double, double> f2 = Add;Console.WriteLine(f2(1.1, 2.2));}static int Add(int a, int b){return a + b;}static double Add(double a, double b){return a + b;}
配合 Lambda 表达式:
//Func<int, int, int> f1 = (int a, int b) => { return a + b; };Func<int, int, int> f1 = (a, b) => { return a + b; };Console.WriteLine(f1(1, 2));
partial 类
减少派生类
学习类的继承时就提到过一个概念,“把不变的内容写在基类里,在子类里写经常改变的内容”。这就导致一个类中只要有经常改变的内容,我们就要为它声明一个派生类,如果改变的部分比较多,还得声明多个或多层派生类,导致派生结构非常复杂。
有 partial 类后,我们按照逻辑将类切分成几块,每块作为一个逻辑单元单独更新迭代,这些分块合并起来还是一个类。
partial 类与 EF
使用 EF 时,EF 会自动创建映射 Book 的实体类。如果你在自动生成的 Book 实体类中声明方法,一会刷新 EF 实体时,你手写的代码将会被覆盖。
同时我们注意到 EF 创建的 Book 实体类是 partial 类,于是我们可以在别的位置声明 partial 类,添加方法。
○其中一部分代码:(必须保证名称空间一致)

○另一部分代码:(必须保证名称空间一致)
public partial class Book{public string Report(){return $"#{ID} Name:{Name} Price:{Price}";}}
详细调用 Report():
static void Main(string[] args){var dbContext = new BookstoreEntities();var books = dbContext.Books;foreach (var book in books){Console.WriteLine(book.Report());}}
partial 类与 WinForm、WPF、ASP.NET Core
partial 类还允许一个类的不同部分,使用不同的编程语言来编写。
(1)WinForm 的 Designer(设计) 部分和后台逻辑代码部分都是用 C# 编写的:

(2)WPF 的界面使用 XAML(最终会被编译成 C#),后台依然是 C#:

(3)新建 ASP.NET Core MVC 项目,找到 Views\Home\Index.cshtml。
cshtml 最终一部分也将被编译为 C# 代码。
cs→C#;html→超文本标记语言缩写;cshtml→看上去像是html,最终也会被编译成c#代码。
枚举类型

- 使用数字? 大小不明确
- 使用字符串? 无法约束程序员的输入
使用枚举,即限定输入,又清晰明了:
class Program{static void Main(string[] args){var employee = new Person{Level = Level.Employee};var boss = new Person{Level = Level.Boss};Console.WriteLine(boss.Level > employee.Level);// TrueConsole.WriteLine((int)Level.Employee);// 0Console.WriteLine((int)Level.Manager); // 100Console.WriteLine((int)Level.Boss); // 200Console.WriteLine((int)Level.BigBoss); // 201}}enum Level{Employee,Manager = 100,Boss = 200,BigBoss,}class Person{public int Id { get; set; }public string Name { get; set; }public Level Level { get; set; }}
●一般情况下(没有给定具体值的情况下),按照0,1,2,3…的顺序从小到大排列。 ●给定具体值,则按照值的顺序(基于上一个值+1 或 给定值)排列下去…
枚举的比特位用法
class Program{static void Main(string[] args){var employee = new Person{Name = "Timothy",Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach//按位取或};Console.WriteLine(employee.Skill); // 15// 过时用法不推荐//Console.WriteLine((employee.Skill & Skill.Cook) == Skill.Cook); // True// .NET Framework 4.0 后推荐的用法Console.WriteLine((employee.Skill.HasFlag(Skill.Cook))); // True}}[Flags]enum Skill{Drive = 1,//二级制0001Cook = 2,//二级制0010Program = 4,//二级制0100Teach = 8,//二级制1000}class Person{public int Id { get; set; }public string Name { get; set; }public Skill Skill { get; set; }}
- 比特位(Flag)用法需给枚举标注 Flags 特性
- 比特位用法的更多内容参考官方文档 Non-exclusive members and the Flags attribute
- 枚举默认父类是 int,你也可以显式指定它的父类
结构体

class Program{static void Main(string[] args){var stu = new Student { Id = 101, Name = "Timothy" };// 装箱:复制一份栈上的 stu ,放到堆上去,然后用 obj 引用堆上的 student 实例object obj = stu;// 拆箱Student stu2 = (Student)obj;Console.WriteLine($"#{stu2.Id} Name:{stu2.Name}");}}struct Student{public int Id { get; set; }public string Name { get; set; }}
因为是值类型,所以拷贝时是值复制:
static void Main(string[] args){var stu1 = new Student { Id = 101, Name = "Timothy" };// 结构体赋值是值复制var stu2 = stu1;stu2.Id = 1001;stu2.Name = "Michael";Console.WriteLine($"#{stu1.Id} Name:{stu1.Name}");// #101 Name:Timothy}
结构体可以实现接口
class Program{static void Main(string[] args){var stu1 = new Student { Id = 101, Name = "Timothy" };stu1.Speak();}}interface ISpeak{void Speak();}struct Student : ISpeak{public int Id { get; set; }public string Name { get; set; }public void Speak(){Console.WriteLine($"I'm #{Id} student {Name}");}}
注:




