泛型

  • 为什么需要泛型:避免成员膨胀或类型膨胀
  • 正交性:泛型类型(类、接口、委托……) 泛型成员(属性、方法、字段……)
  • 类型方法的参数推断
  • 泛型与委托,Lambda 表达式

泛型在面向对象中的地位与接口相当。其内容很多,今天只介绍最常用最重要的部分。

基本介绍

image.png

正交性:泛型和其它的编程实体都有正交点,导致泛型对编程的影响广泛而深刻。

泛化 <-> 具体化

泛型类示例

示例背景:开了个小商店,一开始只卖苹果,卖的苹果用小盒子装上给顾客。顾客买到后可以打开盒子看苹果颜色。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var apple = new Apple { Color = "Red" };
  6. var box = new Box { Cargo = apple };
  7. Console.WriteLine(box.Cargo.Color);
  8. }
  9. }
  10. class Apple
  11. {
  12. public string Color { get; set; }
  13. }
  14. class Box
  15. {
  16. public Apple Cargo { get; set; }
  17. }

后来小商店要增加商品(卖书),有下面几种处理方法。

一:我们专门为 Book 类添加一个 BookBox 类的盒子。

  1. static void Main(string[] args)
  2. {
  3. var apple = new Apple { Color = "Red" };
  4. var box = new AppleBox { Cargo = apple };
  5. Console.WriteLine(box.Cargo.Color);
  6. var book = new Book { Name = "New Book" };
  7. var bookBox = new BookBox { Cargo = book };
  8. Console.WriteLine(bookBox.Cargo.Name);
  9. }

现在代码就出现了“类型膨胀”的问题。未来随着商品种类的增多,盒子种类也须越来越多,类型膨胀,不好维护。

二:用同一个 Box 类,每增加一个商品时就给 Box 类添加一个属性。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var apple = new Apple { Color = "Red" };
  6. var book = new Book { Name = "New Book" };
  7. var box1 = new Box { Apple = apple };
  8. var box2 = new Box { Book = book };
  9. }
  10. }
  11. ...
  12. class Book
  13. {
  14. public string Name { get; set; }
  15. }
  16. class Box
  17. {
  18. public Apple Apple { get; set; }
  19. public Book Book { get; set; }
  20. }

这会导致每个 box 变量只有一个属性被使用,也就是“成员膨胀”(类中的很多成员都是用不到的)。

三:Box 类里面的 Cargo 改为 Object 类型。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var apple = new Apple { Color = "Red" };
  6. var book = new Book { Name = "New Book" };
  7. var box1 = new Box { Cargo = apple };
  8. var box2 = new Box { Cargo = book };
  9. Console.WriteLine((box1.Cargo as Apple)?.Color);
  10. }
  11. }
  12. ...
  13. class Box
  14. {
  15. public Object Cargo{ get; set; }
  16. }

使用时必须进行强制类型转换或 as,即向盒子里面装东西省事了,但取东西时很麻烦。

四:泛型登场

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var apple = new Apple { Color = "Red" };
  6. var book = new Book { Name = "New Book" };
  7. var box1 = new Box<Apple> { Cargo = apple };
  8. var box2 = new Box<Book> { Cargo = book };
  9. Console.WriteLine(box1.Cargo.Color);
  10. Console.WriteLine(box2.Cargo.Name);
  11. }
  12. }
  13. ...
  14. class Box<TCargo>
  15. {
  16. public TCargo Cargo { get; set; }
  17. }

泛型类特化后,都是强类型:
image.png

泛型接口示例

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. //var stu = new Student<int>();
  6. //stu.Id = 101;
  7. //stu.Name = "Timothy";
  8. var stu = new Student<ulong>();
  9. stu.Id = 1000000000000001;
  10. stu.Name = "Timothy";
  11. var stu2 = new Student();
  12. stu2.Id = 100000000001;
  13. stu2.Name = "Elizabeth";
  14. }
  15. }
  16. interface IUnique<T>
  17. {
  18. T Id { get; set; }
  19. }
  20. // 泛型类实现泛型接口
  21. class Student<T> : IUnique<T>
  22. {
  23. public T Id { get; set; }
  24. public string Name { get; set; }
  25. }
  26. // 具体类实现特化化后的泛型接口
  27. class Student : IUnique<ulong>
  28. {
  29. public ulong Id { get; set; }
  30. public string Name { get; set; }
  31. }

泛型集合

.NET Framework 中常用的数据结构基本都是泛型的。
编程中处理的数据很多都存储在如数组、列表、字典、链表等集合中,这些集合都是泛型的,它们的基接口和基类都是泛型的。

这些泛型集合都集中在 System.Collections.Generic 命名空间中。

  1. static void Main(string[] args)
  2. {
  3. IList<int> list = new List<int>();
  4. for (var i = 0; i < 100; i++)
  5. {
  6. list.Add(i);
  7. }
  8. foreach (var item in list)
  9. {
  10. Console.WriteLine(item);
  11. }
  12. }

List 的定义:

  1. public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
  2. {
  3. ...
  4. }
  • IEnumerable:可迭代
  • ICollection:集合,可以添加和移除元素

注:很多泛型类型带有不止一个类型参数,例如 IDictionary

  1. IDictionary<int, string> dict = new Dictionary<int, string>();
  2. dict[1] = "Timothy";
  3. dict[2] = "Michael";
  4. Console.WriteLine($"Student #1 is {dict[1]}");
  5. Console.WriteLine($"Student #2 is {dict[2]}");

泛型算法示例

泛型不仅与面向对象和数据结构有关系,它也和算法密不可分。

Zip 方法:像拉拉链一样合并整型数组。

  1. static void Main(string[] args)
  2. {
  3. int[] a1 = { 1, 2, 3, 4, 5 };
  4. int[] a2 = { 1, 2, 3, 4, 5, 6 };
  5. double[] a3 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
  6. double[] a4 = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
  7. var result = Zip(a1, a2);
  8. Console.WriteLine(string.Join(",", result));
  9. }
  10. static int[] Zip(int[] a, int[] b)
  11. {
  12. int[] zipped = new int[a.Length + b.Length];
  13. int ai = 0, bi = 0, zi = 0;
  14. do
  15. {
  16. if (ai < a.Length) zipped[zi++] = a[ai++];
  17. if (bi < b.Length) zipped[zi++] = b[bi++];
  18. } while (ai < a.Length || bi < b.Length);
  19. return zipped;
  20. }

现在的问题是:当前的 Zip 仅对 int 类型数组有效,无法合并两个 double 数组。

使用泛型后:

  1. static void Main(string[] args)
  2. {
  3. int[] a1 = { 1, 2, 3, 4, 5 };
  4. int[] a2 = { 1, 2, 3, 4, 5, 6 };
  5. var result = Zip(a3, a4);
  6. Console.WriteLine(string.Join(",", result));
  7. double[] a3 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
  8. double[] a4 = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
  9. var result2 = Zip(a3, a4);
  10. Console.WriteLine(string.Join(",", result2));
  11. }
  12. static T[] Zip<T>(T[] a, T[] b)
  13. {
  14. T[] zipped = new T[a.Length + b.Length];
  15. int ai = 0, bi = 0, zi = 0;
  16. do
  17. {
  18. if (ai < a.Length) zipped[zi++] = a[ai++];
  19. if (bi < b.Length) zipped[zi++] = b[bi++];
  20. } while (ai < a.Length || bi < b.Length);
  21. return zipped;
  22. }

泛型委托

C# 内置了很多泛型委托,它们经常会和 Lambda 表达式配合使用,构成 LINQ 查询。

Action 泛型委托:

  1. static void Main(string[] args)
  2. {
  3. Action<string> a1 = Say;
  4. a1("Timothy");
  5. Action<int> a2 = Mul;
  6. a2(1);
  7. }
  8. static void Say(string str)
  9. {
  10. Console.WriteLine($"Hello, {str}!");
  11. }
  12. static void Mul(int x)
  13. {
  14. Console.WriteLine(x * 100);
  15. }

Func 泛型委托:

  1. static void Main(string[] args)
  2. {
  3. Func<int, int, int> f1 = Add;
  4. Console.WriteLine(f1(1, 2));
  5. Func<double, double, double> f2 = Add;
  6. Console.WriteLine(f2(1.1, 2.2));
  7. }
  8. static int Add(int a, int b)
  9. {
  10. return a + b;
  11. }
  12. static double Add(double a, double b)
  13. {
  14. return a + b;
  15. }

配合 Lambda 表达式:

  1. //Func<int, int, int> f1 = (int a, int b) => { return a + b; };
  2. Func<int, int, int> f1 = (a, b) => { return a + b; };
  3. Console.WriteLine(f1(1, 2));

partial 类

下面依次讲解 partial 类的三大用途。

减少派生类

学习类的继承时就提到过一个概念,“把不变的内容写在基类里,在子类里写经常改变的内容”。这就导致一个类中只要有经常改变的内容,我们就要为它声明一个派生类,如果改变的部分比较多,还得声明多个或多层派生类,导致派生结构非常复杂。

有 partial 类后,我们按照逻辑将类切分成几块,每块作为一个逻辑单元单独更新迭代,这些分块合并起来还是一个类。

partial 类与 EF

使用 EF 时,EF 会自动创建映射 Book 的实体类。如果你在自动生成的 Book 实体类中声明方法,一会刷新 EF 实体时,你手写的代码将会被覆盖。

同时我们注意到 EF 创建的 Book 实体类是 partial 类,于是我们可以在别的位置声明 partial 类,添加方法。

  1. public partial class Book
  2. {
  3. public string Report()
  4. {
  5. return $"#{ID} Name:{Name} Price:{Price}";
  6. }
  7. }

调用 Report():

  1. static void Main(string[] args)
  2. {
  3. var dbContext = new BookstoreEntities();
  4. var books = dbContext.Books;
  5. foreach (var book in books)
  6. {
  7. Console.WriteLine(book.Report());
  8. }
  9. }

partial 类与 WinForm、WPF、ASP.NET Core

partial 类还允许一个类的不同部分,使用不同的编程语言来编写。

WinForm 的 Designer 部分和后台代码部分都是用 C# 编写的:
image.png

image.png

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

image.png

新建 ASP.NET Core MVC 项目,找到 Views\Home\Index.cshtml。
cshtml 最终一部分也将被编译为 C# 代码。

枚举类型

  • 人为限定取值范围的整数
  • 整数值的对应
  • 比特位式用法

枚举示例

如何设计员工类的级别属性。

  1. 使用数字? 大小不明确
  2. 使用字符串? 无法约束程序员的输入

使用枚举,即限定输入,又清晰明了:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var employee = new Person
  6. {
  7. Level = Level.Employee
  8. };
  9. var boss = new Person
  10. {
  11. Level = Level.Boss
  12. };
  13. Console.WriteLine(boss.Level > employee.Level);
  14. // True
  15. Console.WriteLine((int)Level.Employee);// 0
  16. Console.WriteLine((int)Level.Manager); // 100
  17. Console.WriteLine((int)Level.Boss); // 200
  18. Console.WriteLine((int)Level.BigBoss); // 201
  19. }
  20. }
  21. enum Level
  22. {
  23. Employee,
  24. Manager = 100,
  25. Boss = 200,
  26. BigBoss,
  27. }
  28. class Person
  29. {
  30. public int Id { get; set; }
  31. public string Name { get; set; }
  32. public Level Level { get; set; }
  33. }

枚举的比特位用法

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var employee = new Person
  6. {
  7. Name = "Timothy",
  8. Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach
  9. };
  10. Console.WriteLine(employee.Skill); // 15
  11. // 过时用法不推荐
  12. //Console.WriteLine((employee.Skill & Skill.Cook) == Skill.Cook); // True
  13. // .NET Framework 4.0 后推荐的用法
  14. Console.WriteLine((employee.Skill.HasFlag(Skill.Cook))); // True
  15. }
  16. }
  17. [Flags]
  18. enum Skill
  19. {
  20. Drive = 1,
  21. Cook = 2,
  22. Program = 4,
  23. Teach = 8,
  24. }
  25. class Person
  26. {
  27. public int Id { get; set; }
  28. public string Name { get; set; }
  29. public Skill Skill { get; set; }
  30. }
  1. 比特位(Flag)用法需给枚举标注 Flags 特性
  2. 比特位用法的更多内容参考官方文档 Non-exclusive members and the Flags attribute
  3. 枚举默认父类是 int,你也可以显式指定它的父类

结构体

  • 值类型,可装/拆箱
  • 可实现接口,不能派生自类/结构体
  • 不能有显式无参构造器

结构体示例

结构体是值类型:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var stu = new Student { Id = 101, Name = "Timothy" };
  6. // 装箱:复制一份栈上的 stu ,放到堆上去,然后用 obj 引用堆上的 student 实例
  7. object obj = stu;
  8. // 拆箱
  9. Student stu2 = (Student)obj;
  10. Console.WriteLine($"#{stu2.Id} Name:{stu2.Name}");
  11. }
  12. }
  13. struct Student
  14. {
  15. public int Id { get; set; }
  16. public string Name { get; set; }
  17. }

因为是值类型,所以拷贝时是值复制:

  1. static void Main(string[] args)
  2. {
  3. var stu1 = new Student { Id = 101, Name = "Timothy" };
  4. // 结构体赋值是值复制
  5. var stu2 = stu1;
  6. stu2.Id = 1001;
  7. stu2.Name = "Michael";
  8. Console.WriteLine($"#{stu1.Id} Name:{stu1.Name}");
  9. // #101 Name:Timothy
  10. }

结构体可以实现接口

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var stu1 = new Student { Id = 101, Name = "Timothy" };
  6. stu1.Speak();
  7. }
  8. }
  9. interface ISpeak
  10. {
  11. void Speak();
  12. }
  13. struct Student : ISpeak
  14. {
  15. public int Id { get; set; }
  16. public string Name { get; set; }
  17. public void Speak()
  18. {
  19. Console.WriteLine($"I'm #{Id} student {Name}");
  20. }
  21. }

注:

  1. 将结构体转换为接口时要装箱
  2. 结构体不能有基类或基结构体,只可以实现接口

结构体不能有显式无参构造器

image.png

因为结构体是值类型,它有默认的无参构造器,该构造器中将字段初始化为 0 或 NULL。

感谢 Tim 老师五年的付出

030 泛型,partial类,枚举,结构体 - 图8

image.png

image.png