简单的介绍 本篇是关于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,刻。它又分为随机刻、游戏刻等。 游戏刻为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变量来接收用户输入的字符
移动小方块 接下来,我们根据用户输入的字符,来移动小方块
那么我们输入的字符只有WASD及wasd8种,所以我们可以使用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); }