简单的介绍 本篇是关于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 { static void Main (string [] args ) { Console.WriteLine("Hello, World!" ); } } }
控制台相关内容 现在我们主要编写控制台应用,所有得来了解了解控制台相关的东西
输入输出 WriteLine
输出并自动换行
Write
输出但不换行
ReadLine
获取输入,并自动换行,直到用户按下回车键
ReadKey
获取输入,用户按下任意键就继续执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Console.WriteLine("Hello, World!" ); 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
清空控制台
控制台窗口 设置控制台大小 SetWindowSize
设置控制台大小,参数为宽
和高
1 Console.SetWindowSize(80 , 30 );
设置控制台缓冲区大小 SetBufferSize
设置控制台缓冲区大小,参数为宽
和高
注意,缓冲区大小不能小于
控制台大小,否则会报错
1 Console.SetBufferSize(80 , 30 );
退出控制台 Exit
退出控制台
光标 设置光标位置 窗口中,左上角坐标为(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
变量声明 不同类型的变量可以储存不同类型的数据
数据类型 好,接下来我们来看看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.0 m;
字符型 char
只能存储一个字符,只能用单引号括起来,占用2字节
布尔型 1 2 bool l = true ;bool m = false ;
字符串 字符串类型的,一般用双引号
括起来,可以存放多个字符,没有固定的占用字节数,根据内含字符的数量来决定的
string
字符串
1 string o = "hello world" ;
变量初始化 一般来说,变量声明后就得初始化
;亦可先定义,再初始化
先定义,再初始化
但不论如何,使用变量之前必须
初始化
另外,可以连续声明相同类型
的变量,用逗号隔开
1 int a = 100 , b = 200 , c = 300 ;
变量占用的内存大小 在C#中,不同类型的变量占用的内存大小是固定的,可以通过sizeof
关键字来获取
1 Console.WriteLine(sizeof (int ));
变量命名规范
变量名必须以字母
、下划线
开头
变量名只能包含字母
、数字
、下划线
,并区分大小写
,不能重复
变量名不能使用关键字
变量名应当顾名思义,即见名知意,而不是去猜测它的意思
一般遵循驼峰命名法,即首字母小写,后续每个单词首字母大写(函数、类可采用大驼峰命名法
,亦称帕斯卡命名法
)
变量的本质 不论是在C#,还是其他的编程语言,变量本质上是二进制
, 因为计算机是通过0
和1
来存储各种数据, 变量是一个有名字、有特定属性的存储单元
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;
在上面的例子中,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 double d = 10.0 ;uint g = 10 ;long h = g; float i = a;double j = b;char m = 'a' ;int n = m;
显式转换 括号法 显示转为为强制转换
,用括号()
变量类型 变量名 = (目标类型)变量名
但要注意的是,强制转换可能导致溢出
的问题,需要注意转换类型之间的范围
同时,其精度
也有可能丢失
1 2 int o = 10 ;short p = (short )o;
Parse方法 变量类型.Parse()
方法可以将字符串转换为其他类型
1 2 3 4 5 int q = int .Parse("123" );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 { string a = Console.ReadLine(); int b = int .Parse(a); Console.WriteLine(b); } catch (Exception ex){ Console.WriteLine("Failed" ); } 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++; Console.WriteLine(a); int c = ++a; Console.WriteLine(a);
复合运算符 它是=
和算术运算符的结合,用于简化代码
+=
加等、-=
减等、*=
乘等、/=
除等、%=
取余等
1 2 3 4 5 6 7 8 9 a += b; a -= b; a *= b; a /= b; 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);
任意数值
可以直接进行比较
但特殊类型string
、char
、bool
只能同类型
进行==
、!=
比较
逻辑运算符 逻辑运算符,用于连接多个条件,结果为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); Console.WriteLine(i1 | j1); Console.WriteLine(i1 ^ j1); Console.WriteLine(~i1); Console.WriteLine(i1 << 1 ); Console.WriteLine(i1 >> 1 );
三目(元)运算符 表达式1 ? 表达式2 : 表达式3
如果表达式1为真,则返回表达式2的值;如果表达式1为假,则返回表达式3的值
1 2 3 int k = 1 ;int l = 2 ;int m = k > l ? k : l;
字符串拼接 字符串拼接可以用算术运算符的+
,将两个字符串连接在一起
1 2 string str = "hello" ;Console.WriteLine(str + " world" );
也可以使用{}
占位符,对字符串进行格式化,占位符中放置从0
开始的索引,表示要插入的变量
1 2 3 string str1;str1 = String.Format("{0} world" , str); Console.WriteLine(str1);
括号 括号()
用于改变运算符的优先级,括号内的表达式先运算
运算优先级(只包括上面提到的)
括号(最高优先级)()
- 强制改变运算顺序
自增/自减运算符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 : 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-移动小方块 需求 控制台显示一个小方块■
,用户输入w
、s
、a
、d
,控制小方块上下左右移动
实现 控制台 首先我们设置一下控制台的相关内容
重新设置背景颜色,设置文字颜色,并隐藏光标
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
,刻
。它又分为随机刻
、游戏刻
等。 游戏刻为20
tick,即程序每秒将执行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
变量来接收用户输入的字符
移动小方块 接下来,我们根据用户输入的字符,来移动小方块
那么我们输入的字符只有WASD
及wasd
8种,所以我们可以使用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
个界面,开始界面
、游戏界面
、结束界面
开始界面在上方中央位置显示给木
,在下方显示开始游戏
、退出游戏
,玩家使用W
或S
进行选择,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
类型的变量吧
那么再后面,就是游戏的循环层了,也就是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 ; } }
开始场景 那么接下来我们就来写第一个场景
不过在开始之前,建议是先执行清空控制台
的命令,因为我们最后可能是会从第三个场景
返回第一个场景
的, 得把前面的场景给先清空
打印“给木” 接下来就是打印“给木”(其实就是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) { 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' : 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
的最后一个case
的if
中加入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 ){ 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 ){ 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 ; case 'j' : case 'J' : if (nowSelectId == 0 ) { nowId = 2 ; isQuitWhile = true ; } else { Environment.Exit(0 ); } break ; } if (isQuitWhile) { break ; } #endregion ...
游戏场景 那么接下来,我们来讲游戏场景的代码
首先还是常规的,先清空控制台,因为我们切换到另外一个场景了,得把前一个场景的内容都清空
接下来,我们一步步来实现上面的需求
绘制边界墙 在需求中,要求我们绘制边界墙,用红色的■
表示
那么边界墙,就是围着屏幕范围画一圈,可以用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); }
这里我们定义了一个playerX
和playerY
,分别表示玩家在屏幕中的位置
再定义一个玩家的颜色,最后根据位置和颜色信息来绘制玩家,现在当我们进入第二个场景时, 就可以看到一个黄色小球了
不过,或许有人会问,为什么墙的绘制没有放在循环中,而是放在了循环外面呢?
因为墙的位置是固定的,不会改变,所以放在循环外面, 而玩家和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, RED, GREEN = 10 , BLUE }
枚举值从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 i = (int )EColor.RED;Console.WriteLine(i);
同样枚举也可以转换为string
类型
1 2 3 string s = EColor.RED.ToString();Console.WriteLine(s);
而string
类型也可以转换为枚举类型
其语法为
1 目标枚举类型 变量名 = (目标枚举类型)Enum.Parse(typeof (目标枚举类型), "枚举成员" );
使用示例
1 2 EColor color2 = (EColor)Enum.Parse(typeof (EColor), "GREEN" ); Console.WriteLine(color2);
数组 数组是一种数据类型,用于存储一组相同类型
的数据,其长度是固定的,不能改变
数组有一维数组、二维数组、多维数组、交错数组等等
一维数组 声明 一维数组的声明方式如下:
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);
数组元素访问、修改 数组元素可以通过索引来访问,索引从0
开始,但索引不得越界
(数组长度 - 1
)
1 2 int [] arr = { 1 , 2 , 3 , 4 , 5 };Console.WriteLine(arr[0 ]);
同样可以利用这种方法来修改数组元素
1 2 3 int [] arr = { 1 , 2 , 3 , 4 , 5 };arr[0 ] = 10 ; Console.WriteLine(arr[0 ]);
数组遍历 数组遍历可以使用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 )); Console.WriteLine(arr.GetLength(1 ));
数组元素访问、修改 二维数组可以通过行索引
和列索引
来访问、修改元素
1 2 3 int [,] arr = new int [3 , 4 ];arr[0 , 0 ] = 10 ; Console.WriteLine(arr[0 , 0 ]);
数组遍历 二维数组可以通过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 ]); 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); Console.WriteLine("arr[0]={0}, arr2[0]={1}" , arr[0 ], arr2[0 ]); b = 20 ; arr2[0 ] = 5 ; Console.WriteLine("a={0}, b={1}" , a, b); Console.WriteLine("arr[0]={0}, arr2[0]={1}" , arr[0 ], arr2[0 ]);
特殊的引用类型-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); }
ref/out ref
和out
关键字可以改变从外部传入函数的参数,即如果函数内部改变了值, 那么函数外部也改了
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); int [] a = { 1 , 2 };ChangeArrayRef(ref a); for (int j = 0 ; j < a.Length; j++){ Console.WriteLine(a[j]); } int 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
参数只能放在参数列表的最后,并且只能有一个, 修饰的参数必须是一维数组
,不能用ref
和out
关键字, 也不能指定默认值
1 2 int sum = Add(1 , 2 , 3 , 4 , 5 );Console.WriteLine(sum);
参数默认值 可以为函数的参数指定默认值,这样在调用函数时可以不传入该参数
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
创建对象,然后通过.
来访问成员变量
访问修饰符
成员变量 成员变量是类的属性,用于存储类的状态, 成员变量可以是值类型
或引用类型
, 成员变量默认是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 { name = value ; } }
使用:
1 2 3 Person person = new Person(); person.Name = "张三" ; Console.WriteLine(person.Name);
加密/解密 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 ; Console.WriteLine(person1.Money);
只读/只写 1 2 3 4 5 public int Age{ get { return age; } private set { age = value ; } }
自动属性 可以不声明成员变量,直接声明属性,编译器会自动生成成员变量
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 { 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 { } 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 { 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 .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 { public sealed override void Eat () { } public override void Speak () { Console.WriteLine("人在说话" ); } } class OtherPerson : Person { }
接口
接口不包含成员变量
只包含方法、属性、事件、索引器
成员不能被实现
接口可以不写访问修饰符,不能是私有的
接口继承接口
类实现接口,实现接口中的所有成员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; 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类常用方法 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 ) { 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 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 StringBuilder sb = new StringBuilder("12312456664" );
容量 StringBuilder
是有容量的,当字符串长度超过容量时,会自动扩容
1 Console.WriteLine(sb.Capacity);
增加 1 2 3 4 5 sb.Append("1234567890" ); Console.WriteLine(sb); Console.WriteLine(sb.Capacity); 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 类
结构体是值类型
,类是引用类型
结构体不能继承
,类可以继承
结构体存在栈
上,类存在堆
上
结构体不能初始化成员变量
,类可以
结构体不能使用protected
修饰符,类可以
结构体不能声明无参构造函数
,类可以
结构体不能声明析构函数
,类可以
结构体声明有参构造函数
时,无参构造函数不会丢失
结构体需要在构造函数中初始化
所有成员变量,类随意
结构体不能被static
修饰,类可以
结构体不能在内部声明与它同名的结构体变量
,类可以
接口 vs 抽象类
抽象类可以包含字段
、属性
、方法
、事件
、构造函数
、析构函数
等,接口只能包含方法
、属性
、事件
、索引器
抽象类可以包含实现的方法,接口只能包含抽象方法
(不能实现)
抽象类可以有构造函数
,接口没有
抽象类单一继承
,而接口可以实现多个
表示对象用抽象类,行为拓展用接口
比如不同对象有相同的行为,可以用接口实现
其他数据结构类 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 ); 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("包含" ); } 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); }
装箱拆箱 因为ArrayList
是object
类型的数组,它在进行数据存储和读取中肯定存在装箱拆箱
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()); if (stack.Contains(1 )){ Console.WriteLine("存在" ); }
栈没有办法改变其中元素,要么清空再次压栈、弹出
遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 Console.WriteLine(stack.Count); foreach (var item in stack){ Console.WriteLine(item); } 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 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); } 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" ); }
改 只能清除,不能改变某一个元素
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 , "赵六" );
删除元素
查找元素 找不到会返回空
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" )); Console.WriteLine(ht.ContainsKey(123 )); Console.WriteLine(ht.ContainsValue("赵六" ));
改
遍历 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); } 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 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();
查找元素 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 delegate void MyDelegate (string str ) ;public delegate int MyDelegate2 (int a, int b ) ;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 ; } }
使用委托 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 action = Say; action();
Func Func
是一个委托,用于表示一个有返回值的方法, 参数类型和数量不限
1 2 3 Func<int , int , int > func = Add;
事件 事件是一种特殊的委托
,基于委托存在,事件是委托的安全包裹,让委托更具有安全性
区别在于:不能在类外部赋值,也不能在类外调用
基本语法
声明 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.fun = null ;
匿名函数 匿名函数是一种没有名字
的函数,使用上主要配合委托和事件
基本语法
在函数中传递委托参数时、委托或事件赋值时可以使用匿名函数
使用 无参无返 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 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 class Item : IComparable <Item >{ public int money; public Item (int money ) { this .money = money; } public int CompareTo (Item? other ) { 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 delegate T Test <out T >() ;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 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 ;
终止线程
休眠 单位为毫秒
线程之间共享数据 多线程使用的内存是共享的,因此需要小心处理线程之间的数据共享问题,以避免数据竞争和死锁等问题
可以使用锁Lock
来保护共享数据,确保同一时间只有一个线程可以访问共享数据
关键字lock
1 2 3 4 5 6 7 8 9 10 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
元数据 元数据是用描述数据的数据,类、函数、变量等都是程序的元数据
反射的概念 反射是程序运行时,动态获取程序集、类型、成员的信息,并动态创建对象、调用方法、访问成员的一种机制
反射的作用 反射可以在程序编译后获得信息,提高了程序的拓展性和灵活性
程序运行时得到所有元数据,包括元数据的特性
程序运行时,实例化对象,操作对象
程序运行时创建新的对象,用这些对象执行任务
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);
其他 获取枚举
获取事件
获取接口
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 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 { [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
遍历的类,都是实现了迭代器的
标准迭代器实现 要实现一个标准迭代器,需要实现以下接口
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();
空合并操作符
左边值 ?? 右边值
如果左边值为空,则返回右边值
如果左边值不为空,则返回左边值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);
值 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 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 = 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 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); }