简单的介绍

本篇是关于C#(读作C Sharp)的笔记

为了后面Unity的开发,所以先学习一下C#

本篇C#内容参考B站唐老狮的相关教程,链接

第一个C# 程序

那么常规操作,第一步打印Hello World

1
2
3
4
5
6
7
8
9
10
11
12
using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}

这个写法是不使用顶级语句的,在C# 9.0引入了顶级语句,可以省略类和Main方法,
直接写代码

注释

接下来看注释,注释与Java类似,分为单行注释多行注释

//单行注释

/* ... */多行注释

///用于注释类、方法、属性等,可以自动生成文档,这个是C#特有的,在类、方法上面敲入三个斜杠,即可自动生成注释模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 命名空间引用
using System;

// 命名空间定义(解决方案的名字)
namespace ConsoleApp1
{
// 类定义(类名)
class Program
{
/// <summary>
/// 程序入口
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// 打印Hello, World!
Console.WriteLine("Hello, World!");
}
}
}

控制台相关内容

现在我们主要编写控制台应用,所有得来了解了解控制台相关的东西

输入输出

WriteLine输出并自动换行

Write输出但不换行

ReadLine获取输入,并自动换行,直到用户按下回车键

ReadKey获取输入,用户按下任意键就继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 打印Hello, World!(并自动空行)
Console.WriteLine("Hello, World!");

// 打印Hello(不自动空行)
Console.Write("Hello");

// 获取输入
Console.ReadLine();
Console.WriteLine("输入完毕");

// 获取输入(这个是按任意键就继续执行)
Console.ReadKey();

Console.WriteLine("按了任意键");

获取输入但不显示

如果你不希望用户输入的内容显示在控制台上,可以使用ReadKey(true)
这样用户输入的内容就不会显示在控制台上,而是存储在KeyChar属性中,
返回为char类型

1
2
3
4
// 获取用户输入的字符,不显示在控制台上
char c = Console.ReadKey(true).KeyChar;
// 输出用户输入的字符
Console.WriteLine("用户输入的字符是:" + c);

清空控制台

Clear清空控制台

1
Console.Clear();

控制台窗口

设置控制台大小

SetWindowSize设置控制台大小,参数为

1
Console.SetWindowSize(80, 30);

设置控制台缓冲区大小

SetBufferSize设置控制台缓冲区大小,参数为

注意,缓冲区大小不能小于控制台大小,否则会报错

1
Console.SetBufferSize(80, 30);

退出控制台

Exit退出控制台

1
Console.Exit(0);

光标

设置光标位置

窗口中,左上角坐标为(0,0),向右为X轴正方向,向下为Y轴正方向

SetCursorPosition设置光标位置,参数为横坐标和纵坐标,单位为字符

1
Console.SetCursorPosition(10, 5); 

在视觉上,1y = 2x,所以要想让光标起始位置看起来像个正方形,
就得将x坐标设置为y坐标的两倍(这一条适用于旧版的控制台)

隐藏光标

CursorVisible设置光标是否可见,参数为布尔值

1
Console.CursorVisible = false;

颜色

文字颜色

ForegroundColor设置文字颜色,参数为枚举类型ConsoleColor,默认为白色

1
Console.ForegroundColor = ConsoleColor.Red;

背景颜色

BackgroundColor设置背景颜色,参数为枚举类型ConsoleColor,默认为黑色

注意设置好背景颜色之后,需要Clear一次,才能看到效果

1
2
Console.BackgroundColor = ConsoleColor.Blue;
Console.Clear();

折叠代码

与直接折叠大括号{}中的代码不同,C#提供了#region#endregion来折叠代码

你可以在代码中直接使用它们来自定义你要折叠的代码块

#region开头,以#endregion结尾,可以折叠代码,方便阅读

执行编译时,#region#endregion会被忽略,不会影响代码的执行

1
2
3
4
#region Region1
Console.WriteLine("Hello, World!");
#endregion
// 上面部分可以折叠为Region1

变量声明

不同类型的变量可以储存不同类型的数据

1
2
// 变量声明方法
变量类型 变量名 = 初始化值;

数据类型

好,接下来我们来看看C#的数据类型

有符号整型

有符号整型是可

sbyte -128~127,占用1字节

short -32768~32767,占用2字节

int -2147483648~2147483647,占用4字节

long -9223372036854775808~9223372036854775807,占用8字节

1
2
3
4
sbyte a = 100;
short b = 100;
int c = 100;
long d = 100;

无符号整型

那么顾名思义,它就是非负的整型

不过它所代表的数值的数量是和有符号整型一样的

byte 0~255,占用1字节

ushort 0~65535,占用2字节

uint 0~4294967295,占用4字节

ulong 0~18446744073709551615,占用8字节

1
2
3
4
byte e = 100;
ushort f = 100;
uint g = 100;
ulong h = 100;

浮点型

浮点数,即小数,它分为单精度双精度

float 7~8位有效数字(有效数字:从第一位非零数字起算),需加f,占用4字节

double 15~16位有效数字,写小数时,默认为此类型,其他两个的需要指定,占用8字节

decimal 28~29位有效数字,需加m,占用16字节

1
2
3
float i = 100.0f;
double j = 100.0;
decimal k = 100.0m;

字符型

char 只能存储一个字符,只能用单引号括起来,占用2字节

1
char n = 'a';

布尔型

1
2
bool l = true;
bool m = false;

字符串

字符串类型的,一般用双引号括起来,可以存放多个字符,没有固定的占用字节数,根据内含字符的数量来决定的

string 字符串

1
string o = "hello world";

变量初始化

一般来说,变量声明后就得初始化;亦可先定义,再初始化

1
int a = 100;

先定义,再初始化

1
2
int a;
a = 100;

但不论如何,使用变量之前必须初始化

另外,可以连续声明相同类型的变量,用逗号隔开

1
int a = 100, b = 200, c = 300;

变量占用的内存大小

在C#中,不同类型的变量占用的内存大小是固定的,可以通过sizeof关键字来获取

1
Console.WriteLine(sizeof(int));

变量命名规范

  • 变量名必须以字母下划线开头
  • 变量名只能包含字母数字下划线,并区分大小写,不能重复
  • 变量名不能使用关键字
  • 变量名应当顾名思义,即见名知意,而不是去猜测它的意思
  • 一般遵循驼峰命名法,即首字母小写,后续每个单词首字母大写(函数、类可采用大驼峰命名法,亦称帕斯卡命名法

变量的本质

不论是在C#,还是其他的编程语言,变量本质上是二进制
因为计算机是通过01来存储各种数据,
变量是一个有名字、有特定属性的存储单元

1字节 = 8位(byte)

常量

常量,即不可改变的量,用const关键字定义,必须初始化,不能修改

一般定义一些不会改变的量,如πe

1
2
const int months = 12;
Console.WriteLine("一年有{0}个月", months);

转义字符

转义字符,即用反斜杠\开头的字符,用于表示一些特殊字符

\n 换行
\t 制表符
\\ 斜杠
\" 双引号
\' 单引号
\b 光标退格
\0 空字符
\a 警报音

1
2
3
4
5
6
7
8
Console.WriteLine("换行符\n换行");
Console.WriteLine("制表符\t123");
Console.WriteLine("斜杠\\123");
Console.WriteLine("\"双引号\"");
Console.WriteLine("\'单引号\'");
Console.WriteLine("光标退格123\b123");
Console.WriteLine("空字符\0123");
Console.WriteLine("警报音\a");

类型转换

隐式转换

隐式转换为程序帮助我们转换,而不用我们手动转换

值得注意的是,数据类型——大范围装小范围

1
2
3
int a = 10;
long b = a; // 隐式转换
// short c = a; // 不能隐式转换,因为short范围 < int范围

在上面的例子中,a可以隐式转换为long类型的

a隐式转换为short类型的,因为short的范围小于int的范围

同样的,还有以下注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// decimal不能隐式转换double和float,但整型可以被转换
double d = 10.0;
// decimal e = d; // 不能隐式转换

// 有符号数不能隐式转换为无符号数
// uint f = a; // uint是无符号整数,int是有符号整数,不能隐式转换

// 无符号数隐式转换为有符号数,要求无符号数的范围必须小于有符号数的范围
uint g = 10;
long h = g; // 可以隐式转换,因为uint范围 < long范围

// 所有整型都可隐式转换为float、double
float i = a;
double j = b;

// string类型和bool型不能和其他类型隐式转换
// bool k = a; // 不能隐式转换

// 其他类型不能隐式转换为char,但char可以隐式转换为其他类型(string,小于int范围的也不能转换)
// char l = a; // 不能隐式转换
char m = 'a';
int n = m; // 其实是转换为ASCII码值

显式转换

括号法

显示转为为强制转换,用括号()

变量类型 变量名 = (目标类型)变量名

但要注意的是,强制转换可能导致溢出的问题,需要注意转换类型之间的范围

同时,其精度也有可能丢失

1
2
int o = 10;
short p = (short)o; // 显式转换

Parse方法

变量类型.Parse()方法可以将字符串转换为其他类型

1
2
3
4
5
int q = int.Parse("123");
// int r = int.Parse("123.45"); // 会报错,因为字符串中包含小数点

short s = short.Parse("123");
double t = double.Parse("123.45");

Convert方法

Convert.ToXXX()方法可以更准确地转换变量类型

每一种类型都有对应的Convert.ToXXX()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int u = Convert.ToInt32("123");
int v = Convert.ToInt32(123.45);

sbyte w = Convert.ToSByte("123");
short x = Convert.ToInt16("123");
int y = Convert.ToInt32("123");
long z = Convert.ToInt64("123");

byte aa = Convert.ToByte("123");
ushort bb = Convert.ToUInt16("123");
uint cc = Convert.ToUInt32("123");
ulong dd = Convert.ToUInt64("123");

float ee = Convert.ToSingle("123.45");
double ff = Convert.ToDouble("123.45");
decimal gg = Convert.ToDecimal("123.45");

bool hh = Convert.ToBoolean("true");
char ii = Convert.ToChar("a");

ToString方法

所有类型都可以通过ToString()方法转换为字符串

1
2
int a1 = 123;
Console.WriteLine(a1.ToString());

另外,补充说明一下,Console.ReadLine()方法会读取用户输入的字符串,并返回一个string类型

异常捕获

在程序运行时,有可能会出现一些错误,导致程序无法正常运行

而有时候我们需要将出现的问题输出,更为具体地通俗易懂地告诉用户出现了什么问题(毕竟报错信息并不是所有人都看得懂的)

这个情况下,我们就要使用try-catch语句

try语句块中,我们放置可能会出现异常的代码,而catch语句块中,我们放置处理异常的代码

另外还有finally语句块,无论是否出现异常,都会执行finally语句块中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基本语法
// 必须写的部分try-catch
try
{
// 放可能出现异常的代码
string a = Console.ReadLine();
int b = int.Parse(a);
Console.WriteLine(b);
}
catch (Exception ex)
{
// 当捕获到异常时执行的代码
Console.WriteLine("Failed");
}
// 可选写的部分finally
finally
{
// 无论是否发生异常都会执行的代码
}

运算符

赋值运算符

赋值运算符,即=,将右边的(表达式)值赋给左边的变量

1
2
3
int a = 10;
int b = 20;
b = b + 2 * 5 / 3 + 5 % 2;

不论=右边是什么,都是先运算右边的,再赋值给左边的变量

算术运算符

就像数学中的加减乘除,在程序中也有对应的

+ 加、- 减、* 乘、/ 除、% 取余(即除不尽的则取它的余数,除得尽的为0)

1
2
3
4
5
6
7
8
9
10
11
int a = 1;
int b = 2;
int c = a + b;

int d = a - b;

int e = a * b;

int f = a / b; // 如果两个整型相除,结果还是整型,且不会四舍五入

int g = a % b;

自增自减运算符

自增运算符,即++,将变量值加1;
自减运算符,即--,将变量值减1

但它们与变量的位置不同,结果也不同

1
2
3
4
5
int a = 1;
int b = a++; // 先赋值,再自增,即先将a的值赋给b,再将a加1
Console.WriteLine(a); // 2
int c = ++a; // 先自增,再赋值,即先将a加1,再将a的值赋给c
Console.WriteLine(a); // 3

复合运算符

它是=和算术运算符的结合,用于简化代码

+= 加等、-= 减等、*= 乘等、/= 除等、%= 取余等

1
2
3
4
5
6
7
8
9
a += b; // 等效 a = a + b;

a -= b; // 等效 a = a - b;

a *= b; // 等效 a = a * b;

a /= b; // 等效 a = a / b;

a %= b; // 等效 a = a % b;

关系运算符

关系运算符,用于比较两个值的大小关系,结果为bool类型

== 等于、!= 不等于、> 大于、< 小于、>= 大于等于、<= 小于等于

1
2
3
4
5
6
7
int h = 3;
Console.WriteLine(a == h);
Console.WriteLine(a != h);
Console.WriteLine(a > h);
Console.WriteLine(a < h);
Console.WriteLine(a >= h);
Console.WriteLine(a <= h);

任意数值可以直接进行比较

但特殊类型stringcharbool只能同类型进行==!=比较

逻辑运算符

逻辑运算符,用于连接多个条件,结果为bool类型

&& 逻辑与(and)、|| 逻辑或(or)、! 逻辑非(not)

1
2
3
Console.WriteLine(a > h && b > h);
Console.WriteLine(a > h || b > h);
Console.WriteLine(!(a > h));

其中,逻辑非!的优先级最高,逻辑与&&的优先级次之,逻辑或||的优先级最低

位运算符

位运算符,用于对二进制位进行操作,结果为int类型

& 按位与、| 按位或、^ 按位异或、~ 按位取反、<< 左移、>> 右移

1
2
3
4
5
6
7
8
9
10
11
12
int i1 = 2;
int j1 = 3;
Console.WriteLine(i1 & j1); // 2
Console.WriteLine(i1 | j1); // 3
Console.WriteLine(i1 ^ j1); // 1

// ~ 取反
Console.WriteLine(~i1); // -3
// << 左移,相当于乘以2的n次方
Console.WriteLine(i1 << 1); // 4
// >> 右移,相当于除以2的n次方
Console.WriteLine(i1 >> 1); // 1

三目(元)运算符

表达式1 ? 表达式2 : 表达式3

如果表达式1为真,则返回表达式2的值;如果表达式1为假,则返回表达式3的值

1
2
3
int k = 1;
int l = 2;
int m = k > l ? k : l; // 2

字符串拼接

字符串拼接可以用算术运算符的+,将两个字符串连接在一起

1
2
string str = "hello";
Console.WriteLine(str + " world");

也可以使用{}占位符,对字符串进行格式化,占位符中放置从0开始的索引,表示要插入的变量

1
2
3
string str1;
str1 = String.Format("{0} world", str); // {}占位符
Console.WriteLine(str1);

括号

括号()用于改变运算符的优先级,括号内的表达式先运算

1
a = (c + d) * e;

运算优先级(只包括上面提到的)

  • 括号(最高优先级)
    () - 强制改变运算顺序

  • 自增/自减运算符
    x++ x-- (后缀)
    ++x --x (前缀)

  • 算数运算符
    * / % (乘、除、取模)
    + - (加、减)

  • 位运算符
    << >> (位移)
    & (按位与)
    ^ (按位异或)
    | (按位或)

  • 关系运算符
    < > <= >=

  • 相等运算符
    == !=

  • 逻辑运算符
    && (逻辑与)
    || (逻辑或)

  • 条件运算符(最低优先级)
    ?: (三元运算符)

流程控制

if语句

if语句用于根据条件执行不同的代码块

1
2
3
4
5
语法:
if (条件)
{
条件为真时执行的代码块
}
1
2
3
4
if (true)
{
Console.WriteLine("条件为真");
}

if-else语句

1
2
3
4
5
6
7
8
9
语法:
if (条件)
{
条件为真时执行的代码块
}
else
{
条件为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
int a = 1;
if (a > 5)
{
Console.WriteLine("a大于5");
}
else
{
Console.WriteLine("a小于等于5");
}

if-else if-else语句

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
if (条件1)
{
条件1为真时执行的代码块
}
else if (条件2)
{
条件2为真时执行的代码块
}
else
{
条件1和条件2都为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
10
11
12
if (a > 5)
{
Console.WriteLine("a大于5");
}
else if (a == 5)
{
Console.WriteLine("a等于5");
}
else
{
Console.WriteLine("a小于5");
}

switch语句

switch类似于简化的if-else if-else语句,用于根据不同的条件执行不同的代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
switch (表达式)
{
case 常量值1:
执行的代码块1
break;
case 常量值2:
执行的代码块2
break;
default:
执行的代码块3
break;
}

case判断的是表达式所得的值是否与所写的常量值一致,后面只能写常量,
而不能写表达式或者变量

同样的,C#switch语句需要写break,不然会贯穿

即执行完一个case后,会继续执行下一个case,直到遇到break或者switch语句结束

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 100;
switch (a)
{
case 1:
Console.WriteLine("a等于1");
break;
case 2:
Console.WriteLine("a等于2");
break;
default: // default可以省略
Console.WriteLine("a不等于1也不等于2");
break;
}

while循环

while循环用于在条件为时重复执行代码块

但要注意,如果条件永远为真,那么将导致死循环,直至内存溢出,程序崩溃

1
2
3
4
5
语法:
while (条件)
{
执行的代码块
}
1
2
3
4
5
6
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i++;
}

break

break用于跳出循环,即当满足某个条件时,跳出循环,不再执行循环内的代码

1
2
3
4
5
6
7
8
9
while (true)
{
Console.WriteLine("请输入一个数字:");
string input = Console.ReadLine();
if (input == "1")
{
break;
}
}

continue

continue用于跳过本次循环,即当满足某个条件时,跳过本次循环,继续下一次循环

1
2
3
4
5
6
7
8
9
10
11
int a = 0;
while (a <= 20)
{
if (a % 2 == 0)
{
a++;
continue;
}
Console.WriteLine(a);
a++;
}

do-while循环

do-while循环与while循环类似,但它在循环体执行完毕后,再判断条件是否为真,
即无论如何都会执行一次循环体

1
2
3
4
5
语法:
do
{
执行的代码块
} while (条件);
1
2
3
4
5
6
int b = 0;
do
{
Console.WriteLine(b);
b++;
}while (b <= 10);

for循环

for循环用于在指定次数内重复执行代码块

1
2
3
4
5
语法:
for (初始化表达式; 条件表达式; 更新表达式)
{
执行的代码块
}
1
2
3
4
for (int c = 0; c < 10; c++)
{
Console.WriteLine(c);
}

控制台小项目#1-移动小方块

需求

控制台显示一个小方块,用户输入wsad,控制小方块上下左右移动

实现

控制台

首先我们设置一下控制台的相关内容

重新设置背景颜色,设置文字颜色,并隐藏光标

1
2
3
4
5
6
7
8
9
// 设置背景颜色
Console.BackgroundColor = ConsoleColor.Red;
Console.Clear();

// 设置文字颜色
Console.ForegroundColor = ConsoleColor.Yellow;

// 隐藏光标
Console.CursorVisible = false;

绘制小方块

接下来绘制小方块

1
2
3
4
5
int x = 0;
int y = 0;

Console.SetCursorPosition(x, y);
Console.Write("■");

不过,对于一个游戏来说,肯定是有循环层的,不然玩家输入一次那就没了可不行

Minecraft来说,有一个东西名为tick。它又分为随机刻游戏刻等。
游戏刻为20tick,即程序每秒将执行20次,以实时改变游戏中的一些东西,
如玩家位置、更新方块等。游戏的循环会一直持续下去,直到玩家退出游戏

所以,我们要将绘制小方块的代码放入循环语句中,一般我们就采用while (true)循环

1
2
3
4
5
6
7
8
int x = 0;
int y = 0;

while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");
}

获取用户输入

接下来,在控制小方块移动之前,我们得先获取用户输入

1
char c = Console.ReadKey(true).KeyChar;

这里我们使用ReadKey(true)不显示输入的字符,同时设置一个char c变量来接收用户输入的字符

移动小方块

接下来,我们根据用户输入的字符,来移动小方块

那么我们输入的字符只有WASDwasd8种,所以我们可以使用switch语句来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (c)
{
case 'w':
case 'W':
break;
case 's':
case 'S':
break;
case 'a':
case 'A':
break;
case 'd':
case 'D':
break;
}

这里我们就直接利用switch贯穿的特性,即同一个字符的大小写都由同一块代码块来执行

随后,我们在各个case中,根据用户输入的字符,来改变小方块的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch (c)
{
case 'w':
case 'W':
y -= 1;
break;
case 's':
case 'S':
y += 1;
break;
case 'a':
case 'A':
x -= 2;
break;
case 'd':
case 'D':
x += 2;
break;
}

好,现在我们输入WASD,小方块会根据输入的字符来移动,但是小方块不会消失,
所以我们需要在每次移动之前,将小方块原来的位置清空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");

char c = Console.ReadKey(true).KeyChar;

// 把之前的方块擦除
Console.SetCursorPosition(x, y);
Console.Write(" ");
switch (c)
{
...
}
}

擦除的话就直接写入空格即可,就可以将原来的位置的字符替换掉

限制移动范围

还没结束嘞!

当我们在在边缘位置移动小方块时,假设小方块此时贴在右边界
而我们没注意,继续按D(d),那么小方块就会超出控制台的范围,导致程序崩溃

所以,在各个case语句中,我们还得判断下一个坐标是否越界

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
switch (c)
{
case 'w':
case 'W':
y -= 1;
if (y < 0)
{
y = 0;
}
break;
case 's':
case 'S':
y += 1;
if (y > Console.WindowHeight - 1)
{
y = Console.WindowHeight - 1;
}
break;
case 'a':
case 'A':
x -= 1;
if (x < 0)
{
x = 0;
}
break;
case 'd':
case 'D':
x += 1;
if (x > Console.WindowWidth - 1)
{
x = Console.WindowWidth - 1;
}
break;
}

完整代码

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
namespace XM1
{
internal class Program
{
static void Main(string[] args)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.Clear();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.CursorVisible = false;

int x = 0;
int y = 0;

while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");

char c = Console.ReadKey(true).KeyChar;

Console.SetCursorPosition(x, y);
Console.Write(" ");
switch (c)
{
case 'w':
case 'W':
y -= 1;
if (y < 0)
{
y = 0;
}
break;
case 's':
case 'S':
y += 1;
if (y > Console.WindowHeight - 1)
{
y = Console.BufferHeight - 1;
}
break;
case 'a':
case 'A':
x -= 1;
if (x < 0)
{
x = 0;
}
break;
case 'd':
case 'D':
x += 1;
if (x > Console.WindowWidth - 1)
{
x = Console.BufferWidth - 1;
}
break;
}
}
}
}
}

控制台小项目#2-打Boss拿宝箱

需求

  • 小项目分为3个界面,开始界面游戏界面结束界面
  • 开始界面在上方中央位置显示给木,在下方显示开始游戏退出游戏,玩家使用WS进行选择,J键确认
  • 游戏界面中,边界有红色的代表边界墙,下方留出一块区域用于输出信息
  • 玩家用黄色的表示,用户输入WASD控制小方块移动,只能在边界墙的范围内移动
  • 生成一个Boss,用绿色的表示,当用户在Boss边上时,按J键进入对战状态
  • 对战状态时,玩家不能移动,按J键攻击,玩家随机给Boss造成伤害,而后Boss对玩家造成伤害
  • 当一方的攻击力小于对方防御力时,无法对对方造成伤害
  • 玩家血量小于等于0时,游戏结束,进入结束场景
  • Boss血量小于等于0时,游戏结束,玩家可以移动,生成一个宝箱,玩家到宝箱边上时,打开宝箱,游戏结束,进入结束场景
  • 结束场景上方中央位置根据输赢显示相应文字,在下方显示重新开始退出游戏,玩家使用w或s进行选择,j键确认

场景框架

这个小项目是比上面那个复杂得多了,不过不要捉急,我们拆解开一步步来

这个小项目是完全可以用上面已经提及的知识写出来的,未来会有更加复杂的小项目

前面我们已经写了一个控制玩家移动的小项目了,至少在玩家移动上是没有多大的问题了

现在,这个小项目分为了3个场景,除了第二个游戏场景需要实现一定的逻辑外,
其他两个场景还是比较简单的

显示的文字可以直接用控制台打印的方法输出,玩家输入字符也有相应的判断方法

但是场景间该怎么切换呢?流程控制,不过我们需要一个标识符变量)来判断当前处于哪个场景

好,那么到这里,我们大体上是可以开始写代码了,我们先为这3个场景打一个基本的框架,
再往里面填充东西

控制台基础设置

(以下代码均写在主函数Main中,毕竟现在也还没学到函数部分)

常规的还是控制台的一些设置,比如设置窗口大小文字颜色隐藏光标

1
2
3
4
5
6
7
8
9
#region 控制台基础设置
// 隐藏光标
Console.CursorVisible = false;
// 设置控制台大小
int w = 50;
int h = 30;
Console.SetWindowSize(w, h);
Console.SetBufferSize(w, h);
#endregion

场景循环判断

好了,接下来就是场景的部分了

上面说了,我们需要一个标识符用于判断当前的场景,所以我们就定义一个int类型的变量吧

1
int nowId = 1;

那么再后面,就是游戏的循环层了,也就是while(true),直到你退出游戏前,这个循环都不会停止

那么,在循环中,我们就得用流程控制来判断上面的标识符
再根据不同的标识符处理不同场景的逻辑

这里流程控制的话,我选择使用switch语句,因为的标识符是有限的,且可以枚举出来的

1
2
3
4
5
6
7
8
9
10
11
12
while (true)
{
switch (nowId)
{
case 1:
break;
case 2:
break;
case 3:
break;
}
}

开始场景

那么接下来我们就来写第一个场景

不过在开始之前,建议是先执行清空控制台的命令,因为我们最后可能是会从第三个场景返回第一个场景的,
得把前面的场景给先清空

1
Console.Clear();

打印“给木”

接下来就是打印“给木”(其实就是Game的音译)了,我们得先设置光标位置
Console.SetCursorPosition方法

1
2
Console.SetCursorPosition(w / 2 - 2, 8);
Console.Write("给木");

这里我们将光标位置设置到当前窗口一半,再往左2个字符(因为中文字符是占2个的)的位置,
高度上暂且设置为8,具体的可以根据大家实际情况调整

打印选项

那么接下来就是打印开始游戏退出游戏这两个选项了

大致也和上面的类似

1
2
3
4
5
6
7
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

不过现在,我们启动调试,也就只是打印了文字,我们输入WS也是没有反应的,所以下一步就是要获取用户输入,并将用户当前选择的选项高亮显示换个颜色

获取用户输入

在获取用户输入之前,我们肯定得先写一个while循环,毕竟总不能按一次就没了吧

用户输入WS,选择选项,直到用户输入了J键,我们才跳出循环,进入相应的场景

1
2
3
4
5
6
7
8
9
10
while (true)
{
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");
}

不过,我们还得要一个变量,用于记录用户当前选择的选项,然后用户按WS时,改变这个变量的值

当然,这个变量的值我们不能让它无限地增大或者减少,所以在判断玩家输入的同时,
也要加个判断来限制它的范围

至于玩家的输入么,还是用switch来判断好了

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
46
47
48
49
50
51
int nowSelectId = 0;

while (true)
{
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

char input = Console.ReadKey(true).KeyChar;

switch (input)
{
// 当玩家按下W(因为有大小写)
case 'w':
case 'W':
nowSelectId--;
// 超出范围时,限制范围
if (nowSelectId < 0)
{
nowSelectId = 0;
}
break;
// 同上
case 's':
case 'S':
nowSelectId++;
if (nowSelectId > 1)
{
nowSelectId = 1;
}
break;

case 'j':
case 'J':
// 按下J时,如果是“开始游戏”选项,则改变场景的标识符,进入游戏场景
if (nowSelectId == 0)
{
nowId = 2;
}
// 如果是“退出游戏”选项,则退出游戏(这里是直接控制台退出)
else
{
Environment.Exit(0);
}
break;
}
}

注意我们上面所说的变量,它是在while外面的,不然每次循环它都会被重置,那就没有意义了

不过现在,我们尚未实现让玩家当前选择的选项高亮显示,所以我们现在运行也不知道玩家选了什么

这里我们就要在各个选项打印之前来判断一下,选择的是否是这个选项,再改个颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Console.ForegroundColor = 
nowSelectId == 0 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

Console.ForegroundColor =
nowSelectId == 1 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");
...

这里我们运用三目运算符来进行判断,以开始游戏为例,
如果当前选择的选项是开始游戏,即选项的标识符为0,那么我们就将颜色设置为红色,否则就是白色

下面的退出游戏同理

那么现在进行测试,我们按下WS就可以看到选项在变化了,不过,当我们在开始游戏按下J之后,还在当前界面

为什么呢?因为我们没有退出这个场景的循环,即便你在switch的最后一个caseif中加入break,也还是不行

因为在switch中的break,只是跳出switch,并不会跳出while,所以我们还需要一个标识符,判断是否要跳出while循环

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
while (true)
{
// isQuitWhile用于判断是否退出当前while循环
bool isQuitWhile = false;

...
switch (input)
{
...
case 'j':
case 'J':
if (nowSelectId == 0)
{
nowId = 2;
isQuitWhile = true;
}
else
{
Environment.Exit(0);
}
break;
}
if (isQuitWhile)
{
break;
}
}

这里我们添加一个bool变量类型的isQuitWhile,默认为false

当玩家按下J时,改变它的值,再在switch的外面,while循环内,
判断一下,如果为true,则跳出循环

OK,那么第一个部分就写完了,下面是第一部分的完整代码

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#region 开始菜单

Console.Clear();
Console.SetCursorPosition(w / 2 - 2, 8);
Console.Write("给木");

int nowSelectId = 0;

while (true)
{
// isQuitWhile用于判断是否退出当前while循环
bool isQuitWhile = false;

// 根据选择的编号设置对应选项的颜色
Console.ForegroundColor =
nowSelectId == 0 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

Console.ForegroundColor =
nowSelectId == 1 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

// 获取用户输入
char input = Console.ReadKey(true).KeyChar;
// 根据用户输入进行判断
switch (input)
{
case 'w':
case 'W':
nowSelectId--;
if (nowSelectId < 0)
{
nowSelectId = 0;
}
break;
case 's':
case 'S':
nowSelectId++;
if (nowSelectId > 1)
{
nowSelectId = 1;
}
break;
// 用户选择J则进行该选项
case 'j':
case 'J':
if (nowSelectId == 0)
{
nowId = 2;
isQuitWhile = true;
}
else
{
Environment.Exit(0);
}
break;
}
if (isQuitWhile)
{
break;
}
#endregion
...

游戏场景

那么接下来,我们来讲游戏场景的代码

首先还是常规的,先清空控制台,因为我们切换到另外一个场景了,得把前一个场景的内容都清空

1
Console.Clear();

接下来,我们一步步来实现上面的需求

绘制边界墙

在需求中,要求我们绘制边界墙,用红色的表示

那么边界墙,就是围着屏幕范围画一圈,可以用for循环来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 调整颜色
Console.ForegroundColor = ConsoleColor.Red;

// 三道横墙
for (int i = 0; i < w; i+=2)
{
Console.SetCursorPosition(i, 0);
Console.Write("■");

Console.SetCursorPosition(i, h - 6);
Console.Write("■");

Console.SetCursorPosition(i, h - 1);
Console.Write("■");
}

h是前面写到的屏幕高度,w是屏幕宽度

h-1是为了防止越界,i的步长为2,也是因为这个小方块占2个字符宽度

这里我们一共绘制3道横墙,第一道墙在屏幕顶部,第二道墙在距离屏幕底部上方5个字符处,
第三道墙在屏幕底部,在第二道和第三道墙之间是打印信息的区域

接下来是绘制竖墙,竖墙的绘制和横墙的绘制类似,只不过横墙是i+=2,竖墙是i++

1
2
3
4
5
6
7
8
9
// 两道竖墙
for (int i = 0; i < h; i++)
{
Console.SetCursorPosition(0, i);
Console.Write("■");

Console.SetCursorPosition(w - 2, i);
Console.Write("■");
}

竖墙就第一列和最后一列要绘制

现在我们测试,进入游戏场景时,就可以看到红色的边界墙了

绘制玩家

那么边界墙已经绘制好了,接下来就是绘制玩家和Boss了

不过要注意的是,因为后面玩家要移动,还有攻击逻辑,所以我们要将它们放在循环中

我们先绘制玩家,玩家用黄色的表示,至于一开始玩家要生成的位置,我们可以直接指定一个位置

不过,如果你要高级一点的话则可以使用Random,来随机给定一个位置,这里我们就指定了

1
2
3
4
5
6
7
8
9
10
while (true)
{
int playerX = 4;
int playerY = 5;
string playerId = "●";
ConsoleColor playerColor = ConsoleColor.Yellow;
Console.ForegroundColor = playerColor;
Console.SetCursorPosition(playerX, playerY);
Console.Write(playerId);
}

这里我们定义了一个playerXplayerY,分别表示玩家在屏幕中的位置

再定义一个玩家的颜色,最后根据位置和颜色信息来绘制玩家,现在当我们进入第二个场景时,
就可以看到一个黄色小球了

不过,或许有人会问,为什么墙的绘制没有放在循环中,而是放在了循环外面呢?

因为墙的位置是固定的,不会改变,所以放在循环外面,
而玩家和Boss的位置和状态(当然在这里我们的Boss是不会动的,但是会死亡)是会改变的,
所以放在循环中

绘制Boss

那么玩家已经绘制好了,接下来就是绘制Boss了,也和玩家绘制差不多

Boss用绿色的表示,位置我们也指定一下好了

1
2
3
4
5
6
7
int bossX = 24;
int bossY = 15;
string bossId = "■";
ConsoleColor bossColor = ConsoleColor.Green;
Console.ForegroundColor = bossColor;
Console.SetCursorPosition(bossX, bossY);
Console.Write(bossId);

随后我们就可以看到一个绿色的小方块

控制玩家移动

那么,接下来就来到游戏的循环层,要获取用户的输入,控制玩家移动

这个东西我们在上面已经写过了,所以这里我们就直接来写

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
switch (input2)
{
case 'w':
case 'W':
playerY--;
if (playerY < 1)
{
playerY = 1;
}
break;
case 's':
case 'S':
playerY++;
if (playerY > h - 7)
{
playerY = h - 7;
}
break;
case 'a':
case 'A':
playerX -= 2;
if (playerX < 2)
{
playerX = 2;
}
break;
case 'd':
case 'D':
playerX += 2;
if (playerX > w - 4)
{
playerX = w - 4;
}
break;
case 'j':
case 'J':
break;
}

复杂数据类型

枚举

枚举是一种数据类型,用于定义一组命名常量(其值为整型),
一般用来表示状态类型等等

还是拿Minecraft举例,很多方块状态就是枚举类型的属性,
列举各个状态,比如东西南北

声明

枚举的声明方式如下:

1
2
3
4
5
6
enum 枚举名
{
枚举项1,
枚举项2,
...
}

枚举名可以加上E以区分其他数据类型,具体使用:

1
2
3
4
5
6
7
enum EColor
{
BLACK, // 其值为0
RED, // 其值为1
GREEN = 10, // 其值为10
BLUE // 其值为11(在上一个基础上累加)
}

枚举值从0开始,也可以手动指定枚举值,后面的枚举值会在此基础上累加

使用示例

枚举类型一般会和switch一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明枚举变量
// 自定义枚举类型 变量名字 = 自定义枚举类型.枚举成员
EColor color = EColor.RED;
switch (color)
{
case EColor.BLACK:
Console.WriteLine("黑色");
break;
case EColor.RED:
Console.WriteLine("红色");
break;
case EColor.GREEN:
Console.WriteLine("绿色");
break;
case EColor.BLUE:
Console.WriteLine("蓝色");
break;
}

类型转换

因为枚举类型中的枚举项实际上是整型常量
所以枚举类型可以和整型进行转换,转换方式如下:

1
2
3
// 枚举的类型转换(int)
int i = (int)EColor.RED;
Console.WriteLine(i);

同样枚举也可以转换为string类型

1
2
3
// 枚举的类型转换(string)
string s = EColor.RED.ToString();
Console.WriteLine(s); // 输出:RED

string类型也可以转换为枚举类型

其语法为

1
目标枚举类型 变量名 = (目标枚举类型)Enum.Parse(typeof(目标枚举类型), "枚举成员");

使用示例

1
2
EColor color2 = (EColor)Enum.Parse(typeof(EColor), "GREEN");
Console.WriteLine(color2); // 输出:GREEN

数组

数组是一种数据类型,用于存储一组相同类型的数据,其长度是固定的,不能改变

数组有一维数组、二维数组、多维数组、交错数组等等

一维数组

声明

一维数组的声明方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数据类型[] 数组名;
int[] arr1;

数据类型[] 数组名 = new 数据类型[数组长度];
int[] arr2 = new int[5];

数据类型[] 数组名 = new 数据类型[] { 数据1, 数据2, 数据3, ... };(这个比较常用)
int[] arr5 = new int[] { 1, 2, 3, 4, 5 };

数据类型[] 数组名 = { 数据1, 数据2, 数据3, ... };
int[] arr3 = { 1, 2, 3, 4, 5 };

数据类型[] 数组名 = new 数据类型[数组长度] { 数据1, 数据2, 数据3, ... };
int[] arr4 = new int[5] { 1, 2, 3, 4, 5 };

数组长度获取

获取数组长度可以使用Length属性

1
2
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr.Length); // 输出:5

数组元素访问、修改

数组元素可以通过索引来访问,索引从0开始,但索引不得越界数组长度 - 1

1
2
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr[0]); // 输出:1

同样可以利用这种方法来修改数组元素

1
2
3
int[] arr = { 1, 2, 3, 4, 5 };
arr[0] = 10;
Console.WriteLine(arr[0]); // 输出:10

数组遍历

数组遍历可以使用for循环,也可以使用foreach循环

1
2
3
4
5
6
7
8
9
10
11
int[] arr = { 1, 2, 3, 4, 5 };
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}

int[] arr2 = { 1, 2, 3, 4, 5 };
foreach (int j in arr2)
{
Console.WriteLine(j);
}

增加元素

由于数组初始化之后就不能再添加新元素,我们要使用搬家的方法来实现

即重新定义一个更长长度的数组,将原数组中的元素复制到新数组中,
最后将新数组赋值给原数组

1
2
3
4
5
6
7
8
int[] arr3 = { 1, 2, 3, 4, 5 };
int [] arr6 = new int[6];
// 搬家
for (int i = 0; i < arr3.Length; i++)
{
arr6[i] = arr3[i];
}
arr3 = arr6;

删除元素

删除元素同样如此

1
2
3
4
5
6
7
int[] arr3 = { 1, 2, 3, 4, 5 };
int[] arr7 = new int[4];
for (int i = 0; i < arr7.Length; i++)
{
arr7[i] = arr3[i];
}
arr3 = arr7;

但其实在实际开发过程中,我们并不会这么干,后续会接触到更好的方法来处理数组

查找元素

查找元素可以使用for循环遍历数组

1
2
3
4
5
6
7
for (int i = 0; i < arr3.Length; i++)
{
if (arr3[i] == 10)
{
Console.WriteLine(i);
}
}

二维数组

声明

声明上与一维数组差不多,只不过需要多一个维度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数据类型[,] 数组名;
int[,] arr;

数据类型[,] 数组名 = new 数据类型[行数, 列数];
int[,] arr2 = new int[3, 4];

数据类型[,] 数组名 = new 数据类型[,] { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr3 = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };

数据类型[,] 数组名 = { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr4 = { { 1, 2, 3 }, { 4, 5, 6 } };

数据类型[,] 数组名 = new 数据类型[行数, 列数] { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr5 = new int[3, 4] { { 1, 2, 3 }, { 4, 5, 6 } };

数组长度获取

对于二维数组来说,GetLength()方法返回的是数组的总元素个数,而GetLength(0)和GetLength(1)分别返回的是数组的行数和列数

1
2
3
int[,] arr = new int[3, 4];
Console.WriteLine(arr.GetLength(0)); // 输出:3
Console.WriteLine(arr.GetLength(1)); // 输出:4

数组元素访问、修改

二维数组可以通过行索引列索引来访问、修改元素

1
2
3
int[,] arr = new int[3, 4];
arr[0, 0] = 10;
Console.WriteLine(arr[0, 0]); // 输出:10

数组遍历

二维数组可以通过for循环来遍历,但是因为有两个维度,需要嵌套两个for循环

1
2
3
4
5
6
7
8
int[,] arr = {{ 1, 2, 3 }, { 4, 5, 6 } };
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
Console.WriteLine(arr[i, j]);
}
}

增加元素

二维数组同样需要使用搬家的方法来实现增加元素

1
2
3
4
5
6
7
8
9
int[,] arr5 = new int[3, 3];
for (int i = 0; i < arr4.GetLength(0); i++)
{
for (int j = 0; j < arr4.GetLength(1); j++)
{
arr5[i, j] = arr4[i, j];
}
}
arr4 = arr5;

删除元素

1
2
3
4
5
6
7
8
9
int [,] arr6 = new int[2, 2];
for (int i = 0; i < arr6.GetLength(0); i++)
{
for (int j = 0; j < arr6.GetLength(1); j++)
{
arr6[i, j] = arr4[i, j];
}
}
arr4 = arr6;

查找元素

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < arr4.GetLength(0); i++)
{
for (int j = 0; j < arr4.GetLength(1); j++)
{
if (arr4[i, j] == 100)
{
Console.WriteLine("找到了");
}
}
}

交错数组

交错数组是数组的数组,即二维数组中的每一行都是一个一维数组

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数据类型[][] 数组名;
int[][] arr;

数据类型[][] 数组名 = new 数据类型[行数][]; // 声明行数,列数不声明,因为列数可能不同
int[][] arr2 = new int[3][];

int[][] jaggedArray2 = new int[3][] {new int[] {1,2,3},
new int[] {4,5},
new int[] {6}};

int[][] jaggedArray3 = new int[][] { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };

int[][] jaggedArray4 = { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };

数组长度获取

GetLength(0)返回的是数组的行数,但列数要先获取某一行的值,再通过Length获取

1
2
Console.WriteLine(jaggedArray4.GetLength(0)); // 行数
Console.WriteLine(jaggedArray4[0].Length); // 第一行的列数

数组元素访问、修改

1
2
3
4
5
// 获取元素
Console.WriteLine(jaggedArray4[1][1]); // 5

// 修改
jaggedArray4[1][1] = 100;

数组遍历

1
2
3
4
5
6
7
for (int i = 0; i < jaggedArray4.GetLength(0); i++)
{
for (int j = 0; j < jaggedArray4[i].Length; j++)
{
Console.Write(jaggedArray4[i][j] + " ");
}
}

后面的增删查都与前面的差不多,这里就不再赘述

值类型 vs 引用类型

区别

  • 值类型存储在中,引用类型存储在
  • 值类型直接存储值,引用类型存储的是引用(地址)
  • 值类型在赋值时是直接复制,创建一个副本,修改副本的值不会影响原始变量;
    引用类型是复制引用,指向的是同一个地址,修改副本的值会影响原始变量‘
1
2
3
4
5
6
7
8
9
10
11
12
int[] arr = { 1, 2, 3 };
int a = 10;

int b = a;
int[] arr2 = arr;
Console.WriteLine("a={0}, b={1}", a, b); // a=10, b=10
Console.WriteLine("arr[0]={0}, arr2[0]={1}", arr[0], arr2[0]); // arr[0]=1, arr2[0]=1

b = 20;
arr2[0] = 5;
Console.WriteLine("a={0}, b={1}", a, b); // a=10, b=20
Console.WriteLine("arr[0]={0}, arr2[0]={1}", arr[0], arr2[0]); // arr[0]=5, arr2[0]=5

特殊的引用类型-string

string是引用类型,但复制之后,修改副本的值不会影响原始变量,
因为它会自动创建指向另外一个内存空间的地址

1
2
3
4
string str1 = "123";
string str2 = str1;
str2 = "321";
Console.WriteLine(str2);

但这种特性也带来一个问题,如果频繁修改string的值,会造成内存垃圾

所有后面有更好的处理字符串的方法

函数(方法)

函数是一段可重复使用的代码块,用于执行特定的任务

函数的声明

1
2
访问修饰符 返回类型 函数名(参数列表) {
}

函数写在class类语句块中,或者是struct结构体语句块中

函数分为有参有返无参有返有参无返无参无返四大类

函数可以同名,但参数列表必须不同,这是函数重载

定义函数

1
2
3
4
static int Add(int a, int b)
{
return a + b;
}

无返回值的可以用void表示

1
2
3
4
static void Print(int a, int b)
{
Console.WriteLine(a + b);
}

使用函数

1
2
3
4
5
6
7
static void Main(string[] args)
{
int a = 1;
int b = 2;
int c = Add(a, b);
Console.WriteLine("a + b = " + c); // a + b = 3
}

ref/out

refout关键字可以改变从外部传入函数的参数,即如果函数内部改变了值,
那么函数外部也改了

ref

1
2
3
4
5
6
7
8
static void ChangeValueRef(ref int a)
{
a = 3;
}
static void ChangeArrayRef(ref int[] a)
{
a = new int[] { 1, 2, 3 };
}

ref传入的参数必须初始化,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 1;
ChangeValueRef(ref i);
Console.WriteLine(i); // 输出:3

int[] a = { 1, 2 };
ChangeArrayRef(ref a);
for (int j = 0; j < a.Length; j++)
{
Console.WriteLine(a[j]);
}

// ref传入的参数必须初始化,但out不用
int i2;
// ChangeArrayRef(ref i2); // 报错

out

out传入的参数不需要初始化,但函数内部必须给参数赋值,否则会报错

1
2
3
4
static void ChangeValueOut(out int a)
{
a = 3; // 如果不赋值就会报错
}

使用:

1
2
3
int i2;
ChangeValueOut(out i2);
Console.WriteLine(i2);

可变参数

可变参数是指函数的参数个数不固定,使用params关键字来定义

1
2
3
4
5
6
7
8
9
static int Add(params int[] nums)
{
int sum = 0;
for(int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}

params参数只能放在参数列表的最后,并且只能有一个,
修饰的参数必须是一维数组,不能用refout关键字,
也不能指定默认值

1
2
int sum = Add(1, 2, 3, 4, 5);
Console.WriteLine(sum); // 15

参数默认值

可以为函数的参数指定默认值,这样在调用函数时可以不传入该参数

1
2
3
4
static int Add(int a, int b, int c = 0)
{
return a + b + c;
}

调用时可以传入参数,也可以不传入参数

1
2
sum = Add(1, 2);
Console.WriteLine(sum);

函数重载

函数重载是指函数名相同,但参数列表不同,
这样在调用函数时可以根据参数列表来选择调用哪个函数

模组开发里面,原版的那些注册语句,一堆register方法,那些都是重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int Add(int a, int b)
{
return a + b;
}

static int Add(params int[] nums)
{
int sum = 0;
foreach (var num in nums)
{
sum += num;
}
return sum;
}

调用时根据参数列表来选择调用哪个函数

1
2
3
4
int a = 1;
int b = 2;
Console.WriteLine(Add(a, b));
Console.WriteLine(Add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

递归

递归是指函数自己调用自己

递归必须要有退出条件,否则会陷入死循环

1
2
3
4
5
6
7
8
9
10
11
static int Fun(int num)
{
if (num == 1 || num == 2)
{
return 1;
}
else
{
return Fun(num - 1) + Fun(num - 2);
}
}

经典的斐波那契数列就可以用递归来解决

1
Console.WriteLine(Fun(3));

结构体

结构体是一种值类型,一种自定义变量类型

因为很多情况下,编程语言自带的数据类型无法满足实际的需要,就要自定义数据类型了

声明结构体

结构体用struct关键字声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Stundent
{
// 结构体成员
public int age;
public bool sex;
public int number;
public string name;

// 结构体方法
public void Speak()
{
Console.WriteLine("我叫{0},今年{1}岁,学号{2}。", name, age, number);
}

// 构造函数
public Stundent(int age, bool sex, int number, string name)
{
this.age = age;
this.sex = sex;
this.number = number;
this.name = name;
}
}

使用结构体

1
2
3
4
5
6
7
8
Stundent stu = new Stundent();
stu.age = 20;
stu.name = "张三";
stu.number = 1001;
stu.Speak();

Stundent stu2 = new Stundent(20, true, 1002, "李四");
stu2.Speak();

使用结构体用new创建对象,然后通过.来访问成员变量和方法

也可在实例化时直接用构造函数初始化

是一种引用类型,也是一种自定义变量类型

声明类

类用class关键字声明

1
2
3
4
5
6
7
8
9
10
11
class Person
{
public string name;
public int age;
// 构造函数
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
}

使用类

1
2
Person p1 = new Person("张三", 18);
Console.WriteLine("我叫{0},今年{1}岁", p1.name, p1.age);

使用类用new创建对象,然后通过.来访问成员变量

访问修饰符

  • public外部可访问

  • private,不写默认private,外部不可访问,内部类可以访问

  • protected外部不可访问,内部类和子类可以访问

成员变量

成员变量是类的属性,用于存储类的状态,
成员变量可以是值类型引用类型
成员变量默认是private的,只能在类的内部访问

1
2
3
4
public string name;
public int age = 18;// 类中的成员变量可以初始化赋值
public E_Sex sex;
Person friends; // 可以用这个类本身的类型(但不能实例化)

使用:

1
2
3
4
5
Person person = new Person();
person.name = "张三";
person.age = 18;
person.sex = E_Sex.Male;
Console.WriteLine("姓名:" + person.name);

成员方法

成员方法是指类的成员函数,用于实现类的功能

1
2
3
4
5
// 成员方法
public void Speak()
{
Console.WriteLine("我叫{0},今年{1}岁", name, age);
}

使用:

1
2
3
4
Person person = new Person();
person.name = "张三";
person.age = 20;
person.Speak();

构造函数

构造函数是类的特殊方法,用于初始化类的成员变量

类在没有声明任何构造函数时,会自动创建一个无参构造函数
但如果声明了有参构造函数,如果不手动创建无参构造函数
就无法使用无参构造函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
// 手动声明无参构造函数
public Person()
{
this.name = "张三";
this.age = 18;
}

// 有参构造函数
public Person(string name,int age)
{
this.name = name;
this.age = age;
}

使用:

1
Person person = new Person("张三", 18);

特殊用法,可以用:this调用其他的构造函数

1
2
3
4
public Person(string name):this(name,18)
{
Console.WriteLine("调用其他构造函数");
}

使用:

1
Person person1 = new Person("李四");

析构函数

析构函数是类的特殊方法,用于释放类的资源

当一个对象被销毁时,会触发垃圾回收机制,自动调用该对象的析构函数
C#有垃圾回收机制,一般不会手动写析构函数

1
2
3
4
~Person()
{
Console.WriteLine("调用析构函数");
}

成员属性

成员属性是为了保护成员变量的,为成员变量的获取和赋值添加逻辑处理,
防止外部类直接修改

get/set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private string name;

public string Name
{
get
{
// 可以获取的内容
return name;
}
set
{
// value关键字,表示传入的值
name = value;
}
}

使用:

1
2
3
Person person = new Person();
person.Name = "张三"; // 执行set
Console.WriteLine(person.Name); // 执行get

加密/解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private int money;

public int Money
{
get
{
// 可以进行加密解密
return money - 5;
}
set
{
// 额外的逻辑处理
if (value < 0)
{
Console.WriteLine("钱不能为负数");
Console.WriteLine("已设置为0");
money = 0;
}
else
{
money = value + 5;
}
}
}

使用:

1
2
3
Person person1 = new Person();
person1.Money = 100; // 执行set
Console.WriteLine(person1.Money); // 执行get

只读/只写

1
2
3
4
5
public int Age
{
get { return age; }
private set { age = value; } // 私有set,外部无法修改
}

自动属性

可以不声明成员变量,直接声明属性,编译器会自动生成成员变量

1
2
public string Address 
{ get; private set; }

索引器

索引器是类的特殊方法,可以让对象像数组这样用索引访问其中的元素

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
private Person[] friends;

// 索引器
public Person this[int index]
{
get
{
if (friends == null)
{
return null;
}
else
{
return friends[index];
}
}
set
{
if (friends == null)
{
friends = new Person[] { value };
}
else if (index > friends.Length)
{
friends[friends.Length - 1] = value;
}

friends[index] = value;
}
}

使用:

1
2
3
4
Person person = new Person();
person[0] = new Person("张三", 18);
person[1] = new Person("李四", 19);
person[2] = new Person("王五", 20);

静态成员/方法

静态成员会在程序启动时被初始化,有单独的内存空间,
静态成员只能用类名来访问,不能通过对象来访问

1
2
3
4
5
6
public static float PI = 3.1415926f;

public static float CalCircle(float f)
{
return PI * f * f;
}

使用:

1
2
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalCircle(5f));

静态类

静态类只能包含静态成员,不能被实例化

1
2
3
4
5
6
7
8
static class Math
{
public static float PI = 3.14f;
public static float Sqrt(float x)
{
return x * x;
}
}

使用:

1
2
Console.WriteLine(Math.PI);
Console.WriteLine(Math.Sqrt(3));

静态构造函数

静态构造函数在类被第一次使用时调用,且只调用一次

1
2
3
4
5
6
7
8
9
class Person
{
public string str = "123";

static Person()
{
Console.WriteLine("静态构造函数被调用");
}
}

使用:

1
2
3
4
Person p = new Person();
Console.WriteLine(p.str); // 只会在这里输出:静态构造函数被调用
Console.WriteLine(p.str);
Console.WriteLine(p.str);

拓展方法

拓展方法可以扩展已有的非静态类,使其拥有新的方法

1
2
3
4
5
6
7
static class Tools
{
public static void SpeakValue(this int value)
{
Console.WriteLine(value);
}
}

比如这里的SpeakValue方法,可以扩展int类型,使其拥有SpeakValue方法

1
2
int a = 10;
a.SpeakValue();

运算符重载

重新定义一些运算符的处理逻辑

条件运算符重载需要成对出现,如重载<,那么必须重载>

不可重载的运算符:||,&&,[],(),.,?,:,=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Tools
{
public int x;
public int y;

// 运算符重载
public static Tools operator +(Tools t1, Tools t2)
{
Tools t = new Tools();
t.x = t1.x + t2.x;
t.y = t1.y + t2.y;
return t;
}
// 同一个运算符可以多次重载
public static Tools operator +(Tools t1, int v)
{
Tools t = new Tools();
t.x = t1.x + v;
t.y = t1.y + v;
return t;
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tools tool = new Tools();
tool.x = 10;
tool.y = 20;

Tools tool2 = new Tools();
tool2.x = 30;
tool2.y = 40;

Tools tool3 = tool + tool2;
Console.WriteLine(tool3.x);
Console.WriteLine(tool3.y);

Tools tool4 = tool + 100;
Console.WriteLine(tool4.x);
Console.WriteLine(tool4.y);

内部类(嵌套类)

在一个类的内部再声明一个类

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

}
}
}

使用:

1
2
3
Person p = new Person();
p.body = new Person.Body();
p.body.leftArm = new Person.Body.Arm();

分布类

将一个类拆分成多个部分,但实际上还是一个类

分布类要用partial关键字声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
partial class Student
{
public string name;
public int num;

// 分布方法要写在分布类中
partial void SpeakValue()
{
Console.WriteLine("我是分布方法");
}

}

partial class Student
{
public string sex;
// 分布方法
partial void SpeakValue();
}

使用:

1
2
3
4
Student s = new Student();
s.name = "张三";
s.num = 10001;
s.sex = "男";

继承

继承是面向对象编程中的一种机制,
它允许一个类(子类)继承另一个类(父类)的属性和方法,它也可以重写父类的方法,
也可以自己添加新的方法,从而实现代码的复用和扩展

子类继承父类,子类只能继承一个父类,子类可以继承父类的父类

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
public string name;
public string sex;
}
class Stuedent : Person
{
public int num;
public void Speak()
{
Console.WriteLine("我是{0},{1},我的学号是{2}", name, sex, num);
}
}

使用:

1
2
3
4
5
Stuedent s = new Stuedent();
s.name = "张三";
s.sex = "男";
s.num = 1001;
s.Speak();

继承中的构造函数

子类构造函数调用时,会先调用父类的无参构造函数,
若无参构造函数被有参构造函数顶替,会报错,再调用自己的构造函数

子类可以用base关键字调用父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class GameObject
{
public GameObject(int i)
{
Console.WriteLine("游戏对象构造函数");
}
}
class Player : GameObject
{
// 通过base指定子类调用父类的构造函数
public Player(int i): base(i)
{
Console.WriteLine("玩家构造函数");
}
public void PlayerAtk()
{
Console.WriteLine("玩家攻击");
}
}

使用:

1
Player player = new Player(1); // 输出:游戏对象构造函数 玩家构造函数

密封类

密封类不能被继承,使用sealed关键字声明

1
2
3
4
5
6
7
8
9
class Father
{

}
// Son类无法再被继承
sealed class Son : Father
{

}

多态

多态是指同一个行为具有多个不同表现形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Father
{
public void Speak()
{
Console.WriteLine("父亲说话");
}
}
class Son : Father
{
// 子类创建同名方法要用new关键字
public new void Speak()
{
Console.WriteLine("儿子说话");
}
}

...

Father f = new Son();
f.Speak(); // 输出:父亲说话

以上代码会调用Father中的Speak方法

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
class GameObject
{
public string name;
public GameObject(string name)
{
this.name = name;
}
// 虚函数
public virtual void Speak()
{
Console.WriteLine("对象说话");
}
}
class Player : GameObject
{
public Player(string name) : base(name)
{

}
// 重写父类的虚函数
public override void Speak()
{
// 用base调用父类的函数
base.Speak();
Console.WriteLine("玩家说话");
}
}

...

GameObject g = new Player("123");
g.Speak(); // 输出:对象说话 玩家说话

以上代码会调用Player中的Speak方法

抽象类与抽象方法

抽象类不能被实例化,只能被继承

抽象类中可以包含抽象方法,也可以包含普通方法;
继承抽象类,子类必须实现抽象类中的所有抽象方法,否则子类也必须声明为抽象类

抽象方法没有方法体,只能声明,不能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Thing
{
public string name;
// 抽象方法,没有方法体
public abstract void Show();
}

class Water : Thing
{
// 实现抽象方法
public override void Show()
{
Console.WriteLine("我是水");
}
}

使用:

1
2
Thing t = new Water();
t.Show();

密封方法

密封方法不能被重写,使用sealed关键字声明

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
abstract class Animal
{
public string name;
public abstract void Eat();
public virtual void Speak()
{
Console.WriteLine("动物在说话");
}
}
class Person : Animal
{
// 用sealed方法修饰的方法不能被重写
public sealed override void Eat()
{

}
public override void Speak()
{
Console.WriteLine("人在说话");
}
}

class OtherPerson : Person
{
// 不能重写
//public override void Eat()
//{

//}
}

接口

  • 接口不包含成员变量
  • 只包含方法、属性、事件、索引器
  • 成员不能被实现
  • 接口可以不写访问修饰符,不能是私有的
  • 接口继承接口
  • 类实现接口,实现接口中的所有成员
    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
    46
    47
    48
    49
    50
    51
    52
    interface IFly
    {
    // 方法
    void Fly();
    string Name
    {
    get;
    set;
    }
    int this[int index]
    {
    get;
    set;
    }
    event Action Event;
    }

    // 类实现
    class Bird : IFly
    {
    public int this[int index]
    {
    get
    {
    return 0;
    }
    set
    {

    }
    }

    public string Name
    {
    get
    {
    return "Bird";
    }
    set
    {

    }
    }

    public event Action Event;

    // 可以为接口中的方法加上virtual关键字,让子类也能重写
    public virtual void Fly()
    {
    Console.WriteLine("Bird fly");
    }
    }
    使用:
    1
    2
    IFly f = new Bird();
    Console.WriteLine(f.Name);

显式实现接口

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

class Player : IAtk, ISuperAtk
{
// 显式实现
void IAtk.Atk()
{
Console.WriteLine("普通攻击");
}
void ISuperAtk.Atk()
{
Console.WriteLine("超级攻击");
}
}

使用:

1
2
3
Player p = new Player();
(p as IAtk).Atk(); // 调用接口方法要将其进行转换
(p as ISuperAtk).Atk();

万物之父-object

object是所有类的基类,所有类都继承自object

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

}
class Son : Father
{

}

...

Father f = new Son();
if (f is Son)
{
Console.WriteLine("f is Son");
}

object o = new Son();
object o2 = new Father();

装箱/拆箱

object类型可以存储任何类型的值,但存储的值需要装箱拆箱

1
2
3
4
// 装箱:将值类型转换为引用类型
object o3 = 1;
// 拆箱:将引用类型转换为值类型
int i = (int)o3; // object转为其他类型要强转

object类常用方法

Equals

判断两个类型是否相等(比较引用)

1
2
3
4
5
Console.WriteLine("Equals: " + Object.Equals(1, 1));

Test t = new Test();
Test t2 = new Test();
Console.WriteLine("Equals: " + Object.Equals(t, t2));

GetType

获取当前对象的类型

1
Console.WriteLine("GetType: " + t.GetType());

MemberwiseClone

浅拷贝,克隆对象,新对象的引用类型变量还是指向同一个,值类型变量是独立的

getHashCode

获取对象的哈希值,对象有唯一的编码,极少情况下,不同对象的哈希码可能相同

ToString

获取对象的字符串表示

ReferenceEquals

判断两个对象是否引用同一个对象(比较引用)

命名空间

命名空间用于组织代码,避免命名冲突

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
using Game2;

namespace Game
{
class GameObject
{

}
}

namespace Game2
{
class Game123
{

}
}

namespace LS63
{
internal class Program
{
static void Main(string[] args)
{
// 其他命名空间的类要指明出处,或者直接使用using
Game.GameObject o = new Game.GameObject();
Game123 g = new Game123();
}
}
}

string类常用方法

指定位置获取

1
2
string str = "Hello World";
Console.WriteLine(str[0]);

拼接

1
2
Console.WriteLine(str + " C#");
Console.WriteLine(string.Format("{0}123", " C#"));

正向查找

1
2
Console.WriteLine(str.IndexOf("World"));
// 没找到会返回 -1

反向查找

1
Console.WriteLine(str.LastIndexOf("l"));

移除指定位置的字符

1
2
3
Console.WriteLine(str.Remove(5, 1)); // 第二个参数为删除的字符个数
// 到末尾则不用填第二个参数
Console.WriteLine(str.Remove(5));

替换

1
Console.WriteLine(str.Replace("World", "C#"));

大小写转换

1
2
Console.WriteLine(str.ToUpper()); // 转大写
Console.WriteLine(str.ToLower()); // 转小写

截取

1
2
Console.WriteLine(str.Substring(6, 5)); // 起始位置,长度(注意不能越界)
Console.WriteLine(str.Substring(3)); // 起始位置,到末尾

分割

1
2
3
4
5
6
str = "1 2 3 4 5 6 7";
string[] str1 = str.Split(' '); // 按空格分割
for (int i = 0; i < str1.Length; i++)
{
Console.WriteLine(str1[i]);
}

StringBuilder

利用StringBuilder,可以修改字符串而不创建新对象

要引用命名空间才能使用

1
using System.Text;

创建

1
StringBuilder sb = new StringBuilder("12312456664");

容量

StringBuilder是有容量的,当字符串长度超过容量时,会自动扩容

1
Console.WriteLine(sb.Capacity); // 16

增加

1
2
3
4
5
sb.Append("1234567890");
Console.WriteLine(sb);
Console.WriteLine(sb.Capacity); // 32
sb.AppendFormat("{0}{1}", "abc", "def");
Console.WriteLine(sb);

插入

1
2
sb.Insert(0, "abc");
Console.WriteLine(sb);

1
2
sb.Remove(0, 3);
Console.WriteLine(sb);

1
Console.WriteLine(sb[0]);

1
2
sb[0] = 'A';
Console.WriteLine(sb);

替换

1
2
sb.Replace("a", "大");
Console.WriteLine(sb);

重新赋值

1
2
3
sb.Clear();
sb.Append("1234567890");
Console.WriteLine(sb);

结构体 vs 类

  1. 结构体是值类型,类是引用类型
  2. 结构体不能继承,类可以继承
  3. 结构体存在上,类存在
  4. 结构体不能初始化成员变量,类可以
  5. 结构体不能使用protected修饰符,类可以
  6. 结构体不能声明无参构造函数,类可以
  7. 结构体不能声明析构函数,类可以
  8. 结构体声明有参构造函数时,无参构造函数不会丢失
  9. 结构体需要在构造函数中初始化所有成员变量,类随意
  10. 结构体不能被static修饰,类可以
  11. 结构体不能在内部声明与它同名的结构体变量,类可以
  • 多态、继承,考虑类
  • 数据集合,考虑结构体

接口 vs 抽象类

  1. 抽象类可以包含字段属性方法事件构造函数析构函数等,接口只能包含方法属性事件索引器
  2. 抽象类可以包含实现的方法,接口只能包含抽象方法(不能实现)
  3. 抽象类可以有构造函数,接口没有
  4. 抽象类单一继承,而接口可以实现多个
  • 表示对象用抽象类,行为拓展用接口
  • 比如不同对象有相同的行为,可以用接口实现

其他数据结构类

ArrayList

ArrayList本质是object类型的数组

C#已经封装好了,有增删查改的方法

使用要引用命名空间

1
using System.Collections;

声明

1
ArrayList list = new ArrayList();

1
2
3
4
5
6
7
8
list.Add(1);
list.Add("hello");
list.Add(1.1);
list.Insert(0, "world"); // 在第一个位置插入

ArrayList list2 = new ArrayList();
list2.Add(2);
list.AddRange(list2);

1
2
3
list.Remove(1); // 删除第一个1
list.RemoveAt(0); // 删除第一个元素
list2.Clear(); // 清空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Console.WriteLine(list[0]);

if (list.Contains(1.1))
{
Console.WriteLine("包含");
}

// 正向查找,返回索引
// 找不到返回-1
int index = list.IndexOf("123");
Console.WriteLine(index);

// 反向查找
index = list.LastIndexOf("123");
Console.WriteLine(index);

1
2
list[0] = "123";
Console.WriteLine(list[0]);

遍历

1
2
3
4
5
6
7
8
9
10
11
12
Console.WriteLine(list.Count); // 数组长度
Console.WriteLine(list.Capacity); // 容量

for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}

foreach (var item in list)
{
Console.WriteLine(item);
}

装箱拆箱

因为ArrayListobject类型的数组,它在进行数据存储和读取中肯定存在装箱拆箱

Stack

Stack是一个object类型的数组,但封装了特殊的存储规则

是栈存储容器,先进后出

使用要引用命名空间

1
using System.Collections;

声明

1
Stack stack = new Stack();

增(压栈)

1
2
3
4
5
stack.Push(1);
stack.Push("123");
stack.Push(1.1);
stack.Push(true);
stack.Push('a');

1
2
3
4
5
object v = stack.Pop();
Console.WriteLine(v);

v = stack.Pop();
Console.WriteLine(v);

1
2
3
4
5
6
7
// 栈无法查看指定位置的内容,只能查看栈顶的内容
Console.WriteLine(stack.Peek()); // Peek不弹出,只查看
// 查看元素是否存在
if (stack.Contains(1))
{
Console.WriteLine("存在");
}

栈没有办法改变其中元素,要么清空再次压栈、弹出

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
Console.WriteLine(stack.Count); // 长度
// 栈不能用for遍历,只能用foreach
foreach (var item in stack)
{
Console.WriteLine(item);
}

// 可以将栈转为object数组,再用for遍历
object[] arr = stack.ToArray();
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}

循环弹栈

1
2
3
4
while (stack.Count > 0)
{
Console.WriteLine(stack.Pop());
}

Queue

Queue是队列,本质也是object数组,遵循先进先出

使用要引用命名空间

1
using System.Collections;

声明

1
Queue q = new Queue();

1
2
3
4
q.Enqueue("a");
q.Enqueue("b");
q.Enqueue(1);
q.Enqueue(2.3f);

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
int l = q.Count; // 获取长度
Console.WriteLine(l);
foreach (var item in q)
{
Console.WriteLine(item);
}

// 如果用for循环的话,可以将其转换为object数组
object[] arr = q.ToArray();
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}

循环出队

1
2
3
4
5
while (q.Count > 0)
{
object o2 = q.Dequeue(); // 取出第一个元素,并删除
Console.WriteLine(o2);
}

1
2
object o = q.Dequeue(); // 取出第一个元素,并删除
Console.WriteLine(o);

1
2
3
4
5
Console.WriteLine(q.Peek()); // 取出第一个元素,但不删除
if (q.Contains(1))
{
Console.WriteLine("包含1");
}

只能清除,不能改变某一个元素

1
q.Clear();

HashTable

HashTable是散列表,是它使用键值对来存储数据,键是唯一的,值可以是重复的。

主要作用是提高数据查找效率,使用键来访问元素

使用要引用命名空间

1
using System.Collections;

声明

1
Hashtable ht = new Hashtable();

添加元素

1
2
3
4
5
6
ht.Add("1", "张三");
ht.Add("123", "李四");
ht.Add(1, "王五");
ht.Add(123, "赵六");
// 不能出现相同的键
// ht.Add("1", "张三"); // 报错

删除元素

1
2
3
4
ht.Remove("1"); // 通过键去删除
// 删除不存在的键也不会出错
// ht.Remove("2"); // 不会报错
//ht.Clear(); // 清空所有元素

查找元素

找不到会返回空

1
2
3
4
5
6
7
8
Console.WriteLine(ht[123]);
Console.WriteLine(ht[4]); // 返回空
Console.WriteLine(ht[1]);

// 是否存在
Console.WriteLine(ht.Contains("123")); // true
Console.WriteLine(ht.ContainsKey(123)); // true
Console.WriteLine(ht.ContainsValue("赵六")); // true

1
ht[123] = "赵六六";// 只能改键对应的内容,但不能改变键本身

遍历

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
// 遍历键
Console.WriteLine(ht.Count); // 获取键的数量
foreach (var item in ht.Keys)
{
Console.Write(item);
Console.WriteLine(ht[item]); // 通过键获取值
}

// 遍历值
foreach (var item in ht.Values)
{
Console.WriteLine(item);
}

// 遍历键值对
// DictionaryEntry是一个结构体,包含两个属性,Key和Value
foreach (DictionaryEntry item in ht)
{
Console.WriteLine(item.Key + " " + item.Value);
}

// 迭代器遍历
IDictionaryEnumerator ie = ht.GetEnumerator();
bool hasNext = ie.MoveNext();
while (hasNext)
{
Console.WriteLine(ie.Key + " " + ie.Value);
hasNext = ie.MoveNext();
}

装箱拆箱

因为Hashtable只能存储object类型的数据,所以需要装箱拆箱

泛型

泛型实现了类型参数化,达到代码重用

泛型类

1
2
3
4
class GenericClass<T>
{
public T value;
}

使用:

1
2
3
4
5
6
7
GenericClass<int> gc = new GenericClass<int>();
gc.value = 10;
Console.WriteLine(gc.value);

GenericClass<string> gc1 = new GenericClass<string>();
gc1.value = "hello";
Console.WriteLine(gc1.value);

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
interface IGenericInterface<T>
{
T Value { get; set; }
}

class TestClass : IGenericInterface<int>
{
public int Value {
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}

泛型方法

1
2
3
4
5
6
7
class GenericMethod
{
public void Show<T>(T value)
{
Console.WriteLine(value);
}
}

使用:

1
2
3
GenericMethod gm = new GenericMethod();
gm.Show<int>(10); // 尖括号里的东西可以省略,编译器会自动判断
gm.Show<string>("hello");

泛型类的泛型方法

1
2
3
4
5
6
7
8
class GenericClassTest<T>
{
public T value;
public void Show<V>(V value)
{
Console.WriteLine(value);
}
}

使用:

1
2
3
4
GenericClassTest<int> gct = new GenericClassTest<int>();
gct.Show<string>("hello");
gct.value = 10;
Console.WriteLine(gct.value);

泛型约束

泛型约束让泛型的类型有一定的约束

  • where T : class 引用类型
  • where T : struct 值类型
  • where T : new() 公共无参构造函数
  • where T : <基类> 类本身或者某个类
  • where T : <接口> 接口的派生类型
  • where T : U 其他泛型或者派生类型

struct

1
2
3
4
5
6
7
8
class Test1<T> where T : struct
{
public T value;
public void TestFun<K>() where K : struct
{
K k = default(K);
}
}

使用:

1
2
3
Test1<int> test1 = new Test1<int>();
test1.value = 1;
test1.TestFun<double>();

class

1
2
3
4
5
6
7
8
class Test2<T> where T : class
{
public T value;
public void TestFun<K>() where K : class
{
K k = null;
}
}

使用:

1
2
3
Test2<string> test2 = new Test2<string>();
test2.value = "1";
test2.TestFun<string>();

new()

1
2
3
4
5
6
7
8
9
10
11
12
class Test1
{

}
class Test3<T> where T : new()
{
public T value;
public void TestFun<K>() where K : new()
{
K k = new K();
}
}

使用:

1
2
Test3<Test1> test3 = new Test3<Test1>();
test3.value = new Test1();

类约束

1
2
3
4
5
6
7
8
9
10
11
12
class Test2 : Test1
{

}
class Test4<T> where T : Test1
{
public T value;
public void TestFun<K>() where K : Test1
{
K k = default(K);
}
}

使用:

1
2
Test4<Test2> test4 = new Test4<Test2>();
test4.value = new Test2();

接口约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ITest
{

}
class Test3 : ITest
{

}
class Test5<T> where T : ITest
{
public T value;
public void TestFun<K>() where K : ITest
{
K k = default(K);
}
}

使用:

1
2
Test5<Test3> test5 = new Test5<Test3>();
test5.value = new Test3();

其他泛型约束

1
2
3
4
5
6
7
8
class Test6<T, K> where T : K
{
public T value;
public void TestFun<T, K>() where T : K
{

}
}

使用:

1
2
Test6<Test3, ITest> test6 = new Test6<Test3, ITest>();
test6.value = new Test3();

泛型数据结构类

List

List是可变类型的泛型数组,也为我们提供了增删查改的方法

声明

1
List<int> list = new List<int>();

1
2
3
4
5
6
list.Add(1);
list.Add(2);
list.Add(346533);
list.Add(4);
list.Add(520);
list.Insert(0, 65536);

1
2
3
4
list.Remove(1);
list.RemoveAt(2);
// 清空
list.Clear();

1
2
3
4
5
6
Console.WriteLine(list[0]);
Console.WriteLine(list.Contains(2));
// 正向查找
Console.WriteLine(list.IndexOf(2));
// 反向查找
Console.WriteLine(list.LastIndexOf(520));

1
2
list[0] = 520;
Console.WriteLine(list[0]);

遍历

1
2
3
4
5
6
7
8
9
10
11
12
Console.WriteLine(list.Count);
Console.WriteLine(list.Capacity);
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}


foreach (var item in list)
{
Console.WriteLine(item);
}

Dictionary

Dictionary可以理解为泛型类型的HashTable,同样也是键值对

声明

1
Dictionary<string, int> dic = new Dictionary<string, int>();

1
2
3
4
5
dic.Add("a", 1);
dic.Add("b", 2);
dic.Add("c", 3);
dic.Add("d", 4);
dic.Add("e", 5);

1
2
3
dic.Remove("a");
// 清空
dic.Clear();

1
2
3
4
5
6
7
8
9
10
11
Console.WriteLine(dic["b"]); // 不存在会报错

if (dic.ContainsKey("b"))
{
Console.WriteLine(dic["b"]);
}

if (dic.ContainsValue(2))
{
Console.WriteLine("存在值为2的键");
}

1
dic["c"] = 6;

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
foreach (var item in dic)
{
Console.WriteLine(item.Key + " " + item.Value);
}


foreach (var item in dic.Keys)
{
Console.Write(item + " ");
Console.WriteLine(dic[item]);
}


foreach (var item in dic.Values)
{
Console.WriteLine(item);
}


foreach (KeyValuePair<string, int> item in dic)
{
Console.WriteLine(item.Key + " " + item.Value);
}

链表

简单实现

原理:

  • 链表由节点组成,每个节点包含一个值和一个指向下一个节点的引用
  • 当链表为空时,头节点和尾节点都为null
  • 当链表不为空时,头节点指向第一个节点,尾节点指向最后一个节点
  • 当向链表中添加节点时,将新节点添加到尾节点后面,并将尾节点指向新节点
  • 当从链表中删除节点时,将前一个节点的引用指向下一个节点,并将尾节点指向最后一个节点
  • 当链表只有一个节点时,删除节点后,头节点和尾节点都为null
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
46
47
48
49
50
51
52
53
// 链表节点
class LinkedNode<T>
{
public T value;
public LinkedNode<T> next;

public LinkedNode(T value)
{
this.value = value;
}
}

class LinkedList<T>
{
public LinkedNode<T> head;
public LinkedNode<T> tail;

public void Add(T value)
{
LinkedNode<T> node = new LinkedNode<T>(value);
if (head == null)
{
head = node;
tail = node;
}
else
{
tail.next = node;
tail = node;
}
}

public void Remove(T value)
{
if (head == null) return;
if (head.value.Equals(value))
{
head = head.next;
if (head == null) tail = null;
return;
}

LinkedNode<T> prev = head;
while (prev.next != null)
{
if (prev.next.value.Equals(value))
{
prev.next = prev.next.next;
break;
}
}
}
}

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
LinkedNode<int> node = new LinkedNode<int>(1);
LinkedNode<int> node2 = new LinkedNode<int>(2);
node.next = node2;
Console.WriteLine(node.value);

LinkedList<int> list = new LinkedList<int>();
list.Add(1);
list.Add(2);
list.Add(3);
LinkedNode<int> node3 = list.head;

// 遍历
while (node3 != null)
{
Console.WriteLine(node3.value);
node3 = node3.next;
}
list.Remove(2);
node3 = list.head;
while (node3 != null)
{
Console.WriteLine(node3.value);
node3 = node3.next;
}

LinkedList

LinkedList是一个封装好的类,本质是一个可变类型的泛型双向链表

声明

1
2
LinkedList<int> list = new LinkedList<int>();
LinkedList<String> list2 = new LinkedList<string>();

添加元素

1
2
3
4
5
6
7
list.AddFirst(1); // 往链表头部添加元素
list.AddLast(2); // 往链表尾部添加元素
list.AddLast(3);
list.AddLast(4);
list.AddLast(5);
list.AddLast(6);
list.AddLast(7);

删除元素

1
2
3
4
list.Remove(2); // 删除指定元素,但不能通过位置去删除
list.RemoveFirst(); // 删除第一个元素
list.RemoveLast(); // 删除最后一个元素
// list.Clear(); // 清空链表

查找元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 头节点
LinkedListNode<int> head = list.First;
Console.WriteLine(head.Value);
// 尾节点
LinkedListNode<int> tail = list.Last;
Console.WriteLine(tail.Value);

// 找指定位置的节点
LinkedListNode<int> node = list.Find(4); // 找不到则返回空
Console.WriteLine(node.Value);
// 是否存在
if (list.Contains(4))
{
Console.WriteLine("存在");
}

1
2
3
// 通过节点去修改
node.Value = 100;
Console.WriteLine(node.Value);

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
foreach (var item in list)
{
Console.WriteLine(item);
}

// 从头到尾
LinkedListNode<int> current = list.First;
while (current != null)
{
Console.WriteLine(current.Value);
current = current.Next;
}

// 从尾到头
current = list.Last;
while (current != null)
{
Console.WriteLine(current.Value);
current = current.Previous;
}

泛型栈与泛型队列

Stack<T>是一个封装好的类,本质是一个可变类型的泛型

Queue<T>是一个封装好的类,本质是一个可变类型的泛型队列

1
2
3
4
5
6
7
8
9
10
11
Stack<int> stack = new Stack<int>();
Queue<int> queue = new Queue<int>();

// 入栈
stack.Push(1);
// 出栈
stack.Pop();
// 入队
queue.Enqueue(2);
// 出队
queue.Dequeue();

数据结构

常用数据结构

数组、栈、队列、链表、树、图等等

线性表

线性表是具有相同数据类型的n个数据元素的有限序列

顺序存储

  • 用一组地址连续的存储单元依次存储线性表中的数据元素
  • 数组、栈、队列、列表都是顺序存储

链式存储

  • 用一组任意的存储单元存储线性表中的数据元素
  • 单向链表、双向链表、循环链表都是链式存储

委托

委托是函数的容器,可以理解为表示函数方法的变量类型,
是用来储存、传递函数方法

基本语法

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

定义委托

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
// 访问修饰符可以省略,默认为public
// 这里只是定义了规则,没有使用
public delegate void MyDelegate(string str);
// 委托不能重名

// 返回值为int,参数为int的委托
public delegate int MyDelegate2(int a, int b);

// 返回值为void,参数为无的委托
public delegate void MyDelegate3();

class Test
{
// 委托可以在类中使用
public MyDelegate myDelegate;
public MyDelegate2 myDelegate2;

public void TestMethod(MyDelegate myDelegate, MyDelegate2 myDelegate2)
{
// 可以先处理一些逻辑
// 再调用委托
int i = 1;
i *= 2;
i += 3;
// myDelegate2(i, 2);
}
}

使用委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 类中的方法
static void SayHello(string str)
{
Console.WriteLine(str);
}
static int Add(int a, int b)
{
return a + b;
}

...

// 委托指向方法
MyDelegate myDelegate = new MyDelegate(SayHello);
myDelegate("Hello World");

// 也可以直接指向函数,但不要写括号
MyDelegate2 add = Add;
Console.WriteLine(add(1, 2));

委托可以存多个函数

委托可以存储多个函数,当调用委托时,会依次调用存储的函数

1
2
3
4
5
6
7
8
9
10
static void Say()
{
Console.WriteLine("Hello World");
}

...

MyDelegate3 myDelegate3 = Say;
myDelegate3 += Say;
myDelegate3(); // 调用两次函数

系统定义好的委托

系统定义好的委托,可以直接使用,不需要自己定义

Action

Action是一个委托,用于表示一个无返回值的方法,
参数类型和数量不限

1
2
3
// Action<T> 委托,无返回值,参数为T
Action action = Say;
action();

Func

Func是一个委托,用于表示一个有返回值的方法,
参数类型和数量不限

1
2
3
// Func<T, TResult> 委托,有返回值,参数为T,返回值为TResult
// Func<>有17个重载,可以指定参数个数和返回值类型
Func<int, int, int> func = Add;

事件

事件是一种特殊的委托,基于委托存在,事件是委托的安全包裹,让委托更具有安全性

区别在于:不能在类外部赋值,也不能在类外调用

基本语法

1
访问修饰符 event 委托类型 事件名称

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test
{
// 委托
public Action fun;
// 事件
public event Action fun2;

public Test()
{
fun = TestFun;
fun();
fun2 = TestFun;
fun2();
}

private void TestFun()
{
Console.WriteLine("1");
}
}

使用

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

t.fun(); // 可以调用
// t.fun2(); // 报错,事件不能在外部调用

t.fun = null; // 委托可以在外部赋值
// t.fun2 = null; // 报错,事件不能在外部赋值,但可以加减

匿名函数

匿名函数是一种没有名字的函数,使用上主要配合委托和事件

基本语法

1
delegate (参数列表) {函数体}

在函数中传递委托参数时、委托或事件赋值时可以使用匿名函数

使用

无参无返

1
2
3
4
5
Action a = delegate ()
{
Console.WriteLine("匿名函数");
};
a();

有参无返

1
2
3
4
5
Action<string> b = delegate (string str)
{
Console.WriteLine(str);
};
b("匿名函数");

有参有返

1
2
3
4
5
Func<int, int, int> c = delegate (int a, int b)
{
return a + b;
};
Console.WriteLine(c(1, 2));

无参有返

1
2
3
4
5
Func<int> d = delegate ()
{
return 1;
};
Console.WriteLine(d());

作为参数传递/返回值

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 Test
{
public Action a;
// 作为参数传递
public void Do(int a, Action fun)
{
Console.WriteLine(a);
fun();
}

// 作为返回值
public Action GetFun()
{
return delegate()
{
Console.WriteLine("匿名函数");
};
}
}

...

Test t = new Test();
t.Do(1, delegate ()
{
Console.WriteLine("匿名函数");
});

// 作为返回值
t.GetFun()();

匿名函数的缺点

添加的到委托或者事件中后,由于没有名字记录,所以无法单独移除

lambda表达式

lambda表达式可以理解为匿名函数的简写

基本语法

1
(参数列表) => {函数体}

使用

无参无返

1
2
3
4
5
Action action = () => 
{
Console.WriteLine("hello");
};
action();

无参有返

1
2
3
4
5
6
Func<int> func = () =>
{
Console.WriteLine("hello");
return 0;
};
func();

有参无返

1
2
3
4
5
Action<int> action2 = (int a) =>
{
Console.WriteLine("hello,{0}", a);
};
action2(1);

有参有返

1
2
3
4
5
6
Func<int, int, int> func2 = (int a, int b) =>
{
Console.WriteLine("hello");
return a + b;
};
Console.WriteLine(func2(1, 2));

缺点

缺点与匿名函数一样

闭包

闭包是指一个函数能够访问并操作其语法作用域外部的变量

内层函数可以引用外层函数的局部变量,即便外层函数执行完后,这些变量依然可以被内层函数访问

但是该变量不是变量创建时的值,而是最终经过一系列操作后形成的最终值

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test
{
public event Action a;
public Test()
{
int value = 10;
// 闭包
a = () =>
{
Console.WriteLine(value);
};
}
}

List排序

自带的排序方法

1
2
3
4
5
6
7
8
9
10
11
12
List<int> list = new List<int>();
list.Add(2);
list.Add(1);
list.Add(7);
list.Add(5);
list.Add(4);

list.Sort();
foreach (var item in list)
{
Console.WriteLine(item);
}

自定义排序方法

自定义排序要重写比较方法

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
// 让自定义的类可以比较,需要实现IComparable接口,并重写CompareTo方法
class Item : IComparable<Item>
{
public int money;
public Item(int money)
{
this.money = money;
}

// 重写比较方法
public int CompareTo(Item? other)
{
// 返回值含义:
// 如果小于0,则放在传入对象的前面;
// 如果等于0,则保持顺序不变;
// 如果大于0,则放在传入对象的后面
if (this.money < other.money)
{
return -1;
}
else
{
return 1;
}

}
}

...

// 实现
List<Item> list2 = new List<Item>();
list2.Add(new Item(23));
list2.Add(new Item(12));
list2.Add(new Item(74));
list2.Add(new Item(55));
list2.Add(new Item(47));

list2.Sort();
foreach (var item in list2)
{
Console.WriteLine(item.money);
}

通过委托排序

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
class Weapon
{
public int id;
public Weapon(int id)
{
this.id = id;
}
}

...

List<Weapon> list3 = new List<Weapon>();
list3.Add(new Weapon(2));
list3.Add(new Weapon(1));
list3.Add(new Weapon(7));
list3.Add(new Weapon(5));
list3.Add(new Weapon(4));
list3.Add(new Weapon(3));

list3.Sort(delegate (Weapon a, Weapon b)
{
if (a.id > b.id)
{
return 1;
}
else
{
return -1;
}
});
foreach (var item in list3)
{
Console.WriteLine(item.id);
}

协变、逆变

  • 协变允许隐式转换,里氏替换
  • 逆变父类可以装子类,但子类不能装父类
  • 协变:out
  • 逆变:in

协变和逆变用于修饰泛型,只有泛型接口泛型委托可以使用

作用

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
// 返回值和参数
// 用out修饰的泛型,只能作为返回值,不能作为参数
delegate T Test<out T>();

// 用in修饰的泛型,只能作为参数,不能作为返回值
delegate void Test2<in T>(T t);

// 结合里氏替换原则
class Father
{

}
class Son : Father
{

}

...

// 协变
Test<Son> test = () =>
{
return new Son();
};
Test<Father> test2 = test;

// 逆变
Test2<Father> test3 = (f) =>
{

};
Test2<Son> test4 = test3;

线程

进程与线程

  • 进程是进程是操作系统进行资源分配和调度的基本单位,是能独立运行最小单位
    每个进程都有自己的地址空间,包括代码段、数据数据段和堆栈段。
    进程之间是相互独立的,它们之间可以通过进程间通信(IPC)机制进行通信
  • 线程是进程内的执行单元,是CPU调度的基本单位
    一个进程可以包含多个线程,它们共享进程的地址。
    每个线程都有自己的执行栈和寄存器状态,但它们共享进程的地址空间和资源。

多线程

多线程是指在一个进程中同时运行多个线程,每个线程可以执行不同的任务

多线程可以提高程序的并发性能,使得程序可以同时处理多个任务,从而提高程序的响应速度和吞吐量。

可以使用多线程处理一些复杂的计算任务,以避免阻塞主线程,提高程序的响应速度。

语法

C#的线程类Thread

声明

声明一个新线程,注意线程执行的代码需要封装到一个函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object obj = new object();

static void DoWork()
{
// 线程执行的代码
while (true)
{
lock (obj)
{
Console.SetCursorPosition(10, 5);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("H");
Thread.Sleep(1000);
}
}
}

...

Thread t = new Thread(DoWork);

启动线程

1
t.Start();

设置为后台线程

后台线程在主线程结束时自动终止,而前台线程需要等待所有前台线程结束后才终止

注意,如果后台线程是死循环,那么主线程会一直等待,导致程序无法正常退出

1
t.IsBackground = true;

关闭释放线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void DoSomething()
{
while (isRunning)
{
Console.WriteLine("执行!");
}
}

...

static bool isRunning = true;

...

Thread t2 = new Thread(DoSomething);
t2.Start();
t2.IsBackground = true;
isRunning = false;

终止线程

1
2
// t.Abort(); // 在控制台中并不能使用
t = null;

休眠

单位为毫秒

1
Thread.Sleep(1000); // 休眠1秒

线程之间共享数据

多线程使用的内存是共享的,因此需要小心处理线程之间的数据共享问题,以避免数据竞争和死锁等问题

可以使用锁Lock来保护共享数据,确保同一时间只有一个线程可以访问共享数据

关键字lock

1
2
3
4
5
6
7
8
9
10
// lock (obj)括号中填引用类型的对象
while (true)
{
lock (obj)
{
Console.SetCursorPosition(0, 0);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("W");
}
}

预处理指令

编译器

编译器将源语言翻译成目标语言,如将C#Java等高级语言翻译成机器语言

预处理指令

预处理指令是编译器在编译程序之前会先执行的一段指令

都以#开头

它不是语句,所以不以;结束

常用指令

  • #define 定义一个符号

  • #undef 取消一个符号,这两个都是写在最前面的,一般配合#if或特性使用

  • #if 判断一个符号是否被定义

  • #else 如果#if的条件不满足,则执行#else后面的代码

  • #elif 如果#if的条件不满足,则执行#elif后面的代码

  • #endif 结束#if的条件判断

  • #warnning 输出一个警告信息

  • #error 输出一个错误信息

1
2
3
#if Unity
Console.WriteLine("Unity");
#endif

反射

程序集

程序集是由编译器编译得到的,供进一步编译执行的中间产物

在Windows系统中,程序集的扩展名是.exe.dll

元数据

元数据是用描述数据的数据,类、函数、变量等都是程序的元数据

反射的概念

反射是程序运行时,动态获取程序集、类型、成员的信息,并动态创建对象、调用方法、访问成员的一种机制

反射的作用

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

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

Type

Type是类的信息类,是反射的基础,它是访问元数据的主要方式,使用Type类可以获取类的所有信息

获取Type对象

1
2
3
4
5
6
7
8
9
int a = 42;
Type type = a.GetType();
Console.WriteLine(type);

Type type2 = typeof(int);
Console.WriteLine(type2);

Type type3 = Type.GetType("System.Int32");
Console.WriteLine(type3);

获得类的程序集

1
Console.WriteLine(type.Assembly);

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

1
2
3
4
5
6
7
8
Type type4 = typeof(Test);
MemberInfo[] memberInfos = type4.GetMembers();

for (int i = 0; i < memberInfos.Length; i++)
{
MemberInfo mi = memberInfos[i];
Console.WriteLine(mi);
}

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

1
2
3
4
5
6
ConstructorInfo[] ctor = type4.GetConstructors();

for (int i = 0; i < ctor.Length; i++)
{
Console.WriteLine(ctor[i]);
}

获取一个构造函数执行

1
2
3
4
5
6
7
8
9
10
11
ConstructorInfo info = type4.GetConstructor(new Type[0]);// 无参构造函数
Test t = info.Invoke(null) as Test;
Console.WriteLine(t.j);

ConstructorInfo info2 = type4.GetConstructor(new Type[] { typeof(int) });// 有参构造函数
Test t2 = info2.Invoke(new object[] { 123 }) as Test;
Console.WriteLine(t2.str);

ConstructorInfo info3 = type4.GetConstructor(new Type[] { typeof(int), typeof(string) });// 有参构造函数
Test t3 = info3.Invoke(new object[] { 123, "abc" }) as Test;
Console.WriteLine(t3.str);

获取类的公共成员变量

1
2
3
4
5
FieldInfo[] fieldInfos = type4.GetFields();
for (int i = 0; i < fieldInfos.Length; i++)
{
Console.WriteLine(fieldInfos[i]);
}

得到指定名称的成员变量

1
2
FieldInfo fi = type4.GetField("j");
Console.WriteLine(fi);

通过反射获取和设置对象的值

1
2
3
4
5
6
7
8
9
Test t4 = new Test();
t4.j = 123;
t4.str = "abc";

FieldInfo fi2 = type4.GetField("j");
Console.WriteLine(fi2.GetValue(t4));

fi2.SetValue(t4, 456);
Console.WriteLine(t4.j);

获得类中的公共成员方法

1
2
3
4
5
6
Type strT = typeof(string);
MethodInfo[] methodInfos = strT.GetMethods();
for (int i = 0; i < methodInfos.Length; i++)
{
Console.WriteLine(methodInfos[i]);
}

获取一个方法

1
2
3
4
5
6
MethodInfo sub = strT.GetMethod("Substring",
new Type[] { typeof(int), typeof(int) });

string str = "Hello World!";
object result = sub.Invoke(str, new object[] { 7, 5 });
Console.WriteLine(result);

其他

获取枚举

  • GetEnumNames
  • GetEnumName

获取事件

  • GetEvents
  • GetEvent

获取接口

  • GetInterfaces
  • GetInterface

获取属性

  • GetProperty
  • GetProperties

Assembly

这是程序集类

  • 主要用来加载其他程序集,加载后获取程序集信息
  • 如果不是自己程序集中的内容,需要先加载程序集,比如dll文件
  • 可以将其视为一种代码仓库,供开发者使用

语法

加载同一个文件下的其他程序集

1
Assembly assembly = Assembly.Load("程序集名称");

加载指定路径下的程序集

1
2
Assembly assembly = Assembly.LoadFrom("路径");
Assembly assembly = Assembly.LoadFile("路径");

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Assembly assembly = Assembly.LoadFrom("...");
Type[] type5 = assembly.GetTypes();

for (int i = 0; i < type5.Length; i++)
{
Console.WriteLine(type5[i]);
}

Console.WriteLine("====================================");

Type program = assembly.GetType("XXX.Program");
MethodInfo[] methodInfos2 = program.GetMethods();

for (int i = 0; i < methodInfos2.Length; i++)
{
Console.WriteLine(methodInfos2[i]);
}

Activator

Activator是反射中创建对象的主要方式,可以快速实例化Type对象

无参构造

1
2
3
Type type21 = typeof(Test);
Test testobj = Activator.CreateInstance(type21) as Test;
Console.WriteLine(testobj.str);

有参构造

1
2
3
4
testobj = Activator.CreateInstance(type21, 123) as Test;
Console.WriteLine(testobj.j);
testobj = Activator.CreateInstance(type21, 123, "abc") as Test;
Console.WriteLine(testobj.str);

特性

特性的概念

  • 特性是一种允许我们向程序集添加元数据的语言结构
  • 用于保持程序结构信息的某种特殊类型的类
  • 可以利用特性类为元数据添加额外的信息
  • 之后可以通过反射来获取这些额外信息

自定义特性

1
2
3
4
5
6
7
8
class TestAttribute : Attribute
{
public string info;
public TestAttribute(string info)
{
this.info = info;
}
}

使用特性

语法

1
[特性名(参数列表)]
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
[Test("这是一个测试特性")]
class TestClass
{
[Test("给变量添加信息")]
public int i;

[Test("给方法添加信息")]
public void TestMethod([Test("给参数添加信息")] int a)
{
}
}

...

TestClass t = new TestClass();
Type type = t.GetType();
if (type.IsDefined(typeof(TestAttribute), false)) // 第一个参数是特性的类型,第二个参数是是否搜索继承链
{
Console.WriteLine("TestClass类有TestAttribute特性");
}
object[] array = type.GetCustomAttributes(true);
for (int i = 0; i < array.Length; i++)
{
if (array[i] is TestAttribute)
{
Console.WriteLine((array[i] as TestAttribute).info);
}
}

限制自定义特性的使用范围

使用AttributeUsage特性来限制自定义特性的使用范围

1
2
3
4
5
6
7
8
9
10
// 第一个参数,特性能用在哪些地方; 第二个参数,是否运行多个特性实例用在同一个目标上; 第三个参数,是否能被子类或重写方法继承
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
class TestSecondAttribute : Attribute
{
public string info;
public TestSecondAttribute(string info)
{
this.info = info;
}
}

系统特性

过时特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestClass2
{
// 第二个参数,true,直接报错; false,编译器发出警告
[Obsolete("该方法已过时,请使用NewSpeak", false)]
public void OldSpeak(string s)
{

}
public void NewSpeak(string s)
{

}

}

调用者信息

CallerMemberName,调用成员的名称;

CallerFilePath,调用成员的文件路径;

CallerLineNumber,调用成员的行号

需要引用using System.Runtime.CompilerServices;

1
2
3
4
5
public void SpeakCaller(string name, [CallerFilePath]string filename = "",
[CallerLineNumber]int line = 0, [CallerMemberName]string member = "")
{

}

条件编译

Conditional特性,用于控制是否编译某个代码块,与预处理指令配合使用

需要引用using System.Diagnostics;

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

...

[Conditional("Fun")]
static void Fun()
{
Console.WriteLine("Fun");
}

...

Fun();

外部Dll包函数特性

DllImport特性,用于调用外部Dll包中的函数

需要引用using System.Runtime.InteropServices;

一般用来调用C或者C++写的函数

迭代器

迭代器有时又称为光标,是程序的软件设计模式

迭代器模式提供了一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部标识

从表现效果上看,是可以在容器对象上遍历访问的接口

不过设计人员无需关心容器对象的内存分配的实现细节,可以用foreach遍历的类,都是实现了迭代器的

标准迭代器实现

要实现一个标准迭代器,需要实现以下接口

  • IEnumerable
  • IEnumerator

foreach的本质

  • 先获取对象的IEnumerator
  • 调用其中的GetEnumerator()方法获取
  • 然后调用MoveNext()方法进行迭代
  • 只要MoveNext()方法返回true,就调用Current属性获取当前元素
  • 然后继续调用MoveNext()方法,直到MoveNext()方法返回false
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
class TestList : IEnumerable, IEnumerator
{
private int[] list;
private int position = -1; // 用来记录当前迭代的位置
public TestList()
{
list = new int[] { 1, 2, 3, 4, 5 };
}

public IEnumerator GetEnumerator()
{
Reset();
return this;
}

public object Current
{
get
{
return list[position];
}
}

public bool MoveNext()
{
position++;
return (position < list.Length);
}

public void Reset()
{
position = -1;
}
}

yield return语法糖实现迭代器

使用yield return语法糖,可以更方便的实现迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestList2 : IEnumerable
{
private int[] list;
public TestList2()
{
list = new int[] { 1,2,3,4,5 };
public IEnumerator GetEnumerator()
{
for (int i = 0; i < list.Length; i++)
{
yield return list[i];
}
}
}

使用迭代器

1
2
3
4
5
TestList testList = new TestList();
foreach (var item in testList)
{
Console.WriteLine(item);
}

特殊语法

var

隐式类型,可以用来表示任意类型的变量

  • 不能用在类中,只能用在方法中
  • var类型必须初始化
    1
    2
    3
    4
    var a = 1;
    var b = "hello";
    Console.WriteLine(a);
    Console.WriteLine(b);

设置对象初始值

在声明对象时,直接用大括号的形式初始化公共成员变量和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
private int money;
public bool sex;
public String Name
{
get;
set;
}
public int Age
{
get;
set;
}
}

...

Person p = new Person { Name = "张三", Age = 18 };

设置集合初始值

在声明集合对象是,可以用大括号初始化集合中的元素

1
2
3
4
5
6
7
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
List<Person> list2 = new List<Person> {
new Person { Name = "张三", Age = 18 },
new Person { Name = "李四", Age = 19 }
};

Dictionary<string, int> dic = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };

匿名类型

var变量可以声明为自定义的匿名类型

1
2
3
var v = new { Name = "张三", Age = 18 };
Console.WriteLine(v.Name);
Console.WriteLine(v.Age);

可空类型

值类型不能赋值为空,但可以在类型后面加上?来表示可空类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int? i = null;
// 判断是否为空
if (i.HasValue)
{
Console.WriteLine(i.Value);
}

// 安全获取可空类型的值
Console.WriteLine(i.GetValueOrDefault()); // 不填会返回这个类型的默认值
Console.WriteLine(i.GetValueOrDefault(1)); // 填了会返回填的值

object o = null;
if (o != null)
{
Console.WriteLine(o.ToString());
}
o?.ToString(); // 安全调用,如果o为空,则不执行后面的代码

空合并操作符

  • 左边值 ?? 右边值
  • 如果左边值为空,则返回右边值
  • 如果左边值不为空,则返回左边值
    1
    2
    int? j = null;
    Console.WriteLine(j ?? 1);

内插字符串

在字符串前加上$符号,可以在字符串中直接使用变量

1
2
string str = "hello";
Console.WriteLine($"hello {str}");

单句逻辑简略写法

1
2
3
4
if (true) Console.WriteLine("hello");
for (int k = 0; k < 10; k++) Console.WriteLine(k);

// 方法中可以用 => 来代替 return

值 vs 引用

判断

对类型按下F12(Visual Studio),查看定义,如果定义中包含class则说明是引用类型,否则是值类型

语句块

命名空间 -> -> 方法 -> 各种逻辑处理

  • 上层语句块:类、结构体
  • 中层语句块:函数
  • 底层语句块:条件分支、循环等

变量的生命周期

语句块执行结束,没有被记录的对象将被回收或变成垃圾

  • 值类型被系统自动回收
  • 引用类型栈上用于存内存地址的空间被系统自动回收,堆上的内容GC回收

如果要让变量有更长的生命周期,可以用更高层级记录或使用静态全局变量

结构体中的值和引用

  • 结构体本身是值类型(该结构体没有作为其他类的成员)
  • 在结构体中的值类型,在上存储具体的内容
  • 在结构体中的引用类型,在上存储内存地址,在上存储具体的内容

类中的值和引用

  • 类本身是引用类型
  • 在类中的值类型,在上存储具体的内容
  • 在类中的引用类型,在上存储内存地址,在上存储具体的内容

数组中的存储规则

  • 数组本身是引用类型
  • 数组中的值类型,在上存储具体的内容
  • 数组中的引用类型,在上存储内存地址,在上存储具体的内容

结构体继承接口

利用里氏替换原则,用接口容器装载结构体存在装箱和拆箱

面向对象编程的7个原则/规则

总体目标是高内聚,低耦合

使程序模块的可重用性、移植性增强

里氏替换原则

LSP,子类对象能够替换其父类对象,并且程序运行结果不变

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
class GameObject
{

}
class Player : GameObject
{
public void PlayerAtk()
{
Console.WriteLine("玩家攻击");
}
}

class Monster : GameObject
{
public void MonsterAtk()
{
Console.WriteLine("怪物攻击");
}
}

class Boss : GameObject
{
public void BossAtk()
{
Console.WriteLine("Boss攻击");
}
}

...

// 使用
// 父类装子类
GameObject player = new Player();

is判断对象是否为某个类的实例

as将对象转换为某个类的实例

1
2
3
4
5
if (player is Player)
{
Player p = player as Player;
p.PlayerAtk();
}

单一职责原则

SRP,一个类只负责一项职责,各司其职

开放封闭原则

OCP,对扩展开放,对修改封闭,比如继承

依赖倒置原则

DIP,高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象

接口隔离原则

ISP,客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上

迪米特法则

LOD,一个对象应该对其他对象有尽可能少的了解,降低耦合度

合成复用原则

CRP,尽量使用合成/聚合的方式,而不是继承

垃圾回收机制

垃圾回收,简称GC,进行垃圾回收时,会遍历上的内容,
如果发现某个对象没有引用指向它,那么就会销毁这个对象,释放内存空间

内存空间分为0代1代2代内存,
当0代内存满时,会触发垃圾回收,将0代内存中的对象销毁,
如果被销毁的对象引用了其他对象,那么这些对象会被移动到1代内存,
1代满了也同理

大对象(>85KB)会直接分配到2代内存中

垃圾回收可以手动触发

1
GC.Collect();

游戏开发中,一般会在加载场景等情况下触发

排序算法

开发过程中,有时会遇到排序问题,比如背包内物品排序任务列表排序等等,
所以得了解一些必要的排序算法

但是,不要背代码,而是要理解算法

冒泡排序

冒泡排序原理(以升序排序为例):

  • 从第一个元素开始,依次比较相邻的两个元素
  • 如果前一个元素大于后一个元素,则交换它们的位置;否则,不交换
  • 重复上述步骤,直到最后一个元素
  • 每次比较后,最大的元素会被冒泡到数组的末尾

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int[] nums = { 1, 3, 2, 5, 4 };
// 冒泡排序
for (int i = 0; i < nums.Length - 1; i++)
{
for (int j = 0; j < nums.Length - 1 - i; j++)
{
if (nums[j] > nums[j + 1])
{
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;

}
}
}

for (int i = 0; i < nums.Length; i++)
{
Console.Write(nums[i] + " ");
}

选择排序

选择排序原理(以升序排序为例):

  • 将数组划分为两个区域,已排序区域和未排序区域
  • 定义一个索引index,初始值为0
  • 从数组中找到最大的元素,将index的值赋值为该元素索引的位置
  • 将index位置的元素与未排序区域的数组末尾的元素交换位置
  • 重复上述步骤,直到未排序区域为空

算法实现:

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
int[] nums = { 1, 5, 3, 7, 9, 2, 4, 6, 8 };
// 选择排序
for (int i = 0; i < nums.Length; i++)
{
int index = 0;

for (int j = 1; j < nums.Length - i; j++)
{
if (nums[j] > nums[index])
{
index = j;

}
}

if (index != nums.Length - 1 - i)
{
int temp = nums[index];
nums[index] = nums[nums.Length - 1 - i];
nums[nums.Length - 1 - i] = temp;
}
}

for (int i = 0; i < nums.Length; i++)
{
Console.Write(nums[i] + " ");
}

插入排序

插入排序原理(以升序排序为例):

  • 将数组划分为两个区域,已排序区域和未排序区域
  • 从未排序区域中取出一个元素,与已排序区域中的元素进行比较
  • 如果已排序区域中的元素大于取出的元素,则将已排序区域中的元素向后移动一位
  • 重复上述步骤,直到无序区为空

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int[] arr = { 5, 3, 8, 6, 2, 7, 1, 4 };
// 排序开始前,先假设第一个元素是有序的,从第二个元素开始遍历
for (int i = 1; i < arr.Length; i++)
{
// 记录当前要插入的元素
int temp = arr[i];
// 记录当前要插入元素的前一个元素的下标
int j = i - 1;
// 从后往前遍历有序区,如果当前元素大于要插入的元素,则将当前元素后移一位
while (j >= 0 && arr[j] > temp)
{
arr[j + 1] = arr[j];
j--;
}
// 将要插入的元素插入到合适的位置
arr[j + 1] = temp;
}

foreach (int item in arr)
{
Console.WriteLine(item);
}

希尔排序

希尔排序是插入排序的升级版,
它将整个待排序序列分割成若干子序列分别进行直接插入排序,
待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序

在极限情况下,可以有效降低时间复杂度

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int[] arr = { 5, 2, 6, 1, 8, 3, 7, 4 };

// 确定步长
for (int step = arr.Length / 2; step > 0; step /= 2)
{
//for (int i = 1; i < arr.Length; i++)
for (int i = step; i < arr.Length; i++)
{
int temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp)
{
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}

foreach (int item in arr)
{
Console.Write(item + " ");
}

归并排序

递归 + 合并,数组分为左右,左右分别排序,然后合并

算法实现:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public static int[] Sort(int[] left, int[] right)
{
// 准备一个新数组
int[] arr = new int[left.Length + right.Length];
int leftIndex = 0;
int rightIndex = 0;

// 填数据
for (int i = 0; i < arr.Length; i++)
{
// 左侧放完了,直接放对面
if (leftIndex >= left.Length)
{
arr[i] = right[rightIndex];
rightIndex++;
}
else if (rightIndex >= right.Length)
{
arr[i] = left[leftIndex];
leftIndex++;
}
else if (left[leftIndex] < right[rightIndex])
{
arr[i] = left[leftIndex];
leftIndex++;
}
else
{
arr[i] = right[rightIndex];
rightIndex++;
}
}
return arr;
}

public static int[] Merge(int[] arr)
{
// 数组分两段
int mid = arr.Length / 2;

// 递归结束条件
if (arr.Length < 2)
{
return arr;
}
// 左数组
int[] left = new int[mid];
int[] right = new int[arr.Length - mid];
// 左右数组初始化内容
for (int i = 0; i < arr.Length; i++)
{
if (i < mid)
{
left[i] = arr[i];
}
else
{
right[i - mid] = arr[i];
}
}
// 递归
return Sort(Merge(left), Merge(right));
}

...

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

int[] result = Merge(arr);
foreach (int item in result)
{
Console.WriteLine(item);
}

快速排序

基本原理:

  • 选取基准,产生左右标识
  • 将比基准小的放在左边
  • 比基准大的放在右边
  • 然后递归

算法实现:

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
public static void QuickSort(int[] arr, int left, int right)
{
// 递归结束条件
if (left >= right)
{
return;
}
// 记录基准值
// 左右游标
int tempLeft, tempRight, temp;
temp = arr[left];
tempLeft = left;
tempRight = right;

// 交换逻辑
while (tempLeft != tempRight)
{
// 从右边开始比较
while (tempLeft < tempRight && arr[tempRight] > temp)
{
tempRight--;
}
arr[tempLeft] = arr[tempRight];

while (tempLeft < tempRight && arr[tempLeft] < temp)
{
tempLeft++;
}
arr[tempRight] = arr[tempLeft];
}
arr[tempLeft] = temp;

// 递归
QuickSort(arr, left, tempRight - 1);
QuickSort(arr, tempLeft + 1, right);
}

...

int[] arr = { 3, 5, 1, 2, 4, 6, 8, 7, 9 };
QuickSort(arr, 0, arr.Length - 1);
foreach (var item in arr)
{
Console.WriteLine(item);
}

堆排序

基本原理:

  • 构建二叉树,大堆顶调整,堆顶往后放,不停变堆顶
  • 最大非叶子节点: n/2-1
  • 父节点下标:i
  • 左子节点下标:2i+1
  • 右子节点下标:2i+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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 实现父节点和左右节点节点比较
/// <summary>
///
/// </summary>
/// <param name="arr">需要排序的数组</param>
/// <param name="parent">当前根节点的索引</param>
/// <param name="length">哪些位置没有确定</param>
public static void HeapAdjust(int[] arr, int parent, int length)
{
// 通过传入的索引 得到它对应的左右叶子节点的索引
int left = 2 * parent + 1;
int right = 2 * parent + 2;
// 用于记录最大数的索引
int maxIndex = parent;
if (left < length && arr[left] > arr[maxIndex])
{
maxIndex = left;
}
if (right < length && arr[right] > arr[maxIndex])
{
maxIndex = right;
}

if (maxIndex != parent)
{
// 交换
int temp = arr[parent];
arr[parent] = arr[maxIndex];
arr[maxIndex] = temp;

// 通过递归 看看是否影响了叶子节点他们的关系
HeapAdjust(arr, maxIndex, length);
}
}

// 构建大堆顶
public static void BuildBigHeap (int[] arr)
{
// 从最后一个非叶子节点开始
for (int i = arr.Length / 2 - 1; i >= 0; i--)
{
HeapAdjust(arr, i, arr.Length);
}
}

public static void HeapSort(int[] arr)
{
BuildBigHeap(arr);

for (int i = arr.Length - 1; i > 0; i--)
{
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
HeapAdjust(arr, 0, i);
}
}

...

int[] arr = { 3, 5, 2, 4, 1, 6, 8, 7, 9 };

HeapSort(arr);
foreach (var item in arr)
{
Console.WriteLine(item);
}