(三)面向对象Object Oriented Programming
1.什么是面向对象
- 面向过程编程的优势就是代码逻辑简单暴力,程序执行单线程流动,完全不需要考虑各种子系统之间的配合。复杂系统则不适用。
- 在面向对象的设计思路中,我们不需要定义代码过程,只需要定义不同的对象、或者说不同的系统之间是如何互相影响、如何互相作用就好了。
![image]()
![image]()
- 创建一个对象的过程就叫做实例化,而实例化的产物就叫做实例、或者叫做对象,或者叫做对象实例。
- 对象与实例可以划上约等号,这两者是我们从不同角度观察世界所得到的不同结论而已。
- 每个对象都是某个类的一个实例
学会从宏观和微观多分析事物的特点和运行规则
2.【拓展】对象与内存管理
2.1 内存生命周期
不管什么程序语言,内存的生命周期基本是一致的:
- 分配内存
- 使用内存(读写操作)
- 用完后释放内存
2.2 内存分区
- 栈区:由编译器自动分配释放 ,存放值类型的数据,引用类型(如对象)的引用地址(指针),如静态区对象的引用地址(指针),常量区对象的引用地址(指针)等。而栈区操作方式顾名思义,类似于数据结构中的栈。
- 堆区(托管堆):用于存放引用类型对象本身,也就是对象数据。在c#中由.net平台的垃圾回收机制(GC)管理。栈,堆都属于动态存储区,可以实现动态分配。
- 静态区及常量区:用于存放静态类,静态成员(静态变量,静态方法),常量对象。由于存在栈内的引用地址都在程序运行开始最先入栈,因此静态区和常量区内的对象的生命周期会持续到程序运行结束时,届时静态区内和常量区内对象才会被释放和回收(编译器自动释放)。所以应限制使用静态类,静态成员(静态变量,静态方法),常量,否则程序负荷高。
- 代码区:存放函数体内的二进制代码。
2.3 内存管理
类似于C这样的底层语言一般都有底层的内存管理方法,比如 分配malloc() 和释放free() 。
相反,C#是一种托管语言,它的垃圾回收机制(GC)是由.net平台负责的。在创建变量(对象,字符串等)的时候自动分配内存,并且在不再使用它们时候“自动”释放。 所以我们在使用过程中极少会考虑到内存使用状况以及项目在运行过程中是如何进行内存管理的。
与C++一样,C#内存也是分区、分类型管理的。
- 值类型:常见的primitive type,比如int char
- 引用类型:继承自System.Object,也就是对象,也包括String
- 指针类型:在内存区中,指向一个类型的引用,通常被称为“指针”(也就是内存地址),它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显式使用。指针在内存中(栈区)占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区(堆区)才是我们真正的数据或者类型。
2.4 栈区管理:
栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;
Int n = 1;// 比如,使用Int声明类型其实就是在为变量n创建了一块大小为4个字节内存,并保存数字1的数据 00000000 00000000 00000000 00000001
栈区内存无需我们管理,也不受GC管理,栈顶元素使用完毕弹出就会立即释放。但是栈内存只能保存可以确定内存大小的数据,也就是只能保存值类型数据。
一个对象所占用的内存大小无法确定,所以不能保存在栈区,只能保存在堆区。
2.5堆区管理:
堆区是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。在C#中堆区内存由GC(Garbage collection:垃圾收集器)负责清理,当对象超出作用域范围或者对象失去指向的引用地址,就会在一定时间内进行统一的处理,无需程序员手动处理。
User user1 = new User() {
age: 1,
name: "alex"
};
// 声明初始化一个对象实例
// 1. 在堆区中创建了一块可以动态拓展大小的内存块,并把数据 "{ age: 1,name: "alex" }" 保存进去
// 2. 在栈区创建了一个指向堆区的指针(也就是堆内存的引用地址),名称叫做user1.
在上面的例子中,当我们的程序使用user1的时候,其实使用的并不是数据 { age: 1,name: "alex" },而是一串指向它的内存地址。所以,我们在在使用user1的时候,实际上使用的是他的内存地址。
User user1 = new User() {
age: 1,
name: "alex"
};
User user2 = new User() {
age: 1,
name: "alex"
};
Console.WriteLine(user1 == user2);
->false,因为user1和user2的所代表的内存地址不一样,在对象中使用 == 操作实际上是在判断他们的内存地址是否相等。
3. 类class与实力instance
Point是坐标class;dynamic是动态类型,会有类型安全的隐患;var不能脱离数据类型使用;
//dynamic a = new { x = 15, y = 10 };
//DrawPoint(a);
Point a = new Point();
a.x = 15;
a.y = 10;
DrawPoint(a);
Console.Read();
return;
}
public class Point
{
public int x;
public int y;
}
public static void DrawPoint(Point point) =>
Console.WriteLine($"左边的点为x:{point.x},y:{point.y}");
4. 对象聚合
功能高内聚,模块低耦合。
思维:要把与这个对象相关的功能放在对象内部,初始化放在构造函数
5.构造函数和方法重载
访问修饰符 class名()
{
代码逻辑,用于初始化数据等
}
这种名称一致、但是参数有所区别的方法声明方式就叫做方法的重载,英文叫做overload。
构造方法的原理:
- 当一个类在new实例化对象时,将会调用构造函数进行初始化对象。
- 不带参数的构造函数称为“默认构造函数”。
无论我们有没有在class中声明构造函数,默认构造函数都会存在。在代码编译的过程中,C#编译器器将为无构造函数的类创建一个公共的、默认的构造函数。可以认为只要使用了new,就一定会启动构造函数来初始化对象。 - 构造函数的访问修饰符可以设置为私有 private,通常在工厂模式或者单例中出现。
6.访问修饰符
q:如果对象属性可以在外部随意修改是非常危险的
可使用访问修饰符指定以下 7 个可访问性级别:
- public:访问不受限制。
- protected:访问限于包含类或派生自包含类的类型。
- internal:访问限于当前程序集。
- protected internal:访问限于当前程序集或派生自包含类的类型。
- private:访问限于包含类。
- private protected:访问限于包含类或当前程序集中派生自包含类的类型。
- file:已声明的类型仅在当前源文件中可见。 文件范围的类型通常用于源生成器。
7. 字段、属性与对象封装
访问修饰符 声明修饰符 class 类名
{
Fields 字段
Properties 属性
Methods 方法
Constructors 构建函数
Destructors 析构函数
}
官方定义:
属性:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/properties
字段:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/fields
- 属性其实是一种特殊的方法
- 而字段则是真正意义上的变量、是数据。
- 从依存关系上来说,字段可以独立存在,但是属性则必须依赖于字段存在的
- 我们可以简单的把属性看作从外部访问class内部字段的操作方法。
- 通过属性,我们可以隐藏字段的细节,同时在赋值或者提取字段的时候设置一些规则,从而让我们的代码更加健壮。
- 属性propety X 是方法,它可以被调用,但是声明方式和使用方式却与变量一样,总之属性是一个比较特殊的语法结构
- 封装就是,能够隐藏对象的属性细节,仅对外公开接口,控制对象属性的读、写访问级别。setter和getter就是封装过程的典型代表。
8.const、readonly与writeonly
从getter、setter的角度来说来说,readonly就是只有getter的属性,而writeonly就是只有setter的属性
实例:



长、宽、高反应在代码中就是x轴、y轴、z轴,他们都有setter和getter。而对于第四个4维度时间来说,我们人类只能感知而不能操纵,黑洞的中心质量无限大、体积无限小,科学家叫它奇点 singularity,这就是我们的第五维。从代码角度来定义,他就是一个只能写入、不能读取的维度,具备writeonly,只写属性。
通过对构造函数的重载,我们实现了beta轴的不同初始化数据,而且因为使用了readonly,beta的数据一旦确定就永远无法修改,与常量类似。
所以,常量const与readonly的区别就是,
- const是常量,readonly是变量;
- const必须在声明同时赋值,而readonly可以在声明暂时不赋值,当class初始化的时候再进行赋值
所以,readonly比const灵活性更高。不过灵活性往往建立在牺牲性能的前提下,const是在代码编译前就能定下来,所以他性能比readonly要高出不少。所以,在同学们真正工作中,我们需要按照项目的实际需要来判断什么时候使用const、什么时候使用readonly。
9.索引(index)范围(range)
- 对于单词列表的第1项到第三项,我们可以把这部分的索引提取出来,变成变量重复使用。
Range phrase = 1..4; - 与范围不同的是,索引需要使用index 类型。
Index dog = ^1;
范围我们需要使用 range 类型,比如 range 1 到 3。
string[] words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
Console.WriteLine(words[0]);
Console.WriteLine(words[^1]);
var list = words[0..3];
for(int i = 0;i<list.Length; i++)
{
Console.WriteLine(list[i]);
}
10.partial类

- 可以用来定义局部类或者局部方法。
- 通过局部类型可以实现将一个类、结构、接口甚至是方法分成几个独立的代码片段,或者放在在几个不同的.cs文件中。
- 而在程序进行编译的时候,编译器将会找出这些独立的片段,并在编译的最后合并、并且自动生成一个完整的类。
- 可以有效降低我们单个文件的代码复杂性,让代码更加容易被维护。
10.1 适用范围
- 类型特别大,不适合放在一个文件中实现。
- 一个类型中的一部分代码为自动化工具生成的代码,不宜与我们自己编写的代码混合在一起。
- 一个类同时需要多个人同时编写的时候。
10.2 注意事项
- 只适用于类、接口、结构,不支持委托和枚举。
- 同一个类型的各个部分必须有修饰符partial。
- 使用局部类型时,一个类型的各个部分必须位于相同的命名空间中。
- 一个类型的各个部分必须同时被编译。
- partial只有和class、struct、interface放在一起时才表示局部类型。
- 一个类型的各部分的访问修饰符必须保证一致性。
- 局部类型的累加效应。
练习代码见仓库




浙公网安备 33010602011771号