面向对象的三大特性

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

封装

类和对象

基本概念

  • 一般在 namespace 中声明,命名所以首字母大小。
  • 具有相同特征、相同行为的一类事物的抽象,类是对象的模板,可以通过类创建对象。
  • 关键词:class
  • 类的声明和类对象声明是两个概念:
  • 类的声明类似枚举和结构体的声明,相当于是声明了一个自定义的变量类型,用来抽象现实实物的。
  • 对象是类创建出来的,是用来表象现实中的对象个体。对象的声明相当于声明一个指定类的变量。类创建对象的过程称之为实例化对象。
  • 类和对象都是引用类型的。
  • null:空引用类型为 null 的时候指的是内存堆没有分配。
1
2
3
4
5
6
7
8
9
10
11
class 类名
{
//特征——成员变量
//行为——成员方法
//保护特征——成员属性

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

实例化对象的基本语法

new来完成实例化。

1
2
3
//类名 变量名; 			 //(没用分配堆内存)
//类名 变量名 = null; //(没用分配堆内存)
//类名 变量名 = new 类名; //(在堆中新开了个房间)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Les
{
class Person
{
}

class Machine
{
}

class Program
{
static void Main(string[] args)
{
//类名 变量名; //(没用分配堆内存)
Person p;
//类名 变量名 = null; //(没用分配堆内存)
Person p2 = null;
//类名 变量名 = new 类名; //(在堆中新开了个房间)
Person p3 = new Person();
}
}
}

成员变量和访问修饰符

成员变量

用于描述对象的特征,可以是任意变量类型(枚举,结构体,类对象)。是否赋值根据需求而定。

1
2
3
4
5
6
7
8
9
10
11
class Person
{
//特征——成员变量
//行为——成员方法
//保护特征——成员属性

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

如果要声明一个和自己相同类型的成员变量时,不能对它进行实例化(会死循环!!)。

1
2
3
4
5
//不能这么做,会死循环
class Person
{
Person boy = new Person();
}

成员变量的使用和初始值

默认的初始值,对值类型来说都是 0(bool 为 false),引用类型来说都是 null

1
default(变量);   // 可以得到一个变量的默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
public int a;
public float b;
public char c;
public bool d;
}

//--------------------------------
Person r = new Person();
//点出来使用,和结构体一样
int a =r. a;
Console.WriteLine(default(int));

访问修饰符

  • public: 公开的,所有对象都能访问和使用。
  • private: 私有的,只有自己内部才能访问和使用,变量前不写默认为该状态。
  • protected: 只有自己内部和子类才能访问和使用,继承的时候用到。
1
2
3
4
5
6
class Person
{
public int a;
private int b;
protected int c;
}

成员方法

和结构体中函数的声明使用差不多,用来描述对象的行为, 在类中声明。
受访问修饰符的影响,不需要加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
28
29
30
31
32
33
34
35
36
37
38
//Person类中增加朋友的数组的方法

class Person
{
//特征——成员变量
public bool sex;
public string nanme;
public float high;
public int age;
public Person[] friend;

//行为——成员方法
/// <summary>
/// 扩容friend数组
/// </summary>
/// <param name="p"></param>
public void AddFriend(Person p)
{
if (friend ==null)
{
friend = new Person[] { p };
}
else
{
//数组扩容+1
Person[] newFriend = new Person[friend.Length + 1];
for (int i = 0; i < friend.Length; i++)
{
newFriend[i] = friend[i];
}

//将新成员p存在新数组的最后一个索引
newFriend[newFriend.Length - 1] = p;
}
}


}

构造函数

在实例化对象时会调用的用于初始化的函数,如果不写就默认存在一个无参构造函数
和结构体中构造函数的写法一致,(类允许自己申明一个无参构造函数 结构体是不允许的)
无返回值,函数名和类名必须相同,一般都是 public,可以重载构造函数
声明有参构造函数之前最好一起声明一个无参构造函数,声明有参构造时默认的无参构造就不存在了,要手动声明

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
class Person
{
//特征——成员变量
public bool sex;
public string nanme;
public float high;
public int age;
public Person[] friend;


//构造函数 实现对象实例化时 初始化
//构造函数可以重载
//无参构造函数
public Person()
{
nanme = "苏同学";
age = 18;
sex = true;
high = 180;
}

//有参构造函数
public Person(string name, int age,bool sex,float high)
{
this.name = name;
this.age = age;
this.sex = sex;
this.high = high;

}


}

//----------------------------------------------
//在main主函数中 使用构造函数初始化对象
Person p = new Person("苏同学",18,true,180f);
Person p1 = new Person("李同学", 18, false, 171f);

特殊写法 (构造函数的继承)较少使用
在构造函数后添加 :this(指定的重载参数)
可以实现执行该构造函数前执行 this 指定的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//无参构造函数
public Person()
{
nanme = "苏同学";
age = 18;
sex = true;
high = 180;
}

//有参构造函数,this指定了先执行无参
public Person(string name, int age,bool sex,float high):this()
{
this.name = name;
this.age = age;
this.sex = sex;
this.high = high;

}

析构函数

当引用类型的堆内存被回收时,会调用该函数。对于需要手动管理内存的语言(比如 C++),需要在析构函数中做一些内存回收处理。C#中有自动垃圾回收机制 GC,所以几乎不使用析构函数。

1
2
3
4
5
6
7
class Person
{
~Person()
{

}
}

垃圾回收机制

垃圾回收, 英文简写 GC (Garbage Collector)。垃圾回收的过程是在遍历堆(HEAP)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用。所谓的垃圾就是没有被任何变量、对象引用的内容。垃圾就需要被回收释放。

垃圾回收有很多种算法,比如:

  • 引用计数(Reference Counting)
  • 标记清除(Mark Sweep)
  • 标记整理(Mark Compact)
  • 复制集合(Copy Collection)

注意:

  • GC 只负责堆(HEAP)内存的垃圾回收。引用类型都是存在堆(HEAP)中的,所以它的分配和释放都通过垃圾回收机制来管理。
  • 栈(STACK)上的内存是由系统自动管理的。值类型在栈(STACK)中分配内存,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放。

C#中内存回收机制的大概原理
0 代内存 1 代内存 2 代内存
代的概念:代是垃圾回收机制使用的一种算法(分代算法)。新分配的对象都会被配置在第 0 代内存中。每次分配都可能会进行垃圾回收以释放内存(0 代内存满时)。在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步:

  1. 标记对象:从根(静态字段, 方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象。不可达对象就认为是垃圾(挂起执行托管代码线程),释放未标记的对象,搬迁可达对象,修改引用地址。
  2. 搬迁对象:压缩堆。

大对象总被认为是第二代内存,目的是减少性能损耗,提高性能。不会对大对象进行搬迁压缩,85000 字节(83KB)以上的对象为大对象。

手动垃圾回收

1
CG.Collect();

成员属性

基础概念

用于保护成员变量,为成员属性的获取和赋值添加逻辑处理。
解决 3p 局限性问题,get,set 可以写一个(起到保护作用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private string name;

public string Name
{
get
{
//可以在返回之前添加逻辑规则
//意味着这个属性可以获取的内容
return name;
}
set
{
//可以在设置前添加逻辑规则
// value 关键字 用于表示外部传入的值
name = value;
}
}

数值保护和加密处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public float High
{
get
{
//可以在返回之前添加逻辑规则
//意味着这个属性可以获取的内容
//解密处理
return High - 5;
}
set
{
//可以在设置前添加逻辑规则
if (High < 0)
{
high = 0;
Console.WriteLine("身高不能为负数,已设置为0");
}
// value 关键字 用于表示外部传入的值
//加密处理
High = value + 5;
}
}

get 和 set 前可加访问修饰符

private
默认不加 会使用属性声明时的访问权限
加的访问修饰符要低于属性的访问权限
不能让 get 和 set 的访问权限都低于属性的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private string name;

public string Name
{
get
{
return name;
}
//只能get不能set,保护作用
private set
{
name = value;
}
}

自动属性

外部能得不能改的特征,很少使用。

1
2
3
4
5
6
//自动属性 少用
public int Money
{
get;
private 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
class Person
{
public string;
public Person friends[];

public Person this[int index]
{
get
{
return friends[index];
}
set
{
if (friends==null)
{
friends = new Person[]{value};
}
friends[index] = value
}
}
}

//---------------------------------------
Person p = new Person();
p[0] = new Person;

静态成员

static关键字修饰的成员变量、成员方法、成员属性等。
特点:不用 new 一个,可以直接类名点出来。静态成员和程序同生共死,静态函数中不能使用非静态成员(生命周期的差异)。

1
2
3
4
5
// 静态成员的特点
// 程序开始运行时,就会分配内存空间。所以我们就能直接使用。
// 静态成员和程序同生共死,只要使用了它,直到程序结束时内存空间才会被释放。
// 所以一个静态成员就会有自己唯一的一个"内存小房间",这让静态成员就有了唯一性。
// 在任何地方使用都是用的小房间里的内容,改变了它也是改变小房间里的内容。

作用:常用变量的申明,常用的唯一的方法声明。如:同规则的数学计算相关函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
计算圆的面积

class MyCalss
{
public static float PI = 3.1415926f;

public static float AreaOfCircle (float r)
{
return Person.PI * r * r;
}
}

//**********************************************
float areaCircle = MyCalss.AreaOfCircle(7);

常量和静态变量

1
2
3
4
5
6
7
8
// CONST(常量)可以理解为特殊的STATIC(静态)
// 相同点
// 他们都可以通过类名点出使用

// 不同点
// 1. CONST必须初始化, 不能修改 STATIC没有这个规则
// 2. CONST只能修饰变量, STATIC可以修饰很多
// 3. CONST一定是写在访问修饰符后面的 ,STATIC没有这个要求

补充
设计模式:单例模式(线程安全相关)

静态类

使用static关键字修饰的类,只能包含静态成员。不能被实例化,具有唯一性,适合用作工具类(计算公式等)。

静态构造函数

使用static关键字修饰的构造函数,无访问修饰符,无参数,自动调用一次。
作用:主要用于初始化静态成员。

静态类和普通类中的静态构造函数功能一样,调用类时都会优先执行静态构造函数进行初始化
与构造函数(针对实例对象)不同的是,静态构造函数(针对类)只执行一次,并且是在第一个实例对象创建前被调用,所以它可以用于那些只需要执行一次的操作;而且它不允许有 public 等修饰符,由程序自动调用,不能被外界调用。
总结:静态构造函数用于初始化任何静态数据,或者用于执行仅需执行一次的操作;在创建第一个实例对象或者引用任何静态变量之前,将自动调用静态构造函数
所以一般静态构造函数用来为静态成员初始化,或者作为单件模式中创建对象的唯一入口。

拓展方法

基本概念

为现有非静态变量类型添加新方法。

作用

  1. 提升程序拓展性
  2. 不需要在对象中重新写方法
  3. 不需要继承来添加方法
  4. 为别人封装的类型写额外的方法

特点

  1. 一定是写在静态类中
  2. 一定是个静态函数
  3. 第一个参数为拓展目标
  4. 第一个参数用 this 修饰

基本语法

1
访问修饰符 static 返回值 函数名(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
public class Tools
{
public void Fun()
{
Console.WriteLine("方法1");
}

public void Fun2()
{
Console.WriteLine("方法2");
}
}

public static class TuoZhan
{
public static void Fun3(this Tools2 value ,int a)
{
Console.WriteLine("方法3");
}
}

//***************************************************
Tools t = new Tools();
t.fun3();

作用

  1. 提升程序的拓展性
  2. 为别人封装的类型写额外方法(在一些闭源的黑盒中拓展)
  3. 不需要继承来添加方法

运算符重载(仅做了解)

基本概念

关键词:operator。实现自定义类型的运算。可以多个重载。
可以多个重载
参数的数量 和运算符对应 (++ 单个参数 * 两个参数)
提升程序的扩展性
特点

  1. 一定是一个公共的静态方法
  2. 返回值写在 operator
  3. 逻辑处理自定义

作用
让自定义类和结构体对象可以进行运算。

注意

  1. 条件运算符需要成对实现
  2. 一个符号可以多个重载
  3. 不能使用 refout

基本语法

1
public static 返回类型 operator 运算符(参数列表)
1
2
3
4
5
6
7
8
9
10
11
12
//运算符重载 实现自定义类型的运算
public static Person operator +(Person p1, Person p2)
{
Person p = new Person();
p.age = p1.age + p2.age;
p.high = p1.high + p2.high;
return p;
}

//***************************************************

Person p2 = p + p1;

可重载的运算符

  • 算数运算符:+ - * / % ++ --
  • 逻辑运算符:!
  • 位运算符:& | ^ ~ << >>
  • 条件运算符:< <= > >= == !=

不可重载的运算符

  • 逻辑运算符:&& ||
  • 其他:[ ] () . = ? :

内部类和分布类(仅做了解)

内部类

在类中再声明一个类,亲密关系的体现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person
{
//人
public int age;
public string name;
public Body body;

public class Body
{
//身体
Arm leftArm;
public class Arm
{
//手臂
}
}
}

分布类

关键词:partial

分部类

把一个类分成几部分声明。

特点

  1. 分部类可以写在多个脚本文件中。
  2. 分部类的访问修饰符要一致。
  3. 分部类中不能有重复成员。
1
2
3
4
5
6
7
8
9
10
11
partial class Student
{
public bool sex;
public string name;
}

partial class Student
{
public int number;
public float high;
}

分部方法

概念
将方法的声明和实现分离。

特点

  1. 不能加访问修饰符,默认私有。
  2. 只能在分部类中声明。
  3. 返回值只能是 void
  4. 可以有参数,但不用 out 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
partial class Student
{
public bool sex;
public string name;
partial void Speak(); // 分部方法声明
}

partial class Student
{
public int number;
public float high;
partial void Speak() // 分部方法实现
{
Console.WriteLine("Hello, my name is " + name);
}
}

继承

继承的基本规则

基本概念

不能多继承,单根性。

一个类 A 继承一个类 B,类 A 将会继承类 B 的所有成员。A 类将拥有 B 类的所有特征和行为。

被继承的类称为父类、基类、超类

继承的类称为子类、派生类

子类可以有自己的特征和行为

特点

  1. 单根性:子类只能有一个父类
  2. 传递性:子类可以间接继承父类的父类

基本语法

1
2
3
4
class 类名 : 被继承的类名
{

}
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 Teacher
{
//名字
public string name;
//工号
public int number;
//介绍名字
public void SpeakName()
{
Console.WriteLine(name);
}
}

//继承Teacher类
class TeachingTeacher : Teacher
{
//科目
public string subject;
//介绍科目
public void SpeakSubject()
{
Console.WriteLine(subject+"老师");
}
}

访问修饰符

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

}

class Player : GameObject
{
public void PlayerAtk()
{
Console.WriteLine("玩家攻击");
}
}

class Moster : GameObject
{
public void MosterAtk()
{
Console.WriteLine("怪物攻击");
}
}

class Boss : GameObject
{
public void BossAtk()
{
Console.WriteLine("Boss攻击");
}
}

//****************************************************
//里氏替换原则,用父类容器转载子类对象
//但是Player类的功能无法使用要用is as转换
GameObject player = new Player();

//✅is和as
//is判断一个对象是否是指定的对象
//返回值bool
if (player is Player)
{
//as:将一个对象转换为指定类对象
//返回值:指定类型对象
//成功返回指定类对象 失败返回null
Player p = player as Player;
}

//可以正常使用Player类的功能了
p.PlayerAtk();

//*******************************************************

//实际使用时和数组配合使用多 方便进行对象存储和管理
GameObject[] objects = new GameObject[] { new Player(), new Moster(), new Boss() };

//遍历objects数组 来判断类和执行类
for (int i = 0; i < objects.Length; i++)
{
if (objects[i] is Player)
{
(player as Player).PlayerAtk();
}
else if (objects[i] is Moster)
{
(player as Moster).MosterAtk();
}
else if (objects[i] is Boss)
{
(player as Boss).BossAtk();
}
}
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 Person
{
public int a;
public int b;
public void Add ()
{
a = 10;
b = 20;
}
}

class Teather : Person
{
public int c;
public int d;
public void TAdd()
{
c = 10;
d = 20;
}
}
//********************************
Person t = new Teather();
//替换后直接使用是只能用父类的方法
t.Add();
//使用as 后才能使用子类和父类的方法
(t as Teather).TAdd();

继承中的构造函数

基本概念

先执行父类的构造函数再执行子类构造函数。子类实例化时,默认调用无参构造函数。若父类没有无参构造函数就会报错。

特点

  1. 当声明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数。
  2. 父类的无参构造很重要。
  3. 子类可以通过 base 关键字(代表父类)调用父类构造函数。

1.始终保持申明一个无参构造 2.通过 base 调用指定父类构造 (注意和 this 的区别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//base调用指定父类构造
class Father
{
int a ;
public Father (int a)
{
this a = a;
}
}

class Son : Father
{
public Son (int a) : base(a)
{

}
}

//*******************************
Son s = new Son(1);

万物之父&装箱拆箱

基本概念

关键词:objectobject 是一个基类,可以装任何东西。

作用

  1. 可以利用里氏替换原则,用 object 容器装所有对象。
  2. 可以用来表示不确定类型,作为函数参数类型。

使用方式

引用类型和里氏替换原则一样用 is 和 as
值类型使用括号强转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Sb
{
public Speak()
{
Console.WriteLine("傻逼");
}
}
//**************************
//装引用类型 和使用 ——里氏替换
object o = new Sb();
if(o is Sb)
{
(o as Sb).Speak;
}
//装值类型 和使用 ——括号强转
object o2 = 10;
int a = (int)o2;
//装特殊类型 string
object o3 = "你好呀";
string str = o3.ToString;//也可以使用引用类型的 o3 as string
//装特殊类型 数组
object o4 = new int[10];
int[] arr = (int[])o4;//也可以使用引用类型的 o4 as int[]

装箱拆箱

值类型和 object 之间发生的转换。

装箱
把值类型用引用类型存储,栈内存移到堆内存中。

拆箱
把引用类型存储的值类型取出来,堆内存移到栈内存中。

1
2
3
4
5
int a = 10;
// 装箱
object o = a;
// 拆箱
int b = (int)o;

密封类

关键词:sealed。作用是让类无法被继承,保证程序的规范性和安全性。

1
2
3
4
sealed class Father
{
// Father 类无法被继承
}

多态

Vob

基本概念

关键词:virtual(虚函数) override(重写) base(父类)
在执行同一方法时有不同的表现
重写的方法 输入参数的类型和数量要一致(也复合面向对象)
多层继承中也可以使用 层层重写回到父类

作用:其实多态的作用就是把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出更通用的程序。

1
2
3
4
5
6
7
8
9
#region 多态的概念
//多态文字面的意思就是“多种状态”
//让继承同一父类的子类们 在执行相同方法时有不同的表现(状态)
//主要目的
//同一父类的对象 执行相同行为(方法)有不同的表现
//解决的问题
//让同一个对象有唯一行为的特征
#endregion

1
2
3
4
5
6
7
8
9
10
#region 多态的实现
//目前已经学过的多态
//编译时多态——函数重载,开始就写好的
//我们将学习的:
//运行时多态(vob,抽象函数,接口)
//我们今天学习 vob
//v: virtual(虚函数)
//o: override(重写)
//b: base(父类)
#endregion

基本语法

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 GameObeject
{
//虚函数 virtual
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}

class Player : GameObeject
{
//重写
public override void Atk()
{
//base可以保留父类的方法
//base.Atk();
Console.WriteLine("玩家对象进行攻击");
}
}

class Monster : GameObeject
{
//重写
public override void Atk()
{
//base可以保留父类的方法
//base.Atk();
Console.WriteLine("怪物对象进行攻击");
}
}

//**********************************************
GameObeject p = new Player();
p.Atk();

GameObeject m = new Monster();
m.Atk();

抽象类和抽象方法

抽象类
关键字:abstract
不能被实例化 可以包含抽象方法 遵循里氏替换

概念:被抽象关键字 ABSTRACT 修饰的类
特点:

  1. 不能被实例化的类
  2. 可以包含抽象方法
  3. 继承抽象类必须重写其抽象方法
1
2
3
4
abstract class Fruits
{

}

如何选择普通类还是抽象类

  • 不希望实例化的对象 相对抽象的类可以使用 如 :人 person 水果 fruit
  • 父类中的行为不太需要被实现 只希望子类去定义具体的规则
  • 整体框架设计时会使用 让基类更安全

抽象函数

又叫纯虚方法
关键字:abstract
特点:

  1. 只能在抽象类中声明
  2. 没有方法体
  3. 不能是私有的
  4. 继承后必须要 override 重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Fruits
{
public string name;
public abstract void Bad ();
}

class Apple : Fruits
{
public override void Bad ()
{
Console.WriteLine("苹果坏了");
}
}
//****************************************
//遵循里氏替换 父类装子类
Fruits f = new Apple();
f.Bad();

虚方法(vritual)和纯虚方法(abstract)的区别

  • 虚方法可以在抽象类和非抽象类中声明 纯虚方法只能在抽象类中声明
  • 虚方法可以由子类选择性实现 纯虚方法必须实现重写。虚方法有方法体可实现逻辑
  • 他们都可以被子类用 override 重写 可以多层重写 子子类重写子类重写父类

接口

基本概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//接口是行为的抽象规范
//它也是一种自定义类型
//关键字:interface
//接口申明的规范
//1.不包含成员变量
//2.只包含方法、属性、索引器、事件
//3.成员不能被实现
//4.成员可以不用写访问修饰符,不能是私有的
//5.接口不能继承类,但是可以继承另一个接口
//接口的使用规范
//1.类可以继承多个接口
//2.类继承接口后,必须实现接口中所有成员
//特点
//1.它和类的申明类似
//2.接口是用来继承的
//3.接口不能被实例化,但是可以作为容器存储对象

基本语法

接口的申明

1
2
3
4
5
6
7
//接口关键字:interface
//语法:interface 接口名
//{
//
//}
//一句话记忆:接口是抽象行为的"基类"
//接口命名规范:帕斯卡前面加个I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface IFly
{
//方法
void Fly();

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

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

//事件 c#进阶讲
event Action doSomthing;
}

接口的使用

1
2
3
4
5
//接口用来继承
//1.类可以继承1个类,N个接口
//2.继承了接口后,必须实现其中的内容,并且必须是PUBLIC的
//3.实现的接口函数,可以加V再在子类重写
//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
45
46
47
48
49
50
51
52
53
54
55
56
//声明接口
interface IFly
{
//方法
void Fly();

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

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

//事件 c#进阶讲
event Action doSomthing;
}

//实现接口
class Animal{}

class Persom : Animal,IFly
{
//实现的接口函数 可以加virtual再在子类中重写
public virtual void Fly()
{

}

public string Name
{
get;
set;
}

public int this[int index]
{
get
{
return 0 ;
}
set;
}

public event Action doSomthing;
}

//***************************************************
//接口遵循里氏替换 父类装子类
IFly f = new Persom1();

接口可以继承接口

1
2
3
//相当于行为合并
//接口继承接口时不需要实现
//待类继承接口后类去实现所有内容
1
2
3
4
5
6
7
8
interface IWalk
{
void Walk();
}

interface IMove : IFly, IWALK
{
}

显示实现接口

1
2
3
//当一个类继承两个接口
//但是接口中存在着同名方法时
//注意:显示实现接口时不能写访问修饰符
1
2
3
4
5
6
7
8
9
10
11
12
class Player : IAtk, ISuperAtk
{
void IAtk.Atk()
{

}

void ISuperAtk.Atk()
{

}
}

接口的作用和总结

作用

  • 抽象行为:把行为抽象出来供子类继承。个别功能独立在基类外,子类需要时继承。
  • 提高程序复用性。

总结

  • 继承类:是对象间的继承,包括特征行为等。
  • 接口继承:是行为间的继承,继承接口的行为规范,按照规范去实现内容。
  • 接口也是遵循里氏替换,所以可以用接口容器装对象,实现装载毫无关系但是却有相同行为的对象。
  • 接口包含:成员方法、属性、索引器、事件,且都不实现,都没有访问修饰符。
  • 接口继承接口相当于行为合并。

密封方法

关键字:sealed 修饰重写函数
让虚方法或者抽象方法不能再被重写
和 override 一同出现

以下是关于命名空间的概念和用法的 Markdown 格式说明:

命名空间

基本概念

命名空间用于组织和复用代码,类似于一个工具包,将类声明放置其中。命名空间可以分开编写。

  • 同一命名空间中的类名不能重复。
  • 不同命名空间中的类可以同名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Mygame
{
class Gameobject
{

}
}

// 命名空间可以分开写
namespace Mygame
{
// 属于同一命名空间,可以正常继承
class Player : Gameobject
{

}
}

引用命名空间

在不同命名空间中相互使用时,需要引用命名空间。可以通过两种方法进行引用:

  1. 使用 using 关键字
1
2
3
4
using Mygame;

// 使用 Mygame 命名空间的 Gameobject 类
Gameobject g = new Gameobject();
  1. 直接使用命名空间的完整路径
1
2
// 使用 Mygame 命名空间的 Gameobject 类
Mygame.Gameobject g = new Mygame.Gameobject();

应对不同命名空间中的同名类

当不同命名空间中有同名类时,可以通过指定完整路径来区分:

1
2
3
4
5
6
using Mygame;
using Mygame2;

// 区分同名类
Mygame.Gameobject g = new Mygame.Gameobject();
Mygame2.Gameobject g2 = new Mygame2.Gameobject();

嵌套命名空间

命名空间可以嵌套使用,类似于小工具包中的小工具包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace MyGame
{
namespace UI
{
class Image
{

}
}

namespace Game
{
class Image
{

}
}
}

使用嵌套命名空间中的类

  1. 通过命名空间的完整路径
1
2
MyGame.UI.Image img = new MyGame.UI.Image();
MyGame.Game.Image img2 = new MyGame.Game.Image();
  1. 使用 using 关键字引入命名空间
1
2
3
4
5
using MyGame.UI;
using MyGame.Game;

Image img = new Image();
Image img2 = new Image();

以下是关于Object类的各种方法的详细说明及代码示例的 Markdown 格式:

Object 类中的方法

知识回顾

  • 所有类型的基类是 Object
  • 可以利用里氏替换原则装载一切对象,涉及装箱和拆箱。

静态方法

Equals

  • 定义: public static bool Equals(object? objA, object? objB);
  • 作用: 判断两个对象是否相等。最终的比较由左侧对象的 Equals 方法决定。
  • 示例:
1
2
3
4
Console.WriteLine(Object.Equals(1, 1)); // 输出 True
Test t1 = new Test();
Test t2 = new Test();
Console.WriteLine(Object.Equals(t1, t2)); // 输出 False
  • 值类型判断: 比较两个值是否相等。
  • 引用类型判断: 比较两个对象是否指向同一内存地址,而不是判断是否相同类型。

ReferenceEquals

  • 定义: public static bool ReferenceEquals(object? objA, object? objB);
  • 作用: 比较两个对象是否是同一个引用。主要用于引用类型的比较,不用于值类型。
  • 示例:
1
2
3
Test t1 = new Test();
Test t2 = t1;
Console.WriteLine(Object.ReferenceEquals(t1, t2)); // 输出 True

成员方法

GetType

  • 定义: public Type GetType();
  • 作用: 获取对象的运行时类型。结合反射相关知识,可以对对象进行各种操作。
  • 示例:
1
2
3
Test t = new Test();
Type type = t.GetType();
Console.WriteLine(type); // 输出 Test

MemberwiseClone

  • 定义: protected object MemberwiseClone();
  • 作用: 获取对象的浅拷贝。返回一个新的对象,新对象中的引用变量与老对象一致。
  • 示例:
1
2
Test original = new Test();
Test copy = (Test)original.MemberwiseClone();
  • 浅拷贝: 对于值类型,直接复制;对于引用类型,复制的是内存地址,改变拷贝后的引用类型变量会影响原变量。

Equals (虚方法)

  • 定义: public virtual bool Equals(object? obj);
  • 作用: 比较两个对象是否相等。可以重写该方法以定义自定义的比较规则。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
public class Test
{
public override bool Equals(object? obj)
{
if (obj is Test)
{
// 自定义比较逻辑
return true;
}
return false;
}
}

GetHashCode (虚方法)

  • 定义: public virtual int GetHashCode();
  • 作用: 获取对象的哈希码。通常用于哈希表中。可以重写该方法定义自定义的哈希码算法。
  • 示例:
1
2
3
4
5
6
7
public class Test
{
public override int GetHashCode()
{
return 12345; // 自定义哈希码
}
}

ToString (虚方法)

  • 定义: public virtual string? ToString();
  • 作用: 返回当前对象的字符串表示形式。可以重写该方法以定义自定义的对象转字符串规则。
  • 示例:
1
2
3
4
5
6
7
public class Test
{
public override string ToString()
{
return "苏老师声明的Test类";
}
}
1
2
Test t = new Test();
Console.WriteLine(t.ToString()); // 输出 "苏老师声明的Test类"

string 的常用方法

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
// 字符串本质是 char 数组
string str = "Hello World";
Console.WriteLine(str[0]);
// 输出 "H"

// 转为 char 数组
char[] chars = str.ToCharArray();

// 获取字符长度
int length = str.Length;

// 字符串拼接
str = string.Format("{0} {1}", "Hello", "World");
// 返回 "Hello World"

// 正向查找字符位置
str = "Hello from CSharp";
int index = str.IndexOf("from");
// 返回 6,字符的索引,找不到返回 -1

// 反向查找字符位置
str = "Hello CSharp CSharp";
index = str.LastIndexOf("CSharp");
// 返回 13,从后面开始查找,找到的第一个字的索引,找不到返回 -1

// 移除指定位置后的字符
str = "Hello CSharp CSharp";
str = str.Remove(6);
// 返回 "Hello "

// 移除指定位置和字符个数
str = "Hello CSharp World";
str = str.Remove(6, 7);
// 返回 "Hello World"

// 替换指定字符串
str = "Hello CSharp World";
str = str.Replace("CSharp", "Programming");
// 返回 "Hello Programming World"

// 大小写转换
str = "csharp";
str = str.ToUpper();
// 返回 "CSHARP"

str = str.ToLower();
// 返回 "csharp"

// 字符串截取
str = "Hello CSharp World";
str = str.Substring(6);
// 返回 "CSharp World"

// 重载,指定开始位置和字符个数
str = "Hello CSharp World";
str = str.Substring(6, 7);
// 返回 "CSharp"

// 字符串切割
str = "Apple|Banana|Cherry|Date";
string[] strs = str.Split('|');
// 返回 string[]{ "Apple", "Banana", "Cherry", "Date" }

StringBuilder

解决字符串性能问题

知识回顾

  • 字符串是特殊的引用类型:
    • 每次重新赋值或拼接时,会分配新的内存空间。
    • 如果一个字符串经常改变,会非常浪费内存空间。

StringBuilder

  • 定义: StringBuilder 是 C# 提供的一个类,用于处理字符串,特别是当需要频繁修改和拼接字符串时。

  • 优点:

    • 修改字符串而不创建新的对象。
    • 提升性能,减少内存浪费。
  • 使用前需要引用的命名空间:

    1
    using System.Text;

示例代码

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
// 创建一个 StringBuilder 实例
StringBuilder strBui = new StringBuilder("InitialValue");

// 获取容量
int capacity = strBui.Capacity;
// 默认为16,当前使用了11,自动扩容可能会变成32, 64, 128等

// 增加内容
strBui.Append("MoreData");
// 结果为 "InitialValueMoreData"

strBui.AppendFormat("{0}{1}", 123, 456);
// 结果为 "InitialValueMoreData123456"

// 插入内容
strBui.Insert(0, "Prefix");
// 结果为 "PrefixInitialValueMoreData123456"

// 删除内容
strBui.Remove(0, 6);
// 结果为 "InitialValueMoreData123456"

// 清空内容
strBui.Clear();
// 结果为 ""

// 重新赋值
strBui.Clear();
strBui.Append("NewValue");

// 查找内容
char character = strBui[3];
// 结果为 'V'

// 修改内容
strBui[0] = 'O';
// strBui 结果为 "OneValue"

// 替换内容
strBui.Replace("Value", "String");
// strBui 结果为 "OneString"

// 判断是否相等
bool isEqual = strBui.Equals("OneString");
// 返回 true

  • StringBuilder 完全具有引用类型的特征,没有值类型的特征。
  • StringBuilder 自动管理内存扩容,不需要手动调整容量。
  • StringBuilder 的所有操作,如增、删、改、查、替换等,都不会创建新的字符串实例,而是直接在现有实例上进行修改,提升了性能。

结构体和类的区别

  • 存储位置:
    • 结构体是值类型,存储在栈上。
    • 类是引用类型,存储在堆上。
  • 使用区别:
    • 结构体和类在使用上很类似,但结构体不具备继承和多态特性。
    • 结构体可以视为封装了面向对象思想的一类对象,但由于不支持继承和多态,它的使用范围较窄。
    • 结构体不能使用 protected 保护访问修饰符。

细节区别

  1. 结构体是值类型,类是引用类型。
  2. 结构体存在栈中,类存在堆中。
  3. 结构体成员不能使用 protected 访问修饰符,而类可以。
  4. 结构体成员变量声明不能指定初始值,而类可以。
  5. 结构体不能声明无参构造函数,而类可以。
  6. 结构体声明有参构造函数后,无参构造函数不会被自动生成。
  7. 结构体不能声明析构函数,而类可以。
  8. 结构体不能被继承,而类可以。
  9. 结构体需要在构造函数中初始化所有成员变量,而类的成员变量可以不初始化。
  10. 结构体不能被 static 修饰(不存在静态结构体),而类可以。
  11. 结构体不能在自己内部声明和自己一样的结构体变量,而类可以。

结构体和类的选择

  1. 使用继承和多态:
    • 当需要使用继承和多态时,选择类。例如,玩家、怪物等对象。
  2. 数据集合:
    • 当对象是数据集合时,优先考虑使用结构体。例如,位置、坐标等。
  3. 值类型和引用类型赋值:
    • 当需要频繁赋值传递且希望改变赋值对象而原对象不跟随变化时,使用结构体。例如,坐标、向量、旋转等。

抽象类和接口的区别

  • 抽象类:

    • 使用 abstract 修饰的类和方法。
    • 抽象类不能实例化。
    • 抽象方法只能在抽象类中声明,必须在子类中实现。
  • 接口:

    • 使用 interface 修饰。
    • 接口是行为的抽象,不包含成员变量。
    • 仅包含方法、属性、索引器、事件,成员都不能实现,访问修饰符默认为 public

相同点

  1. 都可以被继承。
  2. 都不能直接实例化。
  3. 都可以包含方法声明。
  4. 子类或实现类必须实现未实现的方法。
  5. 都遵循里氏替换原则。

区别

  1. 构造函数:
    • 抽象类可以有构造函数;接口不能有构造函数。
  2. 继承:
    • 抽象类只能单继承;接口可以多继承。
  3. 成员变量:
    • 抽象类中可以有成员变量;接口中不能有成员变量。
  4. 方法:
    • 抽象类中可以声明成员方法、虚方法、抽象方法、静态方法;接口中只能声明没有实现的抽象方法。
  5. 访问修饰符:
    • 抽象类的方法可以使用访问修饰符;接口中的方法建议不写访问修饰符,默认为 public

如何选择

  • 表示对象:
    • 使用抽象类来表示对象。
  • 表示行为扩展:
    • 使用接口来表示行为扩展。
  • 举例:
    • 动物是一类对象,可以选择抽象类;飞翔是一个行为,可以选择接口。

面向对象七大原则

七大原则总体要实现的目标是:模块内的高内聚、模块间的低耦合,使程序模块的可重用性、移植性增强。

七大原则

单一职责原则:一个类只处理自己应该处理的内容,不应该啥都写在一起

开闭原则:对拓展开放,对修改封闭。新加功能尽量是加处理而不是改代码

里氏替换原则:任何地方子类都能替代父类,父类容器装子类

依赖倒转原则:不要依赖具体的实现,要依赖抽象(接口)

迪米特法则:一个类要尽量减少对别的类的了解,尽量少用别的类和自己关联

接口隔离原则:一个接口一个行为,不要一个接口 n 个行为

合成复用原则:除非设计上需要继承,否则尽量用组合复用的形式

单一职责原则

SRP(Single Responsibility Principle)

类被修改的几率很大,因此应该专注于单一的功能。

如果把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能。

举例:假设程序、策划、美术三个工种是三个类,他们应该各司其职,在程序世界中只应该做自己应该做的事情。

开闭原则

OCP(Open-Closed Principle)对拓展开发,对修改关闭

拓展开放:模块的行为可以被拓展从而满足新的需求

修改关闭:不允许修改模块的源代码(或者尽量使修改最小化)

举例:继承就是最典型的开闭原则的体现,可以通过添加新的子类和重写父类的方法来实现

里氏替换原则

LSP(Liskov Substitution Principle)

任何父类出现的地方,子类都可以替代

举例:用父类容器装载子类对象,因为子类对象包含了父类的所有内容

依赖倒转原则

DIP(Dependence Inversion Principle)

要依赖于抽象,不要依赖于具体的实现

迪米特原则

LoP(Law of Demeter)

又称最少知识原则

一个对象应当对其它对象尽可能少的了解不要和陌生人说话

举例:一个对象中的成员,要尽可能少的直接和其它类建立关系目的是降低耦合性

接口分离原则

ISP(Interface Segregation Principle)

不应该强迫别人依赖他们不需要使用的方法

一个接口不需要提供太多的行为,一个接口应该尽量只提供一个对外的功能

让别人去选择需要实现什么样的行为,而不是把所有的行为都封装到一个接口当中

举例:飞行接口、走路接口、跑步接口等等虽然都是移动的行为但是我们应该把他们分为一个一个单独的接口,让别人去选择使用

合成复用原则

CRP(Composite Reuse Principle)

尽量使用对象组合,而不是继承来达到复用的目的继承关系是强耦合,组合关系是低耦合

举例:脸应该是眼镜、鼻子、嘴巴、耳朵的组合,而不是依次的继承角色和装备也应该是组合,而不是继承

注意:不能盲目的使用合成复用原则,要在遵循迪米特原则的前提下

如何使用这些原则

在开始做项目之前,整理 UML 类图时先按自己的想法把需要的类整理出来

再把七大原则截图放在旁边,基于七大原则去优化整理自己的设计

整体目标就是:高内聚,低耦合

初学程序阶段

不要过多的纠结于七大原则

先用最适合自己的方法把需求实现了再使用七大原则去优化

不要想着一步到位,要循序渐进

面向对象编程能力提升是需要经验积累的