Csharp学习档案
学习手册:
w3schools 教程 从前端到后端到数据库,各种语言教程一应俱全
枚举
(1)枚举的申明
enum E_ZiDan |
枚举项未赋值时,从0开始自动赋值,往后依次累加,给某个枚举项命名后,从这个项往后依次累加。申明枚举时要在namesapce中命名。
例子:
申明一个怪物类型的枚举项
enum E_MonsterType |
申明一个玩家类型的枚举项
enum E_Player |
(2)枚举变量的申明初始化和使用
//声明 |
变量的类型及转换(显隐转换)
变量类型
//有符号 |
隐式转换(直接等号转换)
大范围包含小范围,基本相同大类型互相转换,特使类型中,char可以转换为int以外,bool和string都不能隐式转换
显示转换
括号转换
1.变量名=(转换类型)变量名;
2 .bool和string不能通过括号转换强转
int x; |
Parse法 (把字符串类型转换为相应的类型)
(例如接受用户输入的字符串,将用户输入的字符串转换为相应的可以进行运算的数值)
语法:变量类型.parse(“字符串”)
int i = int.parse(“123”); |
注意:字符串必须能够转换成相应类型否则报错
Convert法(更准确地将各个类型之间进行转换)
语法: convert.To目标类型(变量或者常量)
例如:
int a = convert.ToInt32(“123”); //结果: 123 |
其他类型转string
1.convert法:
int a = 123; |
2.toString法:
语法: 变量.toString();
string str = 1.toString(); //整型 |
另一种写法
Int a = 6; |
在下面例子中,计算机用到了此方法进行转换
Console.WriteLine(“12345” + 1 + true ); |
数组与数组之间的转换
Array.ConvertAll<转换的对象的元素类型,要转换为的元素类型>(转换对象,转换方法)
语法:
//TInput 和 TOutput 分别是源数组和目标数组。 |
//括号里面的第二个参数使用的lamda表达式。 |
异常捕获
作用:避免代码报错时出现的程序卡死。
语法:
//必备部分 |
例子:
try |
字符串拼接(非打印拼接)
1.字符串用+号拼接
//示例1 |
2.字符串占位拼接
语法: string.Format(“带拼接的内容{0},带拼接的内容{1},”,内容0,内容1);
//此句中,18是整型,但是调用了toString,也可填写true,123.45f等其他类型 |
位运算符和三目运算符
位运算符
位与 & 两个位都为1时,结果才为1
位或 | 两个位都为0时,结果才为0
异或 ^ 两个位相同为0,相异为1
取反 ~ 0变1,1变0
左移 << 各二进位全部左移若干位,高位丢弃,低位补0
右移 >> 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)
三目运算符
// condition ? expression1 : expression2; |
C#中的数组
一维数组
//语法: 变量类型 [] 变量名 |
二维数组
语法: int[,] arr = new int[2,3]{1,2,3,4,5,6};
写法和一维数组相同
使用:
获取数组长度
获取行长: arr1.GetLength(0)
获取列长: arr1.GetLength(1)
遍历二维数组:for(int i=0;i<arr1,GetLength(0);i++) //行
{
For(int j=0;j<arr1.GetLength(1);j++) //列
{
}
}
扩容二维数组:(和一维数组同一思想)
交错数组:
概念:装数组的数组,类似于指针数组;相当于二维数组,但是列长可以不一样
申明: 变量类型[][] 变量名= new 变量类型[交错数组长度][]{一维数组1,2,……};
举例: int[][] array = new int[3][]{ new int[]{1,2,3},
new int[]{4,5},
new int[]{7,8,9,10}};
获取长度 array.GetLength(0); 获取行长
array[i].GetLength(1); 获取某一列的长度
获取其中的元素 array[0][1] 获取第一行第二列的元素
遍历交错数组:
for (int i = 0; i <array.GetLength(0); i++)
{
for (int j = 0; j < array[i].Length; j++)
{
Console.Write(array3[i][j]+” “);
}
}
扩容交错数组:
和数组扩容同一原理
值和引用(相当于c语言中的指针和值类型)
引用类型:
- string 、数组 、Class(类)
- 在堆里申请的内存,在栈里存放的是地址)
- 当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
值类型:
- 除以上三种类型的其他类型( byte,short,int,long,float,double,decimal,char,bool 和 struct )
- (在栈里申请内存)
由此可知
//example 1 |
特殊的引用类型string
string类型是不可变的字符串,一旦创建不能修改。string类型在多次赋值时,会产生新的内存,将新的内容放在新的内存中,然后string存放新内存的地址,但是旧的内容的内存不会被回收,这样,多次给string重新赋值时,会产生内存垃圾。
string str1= “12345”; |
所以当我们对string需要非常频繁地拼接或修改字符串时,会产生非常大的内存消耗。这时候我们可以选用 StringBuilder 类来进行操作。(需要引用命名空间 using System.Text; )
StringBuilder 是可变的字符串缓冲区,适合频繁拼接或修改字符串的情况。
using System.Text; |
| 比较项 | string |
StringBuilder |
|---|---|---|
| 是否可变 | ❌ 不可变,每次修改都会创建新字符串 | ✅ 可变,修改不会创建新对象 |
| 性能(频繁操作) | ❌ 较差:每次拼接都会生成新对象 | ✅ 较好:内部使用字符数组动态扩容 |
| 内存效率 | ❌ 差(频繁拼接会产生很多临时字符串) | ✅ 高效(在原有缓存上操作) |
| 适用场景 | 小量拼接、只读操作、配置读取、UI显示等 | 循环拼接、日志记录、大量字符串拼接场景 |
| 线程安全 | ✅ 线程安全(不可变) | ❌ 默认不是线程安全的 |
| 常用操作 | +、Replace、Substring等 |
Append、Insert、Replace、Remove等 |
| 你要做的事 |
推荐使用类型 |
|---|---|
| 拼接几段字符串 | string |
| 循环中拼接字符串(如生成代码、日志) | StringBuilder |
| 替换、截取少量字符串内容 | string |
| 对性能要求高的字符串处理 | StringBuilder |
深拷贝和浅拷贝
深拷贝和浅拷贝详解
- 对于所有面向对象的语言,复制永远是一个容易引发讨论的题目,C#中也不例外。
- 深拷贝:指的是拷贝一个对象时,不仅仅把对象的引用进行复制,还把该对象引用的值也一起拷贝。这样进行深拷贝后的拷贝对象就和源对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。 (拷贝整个对象的内容,包括其内部引用指向的对象也会新建一份)
- 浅拷贝:指的是拷贝一个对象时,仅仅拷贝对象的引用进行拷贝,但是拷贝对象和源对象还是引用同一份实体。 (拷贝对象的“引用”地址,新对象和原对象指向同一块内存。)
学习过上面的值类型和引用类型后,我们可以知道,值类型是深拷贝,是开辟了新的内存并复制了值。而引用类型存储的是值所在内存的地址,所以引用类型的复制只是复制了值所在的地址,并没有再新开辟一个内存把值复制进去。(要注意C#里的string类型是一个特殊的引用类型)
public class Person |
C#实现自定义对象或数据集合的深拷贝
有时候我们需要创建一个对象副本,且修改对象副本不会改变源对象的数据,这就需要使用深拷贝。那么我们在C#如何让引用类型也能实现深拷贝呢?
1.使用序列化和反序列化
网上有许多使用 BinaryFormatter 进行序列化的方法,但是这个方法在NET 5及以上已被标记为不安全和过时,且有安全风险。现在我们会用 System.Text.Json 序列化来替代。记得引入命名空间 using System.Text.Json;
在.NET Core 3.0及以上版本中,可以使用System.Text.Json库进行深拷贝。原理是使用JsonSerializer.Serialize()将对象转换为Json字符串,再使用JsonSerializer.Deserialize()将Json字符串转换为新的对象。
//注意:在Unity老版本不支持System.Text.Json进行序列化和反序列化,因为Unity老版本使用的是老版本.NET。 |
2.使用Newtonsoft.Json
``` |
函数举例:
static int[] sayhello(int a,int b) |
关于return:
对于有返回值的函数,return需要返回函数声明的返回值类型的值。
对于无返回值函数,单独一个
return可以直接返回函数外部,后面的代码不执行。
static void speak(int a,int b) |
ref和out(用于函数形参和实参的传递)
作用:
- 当传入函数参数时,如果为值类型,在函数内修改形参不会改变实参。
- 若使用
ref 或out,则函数内的修改会影响函数外的实参。
语法: 在传入参数前加ref或out
例子:
static void ChangeVertex(ref int a, ref int b) |
Ref和out的区别
| 区别点 | ref |
out |
|---|---|---|
| 调用前是否必须赋值 | ✅ 必须在调用前初始化 | ❌ 不需要在调用前初始化 |
| 方法内是否必须赋值 | ❌ 不强制赋值 | ✅ 方法内部必须赋值,否则编译错误 |
| 场景 | 双向传值 | 单向输出结果 |
Out示例:
static void ChangeVertexO(out int a, out int b) |
ref 示例
static void ChangeVertex(ref int a, ref int b) |
变长和参数默认值:
变长参数(可变参数)
- 关键字:
params - 语法:
static 返回类型 函数名(params 类型[] 数组名) |
注意事项:
-
params 修饰的参数必须是最后一个参数。 - 它前面可以有其他参数,但后面不能再有其他参数,否则编译报错。
-
示例:
static int Text(params int[] a) |
调用示例:
int result = Text(1, 2, 3, 4); // 输出:10 |
参数默认值(可选参数)
什么是参数默认值?
当函数参数设置了默认值,即可在调用函数时选择是否传入对应实参,如果不传入,则会使用默认值。
示例代码:
static void Speak(string str = "我什么也不想说") // 设置默认值 |
使用方式:
Speak(); // 输出:我什么也不想说 |
注意事项:
- 可选参数必须放在参数列表的最前面。
- 因为如果默认值参数在前,调用函数时会发生实参与形参匹配不明确的问题。
示例代码:
//正确示例 |
函数重载
函数重载:
重载允许在同一作用域内定义多个同名函数。
具有相同函数名的函数,但是参数的类型、个数或者顺序不同的函数,叫做函数重载。
返回值与函数重载无关,即返回值可相同可不同,但是传入参数的类型、个数或者顺序要有一个不同。
函数重载注意用于处理不同参数同一类型的逻辑处理
举例:
//这三个函数的函数名相同,但是参数的个数和顺序或者类型不同,所以这三个函数重载。 |
递归函数
递归函数:就是让函数自己调用自己。
条件:一个正确的递归函数,必须有结束条件,这个条件必须是根据循环的次数改变而改变的,否则就会陷入无限循环。
普通举例:
static void Test_DiGui(int a) |
遍历目录示例:
using System; |
C#结构体
结构体是一种自定义的变量类型,他是数据和函数的集合,在结构体中可以申明各种变量和方法(函数)
作用: 用来表现存在关系的数据集合,比如结构体表现学生、怪物、动物等
基本语法:
- 结构体一般写在
namespace空间里,且里面的变量和函数默认为private类型 - 如果想要外部调用,要在前加
public。函数在结构体里不用加static。结构体里还可以写构造函数。
}
Struct 结构体名 |
举例:
struct Student |
排序算法
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复地遍历待排序的列表,比较相邻的元素并交换它们的位置来实现排序。该算法的名称来源于较小的元素会像”气泡”一样逐渐”浮”到列表的顶端。
算法步骤
- 比较相邻元素:从列表的第一个元素开始,比较相邻的两个元素。
- 交换位置:如果前一个元素比后一个元素大,则交换它们的位置。
- 重复遍历:对列表中的每一对相邻元素重复上述步骤,直到列表的末尾。这样,最大的元素会被”冒泡”到列表的最后。
- 缩小范围:忽略已经排序好的最后一个元素,重复上述步骤,直到整个列表排序完成。
原理说明:
设数组元素个数为 n,从数组下标为 0 的元素开始,每次两两比较相邻元素,把较大的(或较小的)元素“冒泡”移动到后面。
- 第一轮比较 n-1 次
- 第二轮比较 n-2 次
- ……
- 第 n-1 轮比较 1 次
在第 x 轮中,后面的元素已经排好,无需再次比较,因此每轮的比较次数会逐渐减少。
| 第几轮(x) | 比较次数(m-x) |
|---|---|
| 第 1 轮 | m - 1 |
| 第 2 轮 | m - 2 |
| 第 3 轮 | m - 3 |
| … | … |
| 第 n 轮 | m - n = 0 |
说明:m = 数组长度
循环结构说明:
冒泡排序中包含两层 for 循环:
外层循环:控制“轮数”
for (int i = 0; i < arr.Length - 1; i++) |
- 从第 0 轮开始
- 共进行
n - 1 轮
内层循环:每轮中的“两两比较”
for (int j = 0; j < arr.Length - 1 - i; j++) |
- 比较相邻两个元素
arr[j] 和arr[j+1] - 若顺序不对则交换
C#实现冒泡排序:
//降序排序(从大到小) |
优缺点
优点:
- 实现简单,代码易于理解。
- 原地排序,不需要额外的存储空间。
缺点:
- 效率较低,尤其是对于大规模数据集。
- 不适合处理几乎已经有序的列表,因为仍然需要进行多次遍历。
选择排序
什么是选择排序
选择排序基本思想是每次从待排序的数据中选择最小(或最大)的元素,放到已排序序列的末尾,直到全部数据排序完成。
算法步骤
- 初始化:将列表分为已排序部分和未排序部分。初始时,已排序部分为空,未排序部分为整个列表。
- 查找最小值:在未排序部分中查找最小的元素。
- 交换位置:将找到的最小元素与未排序部分的第一个元素交换位置。
- 更新范围:将未排序部分的起始位置向后移动一位,扩大已排序部分的范围。
- 重复步骤:重复上述步骤,直到未排序部分为空,列表完全有序。
设数组长度为 n,排序过程如下:
- 第1轮:从下标
0~n-1 中找到最小值,放到第0位; - 第2轮:从下标
1~n-1 中找到最小值,放到第1位; - …
- 第n-1轮:只剩一个元素,已自动排好。
假设原数组为:[64, 25, 12, 22, 11]
| 轮次 | 当前数组状态 | 操作说明 |
|---|---|---|
| 第1轮 | [64, 25, 12, 22,11] | 找到最小值11,和64交换 |
| [11, 25, 12, 22, 64] | ||
| 第2轮 | [11, 25,12, 22, 64] | 找到12,和25交换 |
| [11, 12, 25, 22, 64] | ||
| 第3轮 | [11, 12, 25,22, 64] | 找到22,和25交换 |
| [11, 12, 22, 25, 64] | ||
| 第4轮 | [11, 12, 22, 25, 64] | 已排好 |
代码实现
//升序(从小到大) |
优缺点
优点:
- 实现简单,代码易于理解。
- 原地排序,不需要额外的存储空间。
- 对于小规模数据集,性能尚可接受。
缺点:
- 时间复杂度较高,不适合大规模数据集。
- 不稳定排序算法(如果存在相同元素,可能会改变它们的相对顺序)。
面向对象编程
面向对象关键知识:
- 类(class)
面向对象三大特征
-
封装:用程序语言来形容对象 -
继承:复用封装对象的代码:儿子继承父亲,复用现成代码。 -
多态:同样行为的不同表现,儿子继承父亲但是有不同的行为表现。
面向对象七大原则
- 开闭原则
- 依赖倒转原则
- 里氏替换原则
- 第一职责原则
- 接口隔离原则
- 合成复用原则
- 迪米特法则
类和(类)对象
什么是类
类的定义: 具有相同特性和行为的对象组成的集合就是类。类是对象的抽象,对象是类的具体实例。 比如你要买一辆车,车有品牌,排量,颜色,款式,这时车就是类,而当你看中了一个品牌,排列,颜色,款式确定的一款车时,这个车就是类的实例。
类是一个引用类型;
类的声明一般声明在
namespace空间中,而不是定义在class Program 里。在类中申明类是类的内部类。
类的声明
访问修饰符 Class 类名 |
类声明实例
Class Person //人类 |
(类)对象
类的申明相当于申明了一个自定义变量类型。
(类)对象的申明是相当于申明了一个指定类的对象。
类创建对象的过程,一般称为实例化对象。
(类)对象是引用类型。
引用类型是在栈上申请了一个内存,存放地址,new后在堆上申请空间,将地址存在栈上。
对象的申明:
类名 变量名; //声明变量但不初始化。类成员的引用类型变量如果没初始化,会自动是 null。 |
三种声明方法对比
| 写法 | 是否赋值 | 是否可以使用变量 | 是否分配内存 |
|---|---|---|---|
Person p; |
❌ | ❌ 编译错误 | ❌ |
Person p = null; |
✅ null | ❌ 运行时错误 | ❌ |
Person p = new Person(); |
✅ 实例 | ✅ 正常使用 | ✅ |
实践建议
| 场景 | 推荐写法 |
|---|---|
| 延迟初始化 | Person p = null; |
| 立即使用 | Person p = new Person(); |
| 先声明,后赋值(临时变量) | Person p;,但马上赋值 |
实例化对象:
假设申明了一个Person类。
class Person |
成员变量和访问修饰符
C#访问修饰符:
| 修饰符 | 可访问范围 | 应用场景常用位置 | 示例 |
|---|---|---|---|
public |
任何地方都可访问 | 类、成员、属性、方法 | public class Person { public string Name; } |
private |
仅在当前类内部可访问 | 类内部成员、字段 | private int age; |
protected |
当前类及其派生类(子类) 中可访问 | 继承场景的成员字段 | protected string password; |
internal |
当前项目(程序集)中可访问 | 库组件、模块间通讯 | internal class Utils { ... } |
protected internal |
当前程序集或子类(无论在不在当前程序集)中可访问 | 跨项目但需要继承支持时使用 | protected internal void Log() {} |
类里的成员变量:
//注:成员变量声明后可赋值,与结构体不同。 |
类里面可以声明自己类的对象(变量),例如:
class Person |
注:
- 如果在类中对(类)对象进行new(构造成员变量)—— Public Person girlfriend=new Person();则会使类进行无限递归,而爆掉内存。
- 类中的成员变量会有默认值,例如,值类型默认值:int默认值为0;,bool为false等。引用类型都是空(NULL)
- 看默认值小技巧Console.WriteLine(default(变量))
成员方法(函数):
成员方法(函数),用来表现对象行为,申明在类语句块中,规则和函数申明规则一致,可受访问修饰符影响,
1.成员方法不要加static
2.成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行了某个行为,
3.成员方法受访问修饰符影响
如果成员方法前加staic:
在C#程序中,当你在一个类中将方法声明为静态(static)时,意味着该方法不属于任何特定的实例对象,而是属于整个类本身。这意味着你可以直接通过类名来访问该静态方法,而不需要实例化类对象。
例如Person类中的Eat()方法,如果你将其前缀声明为static,那么就可以通过Person.Eat()的方式直接调用该方法,而不需要先创建Person类的实例。
如果你在代码中定义了一个静态的Eat()方法,它可以直接执行一些与吃饭相关的操作,而不需要依赖于Person类的实例。这样的静态方法可以在不创建实例对象的情况下被调用,使得代码更加简洁和高效。
静态方法只能访问静态成员(如静态字段或其他静态方法),不能访问实例成员(如实例字段或实例方法),因为静态方法没有与特定对象实例相关联。
如果你需要执行某个与类相关,但是不依赖于类的实例对象的操作,那么可以将该操作声明为静态方法。例如,用于计算数字之间的某个关系,或者用于管理一个公共资源池等。
举例:
Class Person |
例2:
class Person |
构造函数(初始化调用)、析构、垃圾回收
构造函数:
构造函数只能在类中声明,默认有一个0参的(无传入参数)构造函数(用于对变量成员的初始化)。
构造函数用于给类的成员属性进行赋值并初始化一个新的对象。
例如类的实例化: Person p1 = new Person();这句的 new Person() 就相当于调用了无参的构造函数初始化一个空对象。当在类中重新申明一个构造函数时,默认的0参构造函数就会被回收。
构造函数可以重载,如果想要0参的构造函数,可以手动再次申明。
构造函数申明语法:
Class 你的类{ |
构造函数声明示例:
//举例: |
构造函数的特殊写法
Public Perosn(参数):this(参数); |
这种写法的作用是,当调用这个构造函数时,会先调用this(参数)构造函数,this(参数)相当于调用public Person (参数);
这种构造函数的运行逻辑是
class Person |
析构
当引用类型的堆内存被回收时,会调用该函数
C#存在自动垃圾回收机制GC,所以不怎么用析构函数。
只做了解即可。
如果想要在GC回收时执行一些自定义逻辑可以写里面。
基本语法:
~类名() |
举例
class Person |
垃圾回收机制(Garbage collector)
GC只负责堆里面的内存的垃圾回收。
值类型在栈中分配的内存,他们有自己的生命周期,会自动分配和释放。
Heap:堆
Stack:栈
垃圾回收主要算法:
- 标记-清除算法
- 压缩算法
- 分代清除算法
手动调用GC:
GC.collect(); //手动触发垃圾回收|在Unity里面一般都是在加载场景(进度条)时使用,不会频繁调用 |
成员属性(包裹成员变量)
基本概念:
- 用于保护成员变量
- 为成员属性的获取和赋值添加逻辑处理
- 解决3p的局限性(private——内部访问、public——内外、protected——内部和子类)
- 属性可以让成员变量在外部 只能获取,不能修改,或者只能修改,不能获取
- 可以在里面执行一些自定义逻辑,比如对数据进行加密和解密
语法:
访问修饰符 属性类型 属性名 |
申明举例:
|
自动属性
//如果类的成员属性没有什么特殊处理,那么可以用自动属性。 |
索引器
基本概念: 为类定义一个索引器,让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写。
语法:
//声明方式一: |
声明方式一索引器使用举例:
//声明学生类 |
声明方式二索引器举例:
class Student |
静态成员
被 static 修饰的成员,叫做静态成员。静态成员是属于类的,通过类名直接访问。
- 当类第一次被访问时,类下的所有静态成员就会被创建在内存中。
- 静态成员既不在栈中也不在堆中,而是创建在 静态存储区。
- 静态成员 只创建一次,直到程序结束前都不会被销毁。
- 静态成员函数 不能访问非静态成员。
- 静态成员 可以访问其他静态成员或函数。
示例代码:
class Person |
const 和 static 的区别
相同点:
- 都可以通过类名访问。
不同点:
-
const 必须初始化,且不可修改;static 没有此限制。 -
const 只能修饰变量;static 可以修饰变量、方法、类、构造函数等。 -
const 必须写在变量声明前面;static 没有此限制,通常可配合访问修饰符使用。
静态成员实现单例模式(不安全示例)
这是一个不推荐在生产环境中使用的单例实现,因为它在多线程场景下不安全。
public sealed class Singleton |
问题:
- 如果两个线程同时执行
if (instance == null),就可能创建多个实例,违背了单例的设计初衷。
线程安全的单例实现方式
方法一:使用 lock 加锁(懒汉式,线程安全)
这是最常见的一种线程安全实现方法:
public sealed class Singleton |
说明:
-
lock 保证线程互斥地访问实例创建逻辑。 - “双重检查锁定”是为了避免每次访问都加锁,提高性能。
- 缺点是代码稍复杂,但性能和安全兼顾。
方法二:静态构造函数(饿汉式,线程安全,推荐)
利用 C# 静态构造函数的特性:
public sealed class Singleton |
说明:
- 静态构造函数只执行一次,CLR 保证其线程安全。
- 在类被首次使用前就完成实例初始化。
- 缺点是:实例会 在程序启动后就创建,即使永远不会被用到。
方法三:使用 Lazy<T>(懒汉式,线程安全,现代推荐方式)
C# 提供了 System.Lazy<T> 类,专为延迟初始化设计:
public sealed class Singleton |
说明:
-
Lazy<T> 默认采用LazyThreadSafetyMode.ExecutionAndPublication,线程安全。 - 实例在 首次访问时才创建,且线程安全,效率高。
- 是 C# 中实现单例的 现代推荐写法。
| 实现方式 | 是否懒加载 | 是否线程安全 | 是否推荐 |
|---|---|---|---|
| 简单懒汉式 | ✅ 是 | ❌ 否 | ❌ 不推荐 |
| lock双重检查 | ✅ 是 | ✅ 是 | ✅ 推荐 |
| 静态构造函数 | ❌ 否 | ✅ 是 | ✅ 推荐 |
| Lazy | ✅ 是 | ✅ 是 | ✅ 最推荐 |
静态类和静态构造函数
静态类(static class)
特点:
- 只能包含 静态成员
- 不能被实例化
- 不能继承或被继承
作用:
- 将静态成员集中放入静态类中,方便调用
- 静态类不能实例化,更能体现其作为工具类的唯一性
- 常用于存放工具方法(如:数学计算、字符串处理等)
✅ 例如:Console 就是一个静态类
static class Test |
调用方式:
Test.SayHello(); // 不需要实例化 |
静态构造函数(static 构造函数)
特点:
- 可存在于 普通类 和 静态类
- 没有访问修饰符(如 public、private 等)
- 不能带参数
- 系统自动调用:在第一次访问类的成员时自动执行
- 只会被调用 一次
作用:
用于 初始化静态字段或执行类级别的一次性任务
语法格式:
class MyClass |
调用示例:
Console.WriteLine(MyClass.Number); // 首次访问类,静态构造函数被调用 |
拓展方法
可以在不修改某一个类的情况下,为该类扩展新的成员方法,实现方法的扩展。注意,不能为静态类扩展方法。
为一个类添加扩展方法需要满足三个要素:
- 扩展方法所在的类必须是静态类。
- 扩展方法本身必须是静态方法。
- 扩展方法的第一个参数需要使用关键字
this,指定要扩展的类。
示例语法:
// 静态类 |
使用方法也很简单:
int a = 9; |
扩展方法也可以带多个参数:
public static class TestExtensionM |
使用示例:
int a = 10; |
注意事项:
- 如果扩展的方法和类里面原有的方法重名,则调用该方法时运行类里面原有的方法
运算符重载
运算符重载:允许自定义类型(类或结构体)重新定义标准运算符的行为,使得该类型的对象能够像内置类型一样,直接使用运算符进行操作。
作用:
- 让自定义类型的对象更自然地使用
+、-、*、== 等运算符。 - 提升代码的可读性和可维护性。
- 实现特定类型间的运算规则。
注意事项:
- 只能重载已有的运算符,不能创造新的运算符。
- 至少要重载一对相关运算符(比如
== 和!=)。 - 重载运算符必须是
public static 方法。 - 运算符重载不是必须的,只在需要时使用。
基本语法:
public static 返回类型 operator 运算符(参数列表) |
C# 中运算符重载的能力:
| 运算符 | 描述 |
|---|---|
| +, -, !, ~, ++, – | 这些一元运算符只有一个操作数,且可以被重载。 |
| +, -, *, /, % | 这些二元运算符带有两个操作数,且可以被重载。 |
| ==, !=, <, >, <=, >= | 这些比较运算符可以被重载。 |
| &&, || | 这些条件逻辑运算符不能被直接重载。 |
| +=, -=, *=, /=, %= | 这些赋值运算符不能被重载。 |
| =, ., ?:, ->, new, is, sizeof, typeof | 这些运算符不能被重载。 |
继承
类的继承
继承是面向对象编程的基本特性。
继承允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码复用和扩展。
语法: :类名
举例:
class Person() |
继承构造函数:
- 初始化子类时,会先调用父类的默认构造函数,然后再调用子类的构造函数。
- 当父类或子类定义了构造函数时,默认的无参构造函数会被覆盖。
- 在子类声明构造函数时,如果想指定调用父类的某个构造函数,可以使用
base(参数)。 - 如果想在一个类中用自己的构造函数调用该类的另一个构造函数,可以使用
this(参数)。
示例代码:
public class GameObject // 父类 |
万物之父(object),装箱和拆箱
object(System.Object)是所有类型的终极父类(基类),所有类型都可以向上转换为object。
- 可以用里氏替换原则,用object装所有对象
- 可以用来表示不确定型,作为函数参数类型
Object装引用类型:
object 是所有类型的基类,引用类型可以直接赋值给 object 类型变量。
装箱和拆箱
- 装箱:将值类型转换为引用类型(如object),将值从栈内存复制到堆内存,并封装成对象。
- 拆箱:将引用类型中的值转换回对应的值类型,从堆内存复制回栈内存,必须显式强制转换。
装箱的作用
- 允许值类型以引用类型的形式存储和传递,方便在需要引用类型的场景(如集合)中使用值类型。
拆箱的作用
- 将装箱后的引用类型恢复为原始值类型,方便进行值类型的操作。
优缺点
- 好处:方便不同类型的数据存储和传递,增强灵活性。
- 坏处:装箱和拆箱涉及内存复制和堆分配,带来额外的性能开销,频繁使用会影响效率。
装类类型
Object o = new Person(); // Person 对象赋值给 object |
- 使用时推荐里氏替换原则,即用父类(或基类)变量引用子类实例。
- 判断和转换:
if (o is Person) // 判断 o 是否是 Person 类型或其子类实例 |
装字符串类型
Object o = "12342"; // 字符串是引用类型,直接赋值给 object |
装数组类型
object o = new int[10]; |
装值类型:
object o = 1; // 将值类型装箱为引用类型 |
- 装箱:将值类型转换为引用类型(如object),将值从栈内存复制到堆内存,并封装成对象。
- 拆箱:将引用类型中的值转换回对应的值类型,从堆内存复制回栈内存,必须显式强制转换。
密封类
用sealed封装,不能被继承的类。
在面向对象的程序设计中,密封类的主要作用是不允许最底层的子类被继承,这样安全性更高。
语法:
sealed class ClassName |
示例代码
// 定义一个密封类 |
补充说明
- sealed类可以继承自其他类,但不能被继承。
- sealed修饰方法:方法也可以被
sealed修饰(必须是重写方法),防止被进一步重写。 - 如果不希望其他类继承某个类,就可以将该类声明为
sealed。
多态vob
什么是多态(Polymorphism)?
- 多态是指相同的方法在不同的对象上有不同的表现形式。
多态的三要素:V.O.B
缩写 含义 关键词 作用 V Virtual virtual父类中声明虚方法,允许子类重写 O Override override子类中重写父类的虚方法 B Base base子类中调用父类被重写的方法
多态的目的
- 让继承父类的子类对象在调用同一个方法时拥有各自不同的行为
- 统一接口,行为多样化
- 实现行为复用 + 灵活扩展
示例说明(吃饭行为的多态)
- 父类: 父亲吃饭 → 坐着吃
- 子类: 儿子吃饭 → 站着吃(但想先执行父亲的逻辑)
原理:
virtual 修饰父类方法,可被重写
override 用于子类方法的重写
base.方法() 表示在子类中调用父类的实现虚函数可以被子类重写,配合
override使用
举例:
public class Animals |
C# 抽象类与抽象方法(abstract)
抽象类
关键词:abstract
什么是抽象类?
抽象类是用于被其他类继承的类,不能直接被实例化
它通常用来定义一类对象的通用属性和行为
抽象类的作用是作为父类模板,供子类实现具体逻辑
例如:Thing 类是“物品”的统称,是一个抽象概念,不能单独存在,只能被继承。
抽象方法的特点
- 抽象方法没有方法体(即不包含具体实现)
- 必须在子类中进行重写(override)
- 抽象方法必须定义在抽象类中
示例说明:动物类
-
Animal 是抽象类,不能实例化,只能作为父类存在 Shout() 是抽象方法,所有子类必须重写此方法
public abstract class Animal // 抽象类 |
抽象类使用规则总结
| 编号 | 规则描述 |
|---|---|
| 1 | 抽象方法只能出现在抽象类中 |
| 2 | 抽象类中可以包含普通方法(可有方法体) |
| 3 | 抽象方法没有实现,必须被子类重写 |
| 4 | 抽象类不能被实例化(不能new) |
| 5 | 抽象类和抽象方法都需使用abstract关键字 |
| 6 | 子类重写抽象方法时,必须使用override关键字 |
| 7 | 如果子类不是抽象类,它必须实现父类的所有抽象方法 |
使用场景举例
| 场景 | 示例 |
|---|---|
| 动物种类抽象 | Animal→Dog、Cat |
| 形状抽象 | Shape→Circle、Rectangle |
| 员工类型抽象 | Employee→Manager、Developer |
小贴士
- 抽象类是一种模板设计方式,强制子类实现某些功能
- 如果不打算实例化一个类,而只是作为基类,优先使用
abstract
抽象方法(函数)
什么是抽象方法?
- 抽象方法是没有方法体的方法。
- 它只能存在于抽象类中。
- 子类必须使用
override来重写实现该抽象方法,这表示不抽象方法能用private修饰。
抽象方法的定义格式:
-
abstract 关键字放在方法定义前面 - 方法没有方法体(即没有
{},只以; 结尾)
public abstract class 类名 |
示例代码:
// 抽象类 |
抽象方法与普通虚方法的区别
| 比较项 | 抽象方法 (abstract) |
虚方法 (virtual) |
|---|---|---|
| 是否有方法体 | ❌ 没有 | ✅ 有默认实现 |
| 是否必须重写 | ✅ 是 | ❌ 可选 |
| 是否必须在抽象类中 | ✅ 是 | ❌ 否 |
抽象方法的优点
- 提供统一的接口规范
- 强制子类实现特定功能
- 有助于面向对象中的“多态性”设计
使用场景示例
所有子类都必须实现某些功能时:
- 比如:
Animal 类中的MakeSound() 方法 - 或
Shape 类中的CalculateArea() 方法
小结
抽象方法是一种设计规范的体现。
希望所有子类都必须实现某个方法时,就使用抽象方法。
接口
什么是接口?
关键词:Interface
里面的成员不能实现,被类和接口继承,一个类可以继承多个接口,继承接口后必须实现其成员
接口不能被实例化,只能当作存储容器,遵循里氏替换原则
接口相当于抽象行为的基类,是行为的抽象规范,一般用来抽象行为
举例
interface Fly //接口 |
显示实现接口
场景说明
当一个类实现多个接口,而这些接口中有同名的方法或属性时,为了避免命名冲突,必须通过“显式实现”来区分它们。
语法格式示例
interface IOne |
调用方式
MyClass obj = new MyClass(); |
注意
显式实现的方法不会在类实例中直接暴露
- 即:
obj.DoSomething(); ❌ 无法调用
- 即:
只能通过接口引用或者强制类型转换来访问
适用场景
- 实现多个接口中具有相同成员签名的方法
- 防止接口方法被类实例直接访问(封装接口成员)
- 提高类的灵活性和接口解耦能力
小结
| 项目 | 说明 |
|---|---|
| 是否支持重载 | 支持,同名方法根据接口区分 |
| 调用方式 | 必须通过接口引用或强制类型转换 |
| 好处 | 避免命名冲突,增强封装性 |
| 限制 | 类中无法直接访问显式实现的方法 |
密封函数
当在override前加上sealed时,这个函数不能被子类重写,当在继承子类中,在重写的父类函数override关键字前加sealed,则不允许继承子类的子类再次进行重写。
命名空间
命名空间是用来组织和重用代码的
命名空间就像是一个工具包,类就是一个个工具,都是在命名空间申明的
语法:
Namespace 空间名 |
举例
Namespace MyGame |
命名空间可以同名分开写,相当于同一个命名空间
不同命名空间相互使用需要引用命名空间或指明出处
引用:
在命名空间上方
using system 这相当于引用了system命名空间如果要用另一个命名空间,则要加
using 命名空间名
调用某一命名空间的类的方法:
在另一个域里使用某个命名空间下的类时,语法如下
using MyGame; //1.引用命名空间 |
单例模式(singleton)
单例模式的概念:确保一个类只有一个实例,并提供一个全局访问点。
单例模式的多种实现方式:懒汉模式、饿汉模式
区别:懒汉:类在加载时不会实例化自己的对象,饿汉:类在加载时就实例化自己的对象
单例模式在多线程下涉及到线程同步问题,为此我们要设置双重锁定。
代码示例
//饿汉模式 |
|
//使用.NET 4 Lazy<T> type 特性 |
字符串常用方法
字符串常用方法说明
| 功能类别 | 方法名/属性 | 参数说明 | 示例代码 |
|---|---|---|---|
| 获取长度 | Length |
无 | int len = str.Length; |
| 字符访问 | [](索引器) |
int index:要访问的字符索引 |
char c = str[0]; |
| 转为字符数组 | ToCharArray() |
无 | char[] arr = str.ToCharArray(); |
| 查找子串(正向) | IndexOf(string) |
string value:要查找的子串 |
int i = str.IndexOf("鸭"); |
| 查找子串(反向) | LastIndexOf(string) |
string value:要查找的子串 |
int i = str.LastIndexOf("达鸭"); |
| 截取子串 | Substring(int) |
startIndex:起始索引 |
string s = str.Substring(6); |
| 截取子串 | Substring(int, int) |
startIndex:起始索引,length:截取长度 |
string s = str.Substring(2, 3); |
| 移除子串 | Remove(int) |
startIndex:从该位置开始移除到末尾 |
string s = str.Remove(2); |
| 移除子串 | Remove(int, int) |
startIndex,count:移除多少字符 |
string s = str.Remove(2, 1); |
| 替换子串 | Replace(old, new) |
oldValue:原字符串,newValue:替换成的新值 |
string s = str.Replace("可达鸭", "小猫咪"); |
| 拼接字符串 | + |
其他字符串 | string s = str + "2333"; |
| 拼接格式化 | string.Format() |
格式字符串 + 参数 | string s = string.Format("{0}呀", "可达鸭"); |
| 转为大写 | ToUpper() |
无 | string upper = str.ToUpper(); |
| 转为小写 | ToLower() |
无 | string lower = str.ToLower(); |
| 字符串切割 | Split(char[]) |
char[]:分隔符数组 |
string[] arr = str.Split(','); |
示例
using System; |
StringBuilder
什么是StringBuilder?
一个字符串经常被修改会很消耗内存空间,stringBuilder用于解决这一问题。
-
StringBuilder 是System.Text 命名空间下的一个公共类。 - 作用:用于频繁修改或拼接字符串时,避免创建多个新字符串对象,提高性能并节省内存开销。
特点:
| 特性 | 说明 |
|---|---|
| 可变字符串 | 修改字符串内容不会生成新的字符串对象 |
| 自动扩容 | 默认初始容量为16,插入内容超出后会自动扩容 |
| 可指定初始容量 | 可以通过构造函数设置初始容量,避免频繁扩容带来的性能浪费 |
使用命名空间:using System.Text;
构造方式:
// 默认初始容量为16 |
属性说明:
| 属性/方法 | 说明 | 示例 |
|---|---|---|
Capacity |
当前 StringBuilder 的容量 | Console.WriteLine(sb1.Capacity); |
Length |
实际字符长度 | Console.WriteLine(sb1.Length); |
举例:
using System.Text; |
StringBuilder的增删查改:
增:
// 在末尾添加字符串 |
插入:
// 在指定位置插入字符串 |
删除:
// 从索引 3 开始删除 2 个字符 |
清空:
// 清空 StringBuilder 内容 |
查找:
// 输出指定位置字符 |
改:
// 修改指定索引位置字符 |
替换:
// 替换所有 "旧" 字符为 "新" |
重新赋值:
// 清空再重新追加内容 |
判断相等:
if (sb.ToString().Equals("目标字符串")) |
推荐使用场景
- 在循环中频繁拼接字符串时(如日志生成、大批量字符串组合)。
- 替代
string += 等不高效的字符串拼接方式。 - 实现动态内容构造,如 HTML、SQL 构建器等。
数据结构类
Arraylist
ArrayList 的本质

ArrayList 是 C# 提供的 非泛型集合类,属于 System.Collections 命名空间下的一个 动态数组,可以存储 任意类型的对象(object) ,容量可以动态扩展。
- 本质上是一个支持自动扩容的
object[] 数组。 - 存储值类型会发生 装箱,读取时需要 拆箱,性能略低于泛型集合如
List<T>。 - 在 .NET 泛型集合 (
System.Collections.Generic) 出现之前常用,现代开发建议使用List<T> 替代。
Arraylist引用命名空间与声明语法

//引用命名空间 |
// 创建一个空的 ArrayList |
ArrayList的增删查改
//添加(Add) |










插入与遍历
插入:
ArrayList list = new ArrayList(); |
第一个参数传索引位置,第二个传插入内容

它和stringBuilder一样会自己扩容。
Count是已存放的长度
Capacity是数组的总长

迭代器遍历:
当你实现了迭代器,便可用此方法遍历

Var是类型,item是变量名 collection是进行遍历的变量名,由于array是object类型,所以将var改成object,collection改为array,意思是,将array的内容依次遍历存放到object类型当中。

装箱拆箱:


Stack


增

只能用push一个个放
取

用Pop来取
查

Peek用来查看栈顶的内容但是不会弹出
可以查看栈中是否有查找的内容,返回bool类型
改

遍历
由于栈不提供索引器进行遍历,所以不能用for进行遍历



Queue


增

删

查


改
清空或者一个个出队列

遍历
Foreach遍历

转换为object数组遍历

循环出队列

哈希表


键不能重复,值可以相同
增
第一个参数为键,第二个参数为值

删

查


改

遍历


迭代器遍历:

泛型


泛型类

使用(实现了类型参数化)

使用多个泛型占位符

初始化时赋予类型

泛型接口

接口被继承时赋予类型,并必须实现接口的成员
普通类中的泛型方法


Default用于返回类型的默认值

T用于作为返回值

使用多个泛型占位符的泛型方法

泛型类中的泛型方法


泛型的作用

总结

泛型约束

值类型约束


引用类型约束


存在无参公共构造函数:
传入的必须有公共无参构造函数,否则报错。


申明了一个有无参构造函数的类,和一个无参构造函数被顶替掉的类。
有无参构造不会报错

无无参构造报错

如果构造函数用了private或者protected也会报错,使用抽象类的话,因为抽象类不能实例化,所以不能传抽象类
可以传所有的结构体(值类型),默认都有无参构造
某个类本身或者其派生类
传入的必须是这个类或者其派生(继承)类
传本身


传派生类


接口约束


接口不能实例化,但是遵循里氏替换

或者直接填接口的继承子类(类或者接口)也是可以的

另一个泛型本身或者派生类

Test4继承了Ifly接口,Ifly接口继承了U

约束的组合使用
需要先判断是否能够组合使用

多个泛型分别添加约束

泛型数据结构类
List

增
和arraylist语法一致


删

查

改

遍历

字典(dictionary)
可以理解为是一个泛型的哈希表

自定义键和值的类型

增
不能出现相同键

删

查

改

遍历
链表
实现单向链表

用类封装链表和创建方法
因为类是引用类型,所以可以当作指针来用

LinkedList


增


删

查


改

遍历


泛型栈和队列


泛型数据集合使用较多
泛型栈和队列

申明

增删查改遍历和普通的一样
委托
委托的用处和语法



使用前,我们先定义一个无返回值,无参数的函数

然后传入函数名,将函数装在委托里,委托的申明和类一样,

这样,我们可以使用委托来调用函数方法
使用方式:
第一种方式
第二种方式



多播委托

此时用委托存储一个Fun函数,再用+=可以实现存储两个ff函数,使用ff()时,会执行两次Fun函数。
存谁+=谁,或者直接用加号

移除指定元素是移除后被委托的指定元素。使用委托后,函数不会自动移除,可以手动null;
使用系统定义好的委托Action



系统提供的泛型委托: Func<T(指定返回值类型)> 委托名 = 函数名
Func<>传入多个参数时,最后一个为返回值类型,前面的为传入参数类型
系统提供的无参无返回值委托:Action
系统提供的有参无返回值委托:Action<T(指定n个参数类型)>
事件








C#中的事件(委托的发布和订阅、事件的发布和订阅、EventHandler类、Windows事件)
C#中的事件(委托的发布和订阅、事件的发布和订阅、EventHandler类、Windows事件)
C# 浅谈事件监听及任务处理(监听属性值的改变及定时执行任务)
匿名函数
匿名函数语法和用处


匿名函数不能脱离委托或者事件


使用匿名函数:
在类里使用的话,就用使用委托,为委托赋值一个匿名函数。因为事件不能在类里声明。
//无参无返回 |
一般匿名函数可作为参数传递,没学匿名函数之前,我们是先声明一个函数,再将函数名传递进去作为参数,现在我们可以直接写一个匿名函数直接传参。
//声明一个测试类 |
匿名函数还可以作为返回值返回给委托储存
class Test2 |
匿名函数缺点:
委托可以多播委托,当我们传入多个函数给委托时,传入的匿名函数无法被指定清除,要想清除只能用清空方法
回调函数
回调函数的理解
字面上的理解,回调函数就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。这个过程就叫做回调。
其实很好理解,回调,回调,就是回头调用的意思。主函数的事先干完,回头再调用传进来的那个函数。
使用回调函数有两种写法,使用委托或者事件。
回调函数的使用
//使用委托实现回调函数 |
//使用事件实现回调函数 |
Lamda表达式
什么是lamda表达式

Lamda表达式语法
使用
、
缺点和匿名函数缺点一样
闭包(重要)
https://www.cnblogs.com/eventhorizon/p/9535289.html
https://www.cnblogs.com/pangjianxin/p/8400155.html 这个讲的比较好
我们把在Lambda表达式(或匿名方法)中所引用的外部变量称为**捕获变量**。而捕获变量的表达式就称为**闭包**。
捕获的变量会在真正**调用委托**时“赋值”,而不是在捕获时“赋值”,即总是使用捕获变量的**最新的值**。
class Test3 |
List排序(重要)
List自带排序方法
这个排序可以进行值类型(int、float)的排序
List之所以可以用Sort()来进行排序,是因为List继承并实现了接口IComparable,Sort()调用了C#帮我们实现的值类型比较接口。
//创建列表对象 |
自定义类的排序
为了可以让自定义的类也可以进行排序, 我们要让自定义的类继承C#的IComparable<>泛型接口,并实现该接口。
这样使用Sort()方法可以调用我们实现的接口方法。
//自定义类 |
通过委托函数进行排序
List的Sort()方法有个委托重载,这可以让我们使用委托函数来进行排序。这样我们就可以传入函数了,这时我们可以使用匿名函数或者lamda表达式作为参数传入。甚至为了代码简洁,还能用到三目运算符。
//定义自定义的类 |
总结
在实际开发中, 我们会经常用到List 的Sort排序,例如背包物品的排序,商店物品的排序等。

协变逆变
作用:
协变和逆变的使用主要在委托的装载原则上。用out修饰的泛型委托,可以使用父类泛型委托装子类泛型委托。用in修饰的泛型委托,可以用子类泛型委托替代父类泛型委托。根据里氏替换原则,我们认为父类装子类是协和的,而子类装父类则有些违反我们的认知,所以叫协变和逆变。
//2.结合里氏替换原则理解 |
多线程
什么是进程
什么是线程
什么是多线程

语法

class Program |
线程之间的共享数据(重要)
lock解决了当多个线程访问同一个内存的东西时,会导致的逻辑执行顺序问题。这是一种解决方案,但并不是最优解,想要更好的性能优化可以了解其他锁。
class Program |
多线程的意义
当我们进行一些特别复杂的逻辑时,可能会导致卡顿,所以我们可以开一个线程来进行复杂的运算,从而让主线程可以流程运行。

预处理指令


反射和特性
什么是程序集

迭代器
进阶
什么是LinQ?用来干什么?
LINQ(Language Integrated Query)是一种C#语言中的**查询技术**,它允许我们在代码中使用**类似SQL的查询语句来操作各种数据源**。这些数据源可以是集合、数组、数据库、XML文档等等。LINQ提供了一种统一的编程模型,使我们能够使用相同的方式来查询和操作不同类型的数据。
比如我们可以用LinQ来操作SQL server数据库。除此之外,我们还可以操作数组、枚举集合、泛型列表等。
C#中的LINQ语法
使用 LINQ(Language-Integrated Query)语法在 C# 中进行查询操作
1.检索List列表中装载的对象的属性
我们知道,当我们用List类对象装载了一个类的多个对象时,我们不能通过直接List类对象名点出来他包含的类对象的属性,只能通过Foreach循环来通过索引器来遍历。LINQ中的=>语法可以让我们直接通过List类对象直接对其包含的类对象的属性进行检索。
例:
public class Paper |
C#面试题
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引