学习手册:

w3schools 教程 从前端到后端到数据库,各种语言教程一应俱全

枚举

(1)枚举的申明

enum E_ZiDan

{

a,//自定义枚举项的名字,

b=100,

c,

}

枚举项未赋值时,从0开始自动赋值,往后依次累加,给某个枚举项命名后,从这个项往后依次累加。申明枚举时要在namesapce中命名。

例子:

申明一个怪物类型的枚举项

enum E_MonsterType

{

Normal,

Boss,

}

申明一个玩家类型的枚举项

enum E_Player

{

main,

other,

}

(2)枚举变量的申明初始化和使用

//声明
enum E_Player

{

main,

other,

}
//使用
E_Player e_Player = E_Player.main;

变量的类型及转换(显隐转换)

变量类型

//有符号
sbyte
int
short
long
float //c#中初始化:float=数值f,f用于区别于double类型 例: float f = 123.45f
double
//无符号
byte
uint
ushort
ulong
//特殊类型
bool //装true 或 false
char //字符 只能存一个字符
string //字符串

隐式转换(直接等号转换)

大范围包含小范围,基本相同大类型互相转换,特使类型中,char可以转换为int以外,bool和string都不能隐式转换

显示转换

括号转换

1.变量名=(转换类型)变量名;

2 .boolstring不能通过括号转换强转

int x;
long y;
x=(int)y

Parse法 (把字符串类型转换为相应的类型)

(例如接受用户输入的字符串,将用户输入的字符串转换为相应的可以进行运算的数值)

语法变量类型.parse(“字符串”)

int i = int.parse(“123”);

float f = float.parse(“123.45”);

bool b = bool.parse(“true”);

console.write(int.parse(“123”));

注意:字符串必须能够转换成相应类型否则报错

Convert法(更准确地将各个类型之间进行转换)

语法: convert.To目标类型(变量或者常量)

例如:

int a = convert.ToInt32(“123”); //结果: 123

int a = convert.ToInt32(1.692f); //(遵循四舍五入) 结果: 2

int a = convert.ToInt32(true); //(bool类型转int )结果: 1

int a = convert.ToInt32(“A”); //字符类型转int 结果: 65

short a = convert.ToInt16(“1”);//字符串类型转short,short对应的是int16

long a = convert.ToInt64(“12”); //字符串类型转long型,long对应int64

byte a = convert.ToByte(“3”);

uint a = convert.ToUint32(“4”); //无符号类型的转换

float a = convert.ToSingle(“123.45”); //float对应single(单精度)

double a = convert.ToDouble(“123.45”);// double对应double(双精度)

bool b = convert.ToBoolean(“true”); //bool 对应 Boolean

char a =convert.ToChar(“A”);

string str = convert.ToString(123);

其他类型转string

1.convert法:

int a = 123

string str = Convert.ToString(a);

2.toString法:

语法: 变量.toString();

string str = 1.toString(); //整型

string str = true.toString(); //bool类型

string str = 123.45f.toString(); //浮点

string str = “A”.toString(); //字符

另一种写法

Int a = 6

string str = a.toString();

在下面例子中,计算机用到了此方法进行转换

Console.WriteLine(“12345” + 1 + true );

数组与数组之间的转换

Array.ConvertAll<转换的对象的元素类型,要转换为的元素类型>(转换对象,转换方法)

语法:

//TInput 和 TOutput 分别是源数组和目标数组。
//array:一个从零开始的一维数组,用于转换为目标类型。
//converter:一个转换器,用于将每个元素从一种类型转换为另一种类型。
//简单理解:ConvertAll方法就是遍历源数组中的每一个元素,执行一个转换函数(括号里的第二个参数),并返回新的数组。
public static TOutput[] ConvertAll<TInput,TOutput> (TInput[] array, Converter<TInput,TOutput> converter);

//括号里面的第二个参数使用的lamda表达式。
//将int数组转换为字符数组
int[] a = { 5, 4 };
string[] b = Array.ConvertAll<int, string>(a,num=>num.ToString());
//将string数组转为int数组
string[] str = { "123", "456" };
int[] array = Array.ConvertAll<string, int>(str, str => int.Parse(str));
//遍历提取出自定义类型的属性值:
class Program {

class Person {
public string Name { get; set; }
}
static void Main() {
Person[] people = {
new Person { Name = "Aman" },
new Person { Name = "Kumar" },
new Person { Name = "Gupta" }
};
//遍历people源数组,执行person => person.Name函数,并返回一个string数组
string[] names = Array.ConvertAll(people, person => person.Name);
Console.WriteLine("Converted to names:");
foreach (string name in names) {
Console.WriteLine(name);
}
}

异常捕获

作用:避免代码报错时出现的程序卡死。

语法

//必备部分
try
{
//放需要进行异常捕获的代码块
//如果try中的代码块出错,会跳转到catch中
}
//必备部分
catch
{
//如果出错,会执行catch中的代码来进行移除捕获
//catch后还可写为catch(Exception e)具体报错跟踪通过e得到具体的错误
}
//可选部分
finally

{
//最后执行的代码块 不管出没出错都会执行
}

例子:

try

{
Console.WriteLine(“请输入数字”);
String str = console,ReadLine();
Int i = int.parse(str);
Console.WriteLine( i );
}
catch

{
Console.WriteLine(“请输入合法数字”);
}

字符串拼接(非打印拼接)

1.字符串用+号拼接

//示例1
string str = “123”;
str=str + “456”;
str=str + 456; //(456是整形,但是在这里默认调用了toString,将456转换为了字符串)
str += “456”+789; 结果为123456789 //结果为123456789
//注意
str += 4+5+6//先算右侧结果,整型相加为15,接着再拼接,结果为12315
str += “4”+5+6// 右侧式子从左到右运算,结果为123456,先将字符4和5进行拼接,然后依次进行拼接。
Str += 4+5+“”+6//右侧式子为4+5=9,“”为空字符串,9+“”相当于将9转换为字符串,接着与6进行拼接,结果为12396
Str = 4+5+“”+(6+7//结果为123913

2.字符串占位拼接

语法: string.Format(“带拼接的内容{0},带拼接的内容{1},”,内容0,内容1);

//此句中,18是整型,但是调用了toString,也可填写true,123.45f等其他类型
sring str = string.Format(“我是{0},我今年{1},我想要{2}”,“李明”,18,“好好学习”);

位运算符和三目运算符

位运算符

位与 & 两个位都为1时,结果才为1

位或 | 两个位都为0时,结果才为0

异或 ^ 两个位相同为0,相异为1

取反 ~ 0变1,1变0

左移 << 各二进位全部左移若干位,高位丢弃,低位补0

右移 >> 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

三目运算符

//  condition ? expression1 : expression2;
// 条件 ? 为真返回值 : 为假返回值
string str = int a> int b ? “大于” :“小于”;
return a>b ? 1 : -1 ;

C#中的数组

一维数组

//语法: 变量类型 [] 变量名
//写法1:
int[] arr1;
arr1 = new int[5];
//写法2:
int[] arr1 = new int[5];
//写法3:
int[] arr1 = new int[5]{1,2,3,4,5};
//写法4:
int[] arr1 = new int[]{1,2,3,4,5,……};
//写法5:
int[] arr1 = {1,2,3,4,5,……};
//使用
int arr[] = new int[5]; //申明了数组
arr[1]=1; //赋值
arr.Length //获取数组长度:数组名 . Length
console.WriteLine(arr.Length); //打印数组长度
//遍历数组:
for(int i=0; i< arr1.Length;i++)
{
}
//数组扩容:
int[] arr1 = new int[5]{1,2,3,4,5};
int[] arr2 = new int [10];
for(int i=0;I<arr1.Length;i++)
{
arr2[i]=arr1[i];
}
arr1=arr2; //(将将arr1指向arr2数组,实现了arr1数组的扩容)
//缩容的话申请个较小的数组然后赋值后改指针指向

二维数组

语法: 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
int[] arr = new int[3]{{1,2,3}; //arr存放的只是内存的地址
arr = new int[4]{1,2,3,4}; // new代表新开辟的一个内存空间,初始化一块新的内存
//example 2

特殊的引用类型string

string​类型是不可变的字符串,一旦创建不能修改。string​类型在多次赋值时,会产生新的内存,将新的内容放在新的内存中,然后string存放新内存的地址,但是旧的内容的内存不会被回收,这样,多次给string​重新赋值时,会产生内存垃圾。

string str1= “12345”;
str1=“123”; //重新赋值后,“12345”所占内存不会被回收。
string str2=str1; // str2指向str1的内存
Str2=“123456”; //str2重新赋值会重新开辟内存空间,不会修改str1的内容。

所以当我们对string需要非常频繁地拼接或修改字符串时,会产生非常大的内存消耗。这时候我们可以选用 StringBuilder​ 类来进行操作。(需要引用命名空间 using System.Text;​ )

StringBuilder​ 是可变的字符串缓冲区,适合频繁拼接或修改字符串的情况。

using System.Text;
//频繁地拼接或修改字符串时,用StringBuilder可以减少GC压力。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.Append("Item").Append(i).Append("\n");
}

比较项 string StringBuilder
是否可变 ❌ 不可变,每次修改都会创建新字符串 ✅ 可变,修改不会创建新对象
性能(频繁操作) ❌ 较差:每次拼接都会生成新对象 ✅ 较好:内部使用字符数组动态扩容
内存效率 ❌ 差(频繁拼接会产生很多临时字符串) ✅ 高效(在原有缓存上操作)
适用场景 小量拼接、只读操作、配置读取、UI显示等 循环拼接、日志记录、大量字符串拼接场景
线程安全 ✅ 线程安全(不可变) ❌ 默认不是线程安全的
常用操作 +​、Replace​、Substring​等 Append​、Insert​、Replace​、Remove​等

你要做的事
推荐使用类型
拼接几段字符串 string
循环中拼接字符串(如生成代码、日志) StringBuilder
替换、截取少量字符串内容 string
对性能要求高的字符串处理 StringBuilder

深拷贝和浅拷贝

深拷贝和浅拷贝详解

  • 对于所有面向对象的语言,复制永远是一个容易引发讨论的题目,C#中也不例外。
  • 深拷贝:指的是拷贝一个对象时,不仅仅把对象的引用进行复制,还把该对象引用的值也一起拷贝。这样进行深拷贝后的拷贝对象就和源对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。 (拷贝整个对象的内容,包括其内部引用指向的对象也会新建一份)
  • 浅拷贝:指的是拷贝一个对象时,仅仅拷贝对象的引用进行拷贝,但是拷贝对象和源对象还是引用同一份实体。 (拷贝对象的“引用”地址,新对象和原对象指向同一块内存。)

学习过上面的值类型和引用类型后,我们可以知道,值类型是深拷贝,是开辟了新的内存并复制了值。而引用类型存储的是值所在内存的地址,所以引用类型的复制只是复制了值所在的地址,并没有再新开辟一个内存把值复制进去。(要注意C#里的string类型是一个特殊的引用类型)

public class Person
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person A = new Person() { Name = "小李" };
Person B = A; // 浅拷贝
B.Name = "小黄"; // 拷贝对象改变Name值
// 结果都是"小李",因为是浅拷贝,只是将A的值内存地址传递给了B,所以无论对B还是A进行修改,修改的都是同一个内存的值。
Console.WriteLine("Person.Name: [A: {0}] [B:{1}]", A.Name, B.Name);
}
}

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。
//例如我的Unity版本使用的是2020.3.28f1,使用的是NET Standard 2.0。
//查看你的Unity 使用的NET版本请在Unity里点击:ProjectSetting -> Player -> Other Settings,在里面翻一翻,可以看到 NET XXX。
//所以如果想在Unity使用序列化,请了解使用: XML,JsonUtility,Newtonsoft.Json,LitJson等。

using System.Text.Json;

//使用System.Text.Json不需要[Serializable] 特性,这一点和BinaryFormatter不同。
public class MyComplexObject
{
public int Id { get; set; }
public string Name { get; set; }
// 其他属性
}
/// <summary>
/// 对象深拷贝扩展方法
/// </summary>
public static class DeepCopier
{
//深拷贝方法
public static T DeepCopy<T>(T obj)
{ //序列化
string json = JsonSerializer.Serialize(obj);
//反序列化
return JsonSerializer.Deserialize<T>(json);
//原理:通过序列化源对象和反序列化创建了个新的副本对象返回。
}
}
//调用示例:
MyComplexObject original = new MyComplexObject { Id = 1, Name = "Test" };
MyComplexObject copy = DeepCopier.DeepCopy(original);
// 修改原对象不会影响 copy
original.Name = "Changed";
Console.WriteLine(copy.Name); // 输出 "Test"

2.使用Newtonsoft.Json

```



### 3.实现 `ICloneable`​ 接口 + 自定义 `Clone()`​



### 4.使用 `MemberwiseClone()`​



### 5.使用第三方库(推荐 AutoMapper)



### 6.使用表达树



**为什么要使用深拷贝?**
​`深拷贝是一种保持数据独立性和完整性的重要手段,在许多场景下都是不可或缺的操作。 `​使用深拷贝的原因主要有以下几点:

- 独立性:深拷贝创建的是一个完全独立的副本,对副本的操作不会影响原始数据,这在一些场景中非常重要,比如数据备份、多线程编程等。
- 安全性:通过深拷贝,可以避免意外修改原始数据,从而提高代码的安全性和稳定性。
- 可复用性:深拷贝后的副本可以独立使用和修改,方便代码的复用和模块化开发。
- 隔离性:在一些复杂的数据结构中,深拷贝可以确保不同部分的数据相互隔离,避免不必要的关联和影响。
- 性能优化:对于一些需要频繁修改数据的场景,使用深拷贝可以避免不必要的共享和同步操作,提高程序的性能。

应用场景:

无论是浅拷贝还是深拷贝,一般都用于操作Object 或 Array之类的复合类型。

- **数据备份和恢复**:在进行数据备份时,使用深拷贝可以创建数据的完整副本,以便在需要时进行恢复。

- **对象复制和克隆**:当需要创建一个对象的副本时,深拷贝可以确保副本与原始对象完全独立,不会相互影响。

- **多线程编程**:在多线程环境下,深拷贝可以避免多个线程同时修改同一个对象导致的竞态条件。
- **数据传递和共享**:将数据通过深拷贝传递给其他模块或进程,可以确保数据的独立性和完整性。
- **缓存和数据持久化**:深拷贝可以用于缓存数据或将数据持久化到存储介质中,以提高性能和数据的可用性。
- **测试和模拟**:在测试代码中,深拷贝可以用于创建测试数据的副本,以便进行各种测试和模拟。

其他参考:

[C#|.net core 基础 - 深拷贝的五大类N种实现方式](https://blog.csdn.net/zhulianfang1991/article/details/142421562#:~:text=C%23%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%A4%8D%E6%9D%82%EF%BC%8C%E6%96%87%E4%B8%AD%E4%BB%8B%E7%BB%8D%E4%BA%86%E4%BA%94%E5%A4%A7%E7%B1%BBN%E7%A7%8D%E6%B7%B1%E6%8B%B7%E8%B4%9D%E6%96%B9%E6%B3%95%EF%BC%8C%E5%8C%85%E6%8B%AC%E7%AE%80%E5%8D%95%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B%E3%80%81%E6%89%8B%E5%8A%A8%E6%96%B9%E5%BC%8F%E3%80%81%E5%BA%8F%E5%88%97%E5%8C%96%E6%96%B9%E5%BC%8F%E3%80%81%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93%E6%96%B9%E5%BC%8F%E5%92%8C%E6%89%A9%E5%B1%95%E8%A7%86%E9%87%8E%E6%96%B9%E5%BC%8F%EF%BC%8C%E5%B9%B6%E5%AF%B9%E6%AF%94%E4%BA%86%E6%80%A7%E8%83%BD%E3%80%82%20%E5%BB%BA%E8%AE%AE%E4%BD%BF%E7%94%A8AutoMapper%E5%92%8CDeepCloner%E7%AD%89%E6%88%90%E7%86%9F%E5%BA%93%E6%88%96%E6%A0%B9%E6%8D%AE%E6%80%A7%E8%83%BD%E9%9C%80%E6%B1%82%E9%80%89%E6%8B%A9%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%A0%91%E5%92%8CEmit%E3%80%82,_c%23%20%E6%B7%B1%E6%8B%B7%E8%B4%9D)

[深拷贝:概念、使用原因、应用场景、3种常用方法](https://blog.csdn.net/cuclife/article/details/136453132)

# C#函数

### 函数的基本知识

- 函数名使用帕斯卡写法,即每个单词首字母大写

- 函数的调用: 函数名(参数); 有返回值则用可用返回类型接收

- 返回多个值:用数组类型

函数写在classstruct语句块中写法:

```C#
Static void/其他类型 函数名(变量类型参数1,2,……)

{
//函数代码逻辑
return 返回值(如有返回值才返回)
}

//例子:
static void sayhello(string str) //无返回类型有参数
{
Console.WriteLine(str);
}

static string sayhello(string str) //有返回有参数

{

Console.WriteLine(str);
return str;

static int sayhello(int a,int b)

{

return a + b;

}

}

函数举例:

static int[] sayhello(int a,int b)
{
int sum = a + b;
int avg = (a + b) / 2;
int[] arr = { sum,avg };
return arr;
}

关于return​:

  • 对于有返回值的函数,return需要返回函数声明的返回值类型的值。

  • 对于无返回值函数,单独一个 return ​可以直接返回函数外部,后面的代码不执行。

static void speak(int a,int b)
{
if(a+b>10)
{
return; //注意:当返回类型为void时才可以使用return直接返回中止。
}
Console.WriteLine(a + b);
}
static string speak(int a,int b)
{
if(a+b>10)
{
int c = a+b;
string str = "和为:" + c;
return str; //注意:当返回类型为void时才可以使用return直接返回中止。
}
return;
}

ref和out(用于函数形参和实参的传递)

作用:

  • 当传入函数参数时,如果为值类型,在函数内修改形参不会改变实参。
  • 若使用 ref​ 或 out​,则函数内的修改会影响函数外的实参

语法: 在传入参数前加ref​或out

例子:

static void ChangeVertex(ref int a, ref int b)
{
int c;
c = a;
a = b;
b = c;
}

static void Main()
{
int a = 3;
int b = 4;
ChangeVertex(ref a, ref b);
Console.WriteLine(a + " " + b); // 输出:4 3
}

Ref和out的区别

区别点 ref out
调用前是否必须赋值 ✅ 必须在调用前初始化 ❌ 不需要在调用前初始化
方法内是否必须赋值 ❌ 不强制赋值 ✅ 方法内部必须赋值,否则编译错误
场景 双向传值 单向输出结果

Out示例:

static void ChangeVertexO(out int a, out int b)
{
a = 5; // 内部必须赋值
b = 6;
}

static void Main()
{
int a;
int b;
ChangeVertexO(out a, out b);
Console.WriteLine(a + " " + b); // 输出:5 6
}

ref示例

static void ChangeVertex(ref int a, ref int b)
{
int c;
c = a;
a = b;
b = c;
}

static void Main()
{
int a = 3;
int b = 4;
ChangeVertex(ref a, ref b);
Console.WriteLine(a + " " + b); // 输出:4 3
}

变长和参数默认值:

变长参数(可变参数)

  • 关键字: params
  • 语法:
static 返回类型 函数名(params 类型[] 数组名)
  • 注意事项:

    • params​ 修饰的参数必须是最后一个参数
    • 它前面可以有其他参数,但后面不能再有其他参数,否则编译报错。

示例:

static int Text(params int[] a)
{
int sum = 0;
for (int i = 0; i < a.Length; i++)
{
sum += a[i];
}
return sum;
}

调用示例:

int result = Text(1, 2, 3, 4);  // 输出:10

参数默认值(可选参数)

什么是参数默认值?

当函数参数设置了默认值,即可在调用函数时选择是否传入对应实参,如果不传入,则会使用默认值。

示例代码:

static void Speak(string str = "我什么也不想说") // 设置默认值
{
Console.WriteLine(str);
}

使用方式:

Speak();                      // 输出:我什么也不想说
Speak("那我就说几句"); // 输出:那我就说几句

注意事项:

  • 可选参数必须放在参数列表的最前面。
  • 因为如果默认值参数在前,调用函数时会发生实参与形参匹配不明确的问题

示例代码:

//正确示例
static void Text(int a,int b =1,int c=5;int d=6) //可选参数放后面
{

}
//错误示例:
static void Text(int a =1, int b = 2, int c) //未将可选参数都放后面
{

}

函数重载

函数重载:

  • 重载允许在同一作用域内定义多个同名函数。

  • 具有相同函数名的函数,但是参数的类型、个数或者顺序不同的函数,叫做函数重载。

  • 返回值与函数重载无关,即返回值可相同可不同,但是传入参数的类型、个数或者顺序要有一个不同。

  • 函数重载注意用于处理不同参数同一类型的逻辑处理

举例:

//这三个函数的函数名相同,但是参数的个数和顺序或者类型不同,所以这三个函数重载。

static int Test(int a,int b, int c=5,int d=3)
{
return a + b + c + d;
}

static float Test(float a,int b)
{
float c = a + b;
return c;
}

static float Test(float a, float b)
{
return a+b;
}

//params(变长参数)也可用于函数重载
static int CompareVertex(params int[] a)
{
int index=0;
for (int i = 1; i < a.Length; i++)
{
if(a[i]>a[index])
{
index = i;
}
}
return a[index];
}

static float CompareVertex(float a, float b)
{
if (a > b)
{
return a;
}
else if (a < b)
{
return b;
}
else
{
return 0;
}
}

递归函数

递归函数:就是让函数自己调用自己。

条件:一个正确的递归函数,必须有结束条件,这个条件必须是根据循环的次数改变而改变的,否则就会陷入无限循环。

普通举例:

static void Test_DiGui(int a)
{
Console.WriteLine("二哈!!!过来");
if (a > 1)
{
Test_DiGui(a - 1); //递归函数自我调用
}
} //打印a次字符串

遍历目录示例:

using System;
using System.IO;

class Program
{
static void Main()
{
string rootPath = @"C:\Example\MyFolder"; // 设置你要遍历的根目录
TraverseDirectory(rootPath);
}

static void TraverseDirectory(string path)
{
// 打印当前目录路径
Console.WriteLine("目录:" + path);
// 遍历文件
string[] files = Directory.GetFiles(path);
foreach (string file in files)
{
Console.WriteLine("文件:" + Path.GetFileName(file));
}
// 遍历子目录(并递归调用)
string[] directories = Directory.GetDirectories(path);
foreach (string dir in directories)
{
TraverseDirectory(dir); // 递归调用
}
}
}

C#结构体

结构体是一种自定义的变量类型​,他是数据和函数的集合​,在结构体中可以申明各种变量和方法(函数)

作用: 用来表现存在关系的数据集合,比如结构体表现学生、怪物、动物等

基本语法:

  • 结构体一般写在namespace​空间里,且里面的变量和函数默认为private​类型
  • 如果想要外部调用,要在前加public​。函数在结构体里不用加static​。结构体里还可以写构造函数。

}

Struct 结构体名
{
public int age;
//…… ……
void 函数名()
{
//…… ……
}

}

举例:

struct Student
{
public string name;
public int age;
public bool sex;
enum E_sex
{
男生,
女生
}
public Student(string name,int age,bool sex) //构造函数,其中this表示自己的参数
{ //因为传入参数名称和外部变量名称相 同,所以用this来表示这个变量为 结构体自己的参数。
this.name = name;
this.age = age;
this.sex = sex;
}
public void speak()
{
Console.WriteLine("我是{0},我今年{1}岁,我是{2}",name,age,(E_sex) Convert.ToInt32(sex));
}
}
class Program
{
static void Main(string[] args)
{
//结构体初始化方法一:
Student s1;
s1.age = 18;
s1.name = "林语馨";
s1.sex = true;
s1.speak();
//方法二:
Student s2 = new Student("林雨馨",18,true); //对结构体内的变量统一赋值。
}
}

排序算法

冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复地遍历待排序的列表,比较相邻的元素并交换它们的位置来实现排序。该算法的名称来源于较小的元素会像”气泡”一样逐渐”浮”到列表的顶端。

算法步骤

  1. 比较相邻元素:从列表的第一个元素开始,比较相邻的两个元素。
  2. 交换位置:如果前一个元素比后一个元素大,则交换它们的位置。
  3. 重复遍历:对列表中的每一对相邻元素重复上述步骤,直到列表的末尾。这样,最大的元素会被”冒泡”到列表的最后。
  4. 缩小范围:忽略已经排序好的最后一个元素,重复上述步骤,直到整个列表排序完成。

原理说明:

设数组元素个数为 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#实现冒泡排序:

//降序排序(从大到小)
for (int i = 0; i < arr.Length-1; i++)
{
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j + 1] > arr[j])
{
int temp;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
//升序排序(从小到大)
for (int i = 0; i < arr.Length-1; i++)
{
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j + 1] < arr[j])
{
int temp;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}

优缺点

  • 优点

    • 实现简单,代码易于理解。
    • 原地排序,不需要额外的存储空间。
  • 缺点

    • 效率较低,尤其是对于大规模数据集。
    • 不适合处理几乎已经有序的列表,因为仍然需要进行多次遍历。

选择排序

什么是选择排序

选择排序基本思想是每次从待排序的数据中选择最小(或最大)的元素,放到已排序序列的末尾,直到全部数据排序完成。

算法步骤

  1. 初始化:将列表分为已排序部分和未排序部分。初始时,已排序部分为空,未排序部分为整个列表。
  2. 查找最小值:在未排序部分中查找最小的元素。
  3. 交换位置:将找到的最小元素与未排序部分的第一个元素交换位置。
  4. 更新范围:将未排序部分的起始位置向后移动一位,扩大已排序部分的范围。
  5. 重复步骤:重复上述步骤,直到未排序部分为空,列表完全有序。

设数组长度为 n​,排序过程如下:

  1. 第1轮:从下标 0~n-1​ 中找到最小值,放到第0位;
  2. 第2轮:从下标 1~n-1​ 中找到最小值,放到第1位;
  3. 第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] 已排好

代码实现

//升序(从小到大)
static void SelectionSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
// 假设当前位置 i 是最小值索引
int minIndex = i;
// 遍历后面的元素,寻找更小值
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
// 如果找到更小值,进行交换
if (minIndex != i)
{
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
static void Main()
{
int[] data = { 64, 25, 12, 22, 11 };
Console.WriteLine("原始数组: " + string.Join(", ", data));
SelectionSort(data);
Console.WriteLine("排序后: " + string.Join(", ", data));
}

优缺点

  • 优点

    • 实现简单,代码易于理解。
    • 原地排序,不需要额外的存储空间。
    • 对于小规模数据集,性能尚可接受。
  • 缺点

    • 时间复杂度较高,不适合大规模数据集。
    • 不稳定排序算法(如果存在相同元素,可能会改变它们的相对顺序)。

面向对象编程

面向对象关键知识:

  • 类(class)

面向对象三大特征

  • 封装:​用程序语言来形容对象
  • 继承:​复用封装对象的代码:儿子继承父亲,复用现成代码。
  • 多态:​同样行为的不同表现,儿子继承父亲但是有不同的行为表现。

面向对象七大原则

  • 开闭原则
  • 依赖倒转原则
  • 里氏替换原则
  • 第一职责原则
  • 接口隔离原则
  • 合成复用原则
  • 迪米特法则

类和(类)对象

什么是类

  • 类的定义: 具有相同特性和行为的对象组成的集合就是类。类是对象的抽象,对象是类的具体实例。 比如你要买一辆车,车有品牌,排量,颜色,款式,这时车就是类,而当你看中了一个品牌,排列,颜色,款式确定的一款车时,这个车就是类的实例。

  • 类是一个引用类型;

  • 类的声明一般声明在namespace​空间中,而不是定义在 class Program​ 里。在类中申明类是类的内部类。

类的声明

访问修饰符 Class 类名
{
//特征——成员变量
//行为——成员方法
//保护特征——成员属性
//构造函数和析构函数
//索引器
//运算符重载
//静态成员
}

类声明实例

Class Person //人类
{
string name;
int age;
int sex; //假设0为女,1为男
}

Class Machine //机器类
{

}

(类)对象

  • 类的申明相当于申明了一个自定义变量类型。

  • (类)对象的申明是相当于申明了一个指定类的对象。

  • 类创建对象的过程,一般称为实例化对象。

  • (类)对象是引用类型。

  • 引用类型是在栈上申请了一个内存,存放地址,new后在堆上申请空间,将地址存在栈上。

对象的申明:

类名 变量名;				//声明变量但不初始化。类成员的引用类型变量如果没初始化,会自动是 null。	
类名 变量名=null; //声明变量,并显式赋值为 null。
类名 变量名=new 类名(); //声明变量并创建一个新的对象实例。

三种声明方法对比

写法 是否赋值 是否可以使用变量 是否分配内存
Person p; ❌ 编译错误
Person p = null; ✅ null ❌ 运行时错误
Person p = new Person(); ✅ 实例 ✅ 正常使用

实践建议

场景 推荐写法
延迟初始化 Person p = null;
立即使用 Person p = new Person();
先声明,后赋值(临时变量) Person p;​,但马上赋值

实例化对象:

假设申明了一个Person类。

class Person
{
string name;
int age;
}
//1
Person P1;
P1 = new Person();
//2
Person P2=null;
//3
Person P3=new 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 类名
{
//特征——成员变量
//行为——成员方法
//保护特征——成员属性
//构造函数和析构函数
//索引器
//运算符重载
//静态成员
}

类里面可以声明自己类的对象(变量),例如:

class Person

{
public Int age;
public String name;
public Bool sex;
public Person girlfriend; //在类中申明类变量,注意不要用new创建实例。
public Person[] friend=null; //可赋值null,但是不能new;否则循环。
}
//实例化一个对象语法: Person p1=new Person();

注:

  1. 如果在类中对(类)对象进行new(构造成员变量)—— Public Person girlfriend=new Person();则会使类进行无限递归,而爆掉内存。
  2. 类中的成员变量会有默认值,例如,值类型默认值:int默认值为0;,bool为false等。引用类型都是空(NULL)
  3. 看默认值小技巧Console.WriteLine(default(变量))

成员方法(函数):

成员方法(函数),用来表现对象行为,申明在类语句块中,规则和函数申明规则一致,可受访问修饰符影响,

1.成员方法不要加static

2.成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行了某个行为,

3.成员方法受访问修饰符影响

如果成员方法前加staic:

在C#程序中,当你在一个类中将方法声明为静态(static)时,意味着该方法不属于任何特定的实例对象,而是属于整个类本身。这意味着你可以直接通过类名来访问该静态方法,而不需要实例化类对象。

例如Person类中的Eat()方法,如果你将其前缀声明为static,那么就可以通过Person.Eat()的方式直接调用该方法,而不需要先创建Person类的实例。

如果你在代码中定义了一个静态的Eat()方法,它可以直接执行一些与吃饭相关的操作,而不需要依赖于Person类的实例。这样的静态方法可以在不创建实例对象的情况下被调用,使得代码更加简洁和高效。

静态方法只能访问静态成员(如静态字段或其他静态方法),不能访问实例成员(如实例字段或实例方法),因为静态方法没有与特定对象实例相关联。

如果你需要执行某个与类相关,但是不依赖于类的实例对象的操作,那么可以将该操作声明为静态方法。例如,用于计算数字之间的某个关系,或者用于管理一个公共资源池等。

举例

Class Person
{
Public string name;
Public int age;
//说话的方法
Public void Speak(string str)
{
Console.WriteLine(“{0}说:{1}”,name,str)
}
//判断是否成年的方法
Public bool ISAdult()
{
Return age>=18; //如果大于等于返回true,否则返回false
}
}

例2:

class Person
{
public int age;
public string name;
Person[] friends;

public void Addfriend(Person friend)
{
if(friends==null)
{
friends = new Person[] { friend };
}
else
{
Person[] newFriend = new Person[friends.Length + 1];
for (int i = 0; i < friends.Length; i++)
{
newFriend[i] = friends[i];
}
friends = newFriend;
}
Console.WriteLine("{0}添加了一个新朋友:{1}", name,friend.name);
}

}
class Program{

static void SelectionSort(int[] arr)
{
Person p1=new Person();
p1.name = "小可乐";
p1.age = 18;
Person p2 = new Person();
p2.age = 17;
p2.name = "小辣椒";
p1.Addfriend(p2);
//运行结果:小可乐添加了一个新朋友:小辣椒
}
}

构造函数(初始化调用)、析构、垃圾回收

构造函数:

构造函数只能在类中声明,默认有一个0参的(无传入参数)构造函数(用于对变量成员的初始化)。

构造函数用于给类的成员属性进行赋值并初始化一个新的对象。

例如类的实例化: Person p1 = new Person();​这句的 new Person()​ 就相当于调用了无参的构造函数初始化一个空对象。当在类中重新申明一个构造函数时,默认的0参构造函数就会被回收。

构造函数可以重载,如果想要0参的构造函数,可以手动再次申明。

构造函数申明语法

Class 你的类{
string name;
int count;
访问修饰符 类名(传入参数)
{

}
}

构造函数声明示例:

//举例:
class Person
{
public int age;
public string name;
public Person[] friends;

Public Person(int age,string name) //构造函数
{
This.age=age;
This.name =name;
}

Public Person()//0参构造函数,两构造函数重载
{
age=0;
name =””;
friends=null;
}
}

构造函数的特殊写法

Public Perosn(参数):this(参数);
{
//逻辑
}

这种写法的作用是,当调用这个构造函数时,会先调用this(参数)构造函数​,this(参数)​相当于调用public Person (参数);

这种构造函数的运行逻辑是

class Person
{
public int age;
public string name;
public Person[] friends;

public Person()
{
name = "";
age = 0;
friends = null;
//如果在无参构造函数后面加:this(参数)时,由于此构造函数无传入参数,所以无参数传入this(参数)里。
}

public Person(int age):this() //调用此构造函数时会先调用this(),也就是先调用public Person()构造函数
{
age = 18;
}

public Person(int age,string name):this(age);
{
//当调用此构造函数时,传入的参数age和name,其中age会被传入this(age)这个函数里,然后调用public Person(int age)这个构造函数。
}
}

析构

当引用类型的堆内存被回收时,会调用该函数

C#存在自动垃圾回收机制GC,所以不怎么用析构函数。

只做了解即可。

如果想要在GC回收时执行一些自定义逻辑可以写里面。

基本语法:

~类名()
{

}

举例

class Person 
{
//……
}
~Person()
{
//逻辑
}

垃圾回收机制(Garbage collector)

GC只负责堆里面的内存的垃圾回收。

值类型在栈中分配的内存,他们有自己的生命周期,会自动分配和释放。

Heap:堆

Stack:栈

垃圾回收主要算法:

  1. 标记-清除算法
  2. 压缩算法
  3. 分代清除算法

手动调用GC:

GC.collect(); //手动触发垃圾回收|在Unity里面一般都是在加载场景(进度条)时使用,不会频繁调用

成员属性(包裹成员变量)

基本概念:

  1. 用于保护成员变量
  2. 为成员属性的获取和赋值添加逻辑处理
  3. 解决3p的局限性(private——内部访问、public——内外、protected——内部和子类)
  4. 属性可以让成员变量在外部 只能获取,不能修改,或者只能修改,不能获取
  5. 可以在里面执行一些自定义逻辑,比如对数据进行加密和解密

语法:

访问修饰符 属性类型 属性名
{
get{
//逻辑代码
//可以进行解密
return 属性类型变量
}
set{
//逻辑代码
//可以进行加密
// 类中的变量=value(外部传入值)
}
}
//注意:
//1.成员属性中get和set可以加访问修饰符
//2.默认不加 会使用属性申明时的访问权限
//3.加的访问修饰符要低于属性的访问权限
//4.不能让get和set的访问权限都低于属性权限
//5.get和set可以只有一个,另一个不写
//6.成员属性对应一个成员变量,成员属性的名称一般为成员变量名称的大写

申明举例:


//在自定义类中声明使用成员属性
class Person
{
//成员变量name
private string name;
private int age;
//成员属性Name
public string Name
{
get {return name;} //返回成员变量name的值。
set {name=value; } //将value值传入name变量。value 关键字,用于表示外部传入的量。
}
//成员属性Age
Public int Age
{
private get{return age+5;} //get为私有属性,Age属性只能外部修改不能外部获取
set{age = value-5;}
}
}

//成员属性的调用
Person p1 = new Person();
p1.Name = "哇哈啊"; // 实际上调用的set语句块
Console.WriteLine(p1.Name); //实际上调用的get语句块

自动属性

//如果类的成员属性没有什么特殊处理,那么可以用自动属性。
Public float Height //申明了一个只能获取的Height成员属性
{
get; //默认跟随成员属性的访问修饰符
private set; //外部获得不能外部修改
}
//另一种写法
class Person
{
public string str { get; set; }
}
//相当于以下代码
class Person
{
private string str;
public string Str
{
get { return str; }
set { str = value }
}
}

索引器

基本概念: 为类定义一个索引器,让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写。

语法:

//声明方式一:
//索引器可以有多个传入参数
访问修饰符 返回值类型 this[参数类型 参数名 ,参数类型 参数名, ……]
{
get{
//返回参数值
// return 对象名[index];
}
set{
//设置赋值语句
//对象名[index] = value;
}
}
//声明方式二:
//索引类型不一定要用int型,也可以用string等其他类型。
[访问修饰符] 数据类型 this[索引参数类型 index]
{
get
{
//返回参数值,往往跟索引的不同返回不同的字段,用选择语句Switch
// return 对象名[index];
}
set
{
//设置赋值语句
//对象名[index] = value;
}
}
//如何访问索引器:
//像数组那样通过对象名的索引来选择就行 : 对象名[index]

声明方式一索引器使用举例:

//声明学生类
class Student
{
//成员变量
private int age;
private string name;
private Student[] friends;
//成员属性
public string GetName
{
get { return name; }
set { name = value; }
}
//成员属性
public int GetAge
{
get { return age; }
set { age = value; }
}
//索引器
public Student this[int index]
{
set
{
if(friends==null)
{
friends = new Student[] { value };
}
friends[index] = value;
}
get
{
if(friends==null||friends.Length-1<index||index<0)
{
return null;
}
return friends[index];
}
}
//成员方法
public void Init(Student s,int age,string name)
{
s.GetAge = age;
s.GetName = name;
Console.WriteLine(s.GetName);
Console.WriteLine(s.GetAge);
}
//构造函数重载
public Student(int age,string name)
{
this.age = age;
this.name = name;
}
//构造函数
public Student()
{
age = 0;
name = "";
}

}
//主程序
class Program
{
static void Main(string[] args)
{
Student s1 = new Student();
s1.Init(s1, 18, "老黄");
//索引器测试
Console.WriteLine("----------------------");
s1[0] = new Student(18, "老黄的朋友[0]"); //通过索引器访问s1的friends[]
Student s2 = s1[0]; //实例化一个student s2,将刚生成的朋友放在s2
Console.WriteLine(s2.GetName + " " + s2.GetAge + "岁了"); //检查s2是否是生成的朋友
}
}

声明方式二索引器举例:

class Student
{
private string name;
private string sex;
private string tel;
//索引器
public string this[int index]//【访问修饰符】 数据类型 this【索引器类型 index】 语法格式
{
get
{
switch (index)
{
case 1://由于return就有返回功能和结束功能所以这里的break可以省略因为写了运行不到这句代码
return name;
case 2:
return sex;
case 3:
return tel;
default:
throw new ArgumentOutOfRangeException("index");//抛出异常
}
}
set
{
switch (index)
{
case 1://这里必须要有break结束语句,因为每个case的功能语句都是赋值且没有结束语句所以这里需要break
name = value;
break;
case 2:
sex = value;
break;
case 3:
tel = value;
break;
default:
throw new ArgumentOutOfRangeException("index");//抛出异常
}
}
}
//成员方法
public void Speak()
{
//this[] this表示的是索引器,this[]表示访问向对应的字段。
Console.WriteLine("我叫{0},我是{1}生,我的电话是{2}", this[1], this[2], this[3]);
}
}
//主程序
class Program
{
static void Main(string[] args)
{
Student stu = new Student();//实例化对象
stu[1] = "xxx";//给索引器以数组的方式赋值
stu[2] = "男";
stu[3] = "13122022202";
stu.Speak();
Console.ReadKey();
}
}

静态成员

static​ 修饰的成员,叫做静态成员。静态成员是属于类的,通过类名直接访问。

  • 当类第一次被访问时,类下的所有静态成员就会被创建在内存中。
  • 静态成员既不在栈中也不在堆中,而是创建在 静态存储区
  • 静态成员 只创建一次,直到程序结束前都不会被销毁。
  • 静态成员函数 不能访问非静态成员
  • 静态成员 可以访问其他静态成员或函数

示例代码:

class Person
{
// 静态成员
public static int Age;

// 实例成员
public string Name;
}

class Program
{
static void Main(string[] args)
{
// 访问静态成员:通过类名
int age = Person.Age;

// 访问实例成员:通过对象
Person p = new Person();
p.Name = "Jack";
}
}

const 和 static 的区别

相同点

  • 都可以通过类名访问。

不同点

  1. const​ 必须初始化,且不可修改;static​ 没有此限制。
  2. const​ 只能修饰变量;static​ 可以修饰变量、方法、类、构造函数等。
  3. const​ 必须写在变量声明前面;static​ 没有此限制,通常可配合访问修饰符使用。

静态成员实现单例模式(不安全示例)

这是一个不推荐在生产环境中使用的单例实现,因为它在多线程场景下不安全。

public sealed class Singleton
{
private static Singleton instance = null;

private Singleton() { }

public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}

问题

  • 如果两个线程同时执行 if (instance == null)​,就可能创建多个实例,违背了单例的设计初衷。

线程安全的单例实现方式

方法一:使用 lock​ 加锁(懒汉式,线程安全)

这是最常见的一种线程安全实现方法:

public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object lockObj = new object();

private Singleton() { }

public static Singleton Instance
{
get
{
// 双重检查锁定(Double-Check Locking)
if (instance == null)
{
lock (lockObj)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}

说明:

  • lock​ 保证线程互斥地访问实例创建逻辑。
  • “双重检查锁定”是为了避免每次访问都加锁,提高性能。
  • 缺点是代码稍复杂,但性能和安全兼顾。

方法二:静态构造函数(饿汉式,线程安全,推荐)

利用 C# 静态构造函数的特性:

public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();

// 静态构造函数(自动线程安全)
static Singleton() { }

private Singleton() { }

public static Singleton Instance => instance;
}

说明:

  • 静态构造函数只执行一次,CLR 保证其线程安全。
  • 在类被首次使用前就完成实例初始化。
  • 缺点是:实例会 在程序启动后就创建,即使永远不会被用到。

方法三:使用 Lazy<T>​(懒汉式,线程安全,现代推荐方式)

C# 提供了 System.Lazy<T>​ 类,专为延迟初始化设计:

public sealed class Singleton
{
//用到了lamda表达式
private static readonly Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => lazyInstance.Value;
}

说明:

  • Lazy<T>​ 默认采用 LazyThreadSafetyMode.ExecutionAndPublication​,线程安全。
  • 实例在 首次访问时才创建,且线程安全,效率高。
  • 是 C# 中实现单例的 现代推荐写法
实现方式 是否懒加载 是否线程安全 是否推荐
简单懒汉式 ✅ 是 ❌ 否 ❌ 不推荐
lock双重检查 ✅ 是 ✅ 是 ✅ 推荐
静态构造函数 ❌ 否 ✅ 是 ✅ 推荐
Lazy ✅ 是 ✅ 是 ✅ 最推荐

静态类和静态构造函数

静态类(static class​)

特点:

  • 只能包含 静态成员
  • 不能被实例化
  • 不能继承或被继承

作用:

  1. 将静态成员集中放入静态类中,方便调用
  2. 静态类不能实例化,更能体现其作为工具类的唯一性
  3. 常用于存放工具方法(如:数学计算、字符串处理等)

✅ 例如:Console​ 就是一个静态类

static class Test
{
public static void SayHello()
{
Console.WriteLine("Hello from static class!");
}
}

调用方式:

Test.SayHello(); // 不需要实例化

静态构造函数(static 构造函数​)

特点:

  • 可存在于 普通类静态类
  • 没有访问修饰符(如 public、private 等)
  • 不能带参数
  • 系统自动调用:在第一次访问类的成员时自动执行
  • 只会被调用 一次

作用:

用于 初始化静态字段或执行类级别的一次性任务

语法格式:

class MyClass
{
static MyClass()
{
// 静态构造逻辑
Console.WriteLine("Static constructor executed.");
}

public static int Number = 42;
}

调用示例:

Console.WriteLine(MyClass.Number); // 首次访问类,静态构造函数被调用

拓展方法

可以在不修改某一个类的情况下,为该类扩展新的成员方法,实现方法的扩展。注意,不能为静态类扩展方法。

为一个类添加扩展方法需要满足三个要素:

  1. 扩展方法所在的类必须是静态类。
  2. 扩展方法本身必须是静态方法。
  3. 扩展方法的第一个参数需要使用关键字 this​,指定要扩展的类。

示例语法:

// 静态类
public static class TestExtensionM
{
// 静态扩展方法,扩展 int 类型
public static int ExtensionInt(this int s)
{
return s + s; //这个扩展方法是服务于int类型的,返回它自己的2倍;
}
}

使用方法也很简单:

int a = 9;
a = a.ExtensionInt(); // 结果为 18

扩展方法也可以带多个参数:

public static class TestExtensionM
{
// 静态扩展方法,带多个参数
public static int ExtensionInt(this int s, int m, int y)
{
return s + m + y;
}
}

使用示例:

int a = 10;
a = a.ExtensionInt(20, 30); // 结果为 60

注意事项:

  • 如果扩展的方法和类里面原有的方法重名,则调用该方法时运行类里面原有的方法

运算符重载

运算符重载​:允许自定义类型(类或结构体)重新定义标准运算符的行为,使得该类型的对象能够像内置类型一样,直接使用运算符进行操作。

作用:

  • 让自定义类型的对象更自然地使用 +​、-​、*​、==​ 等运算符。
  • 提升代码的可读性和可维护性。
  • 实现特定类型间的运算规则。

注意事项:

  • 只能重载已有的运算符,不能创造新的运算符。
  • 至少要重载一对相关运算符(比如 ==​ 和 !=​)。
  • 重载运算符必须是 public static​ 方法。
  • 运算符重载不是必须的,只在需要时使用。

基本语法:

public static 返回类型 operator 运算符(参数列表)
{
// 实现运算符逻辑
}

C# 中运算符重载的能力:

运算符 描述
+, -, !, ~, ++, – 这些一元运算符只有一个操作数,且可以被重载。
+, -, *, /, % 这些二元运算符带有两个操作数,且可以被重载。
==, !=, <, >, <=, >= 这些比较运算符可以被重载。
&&, || 这些条件逻辑运算符不能被直接重载。
+=, -=, *=, /=, %= 这些赋值运算符不能被重载。
=, ., ?:, ->, new, is, sizeof, typeof 这些运算符不能被重载。

继承

类的继承

继承是面向对象编程的基本特性。

继承允许一个类(子类)继承另一个类(父类)的属性​和方法​,从而实现代码复用和扩展。

语法: :类名

举例:

class Person()
{
public string Name;
public void SayHello()
{
Console.WriteLine("Hello, my name is " + Name);
}
}

class Student():Person
{
public int StudentID;
}
class Program
{
static void Main()
{
Student student = new Student();
student.Name = "Tom"; // 继承自Person
student.StudentID = 123;
student.SayHello(); // 调用继承的方法

Console.WriteLine("Student ID: " + student.StudentID);
}
}

继承构造函数:

  • 初始化子类时,会先调用父类的默认构造函数,然后再调用子类的构造函数。
  • 当父类或子类定义了构造函数时,默认的无参构造函数会被覆盖。
  • 在子类声明构造函数时,如果想指定调用父类的某个构造函数,可以使用 base(参数)​。
  • 如果想在一个类中用自己的构造函数调用该类的另一个构造函数,可以使用 this(参数)​。

示例代码:

public class GameObject  // 父类
{
float x;
float y;
float z;

protected GameObject(int i, int m, int k) // 构造函数
{
x = i;
y = m;
z = k;
Console.WriteLine("GameObject构造函数");
}

protected GameObject() // 构造函数重载
{
Console.WriteLine("GameObject构造函数重载");
}
}

public class Cube : GameObject // Cube类继承GameObject
{
public int vertex;
public int edge;

public int ConstructCube()
{
int c = vertex + edge;
vertex = 0;
edge = 0;
Console.WriteLine("您已创建正方体");
return c;
}

public Cube(int i, int m, int k) : base(i, m, k) // 使用Base()继承父类构造函数
{
vertex = 8;
edge = 12;
Console.WriteLine("Cube继承子类构造函数");
}

public Cube() // 构造函数重载
{
Console.WriteLine("Cube构造函数继承子类重载");
}

public Cube(string str) : this() // 调用自己的无参构造函数
{
Console.WriteLine("我用构造函数调用自己类的构造函数说了:" + str);
}
}

万物之父(object),装箱和拆箱

object​(System.Object)是所有类型的终极父类(基类),所有类型都可以向上转换为object​。

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

Object装引用类型:

object​ 是所有类型的基类,引用类型可以直接赋值给 object​ 类型变量。

装箱和拆箱

  • 装箱:将值类型转换为引用类型(如object),将值从栈内存复制到堆内存,并封装成对象。
  • 拆箱:将引用类型中的值转换回对应的值类型,从堆内存复制回栈内存,必须显式强制转换。

装箱的作用

  • 允许值类型引用类型的形式存储和传递,方便在需要引用类型的场景(如集合)中使用值类型。

拆箱的作用

  • 将装箱后的引用类型恢复为原始值类型,方便进行值类型的操作。

优缺点

  • 好处:方便不同类型的数据存储和传递,增强灵活性。
  • 坏处:装箱和拆箱涉及内存复制和堆分配,带来额外的性能开销,频繁使用会影响效率。

装类类型

Object o = new Person();  // Person 对象赋值给 object
Person s = new Person();
o = s; // object 可以引用 Person 对象(引用赋值)
  • 使用时推荐里氏替换原则,即用父类(或基类)变量引用子类实例。
  • 判断和转换:
if (o is Person)  // 判断 o 是否是 Person 类型或其子类实例
{
Person s2 = o as Person; // 安全转换,失败时返回 null
// 或者 Person s2 = (Person)o; // 强制转换,失败时抛异常
}

装字符串类型

Object o = "12342";  // 字符串是引用类型,直接赋值给 object

// 使用方式:
string str1 = o.ToString(); // 调用 ToString(),返回字符串表示
string str2 = o as string; // 使用 as 转换,成功返回字符串,失败返回 null

装数组类型

object o = new int[10];

// 转换方法1:使用 as(安全转换,失败返回 null)
int[] a = o as int[];

// 转换方法2:强制类型转换(失败抛异常)
int[] a2 = (int[])o;

装值类型:

object o = 1;   // 将值类型装箱为引用类型
// 转换方法 强制类型转换
int i = (int)o; // 将object类型拆箱回int类型

  • 装箱:将值类型转换为引用类型(如object),将值从栈内存复制到堆内存,并封装成对象。
  • 拆箱:将引用类型中的值转换回对应的值类型,从堆内存复制回栈内存,必须显式强制转换。

密封类

  • 用sealed封装,不能被继承的类。

  • 在面向对象的程序设计中,密封类的主要作用是不允许最底层的子类被继承,这样安全性更高。

语法:

sealed class ClassName
{
// 类成员
}

示例代码

// 定义一个密封类
sealed class Animal
{
public void Speak()
{
Console.WriteLine("动物叫声");
}
}

// 下面的代码会报错,因为Animal是sealed,不能被继承
// class Dog : Animal
// {
// }

补充说明

  • 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
{
public string name;
protected Animals(string name)
{
this.name = name;
}

public virtual void Speak() // V: virtual 虚方法
{
Console.WriteLine("I am an animal");
}
}

public class Duck : Animals
{
public Duck(string name) : base(name) { }

public override void Speak() // O: override 重写方法
{
// 如果想继承父类部分逻辑可用 base.Speak();
Console.WriteLine("I am a duck, My name is {0}", name);
}
}

public class Turkey : Animals
{
public Turkey(string name) : base(name) { }

public override void Speak()
{
Console.WriteLine("I am a turkey, My name is {0}", name);
}
}

public class Program
{
public static void Main(string[] args)
{
Animals duck01 = new Duck("Ducky");
duck01.Speak(); // 输出:I am a duck, My name is Ducky

Animals turkey02 = new Turkey("Kidden");
turkey02.Speak(); // 输出:I am a turkey, My name is Kidden
}
}

C# 抽象类与抽象方法(abstract​)

抽象类

关键词:abstract

什么是抽象类?

  • 抽象类是用于被其他类继承的类,不能直接被实例化

  • 它通常用来定义一类对象的通用属性和行为

  • 抽象类的作用是作为父类模板,供子类实现具体逻辑

例如:Thing​ 类是“物品”的统称,是一个抽象概念,不能单独存在,只能被继承。

抽象方法的特点

  • 抽象方法没有方法体(即不包含具体实现)
  • 必须在子类中进行重写(override)
  • 抽象方法必须定义在抽象类中

示例说明:动物类

  • Animal​ 是抽象类,不能实例化,只能作为父类存在
  • Shout()​ 是抽象方法,所有子类必须重写此方法
public abstract class Animal // 抽象类
{
public string name;

// 抽象方法,无方法体
public abstract void Shout();
}

public class Cat : Animal
{
public override void Shout()
{
Console.WriteLine("Meow!");
}
}

public class Dog : Animal
{
public override void Shout()
{
Console.WriteLine("Woof!");
}
}

抽象类使用规则总结

编号 规则描述
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 类名
{
public abstract 返回类型 方法名(参数列表);
}

示例代码:

// 抽象类
public abstract class Animal
{
public abstract void MakeSound(); // 抽象方法
}

// 子类必须实现抽象方法
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}

抽象方法与普通虚方法的区别

比较项 抽象方法 (abstract​) 虚方法 (virtual​)
是否有方法体 ❌ 没有 ✅ 有默认实现
是否必须重写 ✅ 是 ❌ 可选
是否必须在抽象类中 ✅ 是 ❌ 否

抽象方法的优点

  • 提供统一的接口规范
  • 强制子类实现特定功能
  • 有助于面向对象中的“多态性”设计

使用场景示例

所有子类都必须实现某些功能时:

  • 比如:Animal​ 类中的 MakeSound()​ 方法
  • Shape​ 类中的 CalculateArea()​ 方法

小结

抽象方法是一种设计规范的体现。
希望所有子类都必须实现某个方法时,就使用抽象方法。

接口

什么是接口?

关键词:Interface

  • 里面的成员不能实现,被类和接口继承,一个类可以继承多个接口,继承接口后必须实现其成员

  • 接口不能被实例化,只能当作存储容器,遵循里氏替换原则

  • 接口相当于抽象行为的基类,是行为的抽象规范,一般用来抽象行为

举例

interface Fly //接口

{

public void Func_Fly();

}

class Bird : Animals, Fly //小鸟继承了接口

{

public Bird(string name) : base(name) //构造函数

{

}

public void Func_Fly() //实现接口成员

{

Console.WriteLine("小鸟完成了飞行");

}

}

显示实现接口

场景说明

当一个类实现多个接口,而这些接口中有同名的方法或属性时,为了避免命名冲突,必须通过“显式实现”来区分它们

语法格式示例

interface IOne
{
void DoSomething();
}

interface ITwo
{
void DoSomething();
}

class MyClass : IOne, ITwo
{
// 显式实现接口 IOne 的方法
void IOne.DoSomething()
{
Console.WriteLine("IOne.DoSomething");
}

// 显式实现接口 ITwo 的方法
void ITwo.DoSomething()
{
Console.WriteLine("ITwo.DoSomething");
}
}

调用方式

MyClass obj = new MyClass();
// 必须强制转换为接口类型,才能访问显式实现的方法
((IOne)obj).DoSomething(); // 输出:IOne.DoSomething
((ITwo)obj).DoSomething(); // 输出:ITwo.DoSomething

注意

  • 显式实现的方法不会在类实例中直接暴露

    • 即:obj.DoSomething();​ ❌ 无法调用
  • 只能通过接口引用或者强制类型转换来访问

适用场景

  • 实现多个接口中具有相同成员签名的方法
  • 防止接口方法被类实例直接访问(封装接口成员)
  • 提高类的灵活性和接口解耦能力

小结

项目 说明
是否支持重载 支持,同名方法根据接口区分
调用方式 必须通过接口引用或强制类型转换
好处 避免命名冲突,增强封装性
限制 类中无法直接访问显式实现的方法

密封函数

当在override​前加上sealed​时,这个函数不能被子类重写,当在继承子类中,在重写的父类函数override​关键字前加sealed​,则不允许继承子类的子类再次进行重写。

命名空间

命名空间是用来组织和重用代码的

命名空间就像是一个工具包,类就是一个个工具,都是在命名空间申明的

语法:

Namespace 空间名

{

}

举例

Namespace MyGame
{
Class Player
{

}
}
  • 命名空间可以同名分开写,相当于同一个命名空间

  • 不同命名空间相互使用需要引用命名空间或指明出处

引用:

  • 在命名空间上方using system​ 这相当于引用了system​命名空间

  • 如果要用另一个命名空间,则要加 using 命名空间名

调用某一命名空间的类的方法:

在另一个域里使用某个命名空间下的类时,语法如下

using MyGame;		//1.引用命名空间
Class MyfirstScript
{
static void newPlayer()
{
MyGame.Player = new MyGame.Player(); //2. 命名空间名 . 类名 来使用
}
}

单例模式(singleton)

C#设计模式(1)——单例模式

C#实现单例模式的几种方法

C#单例模式最佳实践

单例模式的概念:确保一个类只有一个实例,并提供一个全局访问点。

单例模式的多种实现方式:懒汉模式、饿汉模式

区别:懒汉:类在加载时不会实例化自己的对象,饿汉:类在加载时就实例化自己的对象

单例模式在多线程下涉及到线程同步问题,为此我们要设置双重锁定。

代码示例

//饿汉模式
public class Singleton
{
private static readonly Singleton _instance = new Singleton();
public static Singleton Instance
{
get
{
return _instance;
}
}
}

//懒汉模式,双重锁定的实现方式,解决了线程安全问题并优化了性能。非常经典的写法
public class Singleton
{
// 定义一个静态变量来保存类的实例
private static Singleton uniqueInstance;

// 定义一个标识确保线程同步
private static readonly object locker = new object();

// 定义私有构造函数,使外界不能创建该类实例
private Singleton()
{
}

/// <summary>
/// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
// 当第一个线程运行到这里时,此时会对locker对象 "加锁",
// 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
// lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
// 双重锁定只需要一句判断就可以了
if (uniqueInstance == null)
{
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
//使用.NET 4 Lazy<T> type 特性
public class LazySingleton
{
private static readonly Lazy<LazySingleton> _instance =
new Lazy<LazySingleton>(() => new LazySingleton());

public static LazySingleton Instance
{
get { return _instance.Value; }
}
}
//调用示例:
public class Program
{
public static void Main()
{
var instance = LazySingleton.Instance;
}
}

字符串常用方法

字符串常用方法说明

功能类别 方法名/属性 参数说明 示例代码
获取长度 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;

class Program
{
static void Main()
{
string str = "可达鸭呀";
// 1. 字符串指定位置获取(索引器)
Console.WriteLine(str[0]); // 输出:可
// 遍历字符串
for (int i = 0; i < str.Length; i++)
{
Console.Write(str[i]); // 输出:可达鸭呀
}
Console.WriteLine();
// 转换为 char 数组
char[] chars = str.ToCharArray();
Console.WriteLine(chars[0]); // 输出:可
// 2. 字符串拼接
string st = string.Format(str + "233333");
Console.WriteLine(st); // 输出:可达鸭呀233333
// 3. 查找字符位置
int index = str.IndexOf("鸭"); // 正向查找,找不到返回 -1
Console.WriteLine(index); // 输出:2
index = str.LastIndexOf("达鸭"); // 反向查找,查找的是“达鸭”的起始索引
Console.WriteLine(index); // 输出:1
// 4. 移除字符
Console.WriteLine(str); // 输出:可达鸭呀(原字符串未改变)
string removed1 = str.Remove(2); // 移除索引 2 及其后所有字符
Console.WriteLine(removed1); // 输出:可达
string removed2 = str.Remove(2, 1); // 从索引 2 移除 1 个字符
Console.WriteLine(removed2); // 输出:可达呀
// 5. 替换字符串
string replaced = str.Replace("可达鸭", "小猫咪");
Console.WriteLine(replaced); // 输出:小猫咪呀
// 6. 字母大小写转换
string st4 = "asdawdjwiao";
string upper = st4.ToUpper(); // 转大写
Console.WriteLine(upper); // 输出:ASDAWDJWIAO
string lower = upper.ToLower(); // 转小写
Console.WriteLine(lower); // 输出:asdawdjwiao
// 7. 字符串截取
str = "我是宫崎英高desu";
string substr1 = str.Substring(6); // 从索引 6 开始截取到结尾
Console.WriteLine(substr1); // 输出:desu
string substr2 = str.Substring(2, 3); // 从索引 2 开始截取 3 个字符
Console.WriteLine(substr2); // 输出:宫崎英
// 8. 字符串切割
string st6 = "1,2,3,4,5,6,7,8";
string[] parts = st6.Split(',');
foreach (string part in parts)
{
Console.Write(part); // 输出:12345678
}
Console.WriteLine();
}
}

StringBuilder

什么是StringBuilder​?

一个字符串经常被修改会很消耗内存空间,stringBuilder用于解决这一问题。

  • StringBuilder​ 是 System.Text​ 命名空间下的一个公共类。
  • 作用:用于频繁修改或拼接字符串时,避免创建多个新字符串对象,提高性能并节省内存开销。

特点:

特性 说明
可变字符串 修改字符串内容不会生成新的字符串对象
自动扩容 默认初始容量为16,插入内容超出后会自动扩容
可指定初始容量 可以通过构造函数设置初始容量,避免频繁扩容带来的性能浪费

使用命名空间:using System.Text;

构造方式:

// 默认初始容量为16
StringBuilder sb1 = new StringBuilder("123123");
// 指定初始容量为48
StringBuilder sb2 = new StringBuilder("123123", 48);

属性说明:

属性/方法 说明 示例
Capacity 当前 StringBuilder 的容量 Console.WriteLine(sb1.Capacity);
Length 实际字符长度 Console.WriteLine(sb1.Length);

举例:

using System.Text;

class Program()
{
static void main(string arg[])
{
System.Text.StringBuilder SB01 = new System.Text.StringBuilder("123123");
//StringBuilder存在容量问题,每次往里面增加时,会自动扩容,默认初始容量为16
System.Text.StringBuilder SB01 = new System.Text.StringBuilder("123123",48);//可以指定容量,也可不指定。
Console.WriteLine(SB01.Capacity); //获取容量
}
}

StringBuilder的增删查改:

增:

// 在末尾添加字符串
sb.Append("追加内容");
// 添加格式化字符串
sb.AppendFormat("{0} + {1}", "Hello", "World");

插入:

// 在指定位置插入字符串
sb.Insert(2, "插入内容");

删除:

// 从索引 3 开始删除 2 个字符
sb.Remove(3, 2);

清空:

// 清空 StringBuilder 内容
sb.Clear();

查找:

// 输出指定位置字符
Console.WriteLine(sb[1]);

改:

// 修改指定索引位置字符
sb[0] = '新';

替换:

// 替换所有 "旧" 字符为 "新"
sb.Replace("旧", "新");

重新赋值:

// 清空再重新追加内容
sb.Clear();
sb.Append("新的内容");

判断相等:

if (sb.ToString().Equals("目标字符串"))
{
// 执行逻辑
}

推荐使用场景

  • 在循环中频繁拼接字符串时(如日志生成、大批量字符串组合)。
  • 替代 string +=​ 等不高效的字符串拼接方式。
  • 实现动态内容构造,如 HTML、SQL 构建器等。

数据结构类

Arraylist

ArrayList 的本质

ArrayList​ 是 C# 提供的 非泛型集合类,属于 System.Collections​ 命名空间下的一个 动态数组,可以存储 任意类型的对象(object) ,容量可以动态扩展。

  • 本质上是一个支持自动扩容的 object[]​ 数组。
  • 存储值类型会发生 装箱,读取时需要 拆箱,性能略低于泛型集合如 List<T>​。
  • 在 .NET 泛型集合 (System.Collections.Generic​) 出现之前常用,现代开发建议使用 List<T>​ 替代。

Arraylist引用命名空间与声明语法

//引用命名空间
using System.Collections;
// 创建一个空的 ArrayList
ArrayList list = new ArrayList();
// 创建并初始化
ArrayList list2 = new ArrayList() { 1, "hello", true };

ArrayList的增删查改

//添加(Add)
//由于arraylist是object类型,所以可以加任何类型
list.Add("字符串");
list.Add(123); // 装箱操作(int → object)
list.Add(true);
list.AddRange(list2) //把list2所有元素添加到list的末尾
//删除
list.Remove("字符串"); // 删除第一个匹配项
list.RemoveAt(0); // 删除指定索引处元素
list.Clear(); // 清空所有元素
//查找
var box = list[0]; //根据索引下标查找
bool exists = list.Contains("字符串"); // 是否存在
int index = list.IndexOf(123); // 查找索引(找不到返回 -1)
int index = list.LastIndexOf(123); // 查找索引(找不到返回 -1)
//修改
list[1] = "新的值";

图形用户界面, 文本 描述已自动生成

文本 描述已自动生成

图形用户界面, 网站 描述已自动生成

图形用户界面 描述已自动生成

图形用户界面, 文本 中度可信度描述已自动生成

图片包含 文本 描述已自动生成

图形用户界面, 文本 描述已自动生成

插入与遍历

插入:

ArrayList list = new ArrayList();
list.Insert(1, "中间插入");

第一个参数传索引位置,第二个传插入内容

图形用户界面, 文本 中度可信度描述已自动生成

它和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# 事件Event(个人整理)

C# 浅谈事件监听及任务处理(监听属性值的改变及定时执行任务)

匿名函数

匿名函数语法和用处

图片包含 文本 描述已自动生成

文本 描述已自动生成

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

图形用户界面, 文本, 应用程序 描述已自动生成

使用匿名函数:

在类里使用的话,就用使用委托,为委托赋值一个匿名函数。因为事件不能在类里声明。

image

image

image

image

//无参无返回
Action func1 = delegate ()
{

};
//有参无返回
Action<int> func2 = delegate (int i)
{

};
//有返回值
Func<string> func3 = delegate () {
return "111";
};
//有参有返回值
//<>里只有最后一个参数为返回值,前面的都为传入参数
Func<string, int,string> fun4 = delegate (string a,int b) {
return "11";
};
一般匿名函数可作为参数传递,没学匿名函数之前,我们是先声明一个函数,再将函数名传递进去作为参数,现在我们可以直接写一个匿名函数直接传参。
//声明一个测试类
class Test
{
//委托,存储方法
public Action action;
//函数作为参数的方法
public void Dosomething(int a,Action action)
{
Console.WriteLine(a);
//执行委托里的函数
action();
}
}
//匿名函数作为参数传入
Test test = new Test();
test.Dosomething(10, delegate ()
{
Console.WriteLine("随参数传入的匿名函数逻辑");
});
匿名函数还可以作为返回值返回给委托储存
class Test2
{
//返回值为函数的方法
public Action GetFunc()
{
return delegate () {
//作为返回值的匿名函数逻辑
};
}
}
//使用方法:
Test2 test2 = new Test2();
Action action = test2.GetFunc();
action();
//一步到位
Test2 test2 = new Test2();
test2.GetFunc()();

匿名函数缺点:

委托可以多播委托,当我们传入多个函数给委托时,传入的匿名函数无法被指定清除,要想清除只能用清空方法

image

image

回调函数

回调函数的理解

字面上的理解,回调函数就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。这个过程就叫做回调。

其实很好理解,回调,回调,就是回头调用的意思。主函数的事先干完,回头再调用传进来的那个函数。

使用回调函数有两种写法,使用委托或者事件。

回调函数的使用

//使用委托实现回调函数

using System;

public class Program
{
// 定义一个委托
public delegate void CallbackDelegate(string message);

// 方法1:执行某个任务并接受回调
public static void DoWork(CallbackDelegate callback)
{
Console.WriteLine("执行任务...");
// 模拟任务完成后调用回调函数
callback("任务完成!");
}

public static void Main()
{
// 定义一个回调函数
CallbackDelegate myCallback = (message) =>
{
Console.WriteLine($"回调信息: {message}");
};
// 调用DoWork,并传递回调函数
DoWork(myCallback);
}
}

//使用事件实现回调函数
using System;

public class Program
{
// 方法2:使用Action作为回调
public static void DoWork(Action<string> callback)
{
Console.WriteLine("执行任务...");
callback("任务完成!");
}

public static void Main()
{
// 使用Action传递回调
DoWork((message) =>
{
Console.WriteLine($"回调信息: {message}");
});
}
}

Lamda表达式

什么是lamda表达式

image

Lamda表达式语法

image

使用

image

image

image

image

缺点和匿名函数缺点一样

image

闭包(重要)

https://www.cnblogs.com/eventhorizon/p/9535289.html

https://www.cnblogs.com/pangjianxin/p/8400155.html 这个讲的比较好

我们把在Lambda表达式(或匿名方法)中所引用的外部变量称为**捕获变量**。而捕获变量的表达式就称为**闭包**。

捕获的变量会在真正**调用委托**时“赋值”,而不是在捕获时“赋值”,即总是使用捕获变量的**最新的值**。

image

class Test3
{
public event Action action;
public Test3()
{
int value = 10;
//这里就形成了闭包
//因为当构造函数执行完毕时,其中申明的临时变量value的生命周期被改变了
//因为当构造函数执行时,value就被存到事件函数action里,不会被释放
action = () => {
Console.WriteLine(value);
};

//由于lamda表达式对临时变量i的捕获,延长了其生命周期
//所以当for循环执行完时,事件action里存放的i的值都为10,可以用地址来理解
for (int i = 0; i < 10; i++)
{
action += () =>
{
Console.WriteLine(i);
};
}
}
}

List排序(重要)

List自带排序方法

这个排序可以进行值类型(int、float)的排序

List之所以可以用Sort()来进行排序,是因为List继承并实现了接口IComparable,Sort()调用了C#帮我们实现的值类型比较接口。

//创建列表对象
//可对int、float、double等值类型进行排序
List<int> list = new List<int>();
list.Add(3);
list.Add(5);
list.Add(1);
//自带排序
list.Sort();
//对float进行排序
List<float> list = new List<float>();
list.Add(1.4f);
list.Add(1.23f);
list.Add(2.11f);
list.Sort();

自定义类的排序

为了可以让自定义的类也可以进行排序, 我们要让自定义的类继承C#的IComparable<>泛型接口,并实现该接口。

这样使用Sort()方法可以调用我们实现的接口方法。

//自定义类
class Item :IComparable<Item> //继承IComparable接口
{
public string name { get; set; }
public float money { get; set; }
public Item(string name,float money)
{
this.name = name;
this.money = money;

}
//实现接口包含的比较方法:
//返回值为负数表示在前,为0表示位置不变,为1表示在后
public int CompareTo(Item other)
{
if (this.money>other.money)
{
return 1;
}
else
{
return -1;
}
}
}
//调用List里的Sort()方法
List<Item> items = new List<Item>();
items.Add(new Item("辣条",2.5f));
items.Add(new Item("棒棒糖", 1.5f));
items.Add(new Item("巧克力",1.0f));
items.Sort();
for (int i = 0; i < items.Count; i++)
{
Console.WriteLine(items[i].name + items[i].money);
}

通过委托函数进行排序

List的Sort()方法有个委托重载,这可以让我们使用委托函数来进行排序。这样我们就可以传入函数了,这时我们可以使用匿名函数或者lamda表达式作为参数传入。甚至为了代码简洁,还能用到三目运算符。
//定义自定义的类
class ShopItem
{
public int id;
public ShopItem(int id)
{
this.id = id;
}
}
//使用lamda表达式进行排序
List<ShopItem> shopitem = new List<ShopItem>();
shopitem.Add(new ShopItem(1));
shopitem.Add(new ShopItem(4));
shopitem.Add(new ShopItem(3));
shopitem.Add(new ShopItem(2));
//sort有委托重载,所以可以传入lamda函数
shopitem.Sort((a, b) =>
{
if (a.id>b.id)
{
return 1;
}
else
{
return -1;
}
});
Console.WriteLine("**********");
foreach (ShopItem item in shopitem)
{
Console.WriteLine(item.id);
}
//还可以写得更简单(使用三目运算符):
shopitem.Sort((a, b) =>
{
//使用三目运算符
return a.id > b.id ? 1 :-1;
});

总结

在实际开发中, 我们会经常用到List 的Sort排序,例如背包物品的排序,商店物品的排序等。

image

协变逆变

image

作用:

image

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

//out修饰T 只能作为返回值
delegate T Test_out<out T>();
//In修饰T 只能作为传入参数
delegate void Test_in<in T>(T t);
class Father
{
}
class Son : Father
{
}
//测试:
//1.协变:父类能替代子类
//使用子类返回值委托
Test_out<Son> son = () =>
{
return new Son();
};
//父类委托装子类委托
//如果Test_out委托没有out修饰,就会报错
Test_out<Father> father= son;
//--------------------------------------
//2.逆变:父类可以被子类替代
//使用父类作为传入参数的委托
Test_in<Father> father = (value) =>
{
};
//子类委托装父类委托
Test_in<Son> son = father;

image

多线程

什么是进程

image

image

什么是线程

image

image

什么是多线程

image

image

语法

image

class Program
{
//运行标识
static bool isRuning=true;
static void Main(string[] args)
{
//1.申明一个线程
//将想要执行的函数作为参数传入
Thread t = new Thread(NewThreadFunc);
//2.起动线程
t.Start();
//3.设置为后台线程
//如果线程的逻辑为死循环,那么不设置为后台线程,则新开线程在主线程结束后不会结束
t.IsBackground = true;
//4.关闭释放一个线程
// 如果是死循环,想要释放有两个方法
//1.申明static bool isRunning=true;
//将isRunning作为while的参数,设置isRunning=false,关闭线程
isRuning = false;
//2.使用线程自带方法(在.net core无法使用该方法)
try
{
t.Abort();
t = null;
}
catch (Exception)
{
throw;
}
}
//封装线程执行的逻辑
static void NewThreadFunc()
{
//5.线程休眠
//毫秒为单位,在哪个线程里写,就让哪个线程休眠几秒
Thread.Sleep(1000);
//线程执行的死循环逻辑,可以不是死循环
while (isRuning)
{
Console.WriteLine("线程逻辑");
}
}
}

线程之间的共享数据(重要)image

lock解决了当多个线程访问同一个内存的东西时,会导致的逻辑执行顺序问题。这是一种解决方案,但并不是最优解,想要更好的性能优化可以了解其他锁。
class Program
{
//运行标识
static bool isRuning=true;
//申明一个引用类型,供lock使用
static object obj;
static void Main(string[] args)
{
Thread t = new Thread(NewThreadFunc);
t.Start();
while (true)
{
//使用加锁,参数为引用类型
//意思是,当obj没被锁的时候,就会锁住obj,执行代码
//执行完会解锁
lock (obj)
{
Console.WriteLine("hello");
}

}

}
//封装线程执行的逻辑
static void NewThreadFunc()
{
while (isRuning)
{
//使用加锁,参数为引用类型
//意思是,当obj没被锁的时候,就会锁住obj,执行代码
//执行完会解锁
lock (obj)
{
Console.WriteLine("线程逻辑");
}

}
}
}

多线程的意义

当我们进行一些特别复杂的逻辑时,可能会导致卡顿,所以我们可以开一个线程来进行复杂的运算,从而让主线程可以流程运行。

image

image

预处理指令

image

image

image

image

image

image

image

image

反射和特性

什么是程序集

image

image

image

image

image

image

迭代器

image

image

image

image

image

进阶

什么是LinQ?用来干什么?

LINQ(Language Integrated Query)是一种C#语言中的**查询技术**,它允许我们在代码中使用**类似SQL的查询语句来操作各种数据源**。这些数据源可以是集合、数组、数据库、XML文档等等。LINQ提供了一种统一的编程模型,使我们能够使用相同的方式来查询和操作不同类型的数据。

比如我们可以用LinQ来操作SQL server数据库。除此之外,我们还可以操作数组、枚举集合、泛型列表等。

C#中的LINQ语法

使用 LINQ(Language-Integrated Query)语法在 C# 中进行查询操作

文章:C# Linq基本功 —— 必学的必熟的10个方法

1.检索List列表中装载的对象的属性

我们知道,当我们用List类对象装载了一个类的多个对象时,我们不能通过直接List类对象名点出来他包含的类对象的属性,只能通过Foreach循环来通过索引器来遍历。LINQ中的=>语法可以让我们直接通过List类对象直接对其包含的类对象的属性进行检索。  

例:

public class Paper
{
public String PaperName { get; set; }
// public List<Question> paper = new List<Question>();
public string Content { get; set; }
public string Score { get; set; }
}

public List<Paper> Papers = new List<Paper>();
//假设Papers中已经添加了多个Paper对象,我们可以通过以下方法来检索Paper属性,筛选出来满足条件的对象。
Paper f = Papers.Find(Papers=>Papers.PaperName==“检索内容”);

C#面试题

.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引