CSharp 核心
面向对象的三大特性
封装 : 用程序语言来形容对象
继承 :复用封装对象的代码;儿子继承父亲,复用现成代码
多态:同样行为的不同表现,儿子继承父亲基因但是有不同的行为表现
类(class 关键词)
封装
类和对象
基本概念
- 一般在 namespace 中声明,命名所以首字母大小。
- 具有相同特征、相同行为的一类事物的抽象,类是对象的模板,可以通过类创建对象。
- 关键词:
class
- 类的声明和类对象声明是两个概念:
- 类的声明类似枚举和结构体的声明,相当于是声明了一个自定义的变量类型,用来抽象现实实物的。
- 对象是类创建出来的,是用来表象现实中的对象个体。对象的声明相当于声明一个指定类的变量。类创建对象的过程称之为实例化对象。
- 类和对象都是引用类型的。
null
:空引用类型为 null 的时候指的是内存堆没有分配。
1 | class 类名 |
实例化对象的基本语法
用new
来完成实例化。
1 | //类名 变量名; //(没用分配堆内存) |
1 | namespace Les |
成员变量和访问修饰符
成员变量
用于描述对象的特征,可以是任意变量类型(枚举,结构体,类对象)。是否赋值根据需求而定。
1 | class Person |
如果要声明一个和自己相同类型的成员变量时,不能对它进行实例化(会死循环!!)。
1 | //不能这么做,会死循环 |
成员变量的使用和初始值
默认的初始值,对值类型来说都是 0(bool 为 false),引用类型来说都是 null
1 | default(变量); // 可以得到一个变量的默认值 |
1 | class Person |
访问修饰符
public
: 公开的,所有对象都能访问和使用。private
: 私有的,只有自己内部才能访问和使用,变量前不写默认为该状态。protected
: 只有自己内部和子类才能访问和使用,继承的时候用到。
1 | class Person |
成员方法
和结构体中函数的声明使用差不多,用来描述对象的行为, 在类中声明。
受访问修饰符的影响,不需要加static
关键字。成员方法需要实例化才能使用。
1 | //Person类中增加朋友的数组的方法 |
构造函数
在实例化对象时会调用的用于初始化的函数,如果不写就默认存在一个无参构造函数
和结构体中构造函数的写法一致,(类允许自己申明一个无参构造函数 结构体是不允许的)
无返回值,函数名和类名必须相同,一般都是 public,可以重载构造函数
声明有参构造函数之前最好一起声明一个无参构造函数,声明有参构造时默认的无参构造就不存在了,要手动声明
1 | class Person |
特殊写法 (构造函数的继承)较少使用
在构造函数后添加 :this(指定的重载参数)
可以实现执行该构造函数前执行 this 指定的构造函数
1 | //无参构造函数 |
析构函数
当引用类型的堆内存被回收时,会调用该函数。对于需要手动管理内存的语言(比如 C++),需要在析构函数中做一些内存回收处理。C#中有自动垃圾回收机制 GC,所以几乎不使用析构函数。
1 | class Person |
垃圾回收机制
垃圾回收, 英文简写 GC (Garbage Collector)。垃圾回收的过程是在遍历堆(HEAP)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用。所谓的垃圾就是没有被任何变量、对象引用的内容。垃圾就需要被回收释放。
垃圾回收有很多种算法,比如:
- 引用计数(Reference Counting)
- 标记清除(Mark Sweep)
- 标记整理(Mark Compact)
- 复制集合(Copy Collection)
注意:
- GC 只负责堆(HEAP)内存的垃圾回收。引用类型都是存在堆(HEAP)中的,所以它的分配和释放都通过垃圾回收机制来管理。
- 栈(STACK)上的内存是由系统自动管理的。值类型在栈(STACK)中分配内存,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放。
C#中内存回收机制的大概原理
0 代内存 1 代内存 2 代内存
代的概念:代是垃圾回收机制使用的一种算法(分代算法)。新分配的对象都会被配置在第 0 代内存中。每次分配都可能会进行垃圾回收以释放内存(0 代内存满时)。在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步:
- 标记对象:从根(静态字段, 方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象。不可达对象就认为是垃圾(挂起执行托管代码线程),释放未标记的对象,搬迁可达对象,修改引用地址。
- 搬迁对象:压缩堆。
大对象总被认为是第二代内存,目的是减少性能损耗,提高性能。不会对大对象进行搬迁压缩,85000 字节(83KB)以上的对象为大对象。
手动垃圾回收
1 | CG.Collect(); |
成员属性
基础概念
用于保护成员变量,为成员属性的获取和赋值添加逻辑处理。
解决 3p 局限性问题,get,set 可以写一个(起到保护作用)。
1 | private string name; |
数值保护和加密处理
1 | public float High |
get 和 set 前可加访问修饰符
private
默认不加 会使用属性声明时的访问权限
加的访问修饰符要低于属性的访问权限
不能让 get 和 set 的访问权限都低于属性的权限
1 | private string name; |
自动属性
外部能得不能改的特征,很少使用。
1 | //自动属性 少用 |
索引器
作用:可以以中括号的形式范围自定义类中的元素,规则自定义,访问时和数组一样。
适用于在类中有数组变量时使用。索引器可以重载。
锦上添花的作用,功能和成员属性相同,可以不写。结构体也支持索引器。
1 | class Person |
静态成员
static
关键字修饰的成员变量、成员方法、成员属性等。
特点:不用 new 一个,可以直接类名点出来。静态成员和程序同生共死,静态函数中不能使用非静态成员(生命周期的差异)。
1 | // 静态成员的特点 |
作用:常用变量的申明,常用的唯一的方法声明。如:同规则的数学计算相关函数。
1 | 计算圆的面积 |
常量和静态变量
1 | // CONST(常量)可以理解为特殊的STATIC(静态) |
补充
设计模式:单例模式(线程安全相关)
静态类
使用static
关键字修饰的类,只能包含静态成员。不能被实例化,具有唯一性,适合用作工具类(计算公式等)。
静态构造函数
使用static
关键字修饰的构造函数,无访问修饰符,无参数,自动调用一次。
作用:主要用于初始化静态成员。
静态类和普通类中的静态构造函数功能一样,调用类时都会优先执行静态构造函数进行初始化
与构造函数(针对实例对象)不同的是,静态构造函数(针对类)只执行一次,并且是在第一个实例对象创建前被调用,所以它可以用于那些只需要执行一次的操作;而且它不允许有 public 等修饰符,由程序自动调用,不能被外界调用。
总结:静态构造函数用于初始化任何静态数据,或者用于执行仅需执行一次的操作;在创建第一个实例对象或者引用任何静态变量之前,将自动调用静态构造函数
所以一般静态构造函数用来为静态成员初始化,或者作为单件模式中创建对象的唯一入口。
拓展方法
基本概念
为现有非静态变量类型添加新方法。
作用
- 提升程序拓展性
- 不需要在对象中重新写方法
- 不需要继承来添加方法
- 为别人封装的类型写额外的方法
特点
- 一定是写在静态类中
- 一定是个静态函数
- 第一个参数为拓展目标
- 第一个参数用
this
修饰
基本语法
1 | 访问修饰符 static 返回值 函数名(this 拓展类名 参数名, 参数类型 参数名) |
1 | public class Tools |
作用
- 提升程序的拓展性
- 为别人封装的类型写额外方法(在一些闭源的黑盒中拓展)
- 不需要继承来添加方法
运算符重载(仅做了解)
基本概念
关键词:operator
。实现自定义类型的运算。可以多个重载。
可以多个重载
参数的数量 和运算符对应 (++ 单个参数 * 两个参数)
提升程序的扩展性
特点
- 一定是一个公共的静态方法
- 返回值写在
operator
前 - 逻辑处理自定义
作用
让自定义类和结构体对象可以进行运算。
注意
- 条件运算符需要成对实现
- 一个符号可以多个重载
- 不能使用
ref
和out
基本语法
1 | public static 返回类型 operator 运算符(参数列表) |
1 | //运算符重载 实现自定义类型的运算 |
可重载的运算符
- 算数运算符:
+ - * / % ++ --
- 逻辑运算符:
!
- 位运算符:
& | ^ ~ << >>
- 条件运算符:
< <= > >= == !=
不可重载的运算符
- 逻辑运算符:
&& ||
- 其他:
[ ] () . = ? :
内部类和分布类(仅做了解)
内部类
在类中再声明一个类,亲密关系的体现。
1 | class Person |
分布类
关键词:partial
。
分部类
把一个类分成几部分声明。
特点
- 分部类可以写在多个脚本文件中。
- 分部类的访问修饰符要一致。
- 分部类中不能有重复成员。
1 | partial class Student |
分部方法
概念
将方法的声明和实现分离。
特点
- 不能加访问修饰符,默认私有。
- 只能在分部类中声明。
- 返回值只能是
void
。 - 可以有参数,但不用
out
关键字。
1 | partial class Student |
继承
继承的基本规则
基本概念
不能多继承,单根性。
一个类 A 继承一个类 B,类 A 将会继承类 B 的所有成员。A 类将拥有 B 类的所有特征和行为。
被继承的类称为父类、基类、超类
继承的类称为子类、派生类
子类可以有自己的特征和行为
特点
- 单根性:子类只能有一个父类
- 传递性:子类可以间接继承父类的父类
基本语法
1 | class 类名 : 被继承的类名 |
1 | class Teacher |
访问修饰符
protected
不希望外部访问,但子类可以访问。
里氏替换原则
基本概念
父类容器转载子类对象,方便进行对象存储和管理。
重点
任何父类出现的地方,子类都可以替代。
作用
方便进行对象存储和管理。
基本实现
1 | class GameObject |
1 | class Person |
继承中的构造函数
基本概念
先执行父类的构造函数再执行子类构造函数。子类实例化时,默认调用无参构造函数。若父类没有无参构造函数就会报错。
特点
- 当声明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数。
- 父类的无参构造很重要。
- 子类可以通过
base
关键字(代表父类)调用父类构造函数。
1.始终保持申明一个无参构造 2.通过 base 调用指定父类构造 (注意和 this 的区别)
1 | //base调用指定父类构造 |
万物之父&装箱拆箱
基本概念
关键词:object
。object
是一个基类,可以装任何东西。
作用
- 可以利用里氏替换原则,用
object
容器装所有对象。 - 可以用来表示不确定类型,作为函数参数类型。
使用方式
引用类型和里氏替换原则一样用 is 和 as
值类型使用括号强转
1 | class Sb |
装箱拆箱
值类型和 object
之间发生的转换。
装箱
把值类型用引用类型存储,栈内存移到堆内存中。
拆箱
把引用类型存储的值类型取出来,堆内存移到栈内存中。
1 | int a = 10; |
密封类
关键词:sealed
。作用是让类无法被继承,保证程序的规范性和安全性。
1 | sealed class Father |
多态
Vob
基本概念
关键词:virtual(虚函数) override(重写) base(父类)
在执行同一方法时有不同的表现
重写的方法 输入参数的类型和数量要一致(也复合面向对象)
多层继承中也可以使用 层层重写回到父类
作用:其实多态的作用就是把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出更通用的程序。
1 |
|
1 |
|
基本语法
1 | class GameObeject |
抽象类和抽象方法
抽象类
关键字:abstract
不能被实例化 可以包含抽象方法 遵循里氏替换
概念:被抽象关键字 ABSTRACT 修饰的类
特点:
- 不能被实例化的类
- 可以包含抽象方法
- 继承抽象类必须重写其抽象方法
1 | abstract class Fruits |
如何选择普通类还是抽象类
- 不希望实例化的对象 相对抽象的类可以使用 如 :人 person 水果 fruit
- 父类中的行为不太需要被实现 只希望子类去定义具体的规则
- 整体框架设计时会使用 让基类更安全
抽象函数
又叫纯虚方法
关键字:abstract
特点:
- 只能在抽象类中声明
- 没有方法体
- 不能是私有的
- 继承后必须要 override 重写
1 | abstract class Fruits |
虚方法(vritual)和纯虚方法(abstract)的区别
- 虚方法可以在抽象类和非抽象类中声明 纯虚方法只能在抽象类中声明
- 虚方法可以由子类选择性实现 纯虚方法必须实现重写。虚方法有方法体可实现逻辑
- 他们都可以被子类用 override 重写 可以多层重写 子子类重写子类重写父类
接口
基本概念
1 | //接口是行为的抽象规范 |
基本语法
接口的申明
1 | //接口关键字:interface |
1 | interface IFly |
接口的使用
1 | //接口用来继承 |
1 | //声明接口 |
接口可以继承接口
1 | //相当于行为合并 |
1 | interface IWalk |
显示实现接口
1 | //当一个类继承两个接口 |
1 | class Player : IAtk, ISuperAtk |
接口的作用和总结
作用
- 抽象行为:把行为抽象出来供子类继承。个别功能独立在基类外,子类需要时继承。
- 提高程序复用性。
总结
- 继承类:是对象间的继承,包括特征行为等。
- 接口继承:是行为间的继承,继承接口的行为规范,按照规范去实现内容。
- 接口也是遵循里氏替换,所以可以用接口容器装对象,实现装载毫无关系但是却有相同行为的对象。
- 接口包含:成员方法、属性、索引器、事件,且都不实现,都没有访问修饰符。
- 接口继承接口相当于行为合并。
密封方法
关键字:sealed
修饰重写函数
让虚方法或者抽象方法不能再被重写
和 override 一同出现
以下是关于命名空间的概念和用法的 Markdown 格式说明:
命名空间
基本概念
命名空间用于组织和复用代码,类似于一个工具包,将类声明放置其中。命名空间可以分开编写。
- 同一命名空间中的类名不能重复。
- 不同命名空间中的类可以同名。
1 | namespace Mygame |
引用命名空间
在不同命名空间中相互使用时,需要引用命名空间。可以通过两种方法进行引用:
- 使用
using
关键字
1 | using Mygame; |
- 直接使用命名空间的完整路径
1 | // 使用 Mygame 命名空间的 Gameobject 类 |
应对不同命名空间中的同名类
当不同命名空间中有同名类时,可以通过指定完整路径来区分:
1 | using Mygame; |
嵌套命名空间
命名空间可以嵌套使用,类似于小工具包中的小工具包:
1 | namespace MyGame |
使用嵌套命名空间中的类
- 通过命名空间的完整路径
1 | MyGame.UI.Image img = new MyGame.UI.Image(); |
- 使用
using
关键字引入命名空间
1 | using MyGame.UI; |
以下是关于Object
类的各种方法的详细说明及代码示例的 Markdown 格式:
Object
类中的方法
知识回顾
- 所有类型的基类是
Object
。 - 可以利用里氏替换原则装载一切对象,涉及装箱和拆箱。
静态方法
Equals
- 定义:
public static bool Equals(object? objA, object? objB);
- 作用: 判断两个对象是否相等。最终的比较由左侧对象的
Equals
方法决定。 - 示例:
1 | Console.WriteLine(Object.Equals(1, 1)); // 输出 True |
- 值类型判断: 比较两个值是否相等。
- 引用类型判断: 比较两个对象是否指向同一内存地址,而不是判断是否相同类型。
ReferenceEquals
- 定义:
public static bool ReferenceEquals(object? objA, object? objB);
- 作用: 比较两个对象是否是同一个引用。主要用于引用类型的比较,不用于值类型。
- 示例:
1 | Test t1 = new Test(); |
成员方法
GetType
- 定义:
public Type GetType();
- 作用: 获取对象的运行时类型。结合反射相关知识,可以对对象进行各种操作。
- 示例:
1 | Test t = new Test(); |
MemberwiseClone
- 定义:
protected object MemberwiseClone();
- 作用: 获取对象的浅拷贝。返回一个新的对象,新对象中的引用变量与老对象一致。
- 示例:
1 | Test original = new Test(); |
- 浅拷贝: 对于值类型,直接复制;对于引用类型,复制的是内存地址,改变拷贝后的引用类型变量会影响原变量。
Equals
(虚方法)
- 定义:
public virtual bool Equals(object? obj);
- 作用: 比较两个对象是否相等。可以重写该方法以定义自定义的比较规则。
- 示例:
1 | public class Test |
GetHashCode
(虚方法)
- 定义:
public virtual int GetHashCode();
- 作用: 获取对象的哈希码。通常用于哈希表中。可以重写该方法定义自定义的哈希码算法。
- 示例:
1 | public class Test |
ToString
(虚方法)
- 定义:
public virtual string? ToString();
- 作用: 返回当前对象的字符串表示形式。可以重写该方法以定义自定义的对象转字符串规则。
- 示例:
1 | public class Test |
1 | Test t = new Test(); |
string 的常用方法
1 | // 字符串本质是 char 数组 |
StringBuilder
解决字符串性能问题
知识回顾
- 字符串是特殊的引用类型:
- 每次重新赋值或拼接时,会分配新的内存空间。
- 如果一个字符串经常改变,会非常浪费内存空间。
StringBuilder
类
定义:
StringBuilder
是 C# 提供的一个类,用于处理字符串,特别是当需要频繁修改和拼接字符串时。优点:
- 修改字符串而不创建新的对象。
- 提升性能,减少内存浪费。
使用前需要引用的命名空间:
1
using System.Text;
示例代码
1 | // 创建一个 StringBuilder 实例 |
StringBuilder
完全具有引用类型的特征,没有值类型的特征。StringBuilder
自动管理内存扩容,不需要手动调整容量。- 对
StringBuilder
的所有操作,如增、删、改、查、替换等,都不会创建新的字符串实例,而是直接在现有实例上进行修改,提升了性能。
结构体和类的区别
- 存储位置:
- 结构体是值类型,存储在栈上。
- 类是引用类型,存储在堆上。
- 使用区别:
- 结构体和类在使用上很类似,但结构体不具备继承和多态特性。
- 结构体可以视为封装了面向对象思想的一类对象,但由于不支持继承和多态,它的使用范围较窄。
- 结构体不能使用
protected
保护访问修饰符。
细节区别
- 结构体是值类型,类是引用类型。
- 结构体存在栈中,类存在堆中。
- 结构体成员不能使用
protected
访问修饰符,而类可以。 - 结构体成员变量声明不能指定初始值,而类可以。
- 结构体不能声明无参构造函数,而类可以。
- 结构体声明有参构造函数后,无参构造函数不会被自动生成。
- 结构体不能声明析构函数,而类可以。
- 结构体不能被继承,而类可以。
- 结构体需要在构造函数中初始化所有成员变量,而类的成员变量可以不初始化。
- 结构体不能被
static
修饰(不存在静态结构体),而类可以。 - 结构体不能在自己内部声明和自己一样的结构体变量,而类可以。
结构体和类的选择
- 使用继承和多态:
- 当需要使用继承和多态时,选择类。例如,玩家、怪物等对象。
- 数据集合:
- 当对象是数据集合时,优先考虑使用结构体。例如,位置、坐标等。
- 值类型和引用类型赋值:
- 当需要频繁赋值传递且希望改变赋值对象而原对象不跟随变化时,使用结构体。例如,坐标、向量、旋转等。
抽象类和接口的区别
抽象类:
- 使用
abstract
修饰的类和方法。 - 抽象类不能实例化。
- 抽象方法只能在抽象类中声明,必须在子类中实现。
- 使用
接口:
- 使用
interface
修饰。 - 接口是行为的抽象,不包含成员变量。
- 仅包含方法、属性、索引器、事件,成员都不能实现,访问修饰符默认为
public
。
- 使用
相同点
- 都可以被继承。
- 都不能直接实例化。
- 都可以包含方法声明。
- 子类或实现类必须实现未实现的方法。
- 都遵循里氏替换原则。
区别
- 构造函数:
- 抽象类可以有构造函数;接口不能有构造函数。
- 继承:
- 抽象类只能单继承;接口可以多继承。
- 成员变量:
- 抽象类中可以有成员变量;接口中不能有成员变量。
- 方法:
- 抽象类中可以声明成员方法、虚方法、抽象方法、静态方法;接口中只能声明没有实现的抽象方法。
- 访问修饰符:
- 抽象类的方法可以使用访问修饰符;接口中的方法建议不写访问修饰符,默认为
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 类图时先按自己的想法把需要的类整理出来
再把七大原则截图放在旁边,基于七大原则去优化整理自己的设计
整体目标就是:高内聚,低耦合
初学程序阶段
不要过多的纠结于七大原则
先用最适合自己的方法把需求实现了再使用七大原则去优化
不要想着一步到位,要循序渐进
面向对象编程能力提升是需要经验积累的