泛型(generic)

:::info 大纲

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

    基本介绍

    正交性:泛型和其它的编程实体都有正交点,导致泛型对编程的影响广泛而深刻。
    image.png
    泛化 <-> 具体化

    泛化与具体化就是个别具体与一般e.g. A man and Donald Trump. 就是一般与个别。

泛型类示例

示例背景

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

  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. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. public static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red"};
  9. AppleBox applebox = new AppleBox() { Cargo = apple};
  10. Console.WriteLine(applebox.Cargo.Color);
  11. Book book = new Book() { Name = "Shakespeare" };
  12. BookBox box = new BookBox() { Cargo = book };
  13. Console.WriteLine(box.Cargo.Name);
  14. }
  15. }
  16. class Apple
  17. {
  18. public string Color { get; set; }
  19. }
  20. class Book
  21. {
  22. public string Name { get; set; }
  23. }
  24. class AppleBox
  25. {
  26. public Apple Cargo { get; set; }
  27. }
  28. class BookBox
  29. {
  30. public Book Cargo { get; set; }
  31. }
  32. }

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

二、用同一个 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 = "Shakespeare" };
  7. var box1 = new Box (){ Apple = apple };
  8. var box2 = new Box (){ Book = book };
  9. }
  10. }
  11. class Apple
  12. {
  13. public string Color { get; set; }
  14. }
  15. class Book
  16. {
  17. public string Name { get; set; }
  18. }
  19. class Box
  20. {
  21. public Apple Apple { get; set; }
  22. public Book Book { get; set; }
  23. }

:::danger 这会导致每个 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 = "Shakespeare" };
  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. class Apple
  13. {
  14. public string Color { get; set; }
  15. }
  16. class Book
  17. {
  18. public string Name { get; set; }
  19. }
  20. class Box
  21. {
  22. public Object Cargo{ get; set; }
  23. }

:::warning 使用时必须进行强制类型转换或 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 = "Shakespeare" };
  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. class Apple
  14. {
  15. public string Color { get; set; }
  16. }
  17. class Book
  18. {
  19. public string Name { get; set; }
  20. }
  21. // 泛化 class 类名<类型参数>
  22. class Box<TCargo>
  23. {
  24. public TCargo Cargo { get; set; }
  25. }

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

泛型接口示例

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. //var stu = new Student<int>();
  9. //stu.Id = 101;
  10. //stu.Name = "Timothy";
  11. var stu = new Student<ulong>();
  12. stu.Id = 1000000000000001;
  13. stu.Name = "Timothy";
  14. var stu2 = new Student();
  15. stu2.Id = 100000000001;
  16. stu2.Name = "Elizabeth";
  17. }
  18. }
  19. // 由于类型不确定,将ID改为泛型类
  20. interface IUnique<Tid>
  21. {
  22. Tid Id { get; set; }
  23. }
  24. // 泛型类实现泛型接口
  25. // 如果一个类实现的是泛型接口,则该类本身也是泛型
  26. class Student<Tid> : IUnique<Tid>
  27. {
  28. public Tid Id { get; set; }
  29. public string Name { get; set; }
  30. }
  31. // 具体类实现特化化后的泛型接口
  32. // 之后,我们的类就可实现特化之后的泛型接口
  33. class Student : IUnique<ulong>
  34. {
  35. public ulong Id { get; set; }
  36. public string Name { get; set; }
  37. }
  38. }

泛型集合

.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. }

:::info

  • IEnumerable:可迭代
  • ICollection:集合,可以添加和移除元素 ::: :::warning 很多泛型类型带有不止一个类型参数,例如 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]}");

    泛型算法示例

    泛型不仅与面向对象和数据结构有关系,它也和算法密不可分。 :::info Zip 方法:像拉拉链一样合并整型数组。 ::: ```csharp 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);

  1. return zipped;

}

  1. 现在的问题是:当前的 Zip 仅对 int 类型数组有效,无法合并两个 double 数组。
  2. 使用泛型后:
  3. ```csharp
  4. static void Main(string[] args)
  5. {
  6. int[] a1 = { 1, 2, 3, 4, 5 };
  7. int[] a2 = { 1, 2, 3, 4, 5, 6 };
  8. var result = Zip(a3, a4);
  9. Console.WriteLine(string.Join(",", result));
  10. double[] a3 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
  11. double[] a4 = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
  12. var result2 = Zip(a3, a4);
  13. Console.WriteLine(string.Join(",", result2));
  14. }
  15. static T[] Zip<T>(T[] a, T[] b)
  16. {
  17. T[] zipped = new T[a.Length + b.Length];
  18. int ai = 0, bi = 0, zi = 0;
  19. do
  20. {
  21. if (ai < a.Length) zipped[zi++] = a[ai++];
  22. if (bi < b.Length) zipped[zi++] = b[bi++];
  23. } while (ai < a.Length || bi < b.Length);
  24. return zipped;
  25. }
  26. // 泛型方法在被调用时,类型参数的自动推断

泛型委托

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 = (int a, int b) => { return a + b; };
  3. Console.WriteLine(f1(1, 2));

partial 类

减少类的派生

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

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

partial 类与 EF(Entity Framework)

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

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

  1. public partial class Book
  2. {
  3. public string Report()
  4. {
  5. return $"#{this.ID} Name:{this.Name} Price:{this.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# 代码。

枚举类型

:::info 大纲

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

    枚举示例

    :::warning 如何设计员工类的级别属性。
  1. 使用数字? 大小不明确
  2. 使用字符串? 无法约束程序员的输入 ::: 使用枚举,即限定输入,又清晰明了: ```csharp using System; using System.Collections.Generic;

namespace HelloGeneric { class Program { static void Main(string[] args) { Person person = new Person(); person.Level = Level.Employee;

  1. Person boss = new Person();
  2. boss.Level = Level.Boss;
  3. Console.WriteLine(boss.Level > person.Level);
  4. //var employee = new Person
  5. //{
  6. // Level = Level.Employee
  7. //};
  8. //var boss = new Person
  9. //{
  10. // Level = Level.Boss
  11. //};
  12. // True
  13. Console.WriteLine((int)Level.Employee);// 0
  14. Console.WriteLine((int)Level.Manager); // 100
  15. Console.WriteLine((int)Level.Boss); // 200
  16. Console.WriteLine((int)Level.BigBoss); // 201
  17. }
  18. }
  19. // 定义枚举类型
  20. // 枚举类型的默认值为整数0开始依次+1;
  21. // 可对枚举类型进行赋值,赋值之后的未赋值数也同样在前者基础至上+1
  22. enum Level
  23. {
  24. Employee,
  25. Manager = 100,
  26. Boss = 200,
  27. BigBoss,
  28. }
  29. class Person
  30. {
  31. public int Id { get; set; }
  32. public string Name { get; set; }
  33. public Level Level { get; set; }
  34. }

}

  1. :::danger
  2. 小技巧:<br />按住ALT,使用鼠标选择下拉选择<br />可在同列不同行输出同样的内容
  3. :::
  4. <a name="4fa909fc"></a>
  5. ## 枚举的比特位用法
  6. ```csharp
  7. using System;
  8. using System.Collections.Generic;
  9. namespace HelloGeneric
  10. {
  11. class Program
  12. {
  13. static void Main(string[] args)
  14. {
  15. //Person person = new Person();
  16. //person.Name = "Timothy";
  17. //person.Level = Level.Employee;
  18. //person.Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach;
  19. var employee = new Person
  20. {
  21. Name = "Timothy",
  22. Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach
  23. };
  24. Console.WriteLine(employee.Skill); // 15
  25. // 过时用法不推荐
  26. //Console.WriteLine((employee.Skill & Skill.Cook) == Skill.Cook); // True
  27. // .NET Framework 4.0 后推荐的用法
  28. Console.WriteLine((employee.Skill.HasFlag(Skill.Cook))); // True
  29. }
  30. }
  31. enum Level
  32. {
  33. Employee,
  34. Manager = 100,
  35. Boss = 200,
  36. BigBoss,
  37. }
  38. // [Flags]可直接显示枚举类型中的字符,而不是字符代表的数字
  39. [Flags]
  40. enum Skill
  41. {
  42. Drive = 1,
  43. Cook = 2,
  44. Program = 4,
  45. Teach = 8,
  46. }
  47. class Person
  48. {
  49. public int Id { get; set; }
  50. public string Name { get; set; }
  51. public Level Level { get; set; }
  52. public Skill Skill { get; set; }
  53. }
  54. }
  1. 比特位(Flag)用法需给枚举标注 Flags 特性
  2. 比特位用法的更多内容参考官方文档 Non-exclusive members and the Flags attribute
  3. 枚举默认父类是 int,你也可以显式指定它的父类

    结构体

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

    结构体示例

    结构体是值类型 ```csharp class Program { static void Main(string[] args) {

    1. var stu = new Student { Id = 101, Name = "Timothy" };
    2. // 装箱:复制一份栈上的 stu ,放到堆上去,然后用 obj 引用堆上的 student 实例
    3. object obj = stu;
    4. // 拆箱
    5. Student stu2 = (Student)obj;
    6. Console.WriteLine($"#{stu2.Id} Name:{stu2.Name}");

    } }

struct Student { public int Id { get; set; } public string Name { get; set; } }

  1. 因为是值类型,所以拷贝时是值复制:
  2. ```csharp
  3. static void Main(string[] args)
  4. {
  5. var stu1 = new Student { Id = 101, Name = "Timothy" };
  6. // 结构体赋值是值复制
  7. var stu2 = stu1;
  8. stu2.Id = 1001;
  9. stu2.Name = "Michael";
  10. Console.WriteLine($"#{stu1.Id} Name:{stu1.Name}");
  11. // #101 Name:Timothy
  12. }
  13. struct Student
  14. {
  15. public int Id { get; set; }
  16. public string Name { get; set; }
  17. }

结构体可以实现接口

  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. }

:::warning 注:

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

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

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