面向对象

封装

类的关键字

  • class

声明位置

  • namespace 语句块中声明

成员方法

  • 类中声明的方法

构造函数

  1. 名字和类名一致
  2. 可以实现函数的重载
  3. 默认有一个无参的构造函数
1
2
People p1 = new People(); // 默认有一个无参构造函数 
// 当你写了一个有参的构造函数时,默认的无参构造函数就被顶掉了
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
class People
{
public string Name;
public int Age;

// 无参构造函数
public People()
{
Name = "Unknown";
Age = 0;
}

// 有参构造函数,调用(“Unknown”, age)版本
public People(int age) : this("Unknown", age)
{
Name = "Unknown";
Age = age;
}

// 有参构造函数,调用无参构造函数后设置具体名称与年龄
public People(string name, int age) : this()
{
Name = name;
Age = age;
}
}

析构函数

  • 当垃圾真正被回收的时候,才会执行的函数:~People() { }

垃圾回收机制 (GC)

  1. 垃圾回收的过程中遍历堆(Heap)上动态分配的所有对象
  2. 通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍被引用
  3. 所谓的垃圾就是没有任何变量、对象引用的内容

注意:GC 只负责堆(Heap)上的垃圾回收(因为引用类型在堆上存储),栈(Stack)上的由系统自动管理。

原理:0、1、2 代内存

  • 所有的新对象都会被分配在 0 代内存 中,0 代满时触发 GC。0 代中非垃圾的对象会移到 1 代内存 中;1 代满了,会触发 0、1 代的 GC,并将非垃圾的对象放到 2 代内存 中;2 代满了,就会触发 0、1、2 代的 GC。

注意:不会对大对象进行搬迁,85KB(85000 字节)以上的对象为大对象,直接存储在 2 代内存里。

成员属性

基本概念:

  1. 用于保护成员变量
  2. 为成员属性的获取和赋值添加逻辑处理
  3. 解决 3P(Public、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
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
80
public class Student
{
// 成员变量
public string name;
public bool sex;
public int age;
public float C_Score;
public float Unity_Score;

// 构造函数
public Student(string name, bool sex, int age, float C_Score, float Unity_Score)
{
this.name = name;
this.sex = sex;
this.age = age;
this.C_Score = C_Score;
this.Unity_Score = Unity_Score;
}

// 私有成员方法,计算总成绩
private float Sum
{
get
{
return C_Score + Unity_Score;
}
}

public void SayHello()
{
if (sex)
{
Console.WriteLine($"我叫 {name},今年 {age} 岁了,是男同学,我的总成绩是:{Sum},平均成绩是:{Sum / 2f}");
}
else
{
Console.WriteLine($"我叫 {name},今年 {age} 岁了,是女同学,我的总成绩是:{Sum},平均成绩是:{Sum / 2f}");
}
}

// 成员属性 Age,带逻辑检查
public int Age
{
get
{
return age;
}
set
{
if (value > 150 || value < 0)
{
Console.WriteLine("年龄必须是 0-150 岁之间");
}
else
{
age = value;
}
}
}

// 成员属性 Score,带逻辑检查
public float Score
{
get
{
return C_Score + Unity_Score;
}
set
{
if (value > 100 || value < 0)
{
Console.WriteLine("成绩必须是 0-100 之间");
}
else
{
C_Score = value;
}
}
}
}

索引器

使对象可以像数组一样访问:

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
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<Person> friends = new List<Person>();

public Person(string name, int age)
{
Name = name;
Age = age;
}

// 索引器:通过索引访问 friends 列表
public Person this[int index]
{
get
{
if (friends == null || index >= friends.Count)
{
return null;
}
return friends[index];
}
set
{
if (index < 0)
{
throw new IndexOutOfRangeException();
}
if (index >= friends.Count)
{
friends.Add(value);
}
else
{
friends[index] = value;
}
}
}
}

static void Main(string[] args)
{
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Bob", 24);
Person p3 = new Person("Charlie", 23);
p1.friends.Add(p2);
p1.friends.Add(p3);
Console.WriteLine("{0}, {1}", p1[0].Name, p1[1].Name);
Console.WriteLine("{0}, {1}", p1[0].Age, p1[1].Age);
}

静态成员(static)

特点:可以通过类名访问,与程序同生共死。

静态构造函数

  • 特点:不能使用访问修饰符、没有参数、只会调用一次。
  • 作用:初始化静态成员。

拓展方法

注意:只能写在静态类里。

为已有类添加新方法,而无需修改原类定义。

1
2
3
4
5
// 访问修饰符 static 返回值 函数名(this 拓展类名 参数名, 参数类型 参数名)
public static void SayHello(this string name, int age)
{
Console.WriteLine("这是我为 string 拓展出的一个方法,我叫 " + name + ",今年 " + age + " 岁!");
}

这样就为 string 类拓展出了一个 SayHello() 的成员方法,当你实例化一个 string 类型的对象时,就可以通过该对象调用此方法:

1
2
string name = "John";
name.SayHello(18);

运算符的拓展

让自定义类和对象能够像基本类型一样进行运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Point
{
public float x;
public float y;

public Point(float x, float y)
{
this.x = x;
this.y = y;
}

// 重载加号运算符,让两个 Point 相加得到新的坐标
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.x + p2.x, p1.y + p2.y);
}

public void Show()
{
Console.WriteLine("坐标为(" + x + ", " + y + ")");
}
}

作用:让自定义的类和对象实现运算。


继承

里氏替换

所有的父类都可以用子类来表示,用父类容器来装子类对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
class GameObject
{
}

class Enemy : GameObject
{
}

class Item : GameObject
{
}

GameObject[] objects = { new Enemy(), new Item(), new GameObject() };

is 和 as

1
2
Item item1 = new Item();
GameObject gameObject = new GameObject();
  • is:判断一个对象的类型是否是对应的类,例如:item1 is GameObject 返回 True
  • as:转换,例如:item1 as GameObjectitem1 转换成 GameObject 类型对象(如果无法转换则返回 null)。

继承中的构造函数

1
2
3
4
5
6
7
8
9
public class Monster : Player
{
// 子类的构造函数在执行时会自动调用父类的无参构造函数,
// 但如果父类没有默认构造函数,则会报错。解决方法:使用 base 关键字调用指定的父类构造函数
public Monster(string Name, int HP, int Attack, int Defence)
: base(Name, HP, Attack, Defence)
{
}
}

特有名词解释

  • 装箱和拆箱

    • 装箱(Boxing):将值类型(如 intstruct 等)放到堆上,变成引用类型。
    • 拆箱(Unboxing):将堆上的引用类型转换回值类型。
    • 特点:频繁装箱/拆箱会影响性能。
  • 密封类(sealed):子类不可再被其他类继承。关键字:sealed


多态

virtual、override、base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Duck
{
public virtual void Say()
{
Console.WriteLine("嘎嘎嘎");
}
}

class WoodDuck : Duck
{
public override void Say()
{
base.Say(); // 调用父类的方法
Console.WriteLine("吱吱吱");
}
}

对父类方法的重写,实现多态(同一个函数在不同的对象上有不同表现)。

抽象类(abstract)

  • 特点:

    1. 不能被实例化
    2. 可以包含抽象方法
    3. 继承抽象类必须用 override 重写所有抽象方法
  • 抽象函数

    • 特点:

      1. 只能在抽象类中声明
      2. 没有方法体
      3. 不能是私有的
      4. 继承后必须用 override 重写

接口(interface)

  1. 接口不包含成员变量
  2. 只包含方法、属性、索引器、事件
  3. 成员不能被实现(只有声明)
  4. 成员可以不用写访问修饰符,但不能是私有的
  5. 接口不能继承类,但可以继承另一个接口