类型与变量

C# 的五大数据类型

  • 类(class
  • 结构体(struct
  • 枚举(enum
  • 接口(interface
  • 委托(Delegates)

所有类型都由 Object 派生。

WFSKv4.png

什么是变量

变量就是以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域。
变量的用途是存储数据,变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量。

变量的分类

变量一共有 7 种:静态变量,实例变量(成员变量、字段),数组元素,值参数,引用参数(ref修饰),输出形参(out 修饰),局部变量。
值变量(byte/int/short/ushort...)没有实例,即所谓的实例与变量合而为一。引用类型变量存储的是对象的内存地址。引用参数作为实参的别名,对引用参数的改变都能在实参中体现;与引用参数不同,在方法内部,输出参数在能够被读取之前必须被赋值,这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值, 方法返回之前,方法内部贯穿的任何可能路径都必须为所有输出参数进行一次赋值。

变量内存分配

值变量的实例与变量合而为一,直接按照数据类型所需比特数分配内存,高地址内存对应值的高位。
程序创建引用变量,计算机立刻分配 4 个字节用于存储实例的地址,将每一位置 0,表示引用变量没有引用任何实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Sample {
class Program {
static void Main(string[] args) {
// 分配存实例地址的内存(4 byte)
Student student;
// 为实例分配内存
// student 赋值为实例的首地址
student = new Student();
}
}

class Student {
public uint ID;
public ushort score;
}
}

局部变量在栈中分配内存,实例变量在堆中分配内存。C# 因此还存在装箱与拆箱的概念:当发现 obj 引用的对象是栈上的值变量 x 时,先将 x 在堆中拷贝一份,构成对堆中实例的引用,称此过程为装箱;当局部变量获取 obj 引用的值时,将堆中数据拷贝到栈中,称此过程为拆箱。可以看到,装箱拆箱的过程比较损耗性能。

1
2
3
4
5
6
7
8
9
10
11
namespace Sample {
class Program {
static void Main(string[] args) {
const int x = 100;
// 装箱
object obj = x;
// 拆箱
int y = (int)obj;
}
}
}

类型转换

隐式(implicit)转换

  • 不丢失精度的转换
1
long x = int.MaxValue;
  • 子类向父类的转换
1
Exception e = new StackOverflowException();
  • 装箱

显示(explicit)转换

  • 有可能丢失精度(甚至产生错误)的转换
1
2
int x = ushort.MaxValue + 1;
ushort y = (ushort)x;
  • 拆箱
  • 使用 Convert 类
1
int x = Convert.ToInt32(false);
  • ToString 方法与各数据类型的 Parse/TryParse 方法

自定义类型转换操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <summary>
/// 自定义显示类型转换操作符
/// 相当于目标类型的构造器
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Stone stone = new Stone() {
age = 5000
};
Monkey wukongSun = (Monkey)stone;
Console.WriteLine(wukongSun.age);
}
}

class Stone {
public int age;
public static explicit operator Monkey(Stone stone) {
Monkey monkey = new Monkey {
age = stone.age / 500
};
return monkey;
}
}

class Monkey {
public int age;
}
}

方法

方法的由来

方法(method)的前身是 C/C++ 的函数(function)。C# 中方法不可能独立于类或结构体之外,而 C/C++ 中有全局函数。
“ 为什么需要方法?” 这个问题可这样回答: 隐藏复杂的逻辑,把大算法分解为小算法,利用复用和重用实现复杂功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 方法的复用
/// 修改一个方法,所有关联的方法都可修改。
/// </summary>
namespace Sample {
class Calculator {
public double GetCirCleArea(double r) {
return Math.PI * r * r;
}
public double GetCylinderVolume(double r,double h) {
// 复用GetCirCleArea
return GetCirCleArea(r) * h;
}
public double GetConeVolue(double r,double h) {
// 复用GetCylinderVolume
return GetCylinderVolume(r, h) / 3.0;
}
}
}

构造器

构造器是类型的成员之一,狭义的构造器是实例构造器。
程序员定义构造器后,默认构造器就不存在。

方法重载

方法签名由方法的名称、类型形参的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。方法签名不包含返回类型
实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。
重载决策︰用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。

对方法进行 Debug

  • 设置断点
  • 观察方法调用时的调用堆栈(Call Stack)
  • 逐语句(Step-in),逐过程(Step-over),跳出(Step-out)
  • 观察局部变量的值与变化

方法的调用与栈

调用方法的方法负责压栈。在下面的例子中,Main 调用 GetCircleArea,负责将两个 double 压栈,并记录各种地址信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Sample {
class Program {
static void Main(string[] args) {
Calculator calculator = new Calculator();
Console.WriteLine(calculator.GetCirCleArea(4));
}
}

class Calculator {
public double GetCirCleArea(double r) {
return Math.PI * r * r;
}
}
}

方法的返回值一般存储在寄存器中,而不是栈内存中。当方法返回时,会将方法占用的内存释放。

扩展方法

扩展方法就是为 C# 内置的数据类型设计额外的方法。

  • 扩展方法必须是共有的、静态的。
  • 形参列表中的第一个参数必须用 this 修饰。
  • 扩展方法必须由一个静态类来统一收纳。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 扩展方法
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
const double x = Math.PI;
Console.WriteLine(x.Round(11));
}
}

static class DoubleExtension {
public static double Round(this double input,int digits) {
return Math.Round(input, digits);
}
}
}

操作符的原理和使用

操作符的本质

操作符的本质是函数的简记法。
操作符不能脱离与它关联的数据类型。

操作符举例

  • typeof
1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// 获取数据类型
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Type type = typeof(string);
foreach (var method in type.GetMethods()) {
Console.WriteLine(method.Name);
}
}
}
}
  • default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// 获取数据默认值
/// </summary>
namespace Sample {
enum Level1 {
Mid,
Low,
High
}
enum Level2 {
Mid = 1,
Low = 0,
High = 2
}
enum Level3 {
Mid = 2,
Low = 1,
High = 3
}
class Program {
static void Main(string[] args) {
// 值类型 默认0
Console.WriteLine(default(int));
// 枚举类型
// 默认 Mid
Console.WriteLine(default(Level1));
// 默认 Low
Console.WriteLine(default(Level2));
// 默认 0
Console.WriteLine(default(Level3));
// 引用类型 默认null
Console.WriteLine(default(Form) == null);
}
}
}
  • checked
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 设置抓取数值溢出
/// 默认不抓取(uncheck)
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
uint x = uint.MaxValue;
try {
uint y = checked(x + 1);
Console.WriteLine(y);
} catch (OverflowException e) {
Console.WriteLine(e.Message);
}
}
}
}
  • sizeof
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 获取占用字节数
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Console.WriteLine(sizeof(decimal));
unsafe {
Console.WriteLine(sizeof(Student));
}
}
}
struct Student {
ulong ID;
ulong Score;
}
}
  • ->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 使用指针
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
unsafe {
Student stu;
Student* pStu = &stu;
pStu->ID = 1;
pStu->Score = 100;
Console.WriteLine(pStu->Score);
}
}
}
struct Student {
public ulong ID;
public ulong Score;
}
}
  • is
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <summary>
/// 类型检测
/// 检测所引用的类型
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Teacher teacher = new Teacher();
// 输出true
Console.WriteLine(teacher is Animal);

Car car = new Car();
// 输出false
Console.WriteLine(car is Teacher);

Human human = new Human();
// 输出false
Console.WriteLine(human is Teacher);
}
}

class Animal {
public void Eat() {
Console.WriteLine("Eating...");
}
}

class Human : Animal {
public void Think() {
Console.WriteLine("Who I am?");
}
}
class Teacher : Human {
public void Teach() {
Console.WriteLine("I teach programming.");
}
}

class Car {
public void Run() {
Console.WriteLine("Running...");
}
}
}
  • as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 类型检测
/// 获取合乎逻辑的引用
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Object obj = new Animal();
// 转化不成功返回null
Animal animal = obj as Animal;
if (animal != null) {
animal.Eat();
}
}
}

class Animal {
public void Eat() {
Console.WriteLine("Eating...");
}
}
}
  • ??
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// null值合并
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
// 可空类型
int? x = null;
// 赋值为1
int y = x ?? 1;
Console.WriteLine(y);
}
}
}

字段与属性

字段

字段的概念

字段(field)是一种表示与对象或类型(类与结构体)关联的变量。字段是类型的成员,旧称“成员变量”。与对象关联的字段亦称"实例字段”,与类型关联的字段称为“静态字段”,由 static 修饰。

字段的初始化

无显式初始化时,字段获得其类型的默认值,所以字段“永远都不会未被初始化”。实例字段初始化的时机是对象创建时,静态字段初始化的时机是类型被加载(load)时。

属性

属性的概念

属性(property)是一种用于访问对象或类型的特征的成员,特征反映了状态。属性是字段的自然扩展。
从命名上看,字段更偏向于实例对象在内存中的布局,属性更偏向于反映现实世界对象的特征。对外,属性暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的;对内,属性保护字段不被非法值“污染”。属性由 Get/Set 方法对进化而来。

属性的声明

  • 完整声明——后台(back )成员变量与访问器(注意使用 code snippet 和 refactor 工具)。
  • 简略声明——只有访问器(查看 IL 代码)。
  • 动态计算值的属性。
  • 注意实例属性和静态属性属性的名字一定是名词。
  • 只读属性——只有 getter 没有 setter。
  • 尽管语法上正确,几乎没有人使用“只写属性”,因为属性的主要目的是通过向外暴露数据而表示对象或类型的状态。

属性与字段的关系

—般情况下,它们都用于表示实体(对象或类型)的状态。属性大多数情况下是字段的包装器(wrapper)。
建议永远使用属性而不是字段来暴露数据,即字段永远都是 private 或 protected 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// <summary>
/// 字段与属性
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
try {
Student student = new Student {
Age = 200
};
} catch (Exception e) {
Console.WriteLine(e.ToString());
}
}
}

class Student {
// 字段
private int age;
// 属性
public int Age {
get {
return this.age;
}
set {
if (value < 0 || value > 120) {
throw new Exception("年龄不合法");
} else {
this.age = value;
}
}
}
}
}

索引器概述

什么是索引器

索引器(indexer)是对象的成员,它使得对象能够使用与数组相同的方式(即使用下标)进行索引。

自定义索引器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// <summary>
/// 索引器
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
try {
Student student = new Student();
student["Math"] = 100;
Console.WriteLine(student["Math"]);
} catch (Exception e) {
Console.WriteLine(e.ToString());
}
}
}

class Student {
private Dictionary<string, int> scoreDic = new Dictionary<string, int>();
public int? this[string subject] {
get {
if (scoreDic.ContainsKey(subject)) {
return scoreDic[subject];
} else {
return null;
}
}
set {
if (!value.HasValue) {
throw new Exception("成绩不能为空!");
}
if (scoreDic.ContainsKey(subject)) {
scoreDic[subject] = value.Value;
} else {
scoreDic.Add(subject, value.Value);
}
}
}
}
}

委托

什么是委托

委托是函数指针的 “升级版”。

使用 C# 内置类定义委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// 委托举例
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
action();

Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
Func<int, int, int> func2 = new Func<int, int, int>(calculator.Sub);
Console.WriteLine(func1(4,3));
Console.WriteLine(func2(5,3));
}
}

class Calculator {
public void Report() {
Console.WriteLine("I have 3 methods");
}

public int Add(int a,int b) {
return a + b;
}

public int Sub(int a,int b) {
return a - b;
}
}
}

自定义委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// <summary>
/// 自定义委托
/// </summary>
namespace Sample {

public delegate int Calc(int x, int y);

class Program {
static void Main(string[] args) {
Calculator calculator = new Calculator();

const int a = 100;
const int b = 10;

Console.WriteLine((new Calc(calculator.Add)(a,b)));
Console.WriteLine((new Calc(calculator.Sub)(a, b)));
}
}

class Calculator {
public void Report() {
Console.WriteLine("I have 3 methods");
}

public int Add(int a,int b) {
return a + b;
}

public int Sub(int a,int b) {
return a - b;
}
}
}

委托的使用

模板方法

使用模板方法一般是一部分代码“借用”指定的外部方法(以委托方式传入)来产生结果。这样的方式相当于 “填空题”,常位于代码中部,委托一般有返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/// <summary>
/// 模板方法
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
ProductFactory productFactoruy = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();

Func<Product> func1 = new Func<Product>(productFactoruy.MakePizza);
Func<Product> func2 = new Func<Product>(productFactoruy.MakeToyCar);

Box box1 = wrapFactory.WrapProduct(func1);
Box box2 = wrapFactory.WrapProduct(func2);

Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}

/// <summary>
/// 产品
/// </summary>
class Product {
public string Name { get; set; }
}


/// <summary>
/// 箱子
/// </summary>
class Box {
public Product Product { get; set; }
}


/// <summary>
/// 打包工厂
/// </summary>
class WrapFactory {
public Box WrapProduct(Func<Product> getProduct) {
return new Box() {
Product = getProduct()
};
}
}

/// <summary>
/// 产品公司
/// </summary>
class ProductFactory {
public Product MakePizza() {
return new Product() {
Name = "Pizza"
};
}

public Product MakeToyCar() {
return new Product() {
Name = "Toy Car"
};
}
}
}

回调方法

主调方法接受委托类型参数,调用回调方法。这样的方式相当于“流水线”,常位于代码末尾,委托一般无返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/// <summary>
/// 回调方法logCallback
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
ProductFactory productFactoruy = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();

Func<Product> func1 = new Func<Product>(productFactoruy.MakePizza);
Func<Product> func2 = new Func<Product>(productFactoruy.MakeToyCar);

Logger logger = new Logger();
Action<Product> log = new Action<Product>(logger.Log);

Box box1 = wrapFactory.WrapProduct(func1,log);
Box box2 = wrapFactory.WrapProduct(func2,log);

Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}

class Logger {
public void Log(Product product) {
Console.WriteLine("Product '{0}' created at {1}. Price is {2}", product.Name, DateTime.UtcNow, product.Price);
}
}

/// <summary>
/// 产品
/// </summary>
class Product {
public string Name { get; set; }
public decimal Price { get; set; }
}


/// <summary>
/// 箱子
/// </summary>
class Box {
public Product Product { get; set; }
}


/// <summary>
/// 打包工厂
/// </summary>
class WrapFactory {
public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallback) {
Product product = getProduct();
if (product.Price > 50) {
logCallback(product);
}
return new Box {
Product = product
};
}
}

/// <summary>
/// 产品公司
/// </summary>
class ProductFactory {
public Product MakePizza() {
return new Product() {
Name = "Pizza", Price = 12
};
}

public Product MakeToyCar() {
return new Product() {
Name = "Toy Car", Price = 100
};
}
}
}

多播委托

用一个委托封装多个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// <summary>
/// 多播委托
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Student student1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
Student student2 = new Student() { ID = 2, PenColor = ConsoleColor.Green };
Student student3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };

Action action1 = new Action(student1.DoHomework);
Action action2 = new Action(student2.DoHomework);
Action action3 = new Action(student3.DoHomework);

action1 += action2;
action1 += action3;

action1();
}
}

class Student {
public int ID { get; set; }
public ConsoleColor PenColor { get; set; }

public void DoHomework() {
for (int i = 0; i < 5; i++) {
Console.ForegroundColor = PenColor;
Console.WriteLine("Student {0} has been doing homework for {1} hour(s).", ID, i);
Thread.Sleep(1000);
}
}
}
}

隐式异步调用

用委托开启多线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// 隐式异步调用
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Student student1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
Student student2 = new Student() { ID = 2, PenColor = ConsoleColor.Green };
Student student3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };

Action action1 = new Action(student1.DoHomework);
Action action2 = new Action(student2.DoHomework);
Action action3 = new Action(student3.DoHomework);

// 开启三个线程
action1.BeginInvoke(null, null);
action2.BeginInvoke(null, null);
action3.BeginInvoke(null, null);

Thread.Sleep(10000);
}
}

class Student {
public int ID { get; set; }
public ConsoleColor PenColor { get; set; }

public void DoHomework() {
for (int i = 0; i < 5; i++) {
Console.ForegroundColor = PenColor;
Console.WriteLine("Student {0} has been doing homework for {1} hour(s).", ID, i);
Thread.Sleep(1000);
}
}
}
}

注意委托的使用

应当适时地使用接口取代一些对委托的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/// <summary>
/// 模板方法
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
IProductFactory pizzaFactory = new PizzaFactory();
IProductFactory toyCarFactory = new ToyCarFactory();

WrapFactory wrapFactory = new WrapFactory();

Box box1 = wrapFactory.WrapProduct(pizzaFactory);
Box box2 = wrapFactory.WrapProduct(toyCarFactory);

Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}

interface IProductFactory {
Product Make();
}

class PizzaFactory : IProductFactory {
public Product Make() {
return new Product() { Name = "Pizza" };
}
}

class ToyCarFactory : IProductFactory {
public Product Make() {
return new Product() { Name = "ToyCar" };
}
}

/// <summary>
/// 产品
/// </summary>
class Product {
public string Name { get; set; }
}


/// <summary>
/// 箱子
/// </summary>
class Box {
public Product Product { get; set; }
}


/// <summary>
/// 打包工厂
/// </summary>
class WrapFactory {
public Box WrapProduct(IProductFactory productFactory) {
return new Box() {
Product = productFactory.Make()
};
}
}
}

对于难精通、易使用、功能强大的工具,一旦被滥用后果很严重。

  • 这是一种方法级别的紧耦合,现实工作中要慎之又慎。
  • 委托使代码可读性下降,debug 的难度增加。
  • 把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护。
  • 委托使用不当有可能造成内存泄漏和程序性能下降。

事件

初步了解事件

事件的概念

事件在英文中为 Event,大致意思为“会发生的重要的事”。

事件扮演的角色

事件是一种使对象或类具备通知能力的成员,类似消息响应。
事件的本质是委托字段的一个包装器,这个包装器对委托字段的访问起到限制作用,相当于一个“蒙版”。
事件对外界隐藏了委托实例的大部分功能,仅暴露添加和移除事件处理器的功能。

事件模型的组成部分

  • 事件的拥有者(event source,对象)
  • 事件成员(event,成员)
  • 事件的响应者(event subsciber,对象)
  • 事件处理器(event handler,成员)
  • 事件订阅

事件的现实应用

  • 事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来“驱动”的。
  • 各种编程语言对这个机制的实现方法不尽相同。
  • Java 没有事件成员,也没有委托这种数据类型,Java的“事件"是使用接口来实现的。
  • MVC、MVP、MVM 等模式,是事件模式更高级、更有效的"玩法”。
  • 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以先学使用。

事件的应用

注意

  • 事件处理器是方法成员。
  • 挂接事件处理器的时候,可以使用委托实例,也可以使用直接方法名,这是个“语法糖”。
  • 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测。
  • 事件可以同步调用也可以异步调用。

声明自定义事件

自定义事件实际上就是声明事件处理的委托,然后给委托指明处理事件的方法。

  • 完整声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/// <summary>
/// 完整声明事件
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Customer customer = new Customer();
Waiter waiter = new Waiter();

// 给委托指明处理事件的方法
customer.Order += waiter.Action;

customer.Action();
customer.PayTheBill();
}
}

public class OrderEventArgs {
public string DishName { get; set; }
public string Size { get; set; }
}

// 声明处理事件的委托
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

public class Customer {
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine("I will pay ${0}",this.Bill);
}
public void WalkIn() {
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown() {
Console.WriteLine("Sit down.");
}
public void Think() {
for(int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
// 委托非空则执行
orderEventHandler?.Invoke(this, new OrderEventArgs() {
DishName = "Chicken", Size = "large"
});
}
public void Action() {
Console.ReadKey();
WalkIn();
SitDown();
Think();
}

private OrderEventHandler orderEventHandler;
// 声明事件 委托 事件名
public event OrderEventHandler Order {
add { orderEventHandler += value; }
remove { orderEventHandler -= value; }
}
}

public class Waiter {
public void Action(Customer customer, OrderEventArgs e) {
Console.WriteLine("I will serve you the dish - {0}", e.DishName);
double price = 10;
switch (e.Size) {
case "small":
price *= 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
  • 简略声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/// <summary>
/// 简略声明事件
/// 字段式声明(field-like)
/// </summary>
namespace Sample {
class Program {
static void Main(string[] args) {
Customer customer = new Customer();
Waiter waiter = new Waiter();

// 给委托指明处理事件的方法
customer.Order += waiter.Action;

customer.Action();
customer.PayTheBill();
}
}

public class OrderEventArgs : EventArgs {
public string DishName { get; set; }
public string Size { get; set; }
}

// 声明处理事件的委托
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

public class Customer {
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine("I will pay ${0}",this.Bill);
}
public void WalkIn() {
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown() {
Console.WriteLine("Sit down.");
}
public void Think() {
for(int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
// 委托非空则执行(反编译发现调用的是委托,事件不能Invoke)
Order?.Invoke(this, new OrderEventArgs() {
DishName = "Chicken", Size = "large"
});
}
public void Action() {
Console.ReadKey();
WalkIn();
SitDo wn();
Think();
}

// 声明事件 委托 事件名
// 隐式在事件中包裹委托
public event OrderEventHandler Order;
}

public class Waiter {
public void Action(Customer customer, OrderEventArgs e) {
Console.WriteLine("I will serve you the dish - {0}", e.DishName);
double price = 10;
switch (e.Size) {
case "small":
price *= 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}

事件的必要性

在上面的事件示例代码中,我们完全可以使用委托来实现同样的效果:我们声明委托字段 public OrderHandler Order,将 waiter.Action 挂到委托上即可。不禁要问:有了委托字段,为什么还需要引入事件?
原因是,为了程序的逻辑更加符合实际、更加安全,我们必须引入事件,谨防 “借刀杀人”。在上面的实例中,如果使用委托实现,我们可以在调用委托时填入参数,加入另外一个 Customer’ 需要点菜,而将消息的发送者(sender)参数写为 Customer,那么就 “把自己的账算到了别人的头上”,这种情况显然是不安全的;而事件不可直接调用,只能放在 +=-= 的左边,仅暴露添加或删除事件处理器的功能,事件触发必须由事件的拥有者来做

什么是类

  • 类是一种数据结构。
  • 类是一种数据类型。
  • 类代表现实世界中的“种类”。

类的访问控制

  • public class,类能被其他项目访问。
  • internal class,类仅能被当前程序集访问,默认为 internal。
  • private class,只能在类内嵌套类时使用。

类的继承

重写与隐藏

  • 重写
1
2
3
4
5
6
7
8
9
10
11
class Vehicle {
internal protected virtual void Run() {
Console.WriteLine("I'm running!");
}
}

class Car : Vehicle {
internal protected override void Run() {
Console.WriteLine("Car is running!");
}
}
  • 隐藏
1
2
3
4
5
6
7
8
9
10
11
class Vehicle {
internal protected void Run() {
Console.WriteLine("I'm running!");
}
}

class Car : Vehicle {
internal protected new void Run() {
Console.WriteLine("Car is running!");
}
}

注意事项

  • 类在功能上进行扩展(extend)。
  • 只能有一个基类,但可以实现有多个基接口。
  • 类访问级别对继承有影响。
  • sealed 类不能被继承。
  • 首先调用父类构造器,再调用子类构造器。

接口

抽象类

抽象类是未完全实现逻辑的类,抽象类不允许被实例化。
抽象类可作为基类被继承,可用抽象类的变量引用子类的实例。抽象类为复用而生,撞门作为基类来使用,也具有解耦功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Vehicle {
public abstract void Run();
public void Stopped() {
Console.WriteLine("Stopped!");
}
}

class Car:Vehicle {
public override void Run() {
Console.WriteLine("Car is running...");
}
}

class Truck : Vehicle {
public override void Run() {
Console.WriteLine("Truck is running...");
}
}

开闭原则

我们应该封装不变的、稳定的、确定的成员,而把不确定的、有可能改变的成员,声明为抽象成员,留给子类去实现。

接口简介

接口是完全未实现逻辑的 “类”,只有成员函数,成员全部为 public。
接口为解耦而生,实现 “高内聚,低耦合”,方便单元测试。
接口是一个 “协约”,早已为工业生产所熟知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface IVehicle {
void Run();
void Stop();
}

abstract class Vehicle : IVehicle {
public abstract void Run();
public void Stop() {
Console.WriteLine("Stopped!");
}
}

class Car:Vehicle {
public override void Run() {
Console.WriteLine("Car is running...");
}
}

class Truck : Vehicle {
public override void Run() {
Console.WriteLine("Truck is running...");
}
}

使用接口解耦

依赖关系

Car 依赖在 Engine 上,使得 Engine 变得不可替代。这样的紧耦合会使程序产生链式错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Engine {
public int RPM { get; private set; }
public void Work(int gas) {
RPM = 1000 * gas;
}
}

class Car {
private Engine _engine;
public Car(Engine engine) {
_engine = engine;
}
public int Speed { get; private set; }
public void Run(int gas) {
_engine.Work(gas);
Speed = _engine.RPM / 100;
}
}

解耦

使用接口类型变量作为类的字段。当某一个类产生 bug,可让接口变量引用另一个无 bug 的类。
在下面的示例中,我们指明 IPhone 引用 NokiaPhone,但是我们也能十分容易让 IPhone 引用 EricssonPhone,形成了松耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class PhoneUser {
private IPhone _phone;
public PhoneUser(IPhone phone) {
_phone = phone;
}

public void UsePhone() {
_phone.Dail();
_phone.PickUp();
_phone.Send();
_phone.Receive();
}
}

interface IPhone {
void Dail();
void PickUp();
void Send();
void Receive();
}

class NokiaPhone : IPhone {
public void Dail() {
Console.WriteLine("Nokia calling...");
}

public void PickUp() {
Console.WriteLine("Hello! This is Joe.");
}

public void Receive() {
Console.WriteLine("Nokia message ring...");
}

public void Send() {
Console.WriteLine("hello world!");
}
}

class EricssonPhone : IPhone {
public void Dail() {
Console.WriteLine("Ericsson calling...");
}

public void PickUp() {
Console.WriteLine("Hello! This is Joe.");
}

public void Receive() {
Console.WriteLine("Nokia message ring...");
}

public void Send() {
Console.WriteLine("hello world!");
}
}

接口隔离

接口隔离原则指客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

一般方式

首先给出如下代码,大致意思为司机 Driver 调用 Dirve() 开车。客户端为 Driver,调用 Drive() 只会输出开 Vehicle 的状态。若客户想开 Tank,需要程序员打开 Driver 类,修改类中的代码,而且会多出不需要的方法 Fire(),这违反了接口隔离原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Driver {
private IVehicle _vehicle;
public Driver(IVehicle vehicle) {
_vehicle = vehicle;
}

public void Drive() {
_vehicle.Run();
}
}

interface IVehicle {
void Run();
}

class Car : IVehicle {
public void Run() {
Console.WriteLine("Car is running.");
}
}

class Truch : IVehicle {
public void Run() {
Console.WriteLine("Truck is running.");
}
}

interface ITank {
void Fire();
void Run();
}

class LightTank : ITank {
public void Fire() {
Console.WriteLine("Boom!");
}

public void Run() {
Console.WriteLine("Ka ka ka...");
}
}

class MediumTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!");
}

public void Run() {
Console.WriteLine("Ka! Ka! Ka!");
}
}

class HeavyTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!!");
}

public void Run() {
Console.WriteLine("Ka!! Ka!! Ka!!");
}
}

修改代码,让 ITank 继承两个接口,实现接口隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Driver {
private IVehicle _vehicle;
public Driver(IVehicle vehicle) {
_vehicle = vehicle;
}

public void Drive() {
_vehicle.Run();
}
}

interface IVehicle {
void Run();
}

interface IWeapon {
void Fire();
}

class Car : IVehicle {
public void Run() {
Console.WriteLine("Car is running.");
}
}

class Truch : IVehicle {
public void Run() {
Console.WriteLine("Truck is running.");
}
}

interface ITank : IVehicle, IWeapon { }

class LightTank : ITank {
public void Fire() {
Console.WriteLine("Boom!");
}

public void Run() {
Console.WriteLine("Ka ka ka...");
}
}

class MediumTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!");
}

public void Run() {
Console.WriteLine("Ka! Ka! Ka!");
}
}

class HeavyTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!!");
}

public void Run() {
Console.WriteLine("Ka!! Ka!! Ka!!");
}
}

显示实现接口

在下面的代码中,显示实现接口 IKiller;只有在代码中用 IKiller 引用 WarmKiller 的实例,才能调用 Kill() 方法。显示实现接口同样能实现接口隔离,且更加简介、更加符合现实。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IGentleman {
void Love();
}

interface IKiller {
void Kill();
}

class WarmKiller : IGentleman, IKiller {
public void Love() {
Console.WriteLine("I will love you forever.");
}

void IKiller.Kill() {
Console.WriteLine("Let me kill the enemy.");
}
}

单元测试

接口降低了代码的耦合度,再结合单元测试来监控代码质量,能够使开发人员在检查出程序错误使较为方便地调式。

单元测试使用方法

我们首先要创建单元测试项目,比如 .Net Core 框架下的 xUnit 测试项目;接着在 [Fact] 标签下撰写测试方法,测试用 Assert 进行比对。
首先给出一段程序,大致意思为电源给电风扇供电,对于不同功率,电风扇工作时会返回不同状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace Main {
public class Program {
static void Main(string[] args) {
var fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
}
}

public interface IPowerSupply {
int GetPower();
}

public class PowerSupply : IPowerSupply {
public int GetPower() {
return 130;
}
}

public class DeskFan {
private readonly IPowerSupply _powerSupply;
public DeskFan(IPowerSupply powerSupply) {
_powerSupply = powerSupply;
}
public string Work() {
int power = _powerSupply.GetPower();
if (power <= 0) {
return "Won't work.";
} else if (power < 100) {
return "Work slowly.";
} else if (power < 200) {
return "Work fine.";
} else {
return "Warning!";
}
}
}
}

在测试工程中添加测试方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace InterfaceExample.Tests {
public class DeskFanTests {
[Fact]
public void PowerLowerThanZero_OK() {
var fan = new DeskFan(new PowerSupplyLowerThanZero());
var expected = "Won't work.";
var actual = fan.Work();
Assert.Equal(expected, actual);
}

[Fact]
public void PowerInFineRange_OK() {
var fan = new DeskFan(new PowerSupplyHigherThan200());
var expected = "Warning";
var actual = fan.Work();
Assert.Equal(expected, actual);
}
}

class PowerSupplyLowerThanZero : IPowerSupply {
public int GetPower() {
return 0;
}
}

class PowerSupplyHigherThan200 : IPowerSupply {
public int GetPower() {
return 322;
}
}
}

打开测试资源管理器,运行所有测试,可看到通过和失败的测试。在失败的测试方法出加断点,可进入错误代码查看。
WERYin.png

使用 Mock 简化单元测试代码

在 NuGet 程序包中安装 Moq,即可使用 Mock。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace InterfaceExample.Tests {
public class DeskFanTests {
[Fact]
public void PowerLowerThanZero_OK() {
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(() => 0);
var fan = new DeskFan(mock.Object);
var expected = "Won't work.";
var actual = fan.Work();
Assert.Equal(expected, actual);
}

[Fact]
public void PowerInFineRange_OK() {
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(() => 263);
var fan = new DeskFan(mock.Object);
var expected = "Warning!";
var actual = fan.Work();
Assert.Equal(expected, actual);
}
}
}

反射

初识反射

反射是一种计算机处理方式:程序可以访问、检测和修改它本身状态或行为的这种能力,能提供封装程序集、类型的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
namespace Main {
class Program {
static void Main(string[] args) {
ITank tank = new HeavyTank();
// ===========================
var t = tank.GetType();
object o = Activator.CreateInstance(t);
// 获取方法
var fire = t.GetMethod("Fire");
var run = t.GetMethod("Run");
fire.Invoke(o, null);
run.Invoke(o, null);
}
}

class Driver {
private IVehicle _vehicle;
public Driver(IVehicle vehicle) {
_vehicle = vehicle;
}

public void Drive() {
_vehicle.Run();
}
}

interface IVehicle {
void Run();
}

interface IWeapon {
void Fire();
}

class Car : IVehicle {
public void Run() {
Console.WriteLine("Car is running.");
}
}

class Truch : IVehicle {
public void Run() {
Console.WriteLine("Truck is running.");
}
}

interface ITank : IVehicle, IWeapon { }

class LightTank : ITank {
public void Fire() {
Console.WriteLine("Boom!");
}

public void Run() {
Console.WriteLine("Ka ka ka...");
}
}

class MediumTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!");
}

public void Run() {
Console.WriteLine("Ka! Ka! Ka!");
}
}

class HeavyTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!!");
}

public void Run() {
Console.WriteLine("Ka!! Ka!! Ka!!");
}
}
}

反射的必要性

程序的逻辑有时候不能在编写代码时(静态)就完全确定,需要在用户与程序交互时(动态)确定。如果要求程序员在静态情况下枚举用户动态的操作,就有可能使得程序变得极其臃肿;甚至,我们都无法枚举所有用户的操作。这就需要反射,以不变应万变。

依赖注入

依赖注入指把有依赖关系的类放到容器中,解析出这些类的实例,其目的是实现类的解耦。
在下面的代码示例中,HeavyTank 类依赖于 ITank 接口,一般情况下,需要显示化 new 一个对象用 ITank 引用。采用依赖注入技术后,我们只需要在程序开始时向容器中注入与 ITank 绑定的类 HeavyTank,接下来就可以从容器中取出对应的实例即可。若原有代码多处有 ITank tank = new HeavyTank(),此时想将 LightTank 与 ITank 绑定,需要修改多处代码;使用依赖注入的方式,只需要修改向容器注入时的一小部分代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
namespace Main {
public class Program {
static void Main(string[] args) {
// 创建容器
var sc = new ServiceCollection();
sc.AddScoped(typeof(ITank), typeof(HeavyTank));
var sp = sc.BuildServiceProvider();
// ============================================
ITank tank = sp.GetService<ITank>();
tank.Fire();
tank.Run();
}
}

interface IVehicle {
void Run();
}

interface IWeapon {
void Fire();
}

interface ITank : IVehicle, IWeapon { }

class LightTank : ITank {
public void Fire() {
Console.WriteLine("Boom!");
}

public void Run() {
Console.WriteLine("Ka ka ka...");
}
}

class MediumTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!");
}

public void Run() {
Console.WriteLine("Ka! Ka! Ka!");
}
}

class HeavyTank : ITank {
public void Fire() {
Console.WriteLine("Boom!!!");
}

public void Run() {
Console.WriteLine("Ka!! Ka!! Ka!!");
}
}
}

结合 SDK 进行插件开发

开发一款插件,插件用 DLL 形式保存。主体程序加载插件,插件中是各种动物叫声的资源。
主体程序的过程是:加载插件,拿到动物类,创建动物实例,并调用 Voice(int times) 方法。插件中有 Cat、Cow 等类,但是我们并不知道插件中有哪些资源,不可能直接调用 Cat.Voice(),因此反射的技术是必须要使用的。
首先创建一个类库项目作为 SDK:声明接口 IAnimal,声明类 UnfinishedAttribute。

1
2
3
4
5
6
7
8
namespace BabyStroller.SDK {
public interface IAnimal {
void Voice(int times);
}
}
namespace BabyStroller.SDK {
public class UnfinishedAttribute : Attribute { }
}

接着创建类库,用于存储动物叫声的资源,添加各种动物类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
namespace Animals.Lib1 {
public class Cat : IAnimal {
public void Voice(int times) {
for(int i = 0; i < times; i++) {
Console.Write("喵");
}
Console.WriteLine("\n");
}
}
}
namespace Animals.Lib1 {
class Sheep : IAnimal {
public void Voice(int times) {
for (int i = 0; i < times; i++) {
Console.Write("咩");
}
Console.WriteLine("\n");
}
}
}
namespace Animals.Lib2 {
[Unfinished]
public class Cow : IAnimal {
public void Voice(int times) {
for (int i = 0; i < times; i++) {
Console.Write("哞");
}
Console.WriteLine("\n");
}
}
}
namespace Animals.Lib2 {
public class Dog : IAnimal {
public void Voice(int times) {
for (int i = 0; i < times; i++) {
Console.Write("汪");
}
Console.WriteLine("\n");
}
}
}

然后运行主程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
namespace Main {
class Program {
static void Main(string[] args) {
// 打开资源文件路径
var folder = Path.Combine(Environment.CurrentDirectory, "Animals");
// 得到资源文件
var files = Directory.GetFiles(folder);
// 存储各类动物的集合
var animalTypes = new List<Type>();
foreach (var file in files) {
// 获得DLL的装配集
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
// 获得在装配集中定义的所有类型
var types = assembly.GetTypes();
foreach (var t in types) {
// 过滤掉不是动物的类型或者未完成的内容
if (t.GetInterfaces().Contains(typeof(IAnimal)) &&
!t.GetCustomAttributes(false).Any(a => a.GetType() == typeof(UnfinishedAttribute))) {
animalTypes.Add(t);
}
}
}

while (true) {
for(int i = 0; i < animalTypes.Count; i++) {
Console.WriteLine($"{i+1}.{animalTypes[i].Name}");
}
Console.WriteLine("=================================");
Console.WriteLine("Please choose animal.");
int index = int.Parse(Console.ReadLine());
if (index < 1 || index > animalTypes.Count) {
Console.WriteLine("Input Error");
Thread.Sleep(1500);
Console.Clear();
continue;
}
Console.WriteLine("Please set the time to shout.");
int times = int.Parse(Console.ReadLine());
// 选中的动物
var t = animalTypes[index - 1];
// 取得实例(object)
var o = Activator.CreateInstance(t);
// 获得具体类型
var animal = o as IAnimal;
animal.Voice(times);
Thread.Sleep(1500);
Console.Clear();
}
}
}
}

泛型

泛型存在的必要性

  • 泛型的存在可避免成员膨胀或类型膨胀。
  • 泛型与类、接口、委托等都存在正交性。
  • 泛型可与委托、Lambda 表达式结合,形成简洁优美的代码。

泛型类

用尖括号声明一个未知类型的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace Main {
class Program {
static void Main(string[] args) {
Box<Apple> box = new Box<Apple>() { Cargo = new Apple() { Color = "Red" } };
Console.WriteLine(box.Cargo.Color);
}
}

class Apple {
public string Color { get; set; }
}

class Book {
public string Name { get; set; }
}

class Box<TCargo> {
public TCargo Cargo { get; set; }
}
}

泛型接口

泛型类和具体类都能实现泛型接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace Main {
class Program {
static void Main(string[] args) {
Student<int> student = new Student<int>() {
ID = 101,
Name = "Joe"
};

}
}

interface IUnique<TId> {
TId ID { get; set; }
}

class Student<TId> : IUnique<TId> {
public TId ID { get; set; }
public string Name { get; set; }
}
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace Main {
class Program {
static void Main(string[] args) {
int[] a = { 1, 2, 3, 4, 5 };
int[] b = { 6, 7, 8, 9, 10 };
Console.WriteLine(string.Join(',', Merge(a, b)));
}

static T[] Merge<T>(T[] a, T[] b) {
T[] c = new T[a.Length + b.Length];
for (int i = 0; i < a.Length; i++) {
c[i] = a[i];
}
for (int i = a.Length; i < a.Length + b.Length; i++) {
c[i] = b[i - a.Length];
}
return c;
}
}
}

.Net Core 中的泛型类

1
2
3
List<int> list = new List<int>();
Dictionary<string, int> keyValuePairs = new Dictionary<string, int>();
// ...

partial 类

简介

C# 允许 partial 类的成员定义拆分到多个源文件中。每个源文件包含类定义的一部分,编译应用程序时把所有部分组合起来。
更神奇的是,partial 类的不同部分还能用不同语言编写。

举例

在 Book1.cs 中定义属性 Name。
Wehr4g.png
Book2.cs 中再定义属性 Name 报错。
WehDUS.png