CSharp知识点

数据类型

值类型

类型描述范围默认值
bool布尔值True 或 FalseFalse
byte8 位无符号整数0 到 2550
char16 位 Unicode 字符U +0000 到 U +ffff‘\0’
decimal128 位精确的十进制值,28-29 有效位数(-7.9 x 1028 到 7.9 x 1028) / 100 到 280.0M
double64 位双精度浮点型(+/-)5.0 x 10-324 到 (+/-)1.7 x 103080.0D
float32 位单精度浮点型-3.4 x 1038 到 + 3.4 x 10380.0F
int32 位有符号整数类型-2,147,483,648 到 2,147,483,6470
long64 位有符号整数类型-9,223,372,036,854,775,808 到 9,223,372,036,854,775,8070L
sbyte8 位有符号整数类型-128 到 1270
short16 位有符号整数类型-32,768 到 32,7670
uint32 位无符号整数类型0 到 4,294,967,2950
ulong64 位无符号整数类型0 到 18,446,744,073,709,551,6150
ushort16 位无符号整数类型0 到 65,5350

引用类型

引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用。

换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的 引用类型有:objectdynamicstring

对象(Object)类型

对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。

当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱

1
2
object obj;
obj = 100; // 这是装箱

动态(Dynamic)类型

您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。

声明动态类型的语法:

1
dynamic <variable_name> = value;

例如:

1
dynamic d = 20;

动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。

字符串(String)类型

字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。

例如:

1
string str = "runoob.com";

一个 @引号字符串:

1
@"runoob.com";

C# string 字符串的前面可以加 @(称作”逐字字符串”)将转义字符(\)当作普通字符对待,比如:

1
string str = @"C:\Windows";

等价于:

1
string str = "C:\\Windows";

@ 字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内。

1
2
3
4
string str = @"<script type=""text/javascript"">
<!--
-->
</script>";

用户自定义引用类型有:class(类)、interface(接口) 或 delegate(委托)。我们将在以后的章节中讨论这些类型。

指针类型(不安全代码)

指针

指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。

声明指针类型的语法:

1
type* identifier;

例如:

1
2
3
4
5
char* cptr;
int* p1, p2, p3;
int*[] p4; // p4是 指针的数组 而不是 数组的指针
void* p5; // 指向未知类型的指针
int** p6; // 指针的指针

使用

当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块

您可以使用 ToString() 方法检索存储在指针变量所引用位置的数据。下面的实例演示了这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
namespace UnsafeCodeApplication
{
class Program
{
public static void Main()
{
unsafe
{
int var = 20;
int* p = &var;
Console.WriteLine("Data is: {0} " , var);
Console.WriteLine("Data is: {0} " , p->ToString());
Console.WriteLine("Address is: {0} " , (int)p);
}
Console.ReadKey();
}
}
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
Data is: 20
Data is: 20
Address is: 77128984

编译

为了编译不安全代码,您必须切换到命令行编译器指定 /unsafe 命令行。

例如,为了编译包含不安全代码的名为 prog1.cs 的程序,需在命令行中输入命令:

1
csc /unsafe prog1.cs

枚举

声明

  • 在namespace语句块中声明枚举(常用)
  • 在class、struct语句块中也可以声明枚举
  • 枚举不能在函数语句块中声明!

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;

namespace Enum_practise
{
enum Day {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
};
public class EnumTest
{
static void Main()
{
Day today = Day.Sun;
switch (today)
{
case Day.Sun:
Console.WriteLine("今天星期日");
break;
case Day.Mon:
Console.WriteLine("今天星期一");
break;
case Day.Tue:
Console.WriteLine("今天星期二");
break;
case Day.Wed:
Console.WriteLine("今天星期三");
break;
case Day.Thu:
Console.WriteLine("V我50");
break;
case Day.Fri:
Console.WriteLine("今天星期五");
break;
case Day.Sat:
Console.WriteLine("今天星期六");
break;
}
}
}
}

数组

一维数组

1
2
3
4
5
6
// 变量类型可以是所有变量类型
int[] arr1;
int[] arr2 = new int[5];
int[] arr3 = new int[5] { 1, 2, 3, 4, 5 };
int[] arr4 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] arr5 = { 1, 2, 3, 4, 5 };

二维数组

1
2
3
4
5
6
7
8
9
10
11
int[,] arr;
int[,] arr2 = new int[3, 3];
int[,] arr3 = new int[3, 3] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };
int[,] arr4 = new int[,] { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };
int[,] arr5 = { { 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 } };

交错数组

真正的数组的数组。与二维数组的区别:每个一维数组的长度可以不一样。

1
2
3
4
5
6
7
8
9
10
11
int[][] arr;
int[][] arr2 = new int[3][];
int[][] arr3 = new int[3][] { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };
int[][] arr4 = new int[][] { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };
int[][] arr5 = { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };

方法(函数)

ref 和 out

按引用传递参数(ref)

引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。

在 C# 中,使用 ref 关键字声明引用参数。下面的实例演示了这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;

temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}

static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;

Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);

/* 调用函数来交换值 */
n.swap(ref a, ref b);

Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);

Console.ReadLine();

}
}
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
4
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:200
在交换之后,b 的值:100

结果表明,swap 函数内的值改变了,且这个改变可以在 Main 函数中反映出来。

按输出传递参数(out)

return 语句可用于只从函数中返回一个值。但是,可以使用 输出参数 来从函数中返回两个值。输出参数会把方法输出的数据赋给自己,其他方面与引用参数相似。

下面的实例演示了这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;

namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x )
{
int temp = 5;
x = temp;
}

static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;

Console.WriteLine("在方法调用之前,a 的值: {0}", a);

/* 调用函数来获取值 */
n.getValue(out a);

Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.ReadLine();

}
}
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5

提供给输出参数的变量不需要赋值。当需要从一个参数没有指定初始值的方法中返回值时,输出参数特别有用。请看下面的实例,来理解这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;

namespace CalculatorApplication
{
class NumberManipulator
{
public void getValues(out int x, out int y )
{
Console.WriteLine("请输入第一个值: ");
x = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("请输入第二个值: ");
y = Convert.ToInt32(Console.ReadLine());
}

static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a , b;

/* 调用函数来获取值 */
n.getValues(out a, out b);

Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.WriteLine("在方法调用之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}

当上面的代码被编译和执行时,它会产生下列结果(取决于用户输入):

1
2
3
4
5
6
请输入第一个值:
7
请输入第二个值:
8
在方法调用之后,a 的值: 7
在方法调用之后,b 的值: 8

ref 和 out的区别

  • ref 传入的变量必须初始化,out 不用 (买票上车)
  • out 传入的变量必须在内部赋值,ref 不用 (买票下车)

变长参数

变长参数允许在调用方法时传入不定长度的参数。变长参数是一个语法糖,本质上还是基于数组的实现

使用

使用 params 关键字可以指定被params修饰的参数在传参时数目可变:

1
2
3
4
5
6
7
8
public static void UseParams(params object[] list)
{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}

方法调用

调用方式主要有三种,第一种是传一个数组,第二种是传n个参数,第三种是不传入参数:

1
UseParams(new object[] { 1,3,"test"}); // 传入数组
1
UseParams(1, 'a', "test"); // 传入多个参数
1
UseParams(); // 不传参

注意

  • params关键字后面必为数组
  • 数组类型可以是任意的类型
  • 函数参数可以有别的参数和params关键字修饰的参数
  • 函数参数中只能最多出现一个params关键字并且一定是在最后一组参数,前面可以有n个其他参数

命名实参

通过命名实参,你可以为形参指定实参,方法是将实参与该形参的名称匹配,而不是与形参在形参列表中的位置匹配。有了命名实参,将不再需要将实参的顺序与所调用方法的形参列表中的形参顺序相匹配。 每个形参的实参都可按形参名称进行指定。 例如:

1
2
PrintOrderDetails("Gift Shop", 31, "Red Mug");
// 卖家姓名、订单号和产品名称
1
2
3
// 使用命名实参调用
PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

可选参数

参数默认值的参数,一般称为可选参数。当调用函数时可以不传入该参数,不传就会使用默认值作为参数的值。

例:

1
2
// 定义
public void ExampleMethod(int required, string optionalstr = "default string", int optionalint = 10)
1
2
// 调用
anExample.ExampleMethod(3); // 后两个参数为默认值

注:

  • 支持多参数默认值,每个参数都可以有默认值

  • 如果要混用,可选参数必须写在普通参数后面

  • 默认值必须是以下类型的表达式之一:

    • 常量表达式
    • new ValType() 形式的表达式,其中 ValType 是值类型,例如 enumstruct
    • default(ValType) 形式的表达式,其中 ValType 是值类型
  • 如果调用方为一系列可选形参中的任意一个形参提供了实参,则它必须为前面的所有可选形参提供实参,实参列表中不支持使用逗号分隔的间隔,如:

    1
    //anExample.ExampleMethod(3, ,4);

    如果想要选择性提供实参,可以使用命名实参来实现:

    1
    anExample.ExampleMethod(3, optionalint: 4);

重载

概念

重载允许在同一作用域内定义多个同名函数,但这些函数必须在参数类型、个数或顺序上存在区别,以便编译器可以根据传递给函数的参数来确定调用哪个函数。在C#中,函数的重载是通过函数签名(函数名称和参数类型、数目以及顺序)来确定的,并不包括返回类型。可以定义相同函数名的函数,只要它们的参数有所不同即可。如果有多个函数符合调用条件,则编译器会选择最匹配的函数。

作用

  1. 命名一组功能相似的函数,减少函数名的数量,避免命名空间的污染
  2. 提升代码复用性与可读性

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Program
{
static void Main(string[] args)
{
ClassOverload CO = new ClassOverload();
CO.GetInfo();
CO.GetInfo(2);
CO.GetInfo("我是第3个重载方法", 2);
CO.GetInfo(2, "我是第4个重载方法");
}
}
class ClassOverload
{
public void GetInfo()
{
Console.WriteLine("我是第1个重载方法!");
}
public void GetInfo(int Num)
{
Console.WriteLine($"我是第{Num}个重载方法!");
}
public void GetInfo(string Name, int Num)
{
Console.WriteLine($"{Name}我有{Num}个参数");
}
public void GetInfo(int Num, string Name)
{
Console.WriteLine($"{Name}我有{Num}个参数,我的参数顺序与第3个方法不同");
}
}

注意

  1. 在使用重载时只能通过不同的参数样式

  2. 不能通过访问权限、返回类型进行重新加载

  3. 方法的数目不会对重新加载造成影响

  4. 对于继承来说,如果某一方法在父类中是访问权限是private,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果

  5. 接口不能重载


结构体(与类的区别)

结构体提供了一种轻量级的数据类型,适用于表示简单的数据结构,具有较好的性能特性和值语义。

与类的区别

类和结构体在设计和使用时有不同的考虑因素,类适合表示复杂的对象和行为,支持继承和多态性,而结构体则更适合表示轻量级数据和值类型,以提高性能并避免引用的管理开销。

类和结构体有以下几个基本的不同点:

数据类型:

  • 结构体是值类型(Value Type): 结构体是值类型,它们在栈上分配内存,而不是在堆上。当将结构体实例传递给方法或赋值给另一个变量时,将复制整个结构体的内容。
  • 类是引用类型(Reference Type): 类是引用类型,它们在堆上分配内存。当将类实例传递给方法或赋值给另一个变量时,实际上是传递引用(内存地址)而不是整个对象的副本。

变量:

  • 结构体中声明的变量不能直接初始化。
  • 类可以在声明时对变量初始化。

继承和多态性:

  • 结构体不能继承: 结构体不能继承其他结构体或类,也不能作为其他结构体或类的基类。结构体成员不能指定为 abstract、virtual 或 protected。
  • 类支持继承: 类支持继承和多态性,可以通过派生新类来扩展现有类的功能。

默认构造函数:

  • 结构体不能有无参数的构造函数、不能定义析构函数: 结构体不能包含无参数的构造函数。每个结构体都必须有至少一个有参数的构造函数。不能定义析构函数。
  • 类可以有无参数的构造函数: 类可以包含无参数的构造函数,如果没有提供构造函数,系统会提供默认的无参数构造函数。

实例:

  • 类必须使用 New 操作符才能被实例化。
  • 结构体可以不使用 New 操作符即可被实例化。如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。

赋值行为:

  • 类型为类的变量在赋值时存储的是引用,因此两个变量指向同一个对象。
  • 结构体变量在赋值时会复制整个结构,因此每个变量都有自己的独立副本。

传递方式:

  • 类型为类的对象在方法调用时通过引用传递,这意味着在方法中对对象所做的更改会影响到原始对象。
  • 结构体对象通常通过值传递,这意味着传递的是结构体的副本,而不是原始结构体对象本身。因此,在方法中对结构体所做的更改不会影响到原始对象。

可空性:

  • 结构体是值类型,不能直接设置为 null: 因为 null 是引用类型的默认值,而不是值类型的默认值。如果你需要表示结构体变量的缺失或无效状态,可以使用 Nullable<T> 或称为 T? 的可空类型。
  • 类默认可为null: 类的实例默认可以为 null,因为它们是引用类型。

性能和内存分配:

  • 结构体通常更轻量: 由于结构体是值类型且在栈上分配内存,它们通常比类更轻量,适用于简单的数据表示。
  • 类可能有更多开销: 由于类是引用类型,可能涉及更多的内存开销和管理。

面向对象理论

面向对象三大特性

封装

类和对象

类中一般包含:

  • 特征——成员变量
  • 行为——成员方法
  • 保护特征——成员属性
  • 构造函数和析构函数
  • 索引器
  • 运算符重载
  • 静态成员

对象是类创建出来的,相当于申明一个指定类的变量,类创建对象的过程一般称为实例化对象,类对象都是引用类型的。

实例化

1
2
3
Person p;
Person p2 = null; // 与上面相同,没有分配堆内存,栈中为null
Person p3 = new Person(); // 分配了堆内存

成员变量和访问修饰符

成员变量基本规则

  • 申明在类语句块中
  • 用来描述对象的特征
  • 可以是任意变量类型
  • 数量不作限制
  • 可以初始化
  • 是否赋值根据需求来定

访问修饰符

当前类当前程序集下的类其他程序集下的子类其他程序集下的类
public
protected
internal(默认)
private

protected internal: 成员可以由当前程序集或子类中的代码访问。

private protected: 成员可以在其定义的类及子类中被访问,但仅当这些子类在同一个程序集中时。

注意: 如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal,成员的默认访问标识符是 private

构造函数

基本概念: 类的 构造函数 是类的一个特殊的成员函数,当创建类的新对象时执行。在实例化对象时,会调用的用于初始化的函数。

写法:

  • 构造函数的名称与类的名称完全相同,它没有任何返回类型。
  • 默认的构造函数没有任何参数。但是如果你需要一个带有参数的构造函数可以有参数,这种构造函数叫做参数化构造函数
  • 没有特殊需求时一般都是public的

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
{
private int age;
private string name;

public Person() {}

public Person(string name)
{
this.name = name;
}

public Person(int age, string name)
{
this.age = age;
this.name = name;
}
}

注:

  • 构造函数可以被重载
  • this关键字代表当前调用该函数的对象自己,数据类型为当前对象
  • 如果不自己实现无参构造函数而实现了有参构造函数,会失去默认的无参构造

特殊写法:

可以通过 : 运算符重用其他构造函数代码,例如:

1
2
3
4
5
6
7
public class Manager : Employee
{
public Manager(int annualSalary) : base(annualSalary) // 使用基类构造函数
{
//Add further instructions here.
}
}
1
2
3
4
public Employee(int weeklySalary, int numberOfWeeks) : this(weeklySalary * numberOfWeeks) // 使用当前类其他构造函数
{

}

这样做时,当使用此构造函数,函数会先调用 : 之后的构造函数,再执行当前构造函数的代码。

主构造函数

主构造函数是 C# 12 中的一项新功能,可用于直接在构造函数参数列表中定义和初始化属性。此功能消除了对重复代码的需要,并使代码更加简洁和可读。

概念: 主构造函数是一种简洁的语法,用于声明一个构造函数,其参数在类型的主体中的任何位置都可用。

注意: 主构造函数的参数在整个类定义的范围内,因此可以使用它们来初始化属性、字段或其他成员。但是,默认情况下,它们不会存储为字段或属性,除非您显式将它们分配给一个字段或属性。它们也不能作为 或 访问,因为它们不是类的成员。

示例:

1
2
3
4
5
6
7
public class Employee(string firstName, string lastName, DateTime hireDate, decimal salary)  
{
public string FirstName { get; init; } = firstName;
public string LastName { get; init; } = lastName;
public DateTime HireDate { get; init; } = hireDate;
public decimal Salary { get; init; } = salary;
}

此代码等效于以下没有主构造函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee  
{
public string FirstName { get; init; }
public string LastName { get; init; }
public DateTime HireDate { get; init; }
public decimal Salary { get; init; }

public Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
{
FirstName = firstName;
LastName = lastName;
HireDate = hireDate;
Salary = salary;
}
}

特殊写法: 使用主构造参数时仍可使用 thisbase 关键字,使用特殊写法调用其他构造函数。

析构函数

基本概念: 类的 析构函数 是类的一个特殊的成员函数,当类的对象超出范围时执行。当引用类型的堆内存被回收时,会调用该函数

写法: 析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。

注意:

  • 析构函数用于在结束程序(比如关闭文件、释放内存等)之前释放资源。析构函数不能继承或重载
  • 对于需要手动管理内存的语言(比如c++),需要在析构函数中做一些内存回收处理,但是c#中存在自动垃圾回收机制GC,所以几乎不会这怎么使用析构函数,除非你想在某一个对象被垃圾回收时,做一些特殊处理
  • 在Unity开发中析构函数几乎不会使用,所以该知识点只做了解即可

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
private int age;
private string name;

public Person() {}

public Person(string name)
{
this.name = name;
}

public Person(int age, string name)
{
this.age = age;
this.name = name;
}

~Person() {} // 析构函数
}

垃圾回收机制(GC)

概念:

在编写程序时,会产生很多的数据 比如:int string 变量,这些数据都存储在内存里,如果不合理的管理他们,就会内存溢出导致程序崩溃。C#内置了自动垃圾回收GC,在编写代码时可以不需要担心内存溢出的问题 变量失去引用后 GC会帮我们自动回收,但不包括数据流,和一些数据库的连接,这就需要我们手动的释放资源

垃圾回收,英文简写GCGarbage Collector)。垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用。所谓的垃圾就是没有被任何变量、对象引用的内容,垃圾就需要被回收释放。

垃圾回收有很多种算法,比如引用计数(Reference Counting)、标记清除(Mark Sweep)、标记整理(Mark Compact)、复制整合(Copy Collection)等。

机制:

代的概念:代是垃圾回收机制使用的一种算法(分代算法),新分配的对象都会被配置在弟0代内存中,每次分配都可能会进行垃圾回收以释放内存(0代内存满时),大对象总被认为是第二代内存,不会对大对象进行搬迁压缩,目的是减少性能损耗,85000字节(83kb)以上的对象为大对象。

过程:垃圾回收共分3代,每次创建对象的时候 都是在第0代分配内存,并且每一代都配有初始内存空间。假设现在程序已经跑了一段时间了,而第0代分配的空间已经满了,这时候就会进行垃圾回收,把失去引用的对象释放,此时未使用完的对象将进入到第1代。垃圾回收后,第0代就已经空了,后面创建的对象就会重新放入第0代,以此类推。0代满后,又会重新垃圾回收,还在使用的对象又会放入第1代,此后运行一段时间 1代也已经满了,而0代还在使用的对象也会移动到1代,这时候已经不够放了,又会进行垃圾回收,1代的将移动到2代,0代的将移动到1代(即1代进行垃圾回收时0代也会同时进行垃圾回收),以此类推。假如代数都满了,但对象都还在使用,并没有回收多少,这时GC就会自动的把初始内存给扩大,比如原来2MB扩大到4MB,还不够使用的情况下内存就满了,就会抛出异常。

**注意:**GC只负责堆内存的垃圾回收,引用类型都是存在堆中的,所以它们的分配和释放都通过垃圾回收机制来管理。栈上的内存是由系统自动管理的,值类型是在栈中分配内存的,它们有自己的生命周期,不用对他们进行管理,会自动分配和释放。

手动执行垃圾回收:

垃圾回收一般情况下是自动执行,如果想要手动执行垃圾回收,需要调动此行代码:

1
GC.Collect();

一般情况下不会频繁进行垃圾回收,因为垃圾回收过程比较复杂耗时,可能会造成程序卡顿,通常在Loading界面时顺便进行一次垃圾回收。

成员属性

概念:

用于保护成员变量,为成员变量的获取和赋值添加逻辑处理。解决访问修饰符的局限性。属性可以让成员变量在外部只能获取不能修改或者只能修改不能获取。

基本语法:

1
2
3
4
5
访问修饰符  属性类型  属性名
{
get{} //get需要返回值,没有返回值就会报错
set{}
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person
{
private string name;

public string Name
{
set
{
// 可以在设置之前添加一些逻辑规则
// 实现加密等操作
name = value;
// value关键字,表示外部传入的值
}
get
{
// 可以在返回之前添加一些逻辑规则
// 实现解密等操作
return name;
// 这个属性可以获取内容
}
}
}

调用:

1
2
3
4
5
Person p = new Person();

p.Name = "LiMing"; // '=' 默认调用set

Console.WriteLine(p.Name); // 直接使用默认调用get

成员属性中get和set前可以加访问修饰符: 默认不加会使用属性声明时的权限,加的访问修饰符要低于属性的访问权限,不能让set和get的访问权限都低于属性权限。

自动属性
类似于自动的成员变量,set和get会自动生成成员变量将他们包裹起来。

作用:外部能得不能改的特征,如果类中有一个特征是只希望外部能得不能改的,又没有什么特殊处理,就可以直接使用自动属性。

1
2
3
4
5
6
7
8
9
10
public float Height
{
//没有在set,get中写逻辑的需求或者想法
get;
set;
//可以添加访问修饰符
//如:private set;
}

public bool Sex { set; get; } = false;

索引器

概念: 索引器允许通过类实例的索引来访问该类的实例成员。它的声明类似于属性,但具有参数。通常情况下,索引器用于允许类的实例像数组一样通过索引进行访问。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
private string name;
private int age;
private Person[] friends;

public Person this[int index]
{
get
{
return friends[index];
}
set
{
// value代表传入的值
friends[index] = value;
}
}
}

使用:

1
2
Person p = new Person();
p[0] = new Person();

注:

  • 索引器支持多维索引
  • C#允许对索引器进行重载
  • 索引器可以具有多个参数,但每个参数的类型必须唯一
  • 索引器的参数可以是值类型或引用类型
  • 可以根据需要只声明 get 或 set 访问器,但至少必须实现其中一个

静态成员

概念:

静态关键字:static,用static修饰的成员变量,方法,属性等称为静态成员。静态成员可以直接用类名点出来使用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test
{
//静态成员变量
static public float PI=3.1415926f;
//成员变量
public int testInt=100;

//静态成员方法
public static float CalcCircle(float r)
{
return PI*r*r;
}
//成员方法
public void TestFun()
{
Console.WriteLine("123");
}
}

使用:

1
2
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalcCircle(2));

原理:

为什么可以点出来使用:静态成员会有一片静态存储空间,与程序同生共死,一般不参与垃圾回收机制(除非静态引用变量定义为Null,才会参与垃圾回收)

程序不是无中生有的,我们要使用对象,变量,函数都是要在内存中分配内存空间,在程序中产生一个抽象的对象。

静态成员的特点:程序开始运行时就分配内存空间,所以我们可以直接使用。静态成员与程序同生共死,只要使用了它,直到程序结束才会被释放,一个静态成员有唯一的小房间,静态成员就有了唯一性。

注意:

  • 静态函数不能使用非静态成员,非静态函数可以使用静态成员
  • const常量和静态变量都可以通过类名点出来使用,但const常量必须初始化,不能修改,static没有这个规则,static可以修饰很多。const常量一定是写在访问修饰符后面,而static没有这个要求。

静态类和静态构造函数

静态类:static关键字修饰的类称作静态类,静态类只能包含静态成员且不能被实例化。例如Console就是一个静态类。

作用: 将常用的静态成员写在静态类中,方便使用。静态类不能被实例化,更能体现工具类的唯一性。

静态构造函数:static关键字修饰的构造函数为静态构造函数,静态类和普通类都可以有静态构造函数。静态构造函数不能使用访问修饰符、不能有参数且只会自动调用一次。

作用: 静态构造函数用于初始化静态变量。

静态构造函数不可继承、不可被直接调用,当创建类实例或引用任何静态成员之前,静态构造函数被自动执行,并且只执行一次。

扩展方法

概念: 为现有的非静态变量类型添加新方法。

作用:

  • 提升程序拓展性
  • 不需要在对象中重新写方法
  • 不需要继承来添加方法
  • 为别人封装的类型写额外的方法

特点:

  • 一定是写在静态类中
  • 一定是个静态函数
  • 第一个参数为拓展目标
  • 第一个参数用this修饰

基本语法:

1
访问修饰符 static 返回值 函数名(this 拓展类名 参数名, 参数类型 参数名, 参数类型 参数名, ...)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static class Tools
{
//为int拓展了一个成员方法
//成员方法是需要实例化对象后才能使用的
//value代表使用该方法的实例化对象
public static void SpeakValue(this int value)
{
//拓展的方法的逻辑
Console.WriteLine("为int的拓展方法" + value);
}

public static void SpeakStringInfo(this string str, string str2, string str3)
{
Console.WriteLine("为string拓展的方法");
Console.WriteLine("调用方法的对象" + str);
Console.WriteLine("传的参数" + str2 + str3);
}

public static void Fun2(this Test t)
{
Console.WriteLine("我是为Test类拓展的方法");
}
}

class Test
{
public int i = 10;

public void Fun1()
{
Console.WriteLine("我是自带的成员方法");
}
}

使用:

1
2
3
4
5
6
7
8
int i = 10;
i.SpeakValue();

String str = "000";
str.SpeakStringInfo("Hello", "World");

Test t = new Test();
t.Fun2();

运算符重载

概念: 让自定义类和结构体能够使用运算符。

关键字: operator

特点:

  • 一定是一个公共的静态方法
  • 返回值写在operator前
  • 逻辑处理自定义

作用: 让自定义类和结构体对象可以进行运算。

注意:

  • 二元运算符需要成对实现

    • ==(相等)对应 !=(不等)

    • >(大于)对应 <(小于)

    • >=(大于等于)对应 <=(小于等于)

  • 一个符号可以多个重载

  • 不能使用 ref 和 out

  • 参数列表至少要有一个当前类(或结构体)类型的参数

基本语法:

1
public static 返回类型 operator 运算符( 参数列表 )

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point
{
int x;
int y;

public static Point operator+(Point p1, Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;

return p;
}
}

使用:

1
2
3
4
5
6
7
8
Point p = new Point();
p.x = 1;
p.y = 1;
Point p2 = new Point();
p2.x = 2;
p2.y = 2;

Point p3 = p + p2;

内部类和分部类

内部类: 在一个类中声明的类。

特点: 使用时要用包裹者点出自己。

作用: 亲密关系的变现 。

注意: 访问修饰符作用很大。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
{
public int age;
public string name;
public Body body;

public class Body
{
Arm leftArm;
Arm rightArm;

class Arm { }
}
}
1
2
Person p = new Person();
Person.Body body = new Person.Body();

分部类: 把一个类分成几部分声明(关键字:partial)。

作用: 增加程序的拓展性。

注意:

  • 分部类可以写在不同的脚本文件中
  • 分部类的访问修饰符要一致
  • 分部类中不能有重复成员

分部方法: 将方法的声明和实现分离(局限性大,了解即可)。

特点:

  • 不能加访问修饰符,默认私有
  • 只能在分部类中声明
  • 返回值只能是void
  • 可以有参数但不能用 out 关键字

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
partial class Student
{
public bool sex;
public string name;
}

partial class Student
{
public int number;

public void Speak(string str)
{
// ...
}

partial void Move(int speed);
}

partial class Student
{
public int GetHeight()
{
// ...
}

partial void Move(int speed)
{
// ...
}
}

继承

继承的基本原则

基本概念: 当一个类A继承一个类B时,类A将会继承类B的所有成员,A类将拥有B类的所有特征和行为。被继承的类称为父类、基类、超类,继承的类称为子类、派生类。子类可以有自己的特征和行为。

特点:

  • 单根性:子类只能有一个父类(单继承)
  • 传递性:子类可以间接继承父类的父类

基本语法:

1
2
3
4
class 类名 : 被继承的类名
{

}

同名成员: 在子类中出现与父类同名的成员,默认将父类的成员覆盖,也可以使用new关键字表示覆盖,但极其不建议使用同名成员。

1
public new string name;

里氏替换原则

里氏替换原则是面向对象七大原则中最重要的原则

基本概念: 任何父类出现的地方,子类都可替代。

重点: 语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容。

作用: 方便进行对象存储和管理。

基本实现: 用父类容器装载子类对象

1
2
3
4
5
6
7
class GameObject { }
class Player : GameObject { }
class Monster : GameObject { }
class Boss : GameObject { }

GameObject player = new Player();
GameObject[] objects = new GameObject[] { new Player(), new Monster(), new Boss() };

is 和 as 关键字:

  1. is:判断一个类对象是否是指定类对象,返回值为bool,是为真,不是为假

  2. as:将一个对象转换为指定类对象,返回值为指定类型对象,成功返回指定类型对象,失败返回null

  3. 基本语法:

    1
    2
    3
    4
    5
    // 书接上回
    if (player is Player)
    {
    Player p = player as Player;
    }

注:不能用子类容器装父类对象

继承中的构造函数

特点: 当声明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数。

注意: 父类的无参构造很重要。子类可以通过base关键字代表父类,调用父类构造。

继承中构造函数的执行顺序: 父类的父类的构造 -> 父类构造 -> 子类构造。子类实例化时,默认自动调用的是父类的无参构造,所以如果父类的无参构造被顶掉,会出错。

通过base调用指定的父类构造:

1
2
3
4
5
6
7
8
9
class Father
{
public Father(int i) { }
}

class Son : Father
{
public Son(int i) : base(i) { }
}

万物之父和装箱拆箱

万物之父: object 是所有类型的基类,它是一个类(引用类型)。

作用: 可以利用里氏替换原则,用 object 容器装所有对象。可以用来表示不确定类型,作为函数参数类型。

装箱拆箱:

  • 装箱:把值类型用引用类型存储(栈内存会迁移到堆内存中)
  • 拆箱:把引用类型存储的值取出来(堆内存会迁移到栈内存中)
  • 好处:不确定类型时可以方便参数的存储和传递
  • 坏处:存在内存迁移,增加性能消耗

密封类

概念: 密封类是使用 sealed 密封关键字修饰的类。密封类无法被继承。

示例:

1
sealed class Son : Father { }

作用: 在面向对象程序设计中,密封类的主要作用就是不允许最底层子类被继承,可以保证程序的规范性,安全性。


多态

Vob

多态的概念: 多态按字面的意思就是“多种状态”,让继承同一父类的子类们 在执行相同方法时有不同的表现(状态)。

多态的实现:

  • 编译时多态——函数重载
  • 运行时多态:Vob、抽象函数、接口

Vob:

  • v: virtual(虚函数)
  • o: override (重写)
  • b: base (父类)
  • (n): new (覆盖)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class GameObject
{
public string name;
public GameObject(string name)
{
this.name = name;
}

//虚函数 可以被子类重写
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}

class Player : GameObject
{
public Player(string name):base(name)
{

}

//重写虚函数
public override void Atk()
{
//base的作用
//代表父类 可以通过base来保留父类的行为
base.Atk();
Console.WriteLine("玩家对象进行攻击");
}
}

class Monster : GameObject
{
public Monster(string name):base(name)
{

}

public override void Atk()
{
Console.WriteLine("怪物对象进行攻击");
}
}

主要目的: 同一父类的对象 执行相同行为(方法)有不同的表现。

解决的问题: 让同一个对象有唯一行为的特征。

抽象类和抽象方法

概念: 被抽象关键字 abstract 修饰的类为抽象类,方法称为抽象方法。

特点:

  • 抽象类不能被实例化,但仍可以使用里氏替换原则装它的子类
  • 抽象方法只能在抽象类中声明,抽象方法没有方法体且不能是私有的
  • 继承抽象类必须重写其抽象方法

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Animal
{
public string name;

public abstract void Speak();
}

class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("The dog barks.");
}
}

class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("The cat meows.");
}
}

接口

概念: 接口是行为的抽象规范,它也是一种自定义类型。关键字: interface

接口声明的规范:

  1. 不能包含成员变量
  2. 只能包含方法、属性、索引器、事件
  3. 成员不能被实现
  4. 成员不能是私有的,但可以不写访问修饰符,默认是公共的
  5. 接口不能继承类,但可以继承另一个接口

接口的使用规范:

  1. 类可以继承多个接口
  2. 类继承接口后,必须实现接口中所有成员

特点:

  1. 和类的声明类似
  2. 接口只能用来继承,不能被实例化,但可以作为容器存储对象(里氏替换原则)
  3. 接口名称通常以大写字母 “I” 开头,表示接口(Interface)的含义,然后跟随具体的接口名称

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class GameObject
{
public string num;

public void Atk() => Console.WriteLine("攻击");

}

interface IExampleInterface
{
protected void Method1();
public void Method2();

string name { get; set; }
}

interface IExampleInterface2
{
public void Method4();
}

interface IExampleInterfaceSon : IExampleInterface
{
void Method3();
}

class Book : GameObject, IExampleInterfaceSon, IExampleInterface2
{
public void Method1() { }
public void Method2() { }
public void Method3() { }
public void Method4() { }

public string name { get; set; }
}

显式实现接口:

当一个类继承多个接口,但接口中存在同名方法时,可以使用显式实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IAtk
{
void Atk();
}

interface ISuperAtk
{
void Atk();
}

class Player : IAtk, ISuperAtk
{
void IAtk.Atk() { }

void ISuperAtk.Atk() { }
}

注: 显式实现接口不能写访问修饰符。

密封方法

概念: 密封方法是使用 sealed 密封关键字修饰的重写方法,让虚方法或抽象方法在之后不能再被重写。

特点:override 一起出现。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Animal
{
public abstract void Eat();

public virtual void Speak() { }
}

class Person : Animal
{
public sealed override void Eat() { }
public sealed override void Speak() { }
}

面向对象七大原则

开闭原则OCP

开闭原则是设计原则基础的基础,是面向对象的核心原则,其它原则均围绕开闭原则进行展开。

开闭原则指的是一个软件实体应对对扩展开放,对修改关闭(Software entities should be open for extension, but closed for modification)。 这个原则是说在设计一个模块的时候,应对使这个模块可以在不被修改的前提下被扩展,换言之,应对可以不必修改源代码的情况下改变这个模块的行为。

满足开闭原则的软件系统的优越性:

  • 通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性

  • 已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性

实现开闭原则的关键:

抽象化是解决问题的关键,在面向对象的编程语言里,可以给系统定义出一套相对较为固定的抽象设计,此设计允许无穷无尽的行为在实现层被实现。在语言里,可以给出一个或多个抽象类或者接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层预见了所有的可扩展性,因此,在任何扩展情况下都不会改变。这就使得系统的抽象不需要修改,从而满足了开闭原则的第二条,对修改关闭。

同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了开闭原则的第一条。

对可变性的封装原则:

把变化的东西封装起来,把不变的抽象出来。 这是对开闭原则的另外一种描述,它讲的是找到一个系统的可变因素,将之封装起来。该原则意味着两点:

  • 一种可变性不应当散落在代码的很多角落,而应当封装到一个对象里面。继承应当被看做是封装变化的方法,而不应该被认为是一种从一般对象生成特殊对象的方法

  • 一种可变性不应当与另外一种可变性混合在一起。这意味着一般的继承层次不会超过两层

里氏替换原则LSP

任何基类可以出现的地方,子类一定可以出现。即父类存在的地方,子类是可以替换的。 替换后行为不变,结果会变化。调用子类行为。 子类和父类必须有相同行为才能完全地实现替换。

实现开闭原则的关键是抽象化,而里氏代换原则中的基类和子类的继承关系正是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。违反里氏代换原则一个最经典的例子便是把正方形设计成长方形的子类。

依赖倒置原则DIP

要依赖于抽象,不要依赖于实现。 说的白一点就是要依赖于抽象类和接口不要依赖具体类,具体类也就是我们可以用new关键字实例化的类。依赖倒转原则是实现开闭原则的一个手段。

单一职责原则SRP(体现高内聚)

每一个类应该专注于做一件事情。

接口隔离原则ISP

应当为客户端提供尽可能小的单独接口,而不要提供大的总接口。 暴露行为让后面的实现类知道的越少越好。

迪米特法则(体现松偶合)

又叫最少知识原则,一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用。

合成复用原则

要尽量使用合成/聚合达到复用,而不是继承关系达到复用的目的。尽量少用继承。 就如我们前面说的,如果为了复用,便使用继承的方式将两个不相干的类联系在一起,这样的方式是违反合成/聚合复用原则的,更进一步的后果那便是违反里氏代换原则。合成/聚合复用和里氏代换原则相辅相成,合成/聚合复用原则要求我们在复用时首先考虑合成/聚合关系,而里氏代换原则是要求我们在使用继承时,必须满足一定的条件。

原则:一个类中有另一个类的对象。


面向对象设计模式

面向对象设计的23种设计模式


面向对象相关

命名空间

概念

命名空间是用来组织和重用代码的,就像是一个工具包,类就像是一件一件的工具,都是声明在命名空间中的。命名空间的设计目的是提供一种让一组名称与其他名称分隔开的方式。在一个命名空间中声明的类的名称与另一个命名空间中声明的相同的类的名称不冲突。

定义:

1
2
3
4
namespace namespace_name
{
// 代码声明
}

命名空间可以分开声明且可以嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace MyGame
{
class GameObject { }
}

namespace MyGame
{
class Player : GameObject { }

namespace UI
{
class TestUI { }
}

namespace Image
{
class TestImage { }
}
}

同一命名空间下不能含有同名类,不同命名空间下可以含有同名类:

1
2
3
4
5
6
7
8
9
namespace Run
{
class Move { }
}

namespace Fly
{
class Move { }
}

使用

1.使用 using 关键字引用其他命名空间使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using MyGame; // 只如此引用仍要通过指明出处才能引用该命名空间下的命名空间的类
using MyGame.UI; // 如此引用只能使用UI命名空间的类,可以与MyGame同时引用
using System;

namespace Now
{
class Test
{
GameObject gameobject = new GameObject();
Player player = new Player();
TestUI testUI = new TestUI();

MyGame.Image.TestImage testImage = new MyGame.Image.TestImage();
}
}

2.指明出处使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Now
{
class Test
{
System.Console.WriteLine("111");

MyGame.GameObject gameObject = new MyGame.GameObject();
MyGame.Player player = new MyGame.Player();
MyGame.Image.TestImage testImage = new MyGame.Image.TestImage();

// 同名类只能通过指明出处使用
Run.Move move1 = new Run.Move();
Fly.Move move2 = new Fly.Move();
}
}

(引用命名空间后也可指明出处使用)

注:命名空间中的类默认为 internal 。

Object类

object中的静态方法

静态方法 Equals

判断两个对象是否相等。最终的判断权交给左侧对象的 Equals 成员方法,不管值类型还是引用类型都会按照左侧对象的 Equals 成员方法的规则来进行比较。

静态方法 ReferenceEquals

比较两个对象是否是相同的引用,主要是用来比较引用类型的对象,值类型对象返回值始终是 false 。

object中的成员方法

普通方法 GetType

该方法的主要作用是获取对象运行时的类型 Type ,通过 Type 结合反射相关知识点可以做很多关于对象的操作。该方法在反射相关知识点中是非常重要的方法。

普通方法 MemberwiseClone

该方法用于获取对象的浅拷贝对象,意思就是会返回一个新的对象,但是新对象中的引用变量会和老对象中的一致。

object中的虚方法

虚方法 Equals

默认实现还是比较两者是否为同一个引用,即相当于 ReferenceEquals 。但是微软在所有类型值的基类 System.ValueType 中重写了该方法,用来比较值相等。我们也可以重写该方法,定义自己的比较规则。

虚方法 GetHashCode

该方法时获取对象的哈希码(一种通过算法算出的,表示对象的唯一编码,不同对象的哈希码有可能一样,具体值根据哈希算法决定),我们可以通过重写该函数来自己定义对象的哈希码算法,正常情况下,基本不用。

虚方法 ToString

该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串规则,该方法非常常用。当我们调用打印方法时,默认使用的就是对象的 ToString 方法后打印出来的内容。

String类

字符串拼接

1
2
3
str = string.Format("{0}{1}", 1, 3333);
Console.Write(str);
// 输出 13333

正向查找字符位置

1
2
3
4
str = "你好哈哈哈";
int index = str.IndexOf("好"); // 找不到返回-1
Console.Write(index);
// 输出 1

反向查找指定字符串位置

1
2
3
4
str = "你好哈哈哈哈";
int index = str.LastIndexOf("哈哈"); // 找不到返回-1
Console.Write(index);
// 输出 4

移除指定位置后的字符(包括指定位置)

1
2
3
4
str = "你好哈哈哈哈";
string newstr = str.Remove(3); // 只会返回新字符串,不会修改原字符串
Console.Write(newstr);
// 输出 你好哈
1
2
3
4
str = "你好哈哈哈哈";
string newstr = str.Remove(3, 1); // 第二个参数决定移除的字符个数
Console.Write(newstr);
// 输出 你好哈哈哈

替换指定字符串

1
2
3
4
str = "你好哈哈哈哈";
string newstr = str.Replace("哈哈", "嘻嘻");
Console.Write(newstr);
// 输出 你好嘻嘻哈哈

大小写转换

1
2
3
str = "jhsdgfhsdgfs";
string newstr = str.ToUpper(); // 转大写
newstr = str.ToLower(); // 转小写

字符串截取(包含指定位置)

1
2
3
4
str = "你好哈哈哈哈";
string newstr = str.Substring(2); // 截取从指定位置开始之后的字符串
Console.Write(newstr);
// 输出 哈哈哈哈
1
2
3
4
str = "你好哈哈哈哈";
string newstr = str.Substring(2, 2); // 第二个参数表示截取指定个数
Console.Write(newstr);
// 输出 哈哈

字符串切割

1
2
3
4
5
str = "1,2,3,4,5,6,7,8";
string[] strs = str.Split(','); // 以逗号切割
for (int i = 0; i < strs.Length; i ++)
Console.Write(strs[i] + " ");
// 输出 1 2 3 4 5 6 7 8

StringBuilder类

string 是特殊的引用,每次重新赋值或者拼接时会分配新的内存空间,如果一个字符串经常改变会非常浪费空间。

StringBuilder 是c#提供的一个用于处理字符串的公共类,主要解决的问题是:直接修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,可以提升性能。

注意使用时需要引用命名空间

使用:

1
2
3
4
StringBuilder str = new StringBuilder("12213123123", 50); // 初始化与指定容量(可选)
// StringBuilder始终会有空余容量,超过会自动扩容,每次自动扩容容量翻一倍
Console.WriteLine(str.Capacity); // 获取容量
Console.WriteLine(str.Length); // 获取字符串长度

方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 增加
str.Append("8888"); // 加到字符串末尾
str.AppendFormat("{0}{1}", 444, 666); // 通过拼接的形式添加
// 插入
str.Insert(0, "2784356"); // 插入位置 插入内容
// 删除
str.Remove(0, 10); // 起始位置 删除个数
// 清空
str.Clear();
// 查找
// str[1] 直接索引
// 修改
str[0] = 'A'; // 直接修改
// 替换
str.Replace("34", "283645");

StringBuilder 的所有方法执行后都会直接修改原字符串,不需要重新定义。


数据结构类

ArrayList类(可变数组)

概念: ArrayList 本质是一个 object类型的数组,实现了很多方法。

声明:

1
2
// 需要引用命名空间 System.Collections
ArrayList array = new ArrayList();

常用方法:

  • 增加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 单个增加
    array.Add(1);
    array.Add("3424");

    // 批量增加(把另一个List容器里面的内容全部加到后面)
    array.AddRange(array2);

    // 插入
    array.Insert(1, "12345");
  • 删除

    1
    2
    3
    array.Remove("ghfjk"); // 删除第一个指定元素
    array.RemoveAt(0); // 删除指定位置的元素
    array.Clear(); // 清空
  • 查找

    1
    2
    3
    4
    5
    6
    7
    8
    // 得到指定位置的元素
    Console.WriteLine(array[0]); // 直接读取下标
    // 查看元素是否存在
    array.Contains("123"); // 返回bool类型,有就返回true,没有返回false
    // 正向查找元素位置
    int index = array.IndexOf("gfh"); // 返回元素下标,没有返回-1
    // 反向查找元素位置
    index = array.LastIndexOf("111");
  • 修改

    1
    array[0] = "3334"; // 直接修改

装箱拆箱:

ArrayList 本质上是一个可以自动扩容的 object 数组,当往其中进行值类型存储时就是在装箱,当值类型对象取出来转换使用时就是在拆箱,所以 ArrayList 尽量少用,有更好的数据容器。

注: ArrayList 同样具有容量,原理和作用与 StringBuilder 相同。

Stack类(栈)

概念: Stack 的本质也是 object 数组,封装了栈的存储规则,栈是一种先进后出的数据结构。

声明:

1
2
// 需要引用命名空间 System.Collections
stack stack = new Stack();

常用方法:

  • 增加

    1
    2
    3
    // 压栈
    stack.Push(0);
    stack.Push("123");
  • 取出

    1
    2
    // 弹栈
    object v = stack.Pop();
  • 查看

    1
    2
    3
    4
    5
    // 栈无法查看指定位置的元素,只能查看栈顶的内容
    v = stack.Peek();

    // 查看元素是否存在于栈中
    stack.Contains("123"); // true / false
  • 修改

    1
    2
    // 栈无法改变其中的元素,只能压和弹,实在要改,只有清空
    stack.Clear();

Queue类(队列)

概念: Queue 的本质也是 object 数组,封装了队列的存储规则,队列是一种先进先出的数据结构。

声明:

1
2
// 需要引用命名空间 System.Collections
Queue queue = new Queue();

常用方法:

  • 增加

    1
    2
    queue.Enqueue(1);
    queue.Enqueue("123");
  • 取出

    1
    object v = queue.Dequeue();
  • 查找

    1
    2
    3
    4
    5
    // 查看队列头部元素
    v = queue.Peek();

    // 查看元素是否在队列中
    queue.Contains("123"); // true / false
  • 修改

    1
    2
    // 队列无法改变其中的元素,只能进和出,实在要改,只有清空
    queue.Clear();

Hashtable类(哈希表)

概念: Hashtable (又称散列表)是基于键的哈希代码组织起来的键值对集合,它的主要作用是提高数据查询的效率,使用键来访问集合中的元素。

声明:

1
2
// 需要引用命名空间 System.Collections
Hashtable hashtable = new Hashtable();

常用方法:

  • 增加

    1
    2
    3
    4
    // 注意:不能出现相同键
    hashtable.Add(1, 1);
    hashtable.Add(2, "123");
    hashtable.Add("123", 2); // 第一个参数是key
  • 删除

    1
    2
    3
    4
    5
    // 直接通过键删除
    hashtable.Remove("123");

    // 清空
    hashtable.Clear();
  • 查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 通过键查找值 找不到会返回空
    Console.WriteLine(hashtable["key"]);

    // 查看是否存在
    // 根据键检测
    hashtable.Contains("2");
    hashtable.ContainsKey("2");
    // 根据值检测
    hashtable.Contains.ContainsValue("123");
  • 修改

    1
    2
    // 只能改键对应的内容,无法修改键
    hashtable["123"] = 100.5f;

泛型

泛型

概念

泛型实现了类型参数化,达到代码重用的目的,通过类型参数化来实现同一份代码上操作多种类型。

泛型相当于类型占位符,定义类或方法时使用替代符代表变量类型,当真正使用类或者方法时再具体指定类型。

泛型类和接口

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestClass1<T>
{
public T value;
}

class TestClass1 { }
// 泛型属于类名的一部分,即使它们名字相同,如果泛型不同(是否引用泛型、泛型引用个数),它们也不是同一个类

interface ITest<T>
{
T Value { get; set; }
}

// 泛型占位字母可以有多个,用逗号分开
class TestClass2<T, E, M, XXX>
{
public T value1;
public E value2;
public XXX value3;
public M value4;
}

使用:

1
2
3
4
5
6
TestClass1<int> t1 = new TestClass1<int>();
t1.value = 10;
TestClass1<string> t2 = new TestClass1<string>();
t2.value = "123";

TestClass2<int, string, float, bool> t3 = new TestClass2<int, string, float, bool>();

继承:

1
2
3
4
5
// 继承时实现指定
class Test : ITest<int>
{
public int Value { get; set; }
}

泛型方法

普通类中的泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Test1
{
public void TestFun1<T>(T value) { /* 函数逻辑 */ }

// 泛型可以有多个
public void TestFun2<T, E>()
{
T t = default(T);
// ...
// 可以用泛型类型做一些逻辑处理
}

// 泛型作返回值
public T TestFun3<T>()
{
T Value;
return Value;
}
}
1
2
3
4
5
6
7
8
Test1 t1 = new Test2();

t1.TestFun1<int>(1);
t1.TestFun1<string>("12321312");

t1.TestFun2<bool, string>();

string str = t1.TestFun3<string>();

泛型类中的泛型方法:

1
2
3
4
5
6
7
8
9
10
class Test2<T>
{
public T Value;

// 这种不是泛型方法,T是类的泛型
public void TestFun(T t) { }

// 这种才算泛型方法
public void TestFun<E>(E e) { }
}
1
2
3
Test2<int> t2 = new Test2<int>();

t2.TestFun<string>("123213");

泛型的作用

  • 不同类型对象的相同逻辑处理就可以用泛型
  • 使用泛型可以一定程度上避免装箱拆箱

泛型约束

什么是泛型约束

概念: 让泛型的类型有一定的限制。

关键字: where

泛型约束一共有6种:

  1. 值类型: where 泛型字母 : struct
  2. 引用类型: where 泛型字母 : class
  3. 存在无参公共构造函数: where 泛型字母 : new()
  4. 某个类本身或其派生类: where 泛型字母 : 类名
  5. 某个接口的派生类型: where 泛型字母 : 接口名
  6. 另一个泛型类型本身或者派生类型: where 泛型字母 : 另一个泛型字母

约束的组合使用

1
2
class Test<T> where T : class, new() { }
// 可以通过逗号组合使用

多个泛型有约束

1
2
3
4
class Test<T, M, E> where T : class, new() where M : new() where E : struct
{
// ...
}

泛型数据结构类

List(列表)

概念: List 本质是一个可变类型的泛型数组,实现了很多方法。

声明:

1
2
// 需要引入命名空间 System.Collections.Generic
List<int> list = new List<int>();

常用方法:

  • 增加

    1
    2
    3
    list.Add(1); // 单加
    list.AddRange(list2); // 批量加
    list.Insert(0, 999); // 插入
  • 删除

    1
    2
    3
    list.Remove(1); // 删除指定元素
    list.RemoveAt(0); // 删除指定位置元素
    list.Clear(); // 清空
  • 查找

    1
    2
    3
    4
    // list[0] // 直接通过下标查找指定位置元素
    list.Contains(1); // 查看元素是否存在
    list.IndexOf(2); // 正向查找元素位置
    list.LastIndexOf(2); // 反向查找元素位置
  • 修改

    1
    list[0] = 99; // 直接修改

List排序

  • List自带的排序方法

    1
    2
    3
    List<int> list = new List<int>();
    // ...
    list.Sort(); // 默认升序
  • 自定义类的排序

    继承并实现 ICompareble 接口或 ICompareble<T> 接口中的 CompareTo 方法,自定义排序规则,再使用 Sort 方法。

  • 通过委托函数进行排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int Fun(自定义类名 a, 自定义类名 b)
    {
    // 传入的两个对象为列表中的两个对象
    // 进行两两比较

    // 实现排序规则(返回值规则与CompareTo一样)
    }

    list.Sort(Fun); // 将排序函数传入Sort执行

Dictionary(字典)

概念: 可以将 Dictionary 理解为拥有泛型的 Hashtable ,它也是基于键的哈希代码组织起来的键值对,键值对类型从 object 变为了可以自己制定的泛型。

声明:

1
2
// 需要引入命名空间 System.Collections.Generic
Dictionary<int, string> dictionary = new Dictionary<int, string>();

常用方法:

  • 增加

    1
    2
    // 注意不能出现相同键
    dictionary.Add();
  • 删除

    1
    2
    3
    4
    5
    // 只能通过键删除
    dictionary.Remove(1);

    // 清空
    dictionary.Clear();
  • 查找

    1
    2
    3
    4
    5
    6
    7
    // dictionary[4] // 通过键查看值,找不到会返回空

    // 查看是否存在
    // 根据键检测
    dictionary.ContainsKey(3);
    // 根据值检测
    dictionary.ContainsValue("23874");
  • 修改

    1
    dictionary[1] = "555"; // 直接修改

LinkedList(链表)

概念: LinkedList 本质是一个可变类型的泛型双向链表

声明:

1
2
// 需要引入命名空间 System.Collections.Generic
LinkedList<int> linkedList = new LinkedList<int>();

常用方法:

  • 增加

    1
    2
    3
    4
    5
    6
    7
    8
    // 在链表尾部添加元素
    linkedList.AddLast(10);
    // 在链表头部添加元素
    linkedList.AddFirst(20);
    // 在某一个节点之后添加一个节点
    linkedList.AddAfter(node, 15);
    // 在某一个节点之前添加一个节点
    linkedList.AddBefore(node, 20);
  • 删除

    1
    2
    3
    4
    5
    6
    7
    8
    // 移除头节点
    linkedList.RemoveFirst();
    // 移除尾节点
    linkedList.RemoveLast();
    // 移除指定元素(不是指定位置的元素)
    linkedList.Remove(20);
    // 清空
    linkedList.Clear();
  • 查找

    1
    2
    3
    4
    5
    6
    7
    8
    // 头节点
    LinkedListNode<int> first = linkedList.First;
    // 尾节点
    LinkedListNode<int> last = linkedList.Last;
    // 找到指定值的节点,找不到返回空
    LinkedListNode<int> node = linkedList.Find(3);
    // 判断是否存在
    linkedList.Contains(20);
  • 修改

    1
    2
    // 先得到节点,再修改值
    node.Value = 8;

泛型栈和队列

使用:

1
2
3
4
// 需要引入命名空间 System.Collections.Generic
// 名称不变,加上泛型即可
Stack<int> stack = new Stack<int>();
Queue<string> queue = new Queue<string>();

使用上与普通栈和队列基本没有区别。


委托和事件

委托

概念

委托是函数的容器。 可以理解为表示函数的变量类型,用来存储和传递函数。委托的本质是一个类,用来定义函数的类型(返回值和参数的类型),不同的函数必须对应和各自“格式”一致的委托。

基本语法

关键字: delegate

语法:

1
访问修饰符 delegate 返回值 委托名(参数列表);

可以声明在namespace和class语句块中,更多的写在namespace中,访问修饰符默认为public。

使用

1
2
delegate void MyFun1(); // 声明了一个可以用来存储无参无返回值的函数的容器
delegate int MyFun2(int x);
1
2
3
4
5
6
7
8
9
10
11
12
void Fun() { } // 无参无返回

MyFun1 f1 = new MyFun(Fun); //装载Fun函数
MyFun1 f2 = Fun; // 另一种实现方法

f1.Invoke(); // 调用委托
f2(); // 另一种调用方法

int Fun2(int x) { return x; } // 有参有返回

MyFun2 ff1 = Fun2;
ff1(1);

委托常用在:

  • 作为类的成员
  • 作为函数的参数
1
2
3
4
5
6
7
8
9
10
11
12
class Test
{
public MyFun1 fun1;
public MyFun2 fun2;

public void TestFun(MyFun1 fun1, MyFun2 Fun2)
{
// ...
fun1();
fun2(5);
}
}

多播委托(存储多个函数)

1
2
3
4
5
6
7
8
9
10
11
MyFun1 f1 = Fun;

f1 += Fun; // 存了两个Fun函数
f1 += Funa;
f1 += Funb; // 存储多个函数

f1(); // 调用委托时全部执行(按添加顺序)

f1 -= FUn; // 从f1委托中移除Fun函数

f1 = null; // 相当于清空委托

系统定义好的委托

使用系统自带委托,需要引用 System 命名空间。

Action

一个无参无返回的委托。

Func<out>

可以指定返回值类型的泛型委托。

Action<in…>

可以传n个参数的委托,系统提供了1到16个参数的委托。

Func<in…, out>

可以传n个参数的有返回值的委托,系统也提供了1到16个参数的写法。

事件

概念

事件是一种特殊的变量类型。事件是基于委托的存在,是委托的安全包裹,让委托的使用更有安全性。

使用

委托怎么用,事件就怎么用。

声明:

1
访问修饰符 event 委托类型 时间名;

与委托的区别:

  • 事件只能作为成员存在于类和接口以及结构体中
  • 事件不能在类的外部赋值
  • 事件不能在类的外部调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test
{
// 委托成员变量
public Action myFun;
// 事件成员变量
public event Action myEvent;

public Test()
{
// 事件的使用与委托一样
myFun = TestFun;
myFun += TestFun;
myFun();
myFun.Invoke();

myEvent = TestFun;
myEvent += TestFun;
myEvent();
myEvent.Invoke();
}

public void TestFun() { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Test t = new Test();

// 委托可以在外部赋值
t.myFun = null;
t.myFun = TestFun;

// 事件不能在外部赋值,但可以添加和移除记录的函数
// t.myEvent = TestFun;
t.myEvent += TestFun;
t.myEvent -= TestFun;

// 委托可以在外部调用,事件不能在外面调用
t.myFun();

意义

  1. 防止外部随意置空委托
  2. 防止外部随意调用委托
  3. 事件相当于对委托进行了一次封装,让其更加安全

匿名函数

概念

匿名函数,就是没有名字的函数。匿名函数的使用主要是配合委托和事件进行使用,脱离委托和事件一般不会用到匿名函数。

基本语法

1
2
3
4
delegate (参数列表)
{
// 函数逻辑
}

使用

在函数中传递委托参数时和委托或事件赋值时使用:

1
2
3
4
5
6
7
8
9
Action a = delegate ()
{
Console.WriteLine("我是匿名函数");
};
a += delegate () { /* 函数逻辑 */ };

Action<int, string> b = delegate (int x, string str) { /* 函数逻辑 */ }; // 有参

Func<string> c = delegate () { return "123" }; // 有返回

作为函数参数传递或作为函数返回值使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
public Action action;

// 作为参数
public void Fun1(int a, Action fun) { /* ... */ }

// 作为返回值
public Action Fun2()
{
return delegate () {
// 函数逻辑
};
}
}
1
2
3
4
5
6
7
8
9
10
Test t = new Test();

// 参数传递
t.Fun1(20, delegate () {
// 函数逻辑
});

// 返回值
Action ac = t.Fun2(); // 存起来
t.Fun2()(); // 直接调用

匿名函数的缺点

因为没有名字,所以添加到委托或事件中后不记录无法单独移除。

Lambda表达式

概念

lambda表达式可以理解为匿名函数的简写,它除了写法不同外,使用上和匿名函数一模一样,都是配合委托或事件使用的。

语法

1
2
3
(参数列表) => {
// 函数体
};

使用

1
2
Action a = () => { /* ... */ };
Action<int> b = (int x) => { /* ... */ };

参数类型也可省略,与委托和事件一致

1
2
Action<string, int> a = null;
a += (str, x) => { /* ... */ };

大括号和小括号也可省略,省略大括号默认是返回值且只有一行

1
2
Func<int, int> a = (x) => { return x * 5; }; // 有返回值
a += x => x * 5;

闭包

内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。

注:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
public event Action action;

public Test()
{
int value = 10;

// 这里就形成了闭包
// 因为当构造函数执行完毕时,其中声明的临时变量value的生命周期被改变了
action = () => {
Console.WriteLine(value);
};
}
}

协变逆变

概念

协变: 和谐的变化,自然的变化。例如因为里氏替换原则,所以子类变父类是和谐的。

逆变: 逆常规的变化,不正常的变化。例如父类变子类是不和谐的。

逆变和协变是用来修饰泛型的,关键字:

  • 协变: out
  • 逆变: in

是用于在泛型中修饰泛型字母的,只有泛型接口和泛型委托能使用。

作用

返回值和参数:

out 修饰的泛型只能作为返回值,用 in 修饰的泛型只能作为参数。

1
2
delegate T TestOut<out T>();
delegate void TestIn<in T>();

结合里氏替换原则:

1
2
3
4
5
6
7
8
9
10
// 协变 父类总是能被子类替换
TestOut<Son> os = () => {
return new Son();
};
TestOut<Father> of = os;

// 逆变 父类总是能被子类替换
TestIn<Father> iF = value => { };
TestIn<Son> iS = iF;
iS(new Son()); // 实际上调用的是iF

多线程

相关概念

blog

相关语法

C#提供了线程类 Tread ,需要引用命名空间 System.Threading 使用。

声明一个线程:

1
2
3
4
// Thread类重载了四种构造函数,最常用的需要传入一个无参无返回委托(或函数)
Thread t = new Thread(NewThreadFun);

public void NewThreadFun() { /* ... */ }

启动线程:

1
t.Start();

设置为后台线程:

声明的线程默认为前台线程,当前台线程都结束的时候整个程序才会结束,即使还有后台线程正在运行。后台线程不会防止应用程序的进程被终止掉,因此如果有线程没有设置为后台线程,此线程还未结束的话,可能导致进程无法正常关闭。

1
t.IsBackground = true;

关闭释放一个线程:

如果开启的线程不是死循环,是能够结束的逻辑,那么不用刻意的去关闭它。

如果想要终止一个线程,可以通过线程提供的方法:

1
2
t.Abort(); // 终止线程
t = null; // 置空(GC自动回收)

线程休眠:

1
2
3
Tread.Sleep(1000);
// 线程类的静态函数,让线程休眠n毫秒再继续执行
// 在哪个线程内部执行休眠的就是哪个线程

线程之间共享数据

多个线程使用的内存是共享的,都属于该应用程序(进程),所以要注意,当多线程同时操作同一片内存区域时可能会出现问题,可以通过加锁的形式避免问题。

关键字: lock

原理:当我们在多个线程当中想要访问同样的东西进行逻辑处理时,为了避免不必要的逻辑顺序执行的差错,可以使用 lock 锁避免同时执行。

1
2
3
4
5
6
7
// 线程1
lock( obj ) // 需要传入一个引用类型变量,这里以obj为例
{
// 语句...
a = 10;
Console.WriteLine(a);
}
1
2
3
4
5
6
7
// 线程2
lock( obj )
{
// 语句...
a = 99;
Console.WriteLine(a);
}

当程序执行到 lock 语句块时,会先检测传入的引用类型变量,如果该变量( obj )被锁住了,程序就会一直等到 obj 解锁之后再运行 lock 语句块中的逻辑;相反,如果 obj 没被锁,程序就会执行 lock 语句块中的逻辑,同时将 obj 锁住,在逻辑执行完毕后解锁 obj

多线程的意义

多线程可以用来处理一些复杂耗时的逻辑,比如寻路、网络通信等等,可以专门开一个线程异步处理逻辑,避免卡顿。


预处理器指令

什么是预处理器指令

预处理器指令是指导编译器在实际编译开始之前对信息进行预处理的指令。 预处理器指令都是以 ‘#’ 开始,且因为它们不是语句,所以不以分号结束。

常见的预处理器指令

指令描述
#define定义一个符号,可以用于条件编译。
#undef取消定义一个符号。
#if开始一个条件编译块,如果符号被定义则包含代码块。
#elif如果前面的 #if#elif 条件不满足,且当前条件满足,则包含代码块。
#else如果前面的 #if#elif 条件不满足,则包含代码块。
#endif结束一个条件编译块。
#warning生成编译器警告信息。
#error生成编译器错误信息。
#region标记一段代码区域,可以在IDE中折叠和展开这段代码,便于代码的组织和阅读。
#endregion结束一个代码区域。
#line更改编译器输出中的行号和文件名,可以用于调试或生成工具的代码。
#pragma用于给编译器发送特殊指令,例如禁用或恢复特定的警告。
#nullable控制可空性上下文和注释,允许启用或禁用对可空引用类型的编译器检查。

反射

程序集

程序集是经由编译器编译得到的,供进一步编译执行的中间产物,在windows系统中,它一般表现为 .dll(代码库文件).exe(可执行文件) 的格式。

元数据

元数据就是用来描述数据的数据。

例如程序中的类,类中的函数、变量等信息就是程序的元数据。有关程序以及类型的数据被称为元数据,它们保存在程序集中。

反射的概念

程序正在运行时,可以查看其他程序集或者自身的元数据。一个运行的程序查看本身或者其他程序的元数据的行为就叫做反射。

反射的作用

因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性。

  1. 程序运行时得到所有元数据,包括元数据的特性
  2. 程序运行时实例化对象,操作对象
  3. 程序运行时创建对象,用这些对象执行任务

相关语法

这里先放一个例子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Test
{
private int i = 1;
public int j = 0;
public string str = "123";

public Test() { }
public Test(int i)
{
this.i = i;
}
public Test(int i, string str) : this(i)
{
this.str = str;
}

public void Speak()
{
Console.WriteLine(i);
}

public void Speak(int x) { }

public void Speak(int x, string str) { }
}

Type类

Type(类的信息类) 是反射功能的基础,它是访问元数据的主要方式。使用 Type 的成员获取有关类型声明的信息,有关类型的成员(如构造函数、方法、字段、属性和类的事件)。

获取Type:

  1. object 类中的 GetType() 方法可以获取对象的Type:

    1
    2
    int a = 42;
    Type type = a.GetType();
  2. 通过 typeof 关键字传入类名,也可以等到对象的Type:

    1
    Type type = typeof(Test);
  3. 通过类的名字也可以获取类型(注意类名必须包含命名空间,不然找不到):

    1
    Type type = Type.GetType("System.Int32");

得到类的程序集信息:

1
Console.WriteLine(type.Assembly);

获取类中的所有公共成员:

1
2
3
4
5
// 首先得到Type
Type t = typeof(Test);
// 然后得到所有公共成员
// 需要引用命名空间 System.Reflection
MemberInfo[] infos = t.GetMembers();

获取类的公共构造函数并调用:

  1. 获取所有构造函数

    1
    ConstructorInfo[] ctors = t.GetConstructors();
  2. 获取其中一个构造函数并执行

    得构造函数传入Type数组,数组中内容按顺序是参数类型;

    执行构造函数传入object数组,表示按顺序传入的参数。

    1. 得到无参构造:

      1
      ConstructorInfo info = t.GetConstructor(new Type[0]);

      执行无参构造:

      1
      2
      // 无参构造没有参数需要传null,该方法会返回object对象
      Test test = info.Invoke(null) as Test;
    2. 得到有参构造:

      1
      2
      3
      4
      ConstructorInfo info = t.GetConstructor(new Type[] { typeof(int) });
      // 获取只有一个int类型参数的构造函数
      ConstructorInfo info = t.GetConstructor(new Type[] { typeof(int), typeof(string) });
      // 获取两个参数的构造函数

      执行有参构造:

      1
      2
      Test test1 = info.Invoke(new object[] { 88 }) as Test;
      Test test2 = info.Invoke(new object[] { 88, "Hello"}) as Test;

获取类的公共成员变量:

  1. 得到所有成员变量:

    1
    FieldInfo[] fieldInfos = t.GetFields();
  2. 得到指定名称的公共成员变量

    1
    FieldInfo infoJ = t.GetField("j");
  3. 通过反射获取和设置对象的值

    1. 通过反射获取对象的某个变量的值

      1
      2
      3
      4
      Test test = new Test();
      test.j = 99;

      Console.WriteLine(infoJ.GetValue(test));
    2. 通过反射设置指定对象的某个变量的值

      1
      infoJ.SetValue(test, 100);

获取类的公共成员方法:

  1. 得到所有成员方法:

    1
    MethodInfo[] methods = t.GetMethods();
  2. 获取一个成员方法

    1
    2
    3
    // 如果存在方法重载,用Type数组表示参数类型
    MethodInfo method1 = t.GetMethod("Speak", new Type[] { typeof(int), typeof(string) });
    MethodInfo method2 = t.GetMethod("Speak", new Type[] { typeof(int) });
  3. 调用方法

    1
    2
    3
    4
    5
    6
    Test test = new Test();

    // 第一个参数相当于是那个对象要执行这个成员方法
    // 如果是静态方法,第一个参数传null即可
    method1.Invoke(test, new object[] { 1, "123" });
    method2.Invoke(test, new object[] { 1 });

其他:

  • 得枚举: GetEnumNameGetEnumNames
  • 得事件: GetEventGetEvents
  • 得接口: GetInterfaceGetInterfaces
  • 得属性: GetPropertyGetPropertys

Assembly类

程序集类。主要用来加载其他程序集,加载后才能用Type来使用其他程序集的信息,如果想要使用不是当前程序集的内容,需要先加载程序集。比如 dll(库文件) ,简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。

三种加载程序集的函数:

  • 一般用来加载在同一文件下的其他程序集

    1
    Assembly assembly1 = Assembly.Load("程序集名称");
  • 一般用来加载不在同一文件下的其他程序集

    1
    2
    Assembly assembly2 = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
    Assembly assembly3 = Assembly.LoadFile("要加载的文件的完全限定路径");

示例:

1
2
3
4
5
6
// 先加载一个指定程序集
Assembly assembly Assembly.LoadFrom("...");
// 获取所有Type
Type[] types = assembly.GetTypes();
// 加载程序集中的一个类对象,之后才能使用反射
Type icon = assembly.GetType("...");

Activator类

实例化对象的类,用于将Type对象快捷实例化为对象。

  1. 无参构造:

    1
    2
    Type t = typeof(Test);
    Test testObj = Activator.CreateInstance(t) as Test;
  2. 有参构造:

    1
    2
    testObj = Activator.CreateInstance(t, 99) as Test; // 一个参数的
    testObj = Activator.CreateInstance(t, 99, "123") as Test; // 两个参数的

特性

概念

特性是允许我们向程序的程序集添加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类。特性提供功能强大的方法以将声明信息与C#代码(类型、方法、属性等)相关联。特性与程序实体关联后,即可在运行时使用反射查询特性信息。特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集中,它可以放置在几乎所有的声明中(类、变量、函数等等)。

简而言之:特性本质上是个类,我们可以利用特性类为元数据添加额外信息。比如一个类、成员变量、成员方法等等为它们添加更多的额外信息,之后可以通过反射来获取这些额外信息。

自定义特性

声明一个类继承特性基类 Attribute

1
2
3
4
5
6
7
8
9
10
class MyTestAttribute : Attribute
{
// 根据需求来写特性中的成员
public string info;

public TestAttribute(string info)
{
this.info = info;
}
}

使用特性

基本语法: [特性名(参数列表)]

本质上就是在调用特性类的构造函数,写在类、函数、变量上一行,表示它们具有该特性信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[MyTest("这是一个类")] // 特性名中的Attribute系统会默认省略
class Test
{
// 可以加到成员变量前
[MyTest("这是一个成员变量")]
public int Value;

// 可以加到函数前
[MyTest("这是一个函数")]
public void TestFun() { }

// 甚至可以加到函数参数前
public void TestFun2( [MyTest("这是一个函数参数")]int x ) { }
}

限制特性使用范围

通过为特性类加特性限制其使用范围

1
2
3
4
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
// 参数一:AttributeTargets —— 特性能够用在哪些地方
// 参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
// 参数三:Inherited —— 特性是否能被派生类和重写成员继承

系统自带特性

过时特性

关键字: Obsolete 特性

用于提示用户使用的方法等成员已经过时,建议使用新方法。一般加在函数前的特性。

1
2
3
4
5
6
7
// 参数一:调用过时方法时提示的内容
// 参数二:是否禁止调用该方法(true:报错 false:仅警告)
[Obsolete("Speak方法过时了,建议使用新的方法", false)]
public void Speak(string str)
{
Console.WriteLine(str);
}

调用者信息特性

使用时需要引用命名空间 System.Runtime.CompilerServices ,一般作为参数的特性。

  • 获取哪个文件调用: CallerFilePath 特性
  • 获取哪一行调用: CallerLineNumber 特性
  • 获取哪个函数调用: CallerMemberName 特性
1
2
3
4
5
6
7
8
9
public void SpeakCaller(string str, [CallerFilePath]string fileName = "",
[CallerLineNumber]int line = 0,
[CallerMemberName]string memberName = "")
{
Console.WriteLine(str);
Console.WriteLine("调用的文件是" + fileName);
Console.WriteLine("调用的行数是" + line);
Console.WriteLine("调用的方法名是" + memberName);
}

条件编译特性

关键字: Conditional 特性

它会和预处理指令 #define 配合使用,需要引用命名空间 System.Diagnostics ,主要可以用在一些调试代码上(有时想执行有时不想执行的代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#defiine Fun

// ...

[Conditional("Fun")]
public void Fun()
{
Console.WriteLine("执行");
}

// ...

Fun(); // 加了这个特性函数才会执行

外部dll包函数特性

关键字: DllImport 特性

用来标记非 .Net(C#) 的函数,表明该函数在一个外部的DLL中定义。一般用来调用 C 或 C++ 的DLL包写好的方法。使用时需要引用命名空间 System.Runtime.InteropServices

1
2
[DllImport("Test.dll")] // 假如用一个Test包中的函数
public static extern int Add(int a, int b);

迭代器

概念

迭代器(iterator) 又称光标(cursor)是程序设计的软件设计模式。迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素而又不暴露其内部的标识。

从表面效果上看,迭代器是可以在容器对象(例如链表或数组)上遍历访问的接口,设计人员无需关心容器对象的内存分配的实现细节,可以用 foreach 遍历的类,都是实现了迭代器的。

标准迭代器的实现方法

关键接口: IEnumeratorIEnumerable

命名空间: System.Collections

可以通过同时继承 IEnumeratorIEnumerable 实现其中的方法。

用 yield return 语法糖实现迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CustomList : IEnumerable
{
private int[] list;

public CustomList()
{
list = new int[] {1, 2, 3, 4, 5, 6, 7, 8};
}

public IEnumerator GetEnumerator()
{
for (int i = 0; i < list.Length; ++ i)
{
// yield 关键字 配合迭代器使用
// 可以理解为 暂时返回 保留当前状态
// 一会还会再回来
yield return list[i];
}
}
}

用 yield return 语法糖为泛型类实现迭代器

1
2
3
4
5
6
7
8
9
10
11
12
class CustomList<T> : IEnumerable
{
private T[] array;

public IEnumerator GetEnumerator()
{
for (int i = 0; i < array.Length; ++ i)
{
yield return array[i];
}
}
}

特殊语法

var隐式类型

概念: var 是一种特殊的变量类型,它可以用来表示任意类型的变量。

注意:

  • var 不能作为类的成员,只能用于临时变量声明
  • var 变量必须初始化

示例:

1
2
3
var i = 5;
var s = "123";
var list = new List<int>();

设置对象初始值

概念:声明对象时,可以直接写大括号的形式初始化公共成员变量和属性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
public int x;
public string str;

public string Name
{
get;
set;
}
}

Test t1 = new Test() { x = 1, str = "123", Name = "xxx" };
Test t2 = new Test() { Name = "yyy" };
// 先执行构造函数再执行初始化的内容

设置集合初始值

概念:声明集合对象时也可以通过大括号直接初始化内部属性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
int[] array1 = new int[] { 1, 2, 3, 4};
List<int> list1 = new List<int>() { 1, 2, 3, 4};
List<Test> list2 = new List<Test>() {
new Test(),
new Test() { Name = "xxx" },
new Test() { x = 1, str = "123", Name = "yyy" }
};
Dictionary<int, string> dic = new Dictionary<int, string>() {
{ 1, "123" },
{ 2, "34655" },
{ 3, "45676" }
}

匿名类型

概念:变量可以使用 var 声明为自定义的匿名类型。

示例:

1
2
3
var v = new { age = 10, money = 11, name = "小明" };
COnsole.WriteLine(v.age);
// 匿名类型无法声明成员方法

可空类型

概念:值类型不能赋值为空,但再声明时在值类型后面加 ? 表示这是一个可空类型,就可以赋值为空了。

示例;

1
2
3
4
5
6
7
8
9
10
11
int? c = null;

// 判断是否为空
if (c.HasValue)
{
// ...
}

// 安全获取可空类型值
COnsole.WriteLine(Value.GetValueOrDefault(100));
// 有值就返回值,空的就返回括号里的参数,无参默认返回该类型的默认值

? 还可判断一个对象是否为空:

1
2
3
4
5
6
7
8
9
10
object o = null;

if (o != null)
{
o.Tostring();
}

// 相当于以上代码 能帮我们自动判断o是否为空
// 如果是null就不会执行,也不会报错
o?.Tostring();

空合并操作符

概念:空合并操作符 ?? ,如果左边值为空,就返回右边值,否则返回左边值,只要是可以为空的类型都能使用。

示例:

1
2
3
4
5
6
int? v = null;

int? i = v == null ? 100 : v;

// 相当于以上代码
i = v ?? 100;

内插字符串

概念:用 $ 来构造字符串,让字符串可以拼接变量。

示例:

1
2
3
string name = "123";
int age = 18;
Console.WriteLine($"你好,{name},年龄{age}");

单句逻辑简略写法

概念:使用 => 省略大括号,需要返回值则同时省略 return ,只要是只有一句代码都可以这样用。

示例:

1
2
3
4
5
6
7
8
9
10
11
class Test
{
public string Name
{
get => "小明";
set => sex = true;
}

public int Add(int a, int b) => a + b;
public void Speak() => Console.WriteLine("12312321");
}

end