文章目录
5. 面向对象——封装
5.1 面向对象编程
什么是面向对象编程?
面向对象是一种对现实世界理解和抽象的编程方法,他把相关数据和方法当作一个整体来看待,通过程序来形容对象,从更高的层次来进行程序开发,更加贴近事物的自然运行模式。
为什么要学习面向对象?
使用面向对象的思路进行编程,可以提高代码复用率,提高开发效率,提高程序的可拓展性,同时能够让程序有更清晰的逻辑关系。
面向对象三大特征?
-
封装:将一些属性和相关方法封装在一个类中,对外隐藏内部具体实现细节。外界不需要关心内部函数实现,只需要根据提供的接口去使用即可。他使得代码使用更加方便、内部数据更加安全,同时也让代码更加利于维护。
-
继承:如同现实中子女可以继承父母部分财产一样,面向对象中存在父类和子类的关系,子类可以共享父类的部分资源,提高了代码复用率,其中,父类也叫做基类、超类,子类也叫做派生类。
//Animal基类 class Animal { public virtual void Eat() { Console.WriteLine("动物在吃饭"); } } //Cat子类 class Cat : Animal { public new void Eat() { Console.WriteLine("猫猫在吃饭"); } } //Dog子类 class Dog : Animal { public new void Eat() { Console.WriteLine("狗狗在吃饭"); } }
Animal animal = new(); //指向基类对象 animal.Eat(); //调用基类方法 animal = new Cat(); //指向子类对象 animal.Eat(); //访问子类函数 animal = new Dog(); //指向另一子类对象 animal.Eat(); //访问子类函数
输出结果为:
动物在吃饭 猫猫在吃饭 狗狗在吃饭
-
多态:多态基于继承关系,他让一个变量可以调用多个继承于同一基类的子类的同一个方法,如基类为
动物
类,含有吃
的功能,子类猫
、狗
继承于动物
类,我们可以通过动物
类的变量,通过更改他的指向为猫
或狗
来调用不同类中的吃
方法。
参考链接:
什么是面向对象,它的三个基本特征:封装、继承、多态-CSDN博客
如何理解面向对象编程三大特性------封装、继承、多态 - 知乎 (zhihu.com)
面向对象七大原则
设计目标:开闭原则、里氏替换原则、迪米特原则
设计方法:单一职责原则、接口隔离原则、依赖倒置原则、合成复用原则
开放封闭原则(OCP):
- 扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
- 修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。
里氏替换原则(LSP):
- 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o2都代换成o1时,程序P的行为没有变化,那么类型S是类型T的子类型。
- 所有引用基类(父类)的地方必须能透明地使用其子类的对象。
迪米特法则(LOD)/最少知识原则:
- 每一个类都应该尽可能跟较少的类进行通讯,避免类与类之间的耦合。
单一职责原则(SRP):
- 是一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
接口隔离原则(ISP):
- 对于不同的功能,应该使用专门的接口,尽可能将接口细化。
- 一个类所依赖的接口不应该出现该类所使用不到的功能。
依赖倒置原则(DIP):
- 高层模块不应该依赖低层模块,它们都应该依赖抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
- 要针对接口编程,不要针对实现编程。
合成复用原则(CRP):
- 尽量使用对象组合,而不是继承来达到复用的目的。
5.2 类(class)
什么是类?
- 类的关键词为
class
。 - 类是对一类具有相同特征或行为的事物的抽象,例如我们可以将人、车称为类,单独的一个人称为人的对象。
- 类是对象的模板,可以通过类创建出对象。
如何声明一个类?
访问修饰符 class 类名
{
//属性/字段/特征——成员变量
//行为——成员方法
//构造函数和析构函数
//索引器
//运算符重载
//静态成员
}
例如一个Person
类声明:
public class Person
{
private string name;
private bool sex;
public int age;
public Person(string name, bool sex)
{
this.name = name;
this.sex = sex;
this.age =0;
}
public Person(string name, bool sex, int age)
{
this.name = name;
this.sex = sex;
this.age =age;
}
public string GetName(){
return name; }
public bool GetSex(){
return sex; }
public void SayHello()
{
Conaole.WriteLine($"Hello, my name is {
name}.");
}
}
什么是(类)对象?
- 类的声明不等同于类对象的声明,类的声明与结构体、枚举的声明相似,而类对象的声明则是申请了一个自定义变量类型。
- 类对象是由类创建而来,是一个指定类类型的变量。
- 对象的声明的过程一般又称为实例化对象。
- 类对象是引用类型。
如何实例化对象?
实例化对象原理为先申请对应大小的类空间,再调用构造函数对类中属性进行初始化,最后将类地址存入我们的对象变量中。
他的基础语法为类名 对象名 = new 类名(参数);
(在.Net
高i版本中可以省略new
后面的类名),例如实例化上面的Person
类:
Person person1 = new Person("张三", true);
Person person2 = new Person("李四", false, 30);
除此之外,在申明类对象变量时也可以不进行实例化,而是赋值为空(null
)或不进行赋值(默认为空),如:
Person person3;
Person person4 = null;
5.3 成员变量
什么是成员变量?
- 成员变量是用来描述对象特征的变量,如人的年龄、性别等,它可以是任何数据类型。
- 成员变量声明在类语句块中,一个类中包含的成员变量的数量没有任何限制。
- 成员变量声明时是否赋值进行初始化按照需求来确定。
- 如果成员变量为与自己相同的类,声明时一定不要进行实例化,否则会导致栈内存溢出,程序崩溃。
成员变量的作用?
使用成员变量,可以让属于某一类的特征值存储于类中,如将年龄、性别等存储于人这个类中,实现对数据的封装保存,相对于在外面显式定义这些变量,更加利于我们使用和维护。
如何声明与访问成员变量?
声明成员变量与普通的声明变量相同,唯一区别是要加上访问修饰符(默认为private
),例如在上面Person
类中声明了三个成员变量:
private string name; //姓名,私有
private bool sex; //性别,私有
public int age; //年龄,私有
访问成员变量需要先对对象进行实例化,然后才可以访问他的公有变量(未实例对象不可以访问非静态变量及方法):
Person p1 = new Person("name", true);
p1.age = 20; //可以访问 age 成员变量并对其进行操作
p1.name = " "; //不可进行操作,也不能直接获取对应值,其为私有变量
string name = p1.GetName();//可以通过定义公有方法进行取值操作,同理也可以通过公有方法进行赋值操作
Person p2;
p2.age = 20; //不可进行操作,p2并非实例化对象
访问修饰符是什么?
- 访问修饰符在类中的成员变量和函数方法等最前面加上的一个关键字,它表明了这个类、函数、变量等的可以被访问到的范围,在C#中,类默认是私有的,类中属性和方法也默认是私有的,但结构体中的属性和方法则默认为公有。
- 访问修饰符常见的有三种,分别为
public
、private
、protected
:public
:公共的,自己内部和其他地方(外部)均能访问和使用。private
:私有的,默认访问属性,只有自己内部可以访问,外部不可以进行任何直接操作。protected
:受保护的,自己内部和子类可以进行访问,其他位置不能进行直接操作。
访问修饰符有什么用?
使用访问修饰符可以对被修饰的实现一种保护效果,例如使用private
修饰的变量,他将不能被其他类所使用,保证了变量的安全性。
5.4 成员方法
什么是成员方法及如何声明?
- 成员方法是用来描述对象行为的函数,如人的吃饭、睡觉等都可以看作一个成员方法。
- 他的声明规则与函数相同,声明在类语句块中。
- 成员方法的参数数量、返回值数量及本身在类中的数量不做任何限制。
- 成员方法必须通过实例化对象访问(不可以使用
static
关键字修饰),相当于某一个对象的具体行为。
成员方法的使用
成员方法通过对象名加.
操作符加对应方法名进行使用,例如调用前面Person
类的GetName
方法:
Person p1 = new Person("name", true);
string name = p1.GetName();//返回name
Person p2;
p2.GetName();//无法调用,未进行实例化
5.5 构造函数
什么是构造函数?
- 构造函数时在类进行实例化时会直接调用的函数,他可以对对象进行初始化(如实例化一个人并直接初始化他的名称及年龄),函数名与类名相同,他没有返回值,不需要声明返回值类型,可以使用构造函数来对类中的属性进行初始化赋值等操作。
- 每一个类都会有一个默认无参构造函数。
- 手动书写的构造函数可以设置任意数量的参数,并且构造函数可以进行重载(可以有同名不同参数的多个函数,每个函数都可以进行独立使用),但如果手动写了有参构造函数,默认无参构造函数将不可以进行使用。
- 通常情况下构造函数可访问限制为
public
,但也可以针对具体逻辑进行具体设置。 - 在低版本中允许构造函数中只对部分属性进行初始化,高版本中则必须对所有属性进行初始化。
必须要写构造函数吗?
构造函数并不一定必须写上,写不写构造函数取决于需不需要进行对象的一些初始化逻辑,但不写构造函数时系统会默认生成无参构造函数。当然初始化逻辑也不一定写在构造函数中,可以写成其他函数进行初始化。
如何声明构造函数?
构造函数语法与普通函数一致,唯一区别为函数名比喻为类名且不能声明任何返回值,例如如下的一个类:
public class Person
{
public string name;
public int age;
public char sex;
public Person()
{
name = "无名";
age = 0;
sex = '男';
}
public Person(string name)
{
this.name = name;
age = 0;
sex = '男';
}
public Person(string name, int age)
{
this.name = name;
this.age = age;
sex = '男';
}
public Person(string name, int age, char sex)
{
this.name = name;
this.age = age;
this.sex = sex;
}
}
构造函数的使用如下:
Person p1 = new Person();//无参构造,name = "无名" age = 0 sex = '男'
Person p2 = new Person("张三");//name = "张三" age = 0 sex = '男'
Person p3 = new Person("李四", 20);//name = "张三" age = 20 sex = '男'
Person p4 = new Person("王五", 21, '女');//name = "王五" age = 21 sex = '女'
除了直接写一个方法去初始化之外,还可以用this
来调用已有的函数,执行时会优先调用this
相关函数,在执行函数内部语句块,这样可以简化代码书写量,例如上面的Person
类,我们可以对他进行修改简写,修改后功能相同但是代码量会减少许多:
namespace ClassStudy
{
public class Person
{
public string name;
public int age;
public char sex;
public Person()
{
name = "无名";
age = 0;
sex = '男';
}
public Person(string name) : this()
{
this.name = name;
}
public Person(string name, int age) : this(name)
{
this.age = age;
}
public Person(string name, int age, char sex) : this(name, age)
{
this.sex = sex;
}
}
}
参考链接:构造函数 - C# | Microsoft Learn
什么是静态构造函数?
- 静态构造函数是一个特殊的构造函数,用于初始化函数中的静态变量。
- 静态构造函数语法为
static className(){ ... }
,他不能拥有任何参数。 - 静态函数与普通无参构造函数相互独立,不存在重载的说法。
静态构造函数的作用?
静态构造函数是初始化静态变量的函数,如果类中具有静态变量且需要进行初始化,则一定要写静态构造函数。
静态构造函数和普通构造函数调用的时机
- 普通构造函数在对象进行实例化时调用,每次实例化都会调用对应的构造函数。
- 静态构造函数在类首次进行实例化或首次访问类中静态变量或静态方法时进行调用,整个程序运行中只会调用一次。
5.6 析构函数与垃圾回收机制GC
什么是析构函数?
- 析构函数是在申请的内存被回收时会调用执行的函数,通俗来讲就是在对象使用完成后对他进行清理的函数,可以在析构函数内写入对数据的最后处理逻辑。
- C#中有自动垃圾回收机制GC,他会帮我们堆内存进行回收处理,所以不太会实际应用到析构函数,除非需要做一些特殊的处理。
- 析构函数的语法为:
~类名(){}
。
什么是垃圾,什么是垃圾回收机制(Garbage Collector, GC)?
- 垃圾指没有被任何变量、对象所引用的无用内存,即申请使用后已经没有任何一个变量存储的地址指向他的堆内存。
- 垃圾回收机制则是
.Net
对堆(Heap)内存进行自动化管理,他会对堆进行遍历,在确定某些对象为垃圾时,会自动进行内存释放的机制。 - 垃圾回收有多种算法,如引用计数(Reference Counting)、标记清除(Mark Sweep)、标记整理(Mark Compact)、复制集合(Copy Collection)。
GC如何进行垃圾回收原理?
- 在垃圾回收机制中,不同对象会被使用分带算法划分到不同的内存代中(0代内存、1代内存、2代内存),新分配的对象会被配置在第0代内存中,大对象(内存超过85000字节)则始终被认为存在于第二代内存(目的为减少性能损耗,提高性能)。
- 每一次分配内存时,当第0代内存满时会触发垃圾回收进行内存释放,垃圾回收器将会先将所有对象视为垃圾,再从根(静态字段、方法参数)进行检查引用对象,并将其标记为可达对象,而未被标记的对象则会被视为垃圾进行释放处理;可带对象将会被搬迁移入高一级内存代,并尽可能清理内存碎片,并修改其对应的引用地址(不会对大对象进行搬迁)。
- 1代或2代内存满时会触发低代的内存回收。
- 除等待GC自动回收内存之外,也可以手动触发避免软件运行时突然卡顿(通常会在加载新内容时进行释放而避免在正常运行时释放内存),手动释放方法为调用
GC.Collect()
。
参考链接:.NET 垃圾回收 | Microsoft Learn
5.7 成员属性(Property)
什么是成员属性?
成员属性时用于保护成员变量的字段,他可以在对属性的获取或赋值的时候添加逻辑处理(如加密解密或逻辑判断);属性可以设置只可访问不可修改或只可修改不可访问,解决了原有的访问限制的局限性。
成员属性的声明?
成员属性声明在类代码块中,一般采用大驼峰命名法则,具体形式如下:
public type Name
{
set {
... }
get {
... }
}
-
成员属性代码块中可以仅保留
set
代码块或get
代码块,get
代码块必须要具有一个返回值。 -
成员属性进行修改时,传入数据被存储在变量名为
value
的变量之中,可以直接使用该变量,如在set
中处理逻辑为将传入值赋值给成员变量money
:set { money = value; }
。 -
如果将其赋值给其他成员变量,它本身仍然会被赋值:
-
属性中
get
set
方法前可以添加访问修饰符,当修饰符级别不能大于属性本身访问级别且至少一个方法不进行级别设置,采用属性本身级别。
什么是自动属性?
当我们不需要对某一属性进行任何特殊处理时,可以使用自动属性,即set
和get
不书写任何代码块,类似于public int Money { get; set; }
,这时可以将属性当作成员变量来使用,但若无仅可读等设置,应该尽量使用成员变量而非自动属性,这将节约程序内存占用并提高性能。
对自动属性进行set
赋值操作的结果:
参考链接:使用属性 - C# | Microsoft Learn
5.8 索引器(Indexer)
什么是索引器?
- 索引器类似于属性,他让类或结构的实例化对象可以像数组一样进行访问,我们可以使用索引器来自定义不同索引可以访问的函数或成员变量等内容,实现我们自己的客制化访问。
- 类似于重载
[]
运算符。 - 索引器中
get
set
也可以加入逻辑处理语句。 - 索引器可以像普通函数一样进行重载(修改
[]
中的参数类型或数量)。
索引器基本语法是什么?
element-type this[int index]
{
// get 访问器
get
{
// 返回 index 指定的值
}
// set 访问器
set
{
// 设置 index 指定的值
}
}
例如我们在一个Person
类中让他可以通过[属性名]
获取对应属性值的字符串,可以将索引写为:
public string this[string name]
{
get
{
switch(name)
{
case "name":
return this.name;
case "age":
return age.toString();
case "sex":
return sex.toString();
}
return string.Empty;
}
}
参考链接:
索引器 - C# 编程指南 | Microsoft Learn
C# 索引器(Indexer) | 菜鸟教程 (runoob.com)
5.9 拓展方法
什么是拓展方法?
- 拓展方法指的是对已有的变量类型添加自定义新方法(该类型的私有属性不可访问),通过定义拓展方法可以让我们更方便的使用某一类型变量,以实现开发中个人的需求。
- 拓展方法一定是在静态类中的静态函数,第一个参数必须用
this
修饰,代表被拓展的类型及调用该方法的具体实例。 - 拓展方法第一个参数还可以使用
ref
修饰,他与this
关键字位置可以任意排序。
拓展方法有什么作用?
- 提升程序的拓展性。
- 可以不通过继承来添加方法。
- 不需要在对应类中去书写方法。
- 对别人封装的类添加自己认为有用的额外的方法,如给string添加获取最长重复字符的方法等。
拓展函数的语法是什么?
public static 拓展类类名
{
public static 返回值类型 拓展方法名(ref this 被拓展类型 name, ...)
{
//拓展方法处理语句
}
}
例如,为int拓展修改值为平方并打印的功能:
public static class TestClass
{
public static void GetPow2(ref this int num)
{
int temp = num * num;
Console.WriteLine($"{
num}的平方为{
temp}");
num = temp;
}
}
声明一个int
变量并调用该方法:
int i = 2;
i.GetPow2();
输出结果为:
2的平方为4
参考链接:扩展方法 - C# | Microsoft Learn
5.10 运算符重载
什么是运算符重载?
运算符重载即针对某一类型数据将他对应的操作符进行操作语义的重载,例如自定义水果类实现他们的+
操作相加的是水果的价格这样,用来实现我们需要的一些特殊的功能。
为什么使用运算符重载?
在使用自定义类型时,无法直接通过+
或-
或其他运算符来对对象进行直接的操作,例如我们无法对自定义点坐标进行直接+
或-
的操作,这时我们可以对运算符进行重载让运算符可以实现我们所需的功能。
重载运算符语法?
操作符重载使用operator
关键字,重载函数位置必须在该类之中且为静态公有函数,它的语法如下:
public static 类名 operator 操作符(对应参数)
{
//逻辑语句
return 返回值;
}
例如在Point
类中重载+
运算符使得两点坐标相加:
public class Point
{
public int x;
public int y;
public static Point operator +(Point a, Point b)
{
return new Point()
{
x = a.x + b.x,
y = a.y + b.y
};
}
public Point()
{
x = 0;
y = 0;
}
}
在其他位置使用:
Point point1 = new() {
x = 2, y = 1 };
Point point2 = new() {
x = 1, y = 2 };
Point point3 = point1 + point2;
Console.WriteLine(point3.x + " " + point3.y);
结果为3 3
。
可重载运算符和不可重载运算符
参考官方文档:运算符重载 - C# reference | Microsoft Learn
除运算符外,还可以自定义转换运算:用户定义的显式和隐式转换运算符 - 提供对不同类型的转换 - C# reference | Microsoft Learn