面向对象

  • 面向过程编程:分析出解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用的时候再一个个依次调用

  • 面向对象编程:用程序来抽象(形容)对象,万物皆对象

面向对象三大核心

  • 封装:用程序语言来形容对象
  • 继承:复用封装对象的代码;(儿子继承父亲,复用现成代码)
  • 多态:同样行为的不同表现;(儿子继承父亲的基因但是有不同的行为)

封装

class关键字

class类是对象的模板,可以通过创建类创建对象

a.位置:namespace

b.语法:

1
2
3
4
5
6
7
8
9
10
11
class 类名
{
// 成员变量
// 成员方法

// 保护成员属性
// 析构函数和构造函数
// 索引器
// 运算符重载函数
// 静态成员
}

c.规范:

  • 类名使用帕斯卡命名法,且不能重复
  • 类是引用类型,分配的是堆空间
  • 多个类实例之间是完全独立的

d.使用

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
namespace Progress
{
class Person
{
// 成员变量
// 成员方法

// 保护成员属性
// 析构函数和构造函数
// 索引器
// 运算符重载函数
// 静态成员
}

class Progress
{
static void Main()
{
// 实例化对象
Person p1;
// null代表不分配堆空间
Person p2 = null;
Person p3 = new Person();
}
}
}

练习:

  1. 观察如下代码,说明A等于多少

    1
    2
    3
    GameObject A = new GameObject();
    GameObject B = A;
    B = null;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    namespace Progress
    {
    class GameObject
    {

    }
    class Progress
    {

    static void Main()
    {

    GameObject A = new GameObject();
    GameObject B = A;
    B = null;
    Console.WriteLine(A); // Progress.GameObject
    }
    }
    }

    这一题中,我们要理解null的作用。

    null表示一个引用类型变量不指向任何对象。它的作用是将引用类型变量的地址设置为空,而不是一个实际的值。

    • 当你将一个引用类型变量设置为 [null](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html) 时,例如 [B = null;](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html),你实际上是将栈上的引用地址清空,使其不再指向任何堆上的对象。
    • 这意味着变量 [B](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html) 不再引用任何对象,而不是将某个值赋给 B
    image-20250105083356659
  2. 观察如下代码,说明A与B是否有关系

    1
    2
    3
    GameObject A = new GameObject();
    GameObject B = A;
    B = new GameObject();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    namespace Progress
    {
    class GameObject
    {

    }
    class Progress
    {

    static void Main()
    {

    GameObject A = new GameObject();
    GameObject B = A;
    B = new GameObject();
    Console.WriteLine(Object.ReferenceEquals(A, B)); // false
    }
    }
    }

成员变量与访问修饰符

类中可以有多个成员变量,类型不限,可以有初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Pet
{
// something in this
}

class Person
{
// 可以设置初始值
private string name = "gcnanmu";
public int age;
public bool sex;
// 此处不能创建对象实例,创建后会导致出现循环创建,导致栈溢出
protected Person[] friends = null;

public Pet pet;
}

struct一样,class也有访问修饰符

  • public 公共的 内部和外部都可以使用
  • private 私有的 只有内部才能使用,默认值
  • protected 受保护的 只有内部和子类才能访问

实例化一个对象后,会默认赋值(使用default(变量类型)可查看对应类型的默认赋值)

  • 数字类 0
  • bool false
  • 引用类型 null
  • char ‘’

练习:

  1. 定义一个Person类,有姓名,身高,年龄,家庭住址等特征,用Person创建若干个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Person
    {
    public string name;
    public int age;
    public float height;
    public string address;
    }

    class Program
    {
    static void Main()
    {
    Person p1 = new Person();
    Person p2 = null;
    Person p3 = new Person();
    p3.age = 18;
    p3.name = "gcnanmu";
    }
    }
  2. 定义一个学生类,有姓名,学号,年龄,同桌等特征,有学习方法。用学生类创建若干个学生

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Student
    {
    public string name;
    public int age;
    public float height;
    public string studentId;
    public Student deskmate = null;
    public void LearningMethod()
    {
    Console.WriteLine("记笔记");
    }
    }
  3. 定义一个班级类,有专业名称,教师容量,学生等,创建一个班级对象

    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
    enum E_Profession
    {
    ComputerScience,
    Software,
    IntelligentScience
    }

    class Student
    {
    public string name;
    public int age;
    public float height;
    public string studentId;
    public Student deskmate = null;
    public void LearningMethod()
    {
    Console.WriteLine("记笔记");
    }
    }

    class Class
    {
    public E_Profession prof;
    public int teacherV;
    public Student[] students;
    }

    class Program
    {
    static void Main()
    {
    Class s = new Class();
    s.prof = E_Profession.ComputerScience;
    s.teacherV = 10;
    s.students = [
    new Student { name = "张三", age = 18, height = 1.7f, studentId = "001" },
    new Student { name = "李四", age = 19, height = 1.8f, studentId = "002" },
    new Student { name = "王五", age = 20, height = 1.9f, studentId = "003" }
    ];
    }
    }
  4. 请问p.age是多少

    1
    2
    3
    4
    5
    Person p = new Person();
    p.age = 10;
    Person p2 = new Person();
    p2.age = 20;
    Console.WriteLine(p.age);// 10
  5. 请问p.age是多少

    1
    2
    3
    4
    5
    Person p = new Person();
    p.age = 10;
    Person p2 = p;
    p2.age = 20;
    Console.WriteLine(p.age);// 20
  6. 请问s.age是多少

    1
    2
    3
    4
    5
    Student s = new Student();
    s.age = 10;
    int age = s.age;
    age = 20;
    Console.WriteLine(s.age); // 10
  7. 请问s.deskmate.age是多少

    1
    2
    3
    4
    5
    Student s = new Student();
    s.deskmate = new Student();
    s.deskmate.age = 10;
    Student s2 = s.deskmate;
    s2.age = 20;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Student
    {
    public int age;
    public Student deskmate = null;
    }

    class Program
    {
    static void Main()
    {
    Student s = new Student();
    s.deskmate = new Student();
    s.deskmate.age = 10;
    Student s2 = s.deskmate;
    s2.age = 20;
    Console.WriteLine(s.deskmate.age); // 20
    }
    }

成员方法

也称成员函数,描述对象要进行的操作

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
class Person
{
public string Name;
public int Age;
/// <summary>
/// 说些什么
/// </summary>
/// <param name="str">要说的话</param>
public void SpeakSomething(string str)
{
Console.WriteLine("{0} says: {1}", Name, str);
}

/// <summary>
/// 是否成年
/// </summary>
/// <returns>是否成年</returns>
public bool isAdult()
{
return Age >= 18;
}
}

class Program
{
static void Main()
{
Person p = new Person();
p.Name = "John";
p.Age = 25;
p.SpeakSomething("Hello World");
if (p.isAdult())
{
Console.WriteLine("{0} is an adult", p.Name);
}
else
{
Console.WriteLine("{0} is not an adult", p.Name);
}
}
}

稍微复杂的例子

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
class Person
{
public string name;
public int age;

public Person[] Friends = null;

/// <summary>
/// 增加朋友的方法
/// </summary>
/// <param name="p">人的对象</param>
public void AddFriend(Person p)
{
// 初始Friends为空
if (Friends为空 == null)
{
Friends = new Person[] { p };
}
else
{
// 开创一个更大的Friends数组
Person[] newFriends = new Person[Friends.Length + 1];
for (int i = 0; i < Friends.Length; i++)
{
newFriends[i] = Friends[i];
}

newFriends[newFriends.Length - 1] = p;
// 覆盖 newFriends会被垃圾回收
Friends = newFriends;
}
}
}

class Program
{
static void Main()
{
Person p = new Person();
p.name = "John";
p.age = 25;

Person p2 = new Person();
p2.name = "张三";
p2.age = 18;
p.AddFriend(p2);

for (int i = 0; i < p.Friends.Length; i++)
{
Console.WriteLine(p.Friends[i].name); // 张三
}
}
}

练习:

  1. 基于成员变量的练习题

    为人类定义说话,走路,吃饭等方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Person
    {
    public string name;
    public int age;

    public void Say(string str)
    {
    Console.WriteLine("{0} say {1}", name, str);
    }

    public void Walk()
    {
    Console.WriteLine("正在走路……");
    }

    public void Eat()
    {
    Console.WriteLine("正在吃饭……");
    }
    }
  2. 基于成员变量的练习题
    为学生类定义学习,吃饭等方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Student
    {
    public string name;
    public int age;

    public void Study()
    {
    Console.WriteLine("{0} is studying", name);
    }

    public void Eat()
    {
    Console.WriteLine("{0}正在吃饭……", name);
    }
    }
  3. 定义一个食物类,有名称,热量等特征,思考如何和人类以及学生类联系起来

    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
    class Person
    {
    public string name;
    public int age;

    public void Say(string str)
    {
    Console.WriteLine("{0} say {1}", name, str);
    }

    public void Walk()
    {
    Console.WriteLine("{0}正在走路……", name);
    }

    public void Eat(Foods f)
    {
    Console.WriteLine("{0}正在{1}", name, f.name);
    }
    }

    class Student
    {
    public string name;
    public int age;

    public void Study()
    {
    Console.WriteLine("{0} is studying", name);
    }

    public void Eat(Foods f)
    {
    Console.WriteLine("{0}正在{1}", name, f.name);
    }
    }

    class Foods
    {
    public string name;
    public int heat;
    }

    class Program
    {
    static void Main()
    {
    Person p = new Person();
    p.name = "John";
    p.age = 25;

    Student s = new Student();
    s.name = "gcnanmu";
    s.age = 18;

    Foods coke = new Foods();
    coke.name = "可乐";
    coke.heat = 99;

    p.Eat(coke);
    s.Eat(coke);
    }
    }

构造函数

实例化对象时进行初始化的函数,即使不写,默认也会存在一个无参构造函数

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
class Person
{

//无参构造函数
public Person()
{
name = "gcnanmu";
age = 18;
}

// 遵循函数重构特性
public Person(string name)
{
this.name = name;
}

public Person(int age)
{
this.age = age;
}

public Person(string name, int age)
{
this.name = name;
this.age = age;
}

public string name;
public int age;
}

class Program
{
static void Main()
{
Person p = new Person(
name: "gcnanmu"
);

Person p2 = new Person(
age: 19
);

Person p3 = new Person(
name: "gcnanmu",
age: 19
);

}
}

规范:

  • 如果内部实现了有参构造函数且无手动实现无参构造,那么会失去无参构造的特性

    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
    class Person
    {

    // 无参构造函数被注释
    // public Person()
    // {
    // name = "gcnanmu";
    // age = 18;
    // }

    public Person(string name, int age)
    {
    this.name = name;
    this.age = age;
    }

    public string name;
    public int age;
    }

    class Program
    {
    static void Main()
    {
    // 报错:“Person”不包含采用 0 个参数的构造函数
    Person p = new Person();

    }
    }
  • 一种特殊写法

    构造函数中的冒号 :this() 用于调用同一个类中的另一个构造函数。:this()会被优先调用

    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
    class Person
    {

    public Person()
    {
    name = "gcnanmu";
    age = 18;
    }

    public Person(string name, int age)
    {
    this.name = name;
    this.age = age;
    }

    public Person(string name)
    {
    this.name = name;
    }

    // 先调用无参构造函数
    public Person(int age) : this()
    {
    this.age = age;
    }

    public string name;
    public int age;

    ~Person()
    {
    Console.WriteLine("我被回收了");
    }
    }

    :this()中可以添加参数或常数,会根据参数选择合适的有参构造函数

练习:

写一个Ticket类,有一个距离变量(在构造对象时赋值,不能为负数),有一个价格特征,有一个方法GetPrice可以读取到价格,并且根据距离distance计算price(1元/公里)

  • 0-100公里 不打折
  • 101-200公里 打9.5折
  • 201-300公里 打9折
  • 300公里以上 打8折

有一个显示方法,可以显示这张票的信息。

例如:100公里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
35
36
37
38
39
40
41
42
43
44
class Ticket
{
public Ticket(int distance)
{
this.distance = distance;
}
public int distance;
public int price;
public double GetPrice()
{
if (distance >= 0 && distance <= 100)
{
return distance;
}
else if (distance > 100 && distance <= 200)
{
return distance * 0.95;
}
else if (distance > 200 && distance <= 300)
{
return distance * 0.9;
}
else if (distance > 300)
{
return distance * 8;
}
return -1;
}

public void GetInfo()
{
price = (int)GetPrice();
Console.WriteLine("{0}公里{1}块钱", distance, price);
}
}

class Program
{
static void Main(string[] args)
{
Ticket ticket = new Ticket(distance: 100);
ticket.GetInfo();// 100公里100块钱
}
}

析构函数与垃圾回收

a.析构函数

当引用类型的堆内存被回收时,会自动调用该函数

语法:

1
2
3
4
~类名()
{
// 一些操作
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
{

public Person(string name, int age)
{
this.name = name;
this.age = age;
}

public string name;
public int age;

// 析构函数
~Person()
{
Console.WriteLine("我被回收了");
}
}

当引用类型的堆内存不再被引用时(null),这片内存就变成了“垃圾”

C#存在垃圾自动回收的机制(GC),几乎不需要使用析构函数进行处理

b.C#垃圾回收机制

官方文档:垃圾回收的基本知识 - .NET | Microsoft Learn

垃圾回收(Garbage Collector),简称GC。通过遍历堆上所有动态分配的对象,识别内存是否被引用,来判断当前内存是否需要被释放(垃圾)。

注意点:

  • GC只负责堆(Heap)内存的垃圾回收
  • 栈(Stack)上的内存是由系统自动分配和释放的,不需要手动管理
  • 垃圾回收有很多算法
    • 引用计数(Reference Counting)
    • 标记清除(Mark Sweep)
    • 标记整理(Mark Compact)
    • 复制集合(Copy Collection)

C#中的垃圾回收机制的大致原理:
使用分代算法,将堆内存分为三代(0代,1代,2代),内存的速度按顺序由块到慢,容量由小到大。另外,垃圾回收会造成一定的系统开销。

新分配的对象都会先分配到第0代中,每次分配都可能触发垃圾回收和搬迁压缩。触发回收机制后,0代向1代迁移,1代向2代迁移,其余垃圾被释放,在迁移过程中,为了减少内存碎片,会尽量将搬迁的内容存放到连续的内存地址中。随着时间推移,几乎所有的老对象都会被分配到第2代内存中,新的对象都会优先在第0,1代内存中。

大的对象(>84k)会被直接分配到第2代内存中,这是由于垃圾回收需要一定的开销,在一定时间可能会造成卡顿,直接把大对象分配到第2代内存可以规避大的内存空间回收情况出现。

垃圾回收可以在代内存满时被动触发,也可以主动手动触发(GC.Collect()

成员属性

作用:

  • 保护成员变量
  • 为成员变量的获取和赋值添加逻辑
  • 解决3大访问修饰符的局限

语法

1
2
3
4
5
6
7
8
9
10
11
12
访问修饰符 返回值 属性名
{
get
{
// 代码
return 变量;
}
set
{
// 代码
}
}
  • 属性命名使用帕斯卡命名法
  • 撇除大括号,和函数定义相比少了()

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
private string name;
private int money;

// 原本name不可获取 现在可以通过Name获取
public string Name
{
get
{
// 可以添加其他逻辑
return name;
}
set
{
// 可以添加其他逻辑
name = value;
}
}

}

规范

  • 可以在getset设置访问修饰符,但是访问修饰符的等级要比成员变量低,且不能两个同时使用访问修饰符

    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
    class Person
    {
    private string name;
    private int money;

    public string Name
    {
    // 报错1:不能为属性或索引器“Person.Name”的两个访问器同时指定可访问性修饰符
    protected get
    {
    return name;
    }
    private set
    {
    name = value;
    }
    }

    }

    class Progress
    {
    static void Main()
    {
    Person p = new Person();
    // 报错2:属性或索引器“Person.Name”不能用在此上下文中,因为 set 访问器不可访问(因为设置了private)
    p.Name = "gcnanmu";
    Console.WriteLine(p.Name);
    }
    }
  • getset可以只有一个(这种情况下无法使用访问修饰符)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Person
    {
    private string name;
    private int money;

    public string Name
    {
    // 报错:“Person.Name”: 仅当属性或索引器同时具有 get 访问器和 set 访问器时,才能对访问器使用可访问性修饰符
    private set
    {
    name = value;
    }
    }

    }
  • 自动属性

    特点:无需定义成员变量,自动获得getset的方法,且此种属性无需额外操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Person
    {
    private string name;
    private int money;

    // 之前并没有定义一个height的成员变量
    public float Height
    {
    get;
    set;
    }

    }

    class Progress
    {
    static void Main()
    {
    Person p = new Person();
    p.Height = 1.79f;
    Console.WriteLine(p.Height); // 1.79
    }
    }

练习

定义一个学生类,有五种属性,分别为姓名、年龄、性别、CSharp成绩、Unity成绩

有两个方法:

  • 打招呼:介绍自己叫什么,今年几岁了,是男是女
  • 计算自己的总分数和平均分并显示

使用属性完成,要求如下:

  • 年龄必须在0-150岁之间

  • 成绩必须是0-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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class Student
{
// 无需对名字进行处理,使用自动属性
public string Name
{
set;
get;
}

// 需要对这些变量进行额外的判断
private int age;
private string gender;
private int csharpScore;
private int unityScore;

public void SayHello()
{
Console.WriteLine("我是{0},今年{1}岁,是{2}孩子", Name, Age, Gender);
}

public void CalculateScore()
{
int sum = csharpScore + unityScore;
float avg = sum / 2;
Console.WriteLine("总成绩:{0}\n平均分:{1}", sum, avg);
}

public int Age
{
set
{
if (age < 0 || age > 150)
{
Console.WriteLine("年龄必须在0-150之间");
return;
}
age = value;
}
get
{
return age;
}
}

public string Gender
{
get
{
return gender;
}
set
{
switch (value)
{
case "男":
case "女":
gender = value;
break;
default:
Console.WriteLine("性别只能是男或者女");
return;
}
}
}


private bool JudgeScore(int score)
{
if (score < 0 || score > 100)
{
return false;
}
return true;
}

public int CsharpScore
{
get
{
return csharpScore;
}
set
{
if (JudgeScore(value))
{
csharpScore = value;
}
else
{
Console.WriteLine("成绩必须在0-100之间");
return;
}
}
}

public int UnityScore
{
get
{
return UnityScore;
}
set
{
if (JudgeScore(value))
{
unityScore = value;
}
else
{
Console.WriteLine("成绩必须在0-100之间");
return;
}
}
}
}

class Progress
{
static void Main()
{
Student s = new Student();
s.Name = "gcnanmu";
s.Age = 19;
s.Gender = "男";
s.CsharpScore = 90;
s.UnityScore = 80;
s.SayHello();
s.CalculateScore();
/*
我是gcnanmu,今年19岁,是男孩子
总成绩:170
平均分:85
*/
}
}

索引器

可以向数组一样访问其中的元素,使程序看起来更直观,更容易编写

语法

1
2
3
4
5
访问修饰符 返回类型 this[参数类型 参数名]
{
get{}
set{}
}

例子:

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
class Person
{
private string name;
private int age;

private Person[] friends;

// 自动定位到此索引器
public Person this[int index]
{
get
{
// 可以添加处理逻辑
if (friends == null || index > friends.Length - 1)
{
return null;
}

return friends[index];
}
set
{
// 可以添加处理逻辑
if (friends == null)
{
friends = [value];
}
else if (index > friends.Length - 1)
{
friends[friends.Length - 1] = value;
}
friends[index] = value;
}
}
}

class Program
{
public static void Main(string[] args)
{
Person p = new Person();
p[0] = new Person();
Console.WriteLine(p[0]);
}
}

索引器支持重载

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
{
private string name;
private int age;

private Person[] friends;

private int[,] array;

// 重载后 如果使用[,]会自动定位到此索引器
public int this[int row, int col]
{
get
{
return array[row, col];
}

set
{
array[row, col] = value;
}
}

// 使用[]自动定位到此索引器
public Person this[int index]
{
get
{
// 可以添加处理逻辑
if (friends == null || index > friends.Length - 1)
{
return null;
}

return friends[index];
}
set
{
// 可以添加处理逻辑
if (friends == null)
{
friends = [value];
}
else if (index > friends.Length - 1)
{
friends[friends.Length - 1] = value;
}
friends[index] = 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
class Person
{
private string name;
private int age;

// 可使用Person[变量名]来获取变量
public string this[string str]
{
get
{
switch (str)
{
case "name":
return name;
case "age":
return age.ToString();
default:
return "";
}
}
set
{
switch (str)
{
case "name":
name = value;
break;
case "age":
age = int.Parse(value);
break;
default:
break;
}
}
}
}

class Program
{
public static void Main(string[] args)
{
Person p = new Person();
p["name"] = "gcnanmu";
p["age"] = "18";
Console.WriteLine(p["name"]); // gcnanmu
Console.WriteLine(p["age"]); // 18
}
}

练习:自定义一个整数类,该类中有一个整数数组变量,为他封装增删改查的方法

静态成员

即使用static修饰的变量,函数和对象,无需实例化对象也可使用(和常量一样变为了公有)

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
class Test
{
public static float PI = 3.1415926f;
public int testInt = 100;

public static float CalculateCircle(int r)
{
return PI * r * r;
}

public int TestFun()
{
return 1;
}
}



class Program
{
public static void Main(string[] args)
{
// 无需实例化即通过类名点出来使用
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalculateCircle(2));
}
}

原理:

一旦加上了static关键字,在程序运行开始时,就会将静态成员分配到静态内存区,因此并非无中生有。另外,静态成员与程序同生共死,几乎不会被GC处理。综上,静态成员具有唯一性和全局性。

内存空间是有限的。因为static修饰的静态成员不能被GC,因此如果静态成员足够多,可能会造成其他内存区严重不足,导致程序无法良好运行。

规范:

  • static的位置和访问修饰符的前后顺序无关
  • 静态成员内不能使用非静态成员
  • 静态成员可以没有初始值且可以被修改

关于staticconst

  • 相同点 两者都可以通过类名.出来使用
  • 不同点
    • const必须初始化且不能被修改,而static没有要求
    • const必须写在访问修饰符后,而static没有要求
    • const只能修饰变量

练习:一个类对象,在整个应用程序的生命周期中,有且经会有一个该对象的存在,不能再外部实例化,直接通过该类名就能得到一个唯一的对象。

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
class Test
{
public static Test PI;

// 1. 私有化构造函数 使Test在外部无法被实例化
private Test()
{

}

// 2. 使用静态方法创建一个静态类Test
static Test()
{
PI = new Test();
}

// 1,2综合满足了题目的要求 不可以被实例化,全局也只有一个Test实例,可以通过类名访问
}

class Progress
{
public static void Main(string[] args)
{
Console.WriteLine(Test.PI); // Test
Test t = Test.PI;
Console.WriteLine(t); // Test
// !!!报错:“Test.Test()”不可访问,因为它具有一定的保护级别
Test ts = new Test();
}
}

下一节介绍的静态类能更好题目满足要求

静态类和静态构造函数

使用static修饰的类和构造函数

作用:

  • 静态类: 作为一个工具类,方便使用(如Console
  • 静态构造函数:用来初始化静态成员(静态函数中只能由静态成员)

静态类的特点:

  • 只能包含静态成员
  • 不能被实例化

静态构造函数的特点:

  • 不能使用访问修饰符,不能有参数

    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
    static class Test
    {

    // 不能有参数和访问修饰符
    static Test()
    {
    // 为静态变量初始化
    age = 18;
    name = "gcnanmu";
    }

    public static int age;
    public static string name;

    public static void TestFun()
    {
    Console.WriteLine("testFun");
    }
    }

    class Progress
    {
    public static void Main(string[] args)
    {
    Console.WriteLine(Test.name); // gcnanmu
    Console.WriteLine(Test.age); // 18
    }
    }
  • 有且只会调用一次

    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
    static class Test
    {

    // 只会调用一次
    static Test()
    {
    // 为静态变量初始化
    Console.WriteLine("静态构造函数!");
    age = 18;
    name = "gcnanmu";
    }

    public static int age;
    public static string name;

    public static void TestFun()
    {
    Console.WriteLine("testFun");
    }
    }

    class Progress
    {
    public static void Main(string[] args)
    {
    Console.WriteLine(Test.name);
    Console.WriteLine(Test.age);
    Test.TestFun();
    /*
    静态构造函数!
    gcnanmu
    18
    testFun
    */
    }
    }
  • 静态类和非静态都可以有

    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
    class Test
    {

    // 即便生成了两个实例,也只会调用一次
    static Test()
    {
    // 为静态变量初始化
    Console.WriteLine("静态构造函数!");
    age = 18;
    name = "gcnanmu";
    }

    // 注意:此处并非函数的重载
    public Test()
    {
    // 对象被实例化的时候会调用
    Console.WriteLine("普通的构造函数!");
    age = 18;
    name = "gcnanmu";
    }

    public static int age;
    public static string name;
    }

    class Progress
    {
    public static void Main(string[] args)
    {
    Test ts = new Test();
    Test ts2 = new Test();
    Console.WriteLine(Test.name);
    Console.WriteLine(Test.age);

    /*
    静态构造函数!
    普通的构造函数!
    普通的构造函数!
    gcnanmu
    18
    */
    }
    }

练习:写一个用于数学计算的静态类;该类提供计算圆的面积,圆的周长,矩形面积,矩形周长,取一个树的绝对值等方法

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
static class Calculate
{
public static float CircleArea(int r)
{
return (float)Math.PI * r * r;
}

public static float CircleLength(int r)
{
return (float)Math.PI * 2 * r;
}

public static int RectangleArea(int height, int width)
{
return height * width;
}

public static int RectangleLength(int height, int width)
{
return 2 * (height + width);
}

public static int AbsNumber(int num)
{
if (num < 0) return -num;
else
{
return num;
}
}
}

class Progress
{
public static void Main(string[] args)
{
Console.WriteLine(Calculate.CircleArea(2));
Console.WriteLine(Calculate.CircleLength(2));
Console.WriteLine(Calculate.RectangleArea(height: 2, width: 3));
Console.WriteLine(Calculate.RectangleLength(height: 2, width: 3));
}
}

扩展方法

为现有的非静态类型添加新的方法

作用:

  • 在不修改原始类的条件下,提升程序的扩展性
  • 不需要通过继承来添加方法

特点:

  • 只能写在静态类中
  • 一定是一个静态函数,第一参数为扩展目标且必须使用this修饰
  • 当函数名同名时,如果不触发重载,那么不会修改原有函数的功能

语法:

1
2
3
4
static class Tools
{
访问修饰符 static 返回值 函数名(this 扩展类名 参数名,参数类型 参数名,参数类型 参数名2……)
}

为现有的类添加方法

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
static class Tools
{
public static void SpeakInt(this int value)
{
Console.WriteLine("为int额外添加的方法 变量为{0}", value);
}

public static void SpeakString(this string str)
{
Console.WriteLine("为string额外添加的方法 变量为{0}", str);
}
}

class Progress
{
public static void Main(string[] args)
{
int i = 10;
i.SpeakInt();

string str = "gcnanmu";
str.SpeakString();

/*
为int额外添加的方法 变量为10
为string额外添加的方法 变量为gcnanmu
*/
}
}

为自定义的类添加方法

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
static class Tools
{
// 为Test类添加Fun3成员方法
public static void Fun3(this Test t)
{
Console.WriteLine("789");
}

// 未触发重载 Fun2功能未改变
public static void Fun2(this Test t)
{
Console.WriteLine("000");
}

// 触发重载 修改了Fun1的功能
public static void Fun1(this Test t, int i)
{
Console.WriteLine("触发Fun1重载");
}
}

class Test
{
public void Fun1()
{
Console.WriteLine("123");
}

public void Fun2()
{
Console.WriteLine("456");
}
}

class Progress
{
public static void Main(string[] args)
{
Test ts = new Test();
ts.Fun3();
ts.Fun2();
ts.Fun1(1);

/*
789
456
触发Fun1重载
*/
}
}

练习:

  1. 为整形扩展一个求平方的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static class Tools
    {
    public static int Pow2(this int i)
    {
    return i * i;
    }
    }

    class Progress
    {
    public static void Main(string[] args)
    {
    int i = 2;
    i = i.Pow2();
    Console.WriteLine(i); // 4
    i = i.Pow2();
    Console.WriteLine(i); // 16
    }
    }
  2. 写一个玩家类,包含姓名、血量、攻击力、防御力等特征,攻击、移动、受伤等方法

    为玩家扩展一个自杀的方法

    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
    static class Tools
    {
    public static void Suicide(this Player p)
    {
    p.hp = 0;
    Console.WriteLine("玩家自杀了");
    }
    }

    class Player
    {
    private static Random r = new Random();
    public Player(string name)
    {
    this.name = name;
    }

    public string name;

    public int hp = 100;

    public int hack = r.Next(1, 101);

    public int defense = r.Next(1, 100);

    public void Move()
    {
    Console.WriteLine("{0}正在移动", name);
    }

    public void Attack()
    {
    Console.WriteLine("{0}正在攻击", name);
    }

    public void wounded(int h)
    {
    if (defense < h)
    {
    hp -= h;
    }
    Console.WriteLine("{0}受伤", name);
    }
    }

    class Progress
    {
    public static void Main(string[] args)
    {
    Player p = new("gcnanmu");
    p.Suicide(); // 玩家自杀了
    Console.WriteLine(p.hp); // 0
    }
    }

运算符重载

让自定义类或结构体的对象可以进行运算操作

语法

1
public static 返回值 operator 运算符(参数列表){}

例子

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
class Point
{
public int x;
public int y;

// 重载+号 实现对象相加
public static Point operator +(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
return p;
}


// 支持重载
public static Point operator +(Point p1, int value)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}
}



class Progress
{
public static void Main(string[] args)
{
Point p = new Point();
p.x = 1;
p.y = 1;

Point p2 = new Point();
p2.x = 2;
p2.y = 2;

Console.WriteLine(p + p2);
Console.WriteLine(p + 2);
}
}

规范:

  • 参数中必有一个参数为当前的对象名,返回值类型随意,必须为静态方法

  • 一个符号支持多个重载

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

    // 重载+号 实现对象相加
    public static Point operator +(Point p1, Point p2)
    {
    Point p = new Point();
    p.x = p1.x + p2.x;
    p.y = p1.y + p2.y;
    return p;
    }


    // 支持重载
    public static Point operator +(Point p1, int value)
    {
    Point p = new Point();
    p.x = p1.x + value;
    p.y = p1.y + value;
    return p;
    }
    }
  • 各运算符所需参数不同

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

    // 重载+号 必须有两个参数
    public static Point operator +(Point p1, Point p2)
    {
    Point p = new Point();
    p.x = p1.x + p2.x;
    p.y = p1.y + p2.y;
    return p;
    }

    // 重载! 有且只能有一个参数
    public static bool operator !(Point p)
    {
    return false;
    }
    }
  • 运算符的重载必须成对实现 (实现了>,就必须实现<;实现了==,必须是西安!=;实现了>=,必须实现<=

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

    // 运算符“Point.operator >(Point, int)”要求也要定义匹配的运算符“<”
    public static Point operator >(Point p1, int value)
    {
    return null;
    }
    // 运算符“Point.operator ==(Point, int)”要求也要定义匹配的运算符“!=”
    public static Point operator ==(Point p1, int value)
    {
    return null;
    }
    // 运算符“Point.operator >=(Point, int)”要求也要定义匹配的运算符“<=”
    public static Point operator >=(Point p1, int value)
    {
    return null;
    }
    }
  • 不可重载的运算符

    • 逻辑与和或:&&||
    • 索引符:[]
    • 强制运算符:()
    • 点:.
    • 三目运算符: ?:
    • 赋值运算符:=

练习:

  1. 定义一个位置结构的类,为其重载判断是否相等的运算符(x1,y1)==(x2,y2)=> 两个值相等时才为true

    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
    class Point
    {
    public int x;
    public int y;

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

    public Point()
    {
    }

    // 重载+号 实现对象相加
    public static bool operator ==(Point p1, Point p2)
    {

    if ((p1.x == p2.x) && (p1.y == p2.y))
    {
    return true;
    }
    return false;
    }

    public static bool operator !=(Point p1, Point p2)
    {

    if ((p1.x != p2.x) || (p1.y != p2.y))
    {
    return true;
    }
    return false;
    }
    }



    class Progress
    {
    public static void Main(string[] args)
    {
    Point p = new Point(x: 1, y: 1);


    Point p2 = new Point();
    p2.x = 2;
    p2.y = 2;

    Point p3 = new Point(x: 2, y: 3);
    Point p4 = new Point(x: 2, y: 2);

    Console.WriteLine("{0}", p == p2); // False
    Console.WriteLine("{0}", p2 != p3); // True
    Console.WriteLine("{0}", p2 == p4); // True
    }
    }
  2. 定义一个Vector3类(x,y,z)通过重载运算符实现以下运算

    • (x1,y1,z1)+ (x2,y2,z2) = (x1+x2,y1+y2,z1+z2)
    • (x1,y1,z1)-(x2,y2,z2)= (x1-x2,y1-y2,z1-z2)
    • (x1,y1,z1)* num = (x1 * num,y1 * num,z1 * num)
    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
    class Point
    {
    public int x;
    public int y;
    public int z;

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

    public Point()
    {
    }

    // 重载+号 实现对象相加
    public static Point operator +(Point p1, Point p2)
    {

    Point p = new Point();
    p.x = p1.x + p2.x;
    p.y = p1.y + p2.y;
    p.z = p1.z + p2.z;
    return p;
    }

    public static Point operator -(Point p1, Point p2)
    {

    Point p = new Point();
    p.x = p1.x - p2.x;
    p.y = p1.y - p2.y;
    p.z = p1.z - p2.z;
    return p;
    }

    public static Point operator *(Point p1, int value)
    {
    Point p = new Point();
    p.x = p1.x * value;
    p.y = p1.y * value;
    p.z = p1.z * value;
    return p;
    }

    public void PrintXYZ()
    {
    Console.WriteLine("x:{0} y:{1} z:{2}", x, y, z);
    }
    }



    class Progress
    {
    public static void Main(string[] args)
    {
    Point p1 = new Point(x: 1, y: 1, z: 1);
    Point p2 = new Point(x: 2, y: 2, z: 2);
    (p1 * 2).PrintXYZ();
    (p1 + p2).PrintXYZ();
    (p1 - p2).PrintXYZ();

    /*
    x:2 y:2 z:2
    x:3 y:3 z:3
    x:-1 y:-1 z:-1
    */
    }
    }

内部类和分部类

内部类:在类中再申明一个类,强调的是两类的亲密程度

特点:需要使用.运算访问,且受到访问修饰符的制约

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 Person
{
public int age;
public string name;
// body是公共的,外部可以实例
public class Body
{
Arm LeftArm;
Arm RightArm;

// Arm是私有的 外部无法实例
class Arm
{

}
}
}

class Progress
{
public static void Main(string[] args)
{
Person p = new Person();
Person.Body b = new Person.Body();
}
}

分部类:使用partial将类的实现分成多个部分(还是一个整体)

特点:

  • 分部类可以写在多个脚本文件中
  • 分部类的访问修饰符要一致
  • 分部类不能有重复成员

分部方法:将方法的实现和申明进行分离

  • 不能加访问修饰符,默认私有
  • 只能在分部类中申明
  • 返回值类型只能是void
  • 可以有参数但不用out关键字
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
// 如果要加访问修饰符 上下分部类必须一致
public partial class Person
{
public int age;
public string name;

// 分部方法的申明
partial void Speak();
}

public partial class Person
{
public bool gender;

// 分部方法的实现 默认私有外部无法调用
partial void Speak()
{
Console.WriteLine("部分方法的实现");
}
}

class Progress
{
public static void Main(string[] args)
{
Person p = new Person();
p.gender = true;
}
}

继承

基础概念和语法

一个类B继承另一个类A,B会获得A类允许继承的所有成员、方法、对象。其中A称为父类,B称为子类,子类B也可以在A的基础上添加自己所特有的逻辑。

特点:

  • 单根性:子类只能拥有一个父类
  • 传递性:子类可以继承父类的父类
  • 受到访问修饰符的影响

语法:class 子类: 父类 {}

image-20250106211929700
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 Teacher
{
public string name;

// 只有子类和自身能够访问
protected int number;

public void Speak()
{
Console.WriteLine(name);
}
}


class TeachingTeacher : Teacher
{
public string subject;

public void Teaching()
{
// 子类能够使用
Console.WriteLine(number);
Console.WriteLine("{0}老师", subject);
}
}

class ChineseTeacher : TeachingTeacher
{
public string skill;

public void ShowSkill()
{
Console.WriteLine("念诗");
}
}

class Progress
{
public static void Main(string[] args)
{
ChineseTeacher ch = new ChineseTeacher();
ch.name = "张三";
// ch.number = 1;
ch.subject = "chinese";
ch.skill = "书法";

ch.Speak();
ch.Teaching();
ch.ShowSkill();
}
}

子类中可以出现与父类相同的成员,这样会将父类的成员覆盖,失去继承的目的,因此不推荐这样做

练习:写一个人类,人类中有姓名,有说话的行为,战士类继承人类,有攻击行为

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
class Person
{
public string name;

public void Speak()
{
Console.WriteLine("{0}在发言", name);
}
}

class Warrior : Person
{
public void Attack()
{
Console.WriteLine("{0}发动攻击", name);
}
}

class Progress
{
public static void Main(string[] args)
{
Warrior w = new Warrior();
w.name = "gcnanmu";
w.Speak();
w.Attack();
}
}

里氏替换原则

任何父类出现的地方,子类都可以替代,即父类装子类对象(因为子类包含父类的所有内容),可以方便进行对象存储和管理

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
class GameObject
{

}

class Player : GameObject
{
public void Move()
{
Console.WriteLine("Player is moving");
}
}

class Enemy : GameObject
{
public void Move()
{
Console.WriteLine("Enemy is moving");
}
}


class Progress
{
public static void Main(string[] args)
{
// 使用父类替换子类
GameObject player = new Player();
GameObject enemy = new Enemy();
}
}

常用于用父类数组装载子类对象

1
2
3
4
5
6
7
8
class Progress
{
public static void Main(string[] args)
{
// 使用父类数组装子类对象
GameObject[] objects = [new Player(), new Enemy()];
}
}

常搭配isas关键字使用

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
class GameObject
{

}

class Player : GameObject
{
public void Move()
{
Console.WriteLine("Player is moving");
}
}

class Enemy : GameObject
{
public void Move()
{
Console.WriteLine("Enemy is moving");
}
}


class Progress
{
public static void Main(string[] args)
{
GameObject[] objects = [new Player(), new Enemy()];

for (var i = 0; i < objects.Length; i++)
{
if (objects[i] is Player)
{
(objects[i] as player).Move();
}
else if (objects[i] is Enemy)
{
Enemy enemy = objects[i] as Enemy;
enemy.Move();
}
}
}
}

is关键字:判断当前对象是否属于某个类,属于时返回true,反之false

as关键字:将当前对象转化类型,只支持引用类型相同的转化。如果成功,返回转化对象,反之返回null

另外,还可以通过()进行转化

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
for (var i = 0; i < objects.Length; i++)
{
if (objects[i] is Player)
{
Player player = (Player)objects[i];
player.Move();
}
else if (objects[i] is Enemy)
{
Enemy enemy = (Enemy)objects[i];
enemy.Move();
}
}

练习:

1. 写一个Monster类,它派生出Boss和Gobin这两个类,Boss有技能;小怪有攻击;随机生成十个怪,装载到数组中,遍历这个数组,调用他们的攻击方法,如果是Boss就释放攻击

```C#
class Monster
{
protected Random r = new Random();
}

class Boss : Monster
{
public void Skill()
{
int damage = r.Next(1, 10);
Console.WriteLine("Boss attacked and dealt " + damage + " damage.");
}
}

class Gobin : Monster
{
public void Attack()
{
int damage = r.Next(1, 5);
Console.WriteLine("Gobin attacked and dealt " + damage + " damage.");
}
}

class Program
{
public static void Main(string[] args)
{
Monster[] monsters = new Monster[10];
Random r = new Random();
for (var i = 0; i < 10; i++)
{
if (r.Next(1, 10) % 2 == 0)
{
monsters[i] = new Boss();
}
else
{
monsters[i] = new Gobin();
}
}

foreach (var monster in monsters)
{
if (monster is Boss)
{
((Boss)monster).Skill();
}
else if (monster is Gobin)
{
((Gobin)monster).Attack();
}
}
}
}
  1. FPS游戏模拟

    写一个玩家类,玩家可以拥有武器,现在有冲锋枪,霰弹枪,手枪,匕首四种武器,玩家默认有匕首,请在玩家类中写一个方法,可以拾取不同的武器替换自己拥有的枪械

    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

    class Player
    {
    public Programs.EWeapons weapons = Programs.EWeapons.Knife;

    public void ChangeWeapon(Programs.EWeapons weapon)
    {
    weapons = weapon;
    }
    }

    class Programs
    {
    public enum EWeapons
    {
    /// <summary>
    /// 匕首
    /// </summary>
    Knife,
    /// <summary>
    /// 霰弹枪
    /// </summary>
    Shotgun,
    /// <summary>
    /// 手枪
    /// </summary>
    Pistol,
    }

    public static void Main(string[] args)
    {
    Player player = new Player();
    Console.WriteLine(player.weapons);
    player.ChangeWeapon(EWeapons.Shotgun);
    Console.WriteLine(player.weapons);
    }
    }

继承中的构造函数

继承类也可以有构造函数,构造函数的执行存在由上往下的特点

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
class Father
{
public Father()
{
Console.WriteLine("Father default constructor");
}


public Father(int a, int b)
{
Console.WriteLine("Father 2 parameter constructor");
}
}

class Child : Father
{
public Child()
{
Console.WriteLine("Child default constructor");
}

public Child(int a, int b)
{
Console.WriteLine("Child 2 parameter constructor");
}
}

class Son : Child
{
public Son()
{
Console.WriteLine("Son default constructor");
}

public Son(int a, int b)
{
Console.WriteLine("Son 2 parameter constructor");
}
}

class Program
{
static void Main()
{
Son s = new Son();
/*
Father default constructor
Child default constructor
Son default constructor
*/
}
}

规范:

  • 着重注意父类的无参构造。这是因为在创建子类对象时,必然会触发父类的构造函数,因此必须保证父类的构造函数能被调用

    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
    class Father
    {
    // public Father()
    // {
    // Console.WriteLine("Father default constructor");
    // }

    public Father(int a, int b)
    {
    Console.WriteLine("Father 2 parameter constructor");
    }
    }

    class Child : Father
    {
    // error: 未提供与“Father.Father(int, int)”的所需参数“a”对应的参数
    public Child()
    {
    Console.WriteLine("Child default constructor");
    }

    // error: 未提供与“Father.Father(int, int)”的所需参数“a”对应的参数
    public Child(int a, int b)
    {
    Console.WriteLine("Child 2 parameter constructor");
    }
    }
  • 子类可以通过base关键字调用父类的无参构造

    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
    class Father
    {
    public Father()
    {
    Console.WriteLine("Father default constructor");
    }


    public Father(int a, int b)
    {
    Console.WriteLine("Father 2 parameter constructor");
    }
    }

    class Child : Father
    {
    // 调用父类的无参构造
    public Child()
    {
    Console.WriteLine("Child default constructor");
    }

    // 调用父类的有参构造
    public Child(int a, int b) : base(a, b)
    {
    Console.WriteLine("Child 2 parameter constructor");
    }
    }

作业:有一个打工人基类,有工种,工作内容两个特征,一个工作方法;程序员,策划,美术分别继承打工人;

请使用继承中的构造函数实例化3个对象,分别是程序员,策划,美术

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
class Workers
{
public enum E_Workers
{
/// <summary>
/// 程序员
/// </summary>
Programmer,
/// <summary>
/// 策划
/// </summary>
Planner,
/// <summary>
/// 美术
/// </summary>
Artist,
}
public E_Workers workType;

public string workContent;

public void Working()
{
switch (workType)
{
case E_Workers.Planner:
Console.WriteLine("策划在工作");
break;
case E_Workers.Programmer:
Console.WriteLine("程序员在工作");
break;
case E_Workers.Artist:
Console.WriteLine("美术在工作");
break;
}
}
}

class Programmer : Workers
{
public Programmer()
{
this.workContent = "写代码";
this.workType = E_Workers.Programmer;
}
}

class Planner : Workers
{
public Planner()
{
this.workContent = "设计活动";
this.workType = E_Workers.Planner;
}
}

class Artist : Workers
{
public Artist()
{
this.workContent = "画画";
this.workType = E_Workers.Artist;
}
}

class Program
{
public static void Main(string[] args)
{
Programmer p1 = new Programmer();
Planner p2 = new Planner();
Artist p3 = new Artist();

p1.Working();
p2.Working();
p3.Working();
}
}

万物之父与装箱与拆箱

关键字object,是用来装载梭有类型的基类(引用类型)

大写Object与小写Object是等价的,objectObject的别名

作用:

  • 可以利用里氏替换原则,用object容器装所有对象

    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
    class Father
    {

    }

    class Son : Father
    {

    }

    class Program
    {
    public static void Main(string[] args)
    {
    // object替换值类型
    object s = new Son();

    object i = 1;
    int iInt = (int)i;

    // object替换引用类型
    object arr = new object[10];

    object str = "123456";
    string strString2 = str as string;
    string strString3 = str.ToString();
    }
    }

    引用类型推荐使用as进行类型转化

  • 可以用来表示不确定类型,作为函数参数类型

    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
    class Father
    {

    }

    class Son : Father
    {

    }

    class Program
    {
    public static void Main(string[] args)
    {

    // object作为函数参数
    Function("123", 123, new Father());

    /*
    123 is string
    123 is int
    Father is Father
    */
    }

    public static void Function(params Object[] arr)
    {
    foreach (var item in arr)
    {
    if (item is string)
    {
    Console.WriteLine("{0} is string", item);
    }
    else if (item is int)
    {
    Console.WriteLine("{0} is int", item);
    }
    else if (item is Father)
    {
    Console.WriteLine("{0} is Father", item);
    }
    }
    }
    }

装箱与拆箱

  • 装箱:将值类型用object存储,从栈空间转移到堆空间
  • 拆箱:将引用类型转化为值类型,从堆空间转移到栈空间

好处:方便参数的存储和传递

坏处:存在内存迁移,增加了性能消耗(尽量少用)

密封类

无法被继承的类

语法:在class使用sealed关键字

主要作用:不允许该类被继承,保证程序的规范性和安全性

1
2
3
4
5
6
7
sealed class Father{

}
// error: “Son”: 无法从密封类型“Father”派生
class Son : Father{

}

练习:定义一个载具类,速度、最大速度、可乘人数、司机和乘客等,有上车、下车、行驶和车祸等方法,用载具类申明一个对象,并将若干人装载上车

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class Vehicle
{
public int speed = 0;

public int maxSpeed = 100;

public int maxPerson = 10;

public Person[]? passengers = null;

public void GoInto(Person p)
{
if (passengers == null || passengers.Length == 0)
{
passengers = [p];
Console.WriteLine("{0}上车成功", p.Name);
}
else if (passengers.Length == maxPerson)
{
Console.WriteLine("车辆已满");
}
else
{
Person[] temp = new Person[passengers.Length + 1];
for (var i = 0; i < passengers.Length; i++)
{
temp[i] = passengers[i];
}

temp[temp.Length - 1] = p;
passengers = temp;
Console.WriteLine("{0}上车成功", p.Name);
}
}


public void GoOut(Person p)
{
if (passengers == null || passengers.Length == 0)
{
Console.WriteLine("车上没有乘客");
}
else
{
for (var i = 0; i < passengers.Length; i++)
{
if (passengers[i] == p)
{
Person[] temp = new Person[passengers.Length - 1];
for (var j = 0; j < temp.Length; j++)
{
if (i != j)
{
temp[j] = passengers[j];
}
}

if (i != passengers.Length - 1)
{
temp[i] = passengers[passengers.Length - 1];
}
passengers = temp;
Console.WriteLine("{0}已下车", p.Name);
return;
}
}

Console.WriteLine("{0}不在车上", p.Name);
}
}

public void Driving()
{
speed = maxSpeed;
Console.WriteLine("车辆行驶中,请注意安全");
}


public void Accident()
{
speed = 0;
passengers = [];
}
}


class Person
{

public Person() { }
public string Name
{
get;
set;
}
}

class Program
{
public static void Main(string[] args)
{
Vehicle v = new Vehicle();

for (var i = 0; i < 11; i++)
{
Person p = new();
p.Name = $"person{i}";
v.GoInto(p);
}
Console.WriteLine(v.passengers.Length);

v.Driving();

// 让第一个人下车
v.GoOut(v.passengers[0]);
Console.WriteLine(v.passengers.Length);
/*
person0上车成功
person1上车成功
person2上车成功
person3上车成功
person4上车成功
person5上车成功
person6上车成功
person7上车成功
person8上车成功
person9上车成功
车辆已满
10
车辆行驶中,请注意安全
person0已下车
9
*/
}
}

多态

vob

让同一类型的对象,执行相同行为时有不同的表现,保证同一对象有不同的行为表现

vob是指三个关键字:virtualoverridebase

  • virtual 将函数变为虚函数 支持在子类重写该函数

  • override 配对virtual来实现函数重写

  • base 代指父类

以之前学习继承为例

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 Father
{
public void SpeakName()
{
Console.WriteLine("I am the father");
}
}

class Child : Father
{
public new void SpeakName()
{
Console.WriteLine("I am the child");
}
}

class Program
{
public static void Main(string[] args)
{
Father p = new Child();
p.SpeakName(); // I am the father

(p as Child).SpeakName(); // I am the child
}
}

以上的例子,使用父类来装载子类,结果是一个对象呈现出两种不同的方法,这违背了面向对象的初衷。

使用virtualoverride来解决这个问题,让同一对象有唯一的行为特征

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
class Father
{
public virtual void SpeakName()
{
Console.WriteLine("I am the father");
}
}

class Child : Father
{
public override void SpeakName()
{
// base.SpeakName();
Console.WriteLine("I am the child");
}
}

class Program
{
public static void Main(string[] args)
{
Father p = new Child();
p.SpeakName(); // I am the child

(p as Child).SpeakName(); // I am the child
}
}

想要保留父类中的方法,可以使用base进行调用

练习

  1. 真的鸭子嘎嘎叫,木头鸭子吱吱叫,橡皮鸭子唧唧叫

    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
    class Duck
    {
    public virtual void Call()
    {
    Console.WriteLine("嘎嘎");
    }
    }

    class WoodDuck : Duck
    {
    public override void Call()
    {
    // base.Call();
    Console.WriteLine("吱吱");
    }
    }

    class RubberDuck : Duck
    {
    public override void Call()
    {
    // base.Call();
    Console.WriteLine("唧唧");
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {
    Duck d = new Duck();
    d.Call();

    Duck wd = new WoodDuck();
    wd.Call();

    Duck rd = new RubberDuck();
    rd.Call();
    }
    }
  2. 所有员工九点打卡,经理十一点打卡,程序员不打卡

    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
    class Employee
    {
    public virtual void Clock()
    {
    Console.WriteLine("九点打卡");
    }
    }

    class Manager : Employee
    {
    public override void Clock()
    {
    // base.Clock();
    Console.WriteLine("十一点打卡");
    }
    }

    class Programer : Employee
    {
    public override void Clock()
    {
    // base.Clock();
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {
    Employee m = new Employee();
    m.Clock();

    Employee ma = new Manager();
    ma.Clock();

    Employee pr = new Programer();
    pr.Clock();
    }
    }
  3. 创建一个圆形类,有求周长与面积的两个方法

    创建矩形类,正方形类,图形继承面积类,实例化矩形,正方形,圆形对象求面积和周长

    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
    class Graphics
    {

    public virtual float Area(params int[] arr)
    {
    return 0;
    }

    public virtual float Circumference(params int[] arr)
    {
    return 0;
    }
    }

    class Rectangle : Graphics
    {
    public override float Area(params int[] arr)
    {
    // return base.Area(arr);
    return arr[1] * arr[0];
    }

    public override float Circumference(params int[] arr)
    {
    return (arr[1] + arr[0]) * 2;
    }
    }

    class Square : Graphics
    {
    public override float Area(params int[] arr)
    {
    return arr[0] * arr[0];
    }

    public override float Circumference(params int[] arr)
    {
    return arr[0] * 4;
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {
    Graphics g = new Graphics();
    Console.WriteLine(g.Area());
    Console.WriteLine(g.Circumference());

    Graphics r = new Rectangle();
    Console.WriteLine(r.Area(1, 2));
    Console.WriteLine(r.Circumference(1, 2));

    Graphics s = new Square();
    Console.WriteLine(s.Area(2));
    Console.WriteLine(s.Circumference(2));
    }
    }

抽象类和抽象方法

指的是被abstract修饰的类和方法

特点:

  • 抽象类:无法被实例化
  • 抽象方法:
    • 只能在抽象类中定义
    • 没有方法体
    • 必须在继承类中重写,因此不能是私有的
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
abstract class Thing
{
public abstract void Bad();
}

abstract class Fruit : Thing
{
// 必须重写 否则报错
public override void Bad()
{
throw new NotImplementedException();
}
}

class Apple : Fruit
{
// 此处并非必须重写 但是也支持重写
public override void Bad()
{
base.Bad();
}
}

class Program
{
public static void Main(string[] args)
{
// error :无法创建抽象类型或接口“Thing”的实例
Thing t = new Thing();

// 遵循里氏替换原则
Thing apple = new Apple();
}
}

作用:适用于不希望对象被实例化,或者父类的行为不需要被实现,只希望子类去实现定义具体的规则的情况

练习:

  1. 写一个动物抽象类,写三个子类 人叫 狗叫 猫叫

    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
    abstract class Animal
    {
    public abstract void Speak();
    }

    class People : Animal
    {
    public override void Speak()
    {
    Console.WriteLine("人叫");
    }
    }

    class Dog : Animal
    {
    public override void Speak()
    {
    Console.WriteLine("狗叫");
    }
    }

    class Cat : Animal
    {
    public override void Speak()
    {
    Console.WriteLine("猫叫");
    }
    }
  2. 创建一个圆形类,有求周长与面积的两个方法

    创建矩形类,正方形类,图形继承面积类,实例化矩形,正方形,圆形对象求面积和周长

    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
    abstract class Graphics
    {
    public abstract float Area(params int[] arr);
    public abstract float Circumference(params int[] arr);
    }

    class Rectangle : Graphics
    {
    public override float Area(params int[] arr)
    {
    // return base.Area(arr);
    return arr[1] * arr[0];
    }

    public override float Circumference(params int[] arr)
    {
    return (arr[1] + arr[0]) * 2;
    }
    }

    class Square : Graphics
    {
    public override float Area(params int[] arr)
    {
    return arr[0] * arr[0];
    }

    public override float Circumference(params int[] arr)
    {
    return arr[0] * 4;
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {

    Graphics r = new Rectangle();
    Console.WriteLine(r.Area(1, 2));
    Console.WriteLine(r.Circumference(1, 2));

    Graphics s = new Square();
    Console.WriteLine(s.Area(2));
    Console.WriteLine(s.Circumference(2));
    }
    }

接口

接口是行为的抽象规范,也是一种自定义的类型

语法

1
2
3
4
interface 接口名
{

}

申明规范

  1. 不包含成员变量,只包含方法、属性、索引器、事件
  2. 成员不可以被实现
  3. 成员可以不写访问修饰符(默认是public),不能写private
  4. 接口不能继承类,但可以继承接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IFly
{
// 方法
void Fly();

// 索引器
string this[int index] { get; set; }

// 属性
string Name { get; set; }

// 事件
event EventHandler FlyEvent;
}

使用规范

  1. 类可以继承多个接口
  2. 类继承接口后,必须实现其中所有的成员
  3. 接口继承接口无需实现接口方法(相当于接口的合并)
  4. 接口是用来继承的,不能被实例化,但可以使用里氏替换原则作为容器
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
interface IAttack
{
void Attack();
}

interface ISuperAttack : IAttack
{
// 接口继承接口 无需实现其中方法
void SuperAttack();
}

class Person {}

// 可以继承多个接口 但只能继承一个类
class Character : Person, IAttack, ISuperAttack
{
// 必须实现两个接口内的所有方法
public void Attack()
{
Console.WriteLine("Attack");
}

public void SuperAttack()
{
Console.WriteLine("Super Attack");
}
}

class Program
{
public static void Main(string[] args)
{
Character character = new Character();
character.Attack(); // Attack
character.SuperAttack(); // Super Attack

// 遵循里氏替换原则
IAttack attack = character;
attack.Attack(); // Attack

ISuperAttack superAttack = character;
superAttack.Attack(); // Attack
}
}

显示实现接口:适用于两个接口中有着同名方法的情况

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
interface IAttack
{
void Attack();
}

interface ISuperAttack : IAttack
{
void Attack();
}

class Person {}

class Character : Person, IAttack, ISuperAttack
{
// 显示实现接口方法
void IAttack.Attack()
{
System.Console.WriteLine("Attack");
}

void ISuperAttack.Attack()
{
System.Console.WriteLine("Super Attack");
}
}

class Program
{
public static void Main(string[] args)
{
Character character = new Character();
// “Character”未包含“Attack”的定义,并且找不到可接受第一个“Character”类型参数的可访问扩展方法“Attack”
// character.Attack();
// 缺点:无法直接调用接口方法 需要进行类型的转化
((IAttack)character).Attack();
((ISuperAttack)character).Attack();
}
}

支持在继承类中使用virtual让子类重写接口的方法

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
interface IAttack
{
void Attack();
}

class Person : IAttack
{
// 支持使用virtual关键字让子类重写接口方法
public virtual void Attack()
{
Console.WriteLine("Person Attack");
}
}


class Player : Person
{
// 重写
public override void Attack()
{
base.Attack();
}
}

class Program
{
public static void Main(string[] args)
{
Player player = new Player();
player.Attack();
}
}

练习

  1. 人、汽车、房子都需要登记;人需要到派出所登记,汽车需要去车管所登记,房子需要去房管局登记
    使用接口实现登记方法

    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
    interface ISignIn
    {
    void SignIn();
    }


    class Person : ISignIn
    {
    public void SignIn()
    {
    Console.WriteLine("派出所登记");
    }
    }


    class Car : ISignIn
    {
    public void SignIn()
    {
    Console.WriteLine("车管所登记");
    }
    }

    class House : ISignIn
    {
    public void SignIn()
    {
    Console.WriteLine("房管局登记");
    }
    }
  2. 麻雀、 驼鸟、 企鹅、 鹦鹉、直升机、 天鹅
    直升机和部分鸟能飞
    驼鸟和企鹅不能飞
    企鹅和天鹅能游泳
    除直升机,其它都能走
    请用面向对象相关知识实现

    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
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    interface IFly
    {
    void Fly();
    }

    interface ISwim
    {
    void Swim();
    }

    interface IWalk
    {
    void Walk();
    }

    /// <summary>
    /// 直升飞机
    /// </summary>
    class Helicopter : IFly
    {
    public void Fly()
    {
    Console.WriteLine("Helicopter is flying");
    }
    }

    /// <summary>
    /// 企鹅
    /// </summary>
    class Penguin : ISwim, IWalk
    {
    public void Swim()
    {
    Console.WriteLine("Penguin is swimming");
    }

    public void Walk()
    {
    Console.WriteLine("Penguin is walking");
    }
    }

    /// <summary>
    /// 天鹅
    /// </summary>
    class Swan : IFly, ISwim, IWalk
    {
    public void Fly()
    {
    Console.WriteLine("Swan is flying");
    }

    public void Swim()
    {
    Console.WriteLine("Swan is swimming");
    }

    public void Walk()
    {
    Console.WriteLine("Swan is walking");
    }
    }

    /// <summary>
    /// 麻雀
    /// </summary>
    class Sparrow : IFly, IWalk
    {
    public void Fly()
    {
    Console.WriteLine("Sparrow is flying");
    }

    public void Walk()
    {
    Console.WriteLine("Sparrow is walking");
    }
    }

    /// <summary>
    /// 鸵鸟
    /// </summary>
    class Ostrich : IWalk
    {
    public void Walk()
    {
    Console.WriteLine("Ostrich is walking");
    }
    }

    /// <summary>
    /// 鹦鹉
    /// </summary>
    class Parrot : IFly, IWalk
    {
    public void Fly()
    {
    Console.WriteLine("Parrot is flying");
    }

    public void Walk()
    {
    Console.WriteLine("Parrot is walking");
    }
    }

    个人理解:由于部分鸟不能飞,因此不能创建一个名为bird的类并继承了IFly

  3. 多态来模拟移动硬盘、U盘、MP3插到电脑上读取数据
    移动硬盘与U盘都属于存储设备
    MP3属于播放设备
    但他们都能插在电脑上传输数据
    电脑提供了一个USB接口
    请实现电脑的传输数据的功能

    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
    81
    interface IUSB
    {
    void Write(string s);
    string Read();
    }


    abstract class StoreDevices : IUSB
    {
    public string name;
    public abstract void Write(string s);

    public abstract string Read();
    }

    abstract class PlayerDevices : IUSB
    {
    public string name;
    public abstract void Write(string s);

    public abstract string Read();
    }

    class UCD : StoreDevices
    {
    public override void Write(string s)
    {
    Console.WriteLine("UCD Write: " + s);
    }

    public override string Read()
    {
    return "UCD Read";
    }
    }

    class SataSSD : StoreDevices
    {
    public override void Write(string s)
    {
    Console.WriteLine("SataSSD Write: " + s);
    }

    public override string Read()
    {
    return "SataSSD Read";
    }
    }

    class MP3 : PlayerDevices
    {
    public override void Write(string s)
    {
    Console.WriteLine("MP3 Write: " + s);
    }

    public override string Read()
    {
    return "MP3 Read";
    }
    }

    class Computer
    {
    public void TransferData(IUSB device)
    {
    device.Write("Hello");
    Console.WriteLine(device.Read());
    }
    }

    class Program
    {
    static void Main(string[] args)
    {
    Computer computer = new Computer();
    computer.TransferData(new UCD());
    computer.TransferData(new SataSSD());
    computer.TransferData(new MP3());
    }
    }

密封方法

sealed关键字修饰的重写函数,和override配合使用

作用:让虚方法和抽象方法之后不能被重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Father
{
public abstract void Attack();
}

class Child : Father
{
public sealed override void Attack()
{
throw new NotImplementedException();
}
}

class Son : Child
{
// “Son.Attack()”: 继承成员“Child.Attack()”是密封的,无法进行重写
public override void Attack()
{
System.Console.WriteLine("Attack");
}
}

命名空间

是用来组织和重用代码的

作用:类似于一个工具包,类就像是一件件的工具

语法

1
2
3
4
5
namespace 空间名
{
1
2
}

使用:

  • 支持分离式的写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    namespace Thing
    {
    class Person
    {

    }
    }

    // 实际上都同属于一个Thing
    namespace Thing
    {
    class Boss
    {

    }
    }
  • 不同的命名空间相互使用

    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
    using Thing;

    namespace Thing
    {
    class Person
    {

    }
    }


    namespace Thing
    {
    class Boss
    {

    }
    }

    namespace Program
    {
    class Program
    {
    static void Main(string[] args)
    {
    // 用法一:指明出处
    Thing.Person person = new Thing.Person();
    // 用法二:引用当前命名空间
    Person person2 = new Person();
    }
    }
    }
  • 不同命名空间允许有相同类名

    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
    namespace Demo1
    {
    class Person
    {

    }
    }

    namespace Demo2
    {
    class Person
    {

    }
    }

    namespace Program
    {
    class Program
    {
    static void Main(string[] args)
    {
    // 只能使用指明出处的方法访问
    Demo1.Person p1 = new Demo1.Person();
    Demo2.Person p2 = new Demo2.Person();
    }
    }
    }
  • 命名空间可以包裹命名空间

    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
    using GameObject.Player;

    namespace GameObject
    {
    namespace Player
    {
    class Player
    {
    public void Move() { }
    }
    }
    }

    namespace Program
    {
    class Program
    {
    static void Main(string[] args)
    {
    // 方式1 指明出处
    GameObject.Player.Player player = new GameObject.Player.Player();
    player.Move();

    // 方式2 使用using
    Player player2 = new Player();
    }
    }
    }
  • 访问修饰符internalnamespace中的类默认为internal修饰,指的是只能在当前程序集中使用。

    Visual Studio为例,一个解决方案下支持创建多个项目,一个解决方案就是一个程序集,项目之间也支持相互调用(但不支持使用private修饰namespace中的类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    namespace UI
    {
    // internal:指的是只有在同一程序集中才能访问
    internal class Image
    {
    public Image()
    {
    Console.WriteLine("UI Image");
    }
    }
    }


    namespace Program
    {
    class Program
    {
    static void Main(string[] args)
    {
    UI.Image image1 = new UI.Image(); // UI Image
    }
    }
    }

练习:有两个命名空间, UI(用户界面)和Graph (图表),两个命名空问中都有一个lmage 类,请在主函数中实例化两个 不同命名空间中的lmage对象

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
namespace UI
{
class Image
{
public Image()
{
Console.WriteLine("UI Image");
}
}
}

namespace Graph
{
class Image
{
public Image()
{
Console.WriteLine("Graph Image");
}
}
}


namespace Program
{
class Program
{
static void Main(string[] args)
{
UI.Image image1 = new UI.Image(); // UI Image
Graph.Image image2 = new Graph.Image(); // Graph Image
}
}
}

万物之父中的方法

C#中Object源码部分如下(删除了部分的注释)

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
public partial class Object
{
public Object()
{
}

~Object()
{
}

public virtual string? ToString()
{
return GetType().ToString();
}

public virtual bool Equals(object? obj)
{
return this == obj;
}

public static bool Equals(object? objA, object? objB)
{
if (objA == objB)
{
return true;
}
if (objA == null || objB == null)
{
return false;
}
return objA.Equals(objB);
}

public static bool ReferenceEquals(object? objA, object? objB)
{
return objA == objB;
}

public virtual int GetHashCode()
{
return RuntimeHelpers.GetHashCode(this);
}
}

public partial class Object
{
public extern Type GetType();

protected unsafe object MemberwiseClone()
{
object clone = RuntimeHelpers.AllocateUninitializedClone(this);

nuint byteCount = RuntimeHelpers.GetRawObjectDataSize(clone);
ref byte src = ref this.GetRawData();
ref byte dst = ref clone.GetRawData();

if (RuntimeHelpers.GetMethodTable(clone)->ContainsGCPointers)
Buffer.BulkMoveWithWriteBarrier(ref dst, ref src, byteCount);
else
Buffer.Memmove(ref dst, ref src, byteCount);

return clone;
}
}

除了无参构造函数和析构函数,剩下的成员简单分类为:

  1. 静态方法 ReferenceEqualsEquals
  2. 成员方法 GetTypeMemberwiseClone
  3. 虚函数 ToStringEqualsGetHashCode

静态方法

Equals用于比较值类型是否相等,返回bool

ReferenceEquals用于比较引用类型是否相等,返回bool值 如果传入两个值类型 返回False

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
class Thing
{

}

class Program
{
public static void Main(string[] args)
{
Console.WriteLine(object.Equals(1, 1)); // True
Console.WriteLine(object.Equals(1, 2)); // False

Thing t1 = new Thing();
Thing t2 = new Thing();
Thing t3 = t1;
Console.WriteLine(object.Equals(t1, t2)); // False
Console.WriteLine(object.Equals(t1, t3)); // True 地址相同

Console.WriteLine(object.ReferenceEquals(t1, t2)); // False
Console.WriteLine(object.ReferenceEquals(t1, t3)); // True 地址相同

// ReferenceEquals比较值类型默认返回false
Console.WriteLine(object.ReferenceEquals(1,1)); // False
}
}

成员方法

GetType 获取当前成员变量的类型 返回值为Type

MemberwiseClone (protected) 对对象进行浅克隆 返回类型为object

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

class Thing
{

public int i;
public Thing thing;
public Thing Clone()
{
return MemberwiseClone() as Thing;
}
}

class Program
{
public static void Main(string[] args)
{
Thing t = new Thing();
t.i = 1;
t.thing = new Thing();
t.thing.i = 2;

Thing t2 = t.Clone();
t2.i = 2;
// 此处的t2.thing是浅拷贝,所以t2.thing和t.thing指向同一个对象
t2.thing.i = 3;

Console.WriteLine(t.GetType());// Thing
Console.WriteLine(t2.GetType());// Thing

Console.WriteLine(t == t2); // False
Console.WriteLine(object.ReferenceEquals(t, t2)); // False

Console.WriteLine(t.i); // 1
Console.WriteLine(t.thing.i); // 3

Console.WriteLine(t2.i); // 2
Console.WriteLine(t2.thing.i); // 3
}
}

虚函数

以下函数支持重写

ToString 默认返回当前对象所属类的命名空间+类名(string)

Equals 默认为ReferenceEquals效果,比较两个对象的引用是否相等 返回bool

GetHashCode 获取当前变量的Hash值(变量的唯一标识符)(不演示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Program
{

class Thing
{

public int i;
public Thing thing;
public Thing Clone()
{
return MemberwiseClone() as Thing;
}
}

class Program
{
public static void Main(string[] args)
{
Thing t = new Thing();
Console.WriteLine(t.ToString()); // Program.Thing
}
}
}

支持自定义EqualsToString的规则

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
namespace Program
{

class Person
{
public string name;
public int age;
// 重写Equals
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
Person p = (Person)obj;
return name == p.name && age == p.age;
}
// 重写ToString
public override string ToString()
{
// return base.ToString();
return name + " " + age;
}
}

class Program
{
public static void Main(string[] args)
{
Person p = new Person();
p.name = "John";
p.age = 30;

Person p2 = new Person();
p2.name = "John";
p2.age = 30;

Console.WriteLine(p.Equals(p2)); // True
Console.WriteLine(p.ToString()); // John 30
}
}
}

练习

  1. 有一个玩家类,有姓名,血量,攻击力,防御力,闪避率等特征
    请在控制台打印出 “玩家XX,血量XX,攻击力XX,防御力XX” XX为具体内容

    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
    static class Tool
    {
    public static int RandomInt(int min, int max)
    {
    return new Random().Next(min, max);
    }
    // 生成0-1的百分比
    public static float RandomFloat()
    {
    return (float)new Random().NextDouble();
    }
    }
    class Player
    {
    public string Name { get; set; }

    public int hp = 100;

    public int attack = Tool.RandomInt(5, 10);

    public int defense = Tool.RandomInt(2, 5);

    public float missRate = Tool.RandomFloat();

    public override string ToString()
    {
    // return base.ToString();

    return $"{Name} has {hp} hp, {attack} attack, {defense} defense, {missRate} miss rate";
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {
    Player player = new Player();
    player.Name = "Player1";

    Player player2 = new Player();
    player2.Name = "Player2";

    Console.WriteLine(player);
    Console.WriteLine(player2);
    /*
    Player1 has 100 hp, 5 attack, 4 defense, 0.56745094 miss rate
    Player2 has 100 hp, 9 attack, 2 defense, 0.87566304 miss rate
    */
    }
    }
  2. 一个Monster类的引用对象A, Monster类有攻击力、防御力、血量、技能ID等属性。我想复制一个和A对象一模一样的B对象。并且改变了B的属性,A是否会受到影响?请问如何买现?

    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
    class Monster
    {
    public int attack;
    public int defense;
    public int health = 100;

    public int skillCD = 0;

    public Monster Clone()
    {
    return (Monster)this.MemberwiseClone();
    }

    public override string ToString()
    {
    // return base.ToString();
    return "Monster: " + attack + " " + defense;
    }
    }

    class Program
    {
    public static void Main(string[] args)
    {
    Monster monster = new Monster();
    monster.attack = 10;
    monster.defense = 5;

    Monster monster1 = monster.Clone();
    monster1.attack = 20;
    monster1.defense = 10;

    Console.WriteLine(monster);
    Console.WriteLine(monster1);

    /*
    Monster: 10 5
    Monster: 20 10
    */

    }
    }

    从结果来看,A和B并不会相互影响

string

本小节补充string常用的方法

  1. 字符串的本质是数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    string str = "小市民";

    // 支持索引访问其中的元素
    for (var i = 0; i < str.Length; i++)
    {
    Console.WriteLine(str[i]);
    }

    // 支持转化为char数组
    char[] chars = str.ToCharArray();
    foreach (var c in chars)
    {
    Console.WriteLine(c);
    }
  2. 字符串拼接

    1
    2
    3
    4
    5
    6
    7
    string str = "小市民";

    string result = string.Format("{0}是一个{1}。", "小明", str);
    Console.WriteLine(result); // 小明是一个小市民。

    string result2 = $"小明是一个{str}。";
    Console.WriteLine(result2); // 小明是一个小市民。
  3. 正向查找字符位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    string str = "小市民";

    // 返回第一个匹配字符的索引
    int index = str.IndexOf("市");
    Console.WriteLine(index); // 1

    // 如果没有找到匹配字符,则返回 -1。
    index = str.IndexOf("w");
    Console.WriteLine(index); // -1
  4. 反向查找字符位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    string str = "小市民小市民";

    // 返回第一个匹配字符的索引
    int index = str.IndexOf("小市民");
    Console.WriteLine(index); // 0

    // 返回最后一个匹配字符的索引
    index = str.LastIndexOf("小市民");
    Console.WriteLine(index); // 3
  5. 移除指定位置后的字符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string str = "小市民小市民";

    // 将字符串中指定位置的字符删除,返回新的字符串。

    // 一个参数:从指定位置开始删除到字符串末尾(包括这个位置)
    string str2 = str.Remove(3);
    Console.WriteLine(str2); // 小市民

    // 两个参数:从指定位置开始删除指定长度的字符。
    string str3 = str.Remove(1, 2);
    Console.WriteLine(str3); // 小小市民
  6. 替换指定字符串

    1
    2
    3
    4
    5
    6
    7
    8
    string str = "小市民小市民";

    string str2 = str.Replace("小市民", "大富豪");
    Console.WriteLine(str2); // 大富豪大富豪

    // 清空字符串
    string str3 = str.Replace("小市民", "");
    Console.WriteLine(str3); //
  7. 大小写转换

    1
    2
    3
    4
    5
    6
    7
    8
    string name1 = "gcnanmu";
    string name2 = "GCNANMU";

    string str1 = name1.ToUpper();
    Console.WriteLine(str1); // GCNANMU

    string str2 = name2.ToLower();
    Console.WriteLine(str2); // gcnanmu
  8. 字符串截取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string str = "小市民小市民";

    // 从字符串中提取子字符串 从索引3开始提取到结尾
    string str2 = str.Substring(3);

    // 从字符串中提取子字符串 从索引0开始提取三个字符 不能超过字符串长度
    string str1 = str.Substring(0, 3);
    string str3 = str.Substring(0, 10); // error:会抛出异常

    Console.WriteLine(str1); // 小市民
    Console.WriteLine(str2); // 小市民
  9. 字符串切割

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    string str = "小市民-日文-轻文学-米泽穗信";

    string[] arr = str.Split('-');

    foreach (string s in arr)
    {
    Console.WriteLine(s);
    }

    /*
    小市民
    日文
    轻文学
    米泽穗信
    */

练习

  1. 请将字符串1|2|3|4|5|6|7 变为2|3|4|5|6|7|8

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    string str = "1|2|3|4|5|6|7";

    string[] arr = str.Split("|");

    string result = "";
    for (var i = 1; i < arr.Length; i++)
    {
    result += arr[i] + "|";

    }
    result += "8";
    Console.WriteLine(result); // 2|3|4|5|6|7|8
  2. string str = null;
    
    str = "123";
    
    string str2 = str;
    
    str2 = "321";
    
    str2 += "123";
    
    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

    请说明上述代码分配了多少个堆空间

    - `str = "123"`分配了一次堆空间
    - `str2 = "321"`分配了一次堆空间
    - `str2 += "123"` 创建了一个新的实例,并将地址赋值给`str2`

    3. 编写一个函数,将输入的字符串反转,不要使用中间商,你必须原地修改输入的数组,交换过程不适用额外的空间

    如:输入:`{'h','e','l','l','o'}` 输出:`{'o','l','l','e','h'}`

    ```C#
    class Program
    {

    static void ReverseString(char[] str)
    {
    int left = 0;
    int right = str.Length - 1;

    while (left < right)
    {
    // 异或两次的结果为自身
    /*
    假设 str[left] = a str[right] = b
    a = a ^ b
    b = b ^ a ^ b = a
    a = a ^ b ^ a = b
    */
    str[left] ^= str[right];
    str[right] ^= str[left];
    str[left] ^= str[right];

    left++;
    right--;
    }
    }
    static void Main(string[] args)
    {
    char[] str = "Hello".ToCharArray();
    ReverseString(str);
    Console.WriteLine(new string(str)); // 输出: "olleH"
    }
    }

stringBuilder

从上小节的练习能够知道,字符串只要进行赋值、改变(增删改查)都会产生新的堆空间,如果操作频繁会产生大量的内存垃圾,更频繁的触发GC,造成不必要的性能开销。StringBuilder可以很好解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Text;


StringBuilder str = new StringBuilder("小市民");

Console.WriteLine(str); //小市民
// 实际字符串的长度
Console.WriteLine(str.Length); // 3

// 预分配的空间
Console.WriteLine(str.Capacity); // 16

str.Append("123456789123456789");

Console.WriteLine(str); // 小市民123456789123456789
// 实际字符串的长度
Console.WriteLine(str.Length); // 21

// 预分配的空间
Console.WriteLine(str.Capacity); // 32

使用StringBuilder需要引用System.Text的命名空间,可以看到StringBuilder预先扩大了字符串的容量,减少因为空间变化造成的内存开辟和迁移,明显降低垃圾的产生速度。根据字符串长度的变化,会自动进行扩容(默认为当前大小乘以2)。

StringBuilder操作和string类有明显的不同

  1. 增加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    str.AppendFormat("{0}", 123);
    str.Append(456);

    Console.WriteLine(str); // 小市民123456
  2. 插入

    1
    2
    3
    4
    5
    6
    7
    8
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    str.Insert(0,"我是");

    Console.WriteLine(str); // 我是小市民
  3. 查找

    1
    2
    3
    4
    5
    6
    7
    8
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    Console.WriteLine(str[0]); // 小
    Console.WriteLine(str[1]); // 市
    Console.WriteLine(str[2]); // 民
  4. 修改

    1
    2
    3
    4
    5
    6
    7
    8
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    // 通过索引器访问字符串中的字符 只能是char
    str[0] = '大';
    Console.WriteLine(str); // 大市民
  5. 删除

    1
    2
    3
    4
    5
    6
    7
    using System.Text;

    StringBuilder str = new StringBuilder("小市民");

    // 从索引 0 开始删除 1 个字符
    str.Remove(0, 1);
    Console.WriteLine(str); // 输出:市民
  6. 清空

    1
    2
    3
    4
    5
    6
    using System.Text;

    StringBuilder str = new StringBuilder("小市民");

    str.Clear();
    Console.WriteLine(str); // 输出:
  7. 替换

    1
    2
    3
    4
    5
    6
    7
    8
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    // 从索引 0 开始删除 1 个字符
    str.Replace("小", "大");
    Console.WriteLine(str); // 大市民
  8. 重新赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    // 重新赋值
    str.Clear();
    str.Append("大老板");

    Console.WriteLine(str); // 大老板
  9. 判断是否相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    using System.Text;


    StringBuilder str = new StringBuilder("小市民");

    if(str.Equals("小市民"))
    {
    Console.WriteLine("相等"); // 打印
    }
    else
    {
    Console.WriteLine("不相等");
    }

结构体和类的区别

概述:

  • 结构体是值类型,类是引用类型,因此所在的内存区域不同
  • 结构体不具备继承和多态的特性,因此不能使用protected访问修饰符

细节:

  • ❌结构体变量在申明时不能赋予初始值,而类可以(这是视频原话,但是翻看笔记和事件发现.net 8是可以的,赋予初值不会报错)
  • 结构体无法申明无参构造函数,而类可以
  • 结构体申明有参构造后,无参构造不会被顶掉
  • 结构题要在构造函数中初始化所有变量的值,而类不需要
  • 结构体不能被static修饰,而类可以
  • 结构体不能在内部申明和自己一样的结构体变量,但是类可以
  • 结构体不能被继承,而类可以
  • 类可以继承接口(特殊)

如何选择:

  • 想要继承和多态特性,选择类
  • 对象仅仅是数据集合时,选择结构体
  • 从赋值方面考虑,是否原对象需要一起跟着变化

抽象类和接口类的区别

相同点:

  • 都可以被继承
  • 都不能被实例化
  • 都包含方法的申明(不含方法体)
  • 子类都必须实现未实现的方法
  • 都遵循里氏替换原则

不同点:

  • 抽象类可以有构造函数,结构没有
  • 抽象类只能被单一继承,而接口可以被继承多个
  • 抽象类可以有成员变量,接口不能有成员变量
  • 接口中只能申明没有实现的抽象方法
  • 抽象类的方法可以使用访问修饰符,接口中建议不写,默认为public

如何选择:

  • 表示对象用抽象类,表示行为用接口
  • 不同对象的相同行为,用接口实现

参考文献