C#基础到入门
Excerpt
C#使得C++程序员可以高效的开发程序,且因可调用由 C/C++ 编写的本机原生函数,而绝不损失C/C++原有的强大的功能。因为这种继承关系,C#与C/C++具有极大的相似性,熟悉类似语言的开发者可以很快的转向C#。 …
C#😀😀😀
- ✨文章有误请指正,如果觉得对你有用,请点赞收藏关注一波,谢谢支持😘
- C#
- 总结
✨文章有误请指正,如果觉得对你有用,请点赞收藏关注一波,谢谢支持😘
提示:以下是本篇文章正文内容
C#
简介
- C#是微软公司发布的一种由C和C++衍生出来的面向对象的编程语言,它不仅去掉了 C++ 和 Java 语言中的一些复杂特性,还提供了可视化工具,能够高效地编写程序。
- C#是由C和C++衍生出来的一种安全的、稳定的、简单的、优雅的面向对象编程语言。它在继承C和C++强大功能的同时去掉了一些它们的复杂特性(例如没有宏以及不允许多重继承)。
- C#使得C++程序员可以高效的开发程序,且因可调用由 C/C++ 编写的本机原生函数,而绝不损失C/C++原有的强大的功能。因为这种继承关系,C#与C/C++具有极大的相似性,熟悉类似语言的开发者可以很快的转向C#。
C#关键字
abstract | decimal | finally | in | partial | short | typeof | volatile |
可以和类、方法、属性、索引器及事件一起使用,标识一个可以扩展但不能被实体化的、必须被实现的类或方法。 | ~ | 定义一个代码块,在程序控制离开try代码块后执行。参见try和catch。 | ~ | ~ | ~ | 一个操作符,返回传入参数的类型。 | 标识一个可被操作系统、某些硬件设备或并发线程修改的attribute。 |
as | default | fixed | int | out | set | uint | where |
一个转换操作符,如果转换失败,就返回null。 | ~ | 在一个代码块执行时,在固定内存位置为一个变量指派一个指针。 | ~ | 标识一个参数值会受影响的参数,但在传入方法时,该参数无需先初始化。 | ~ | ~ | ~ |
base | delegate | float | interface | namespace | stackalloc | unchecked | while |
用于访问被派生类或构造中的同名成员隐藏的基类成员。 | 指定一个声明为一种委托类型。委托把方法封装为可调用实体,能在委托实体中调用。 | ~ | 将一个声明指定为接口类型,即实现类或构造必须遵循的合同。 | 定义一个逻辑组的类型和命名空间。 | 返回在堆上分配的一个内存块的指针。 | 禁止溢出检查。 | ~ |
bool | continue | for | internal | override | sizeof | ulong | yield |
true/false | ~ | ~ | 一个访问修饰符。 | ~ | 一个操作符,以byte为单位返回一个值类型的长度。 | ~ | ~ |
break | double | foreach | is | private | static | unsafe | class |
终止循环语句 | ~ | 用于遍历一个群集的元素。 | ~ | ~ | ~ | 标注包含指针操作的代码块、方法或类。 | ~ |
byte | do | get | lock | ref | this | void | true |
范围是0~255 | ~ | ~ | ~ | 标识一个参数值可能会受影响的参数。 | ~ | ~ | ~ |
case | else | goto | long | readonly | struct | ushort | extern |
~ | ~ | 一个跳转语句,将程序执行重定向到一个标签语句。 | ~ | 标识一个变量的值在初始化后不可修改。 | 是一种值类型,可以声明常量、字段、方法、property、索引器、操作符、构造器和内嵌类型。 | ~ | 标识一个将在外部(通常不是c#语言)实现的方法。 |
catch | enum | if | new | public | throw | using | false |
定义一个代码块,在特定类型异常抛出时,执行块内代码。 | 表示一个已命名常量群集的值类型。 | ~ | ~ | ~ | 抛出一个异常。 | 当用于命名空间时,using关键字允许访问该命名空间中的类型,而无需指定其全名。也用于定义finalization操作的范围。 | ~ |
char | event | implicit | null | return | try | value | sbyte |
~ | 允许一个类或对象提供通知的成员,他必须是委托类型。 | 一个操作符,定义一个用户定义的转换操作符,通常用来将预定义类型转换为用户定义类型或反向操作,隐式转换操作符必须在转换时使用。 | ~ | ~ | 异常处理代码块的组成部分之一。try代码块包括可能会,抛出异常的代码。参阅catch和finally关键字。 | ~ | ~ |
checked | explicit | const | object | protected | switch | virtual | sealed [18] |
既是操作符又是语句,确保编译器运行时,检查整数类型操作或转换时出现的溢出。 | 一个定义用户自定义转换操作符的操作符,通常用来将内建类型转换为用户定义类型或反向操作,必须再转换时调用显示转换操作符。 | 标识一个可在编译时计算出来的变量值,即一经指派不可修改的值。 | ~ | ~ | ~ | 一个方法修饰符,标识可被覆载的方法。 | 防止类型被派生,防止方法和property被覆载。 |
🍺C#初次体验
⌨使用Visual Studio创建第一个项目 打印Hello world!
1.1 创建项目
打开VS->新键项目->找到控制台程序(.NET Framework)->下一步->创建
eg:
1.2 打印Hello world!
🔔如何编译当前程序?
1.C#程序–人能看懂,机器看不懂
2.执行程序的确是机器
3.需要将C#程序编译(翻译)成机器能够读懂的语言(二进制)
4.这样程序就可以被机器执行了
5.Windows:生成->生成解决方案 Ctrl + Shift + B
🔔如何运行当前程序?
1.Windows:运行而不调试(Ctrl + F5/F5)
eg:
🔔注释
1.注释是不会被编译的,更不会被执行
2.注释的作用:
3.解释说明当前的代码是什么含义
3.1、强调
1.在目前学习代码阶段,保证你写的每行代码都要配一行注释
2.解释说明,你这句代码是什么含义
3.1、暂时取消某些代码的执行
快捷键:
注释当前选中行的代码:Ctrl + K + C
取消注释当前选中行的代码:Ctrl + K + U
🍺MSDN
⌨MSDN地址
https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.fieldinfo?view=netframework-4.7.2
🍺数据类型
⌨数据的量级
- 1024字节(byte)=1KB
- 1024KB = 1MB
- 1024MB = 1GB
- 1024GB = 1TB
- 1024TB = 1PB
⌨数据类型
- bool 0000 0000 false 0000 0001 true
- sbyte 有符号的8位整数 000 0000 — 111 1111 (0-127)
1.第一位表示符号(+ -)
2.特殊0:1000 0000 和 0000 0000
3.硬性规定:1000 0000 表示-128
4.取值范围:-128—+127 - byte无符号的8位整数
1.0000 0000 —— 1111 1111
2.取值范围 0 ——255 - short有符号的16位整数(文档中是Int16)
1.000 0000 0000 0000 —— 111 1111 1111 1111
2.负32768到正32767 - int有符号的32位整数(文档中是Int32)
- 最常用的整数类型
- 一般说整型即int类型
- 无特殊情况,存储一个整数都用int
- int占4个字节(面试经常问)
- long有符号的64位整数(文档中是Int64)
- 浮点数(通常理解就是生活中的小数)
- float(单精度浮点数)【文档中查:Single】
- 一般情况下,float足够
- double(双精度浮点数)【文档中查:Double】
- 如果需要精度更高一点,用Double
- decimal(高精度浮点数)
- 极少用到,天文数字可能会用到这个
- float(单精度浮点数)【文档中查:Single】
⌨常量、变量
- 程序运行期间
- 程序开始到程序结束
- 变量:在程序运行期间,可以被改变
- 变量的声明
- 数据类型 变量名 = 初值;
- ⌨ int a = 10;
- 变量可以不赋初值(在特殊情况下必须要赋初值)
- 不赋初值时,当前变量的值是默认值
- int/float 默认值是0
- char 默认值’\0’(表示空字符)
- 不赋初值时,当前变量的值是默认值
- 变量的声明
- 常量:在程序运行期间,不能被改变
- 常量的声明
- const 数据类型 变量名 = 初值;
- 大写(潜规则)
- ⌨ const float money = 100.35f;
- 常数必须要赋初值
- 常量的声明
- 浮点型声明时注意:
- float flo = 1.11f;【float数字后面要加f】
- double damage = 1.11d【double后面要加d】
- decimal damage = 1.223m【decimal后面要加m】
- 字符型声明注意:
- 字符类型(一定要用单引号括起来)
- char cha = ‘name’;
- 常量及变量的命名规则
- 只能由字母、数字、@和下划线(_)这些组成
- 数字不能开头,⌨ 1a❌、3f❌、xiaoming1✅
- @符号,要么不用,要用必须放在首位,🌰@zhansan✅,zhang@san❌
- 不能与系统的关键词同名,⌨ int,long,sbyte❌
- 变量名不能重复
- 中文变量名语法上是可以的,但极为不推荐
- 常量及变量的命名规范
- 全是英文单词,不要用拼音
- 驼峰命名法
- 大驼峰(每个单词的首字母大写,其余字母小写)
- MyHeroDamage、HeroAttack
- 小驼峰:(第一个单词首字母不大写,后面每个单词的首字母大写,其余字母小写)
- myHeroDamage、heroAttack【目前阶段都用小驼峰】
- 大驼峰(每个单词的首字母大写,其余字母小写)
- 见名知意
- yyds(你晓得这是啥?评论区见)❌
🍺运算符
⌨数据的量级
- 赋值运算符 “=”,是一个运算,将后面的结果赋给前面的变量或常量
- 前面 = 后面;后面的值赋给前面
- 前面必须是个变量【不能是具体是数值(2,3,’A‘,“123”)】
- 后面可以是具体的数值,也可以是变量,也可以是常量
⌨算术运算符
- +、- 加减法
- *、/ 乘除法
- a / b 运算之后得到一个结果
- 被除数 ÷ 除数 = 商
- 除法有一个禁忌:除数不能为0
- % 取余(做除法取余数)
- 5 % 3 : 5除以3,得到的余数,是结果
- 上面的+、-、*、/、%都是二元运算符
- ++运算符和–运算符
- 举例:a++; 等价于 a = a+1;
- ++、- -是一元运算符
//int showYouAge = age++;//结果是18
//意味着 age++ 得到的结果是 age
//解析:
//第一步:将age的值赋给showYouAge
//第二步:age自增
int showYouAge = ++age;
//意味着 ++age 得到的结果是 age+1
//解析:
//第一步:age自增
//第二步:将age的值赋给showYouAge
//总结:
//age++;++age;
//++符号在前就先自增,后赋值
//++符号在后就先赋值,后自增
练习题:
⌨符合运算符
- a+=b;等价于a=a+b;
- a=a+4; ==> a+=4;
- a-=b;等价于a=a-b;
- a*=b;等价于a=a*b;
- a/=b;等价于a=a/b;
- a%=b;等价于a=a%b;
⌨输入与输出
- 输出
- Console.WriteLine();
- 输出内容,并换行
- Console.Write();
- 输出内容,不换行
- Console.WriteLine();
- 输入
- Console.Read();
- 从屏幕读取一个字符,并返回该字符所对应的整型数字
- Console.ReadLine();
- 从屏幕读取一串字符,并返回该字符串
- 字符串是一个数据类型
- 关键词string
- 表示一串字符
- 用双引号括起来
- 字符串相加可以得到两个字符串组合到一起的字符串
- Console.Read();
📚预编译执行 region
- 作用:代码分成一个区域,方便折叠和展开
- 区域首部:#region
- 区域尾部:#endregion
eg:
⌨类型转换
- 隐式转换
- 将占用字节小的、取值范围小的、精度小的,转换为占用字节大的、取值范围大的、精度高
- 不需要任何的修饰符,会自动转换
//整型
//1 2 4 8
//sbyte short int long
sbyte englishScore = 100;
//sbyte --> int
int myScore = englishScore;
//int --> long
long classScore = myScore;
//int --> float
float newScore = myScore;
//float --> double
double newClassScore = newScore;
- 显式转换(强制转换)
- 将占用字节大的、取值范围大的、精度高,转换为占用字节小的、取值范围小的、精度小的
- 需要强制转修饰符,会有精度的缺失,甚至数据的错误
- 转换情况:知道这个数字,在小的数据类型的取值范围内
//强制转换
int damage = 10000;
//int --> sbyte
sbyte health = (sbyte)damage;
//输出结果
Console.WriteLine(health); //16
float mathScore = 90.5f;
//float --> int
int myAllScore = (int)mathScore;
//会把小数点后面的内容全部舍去
Console.WriteLine(myAllScore); //90
- int和char之间的类型转换
//int 和 char之间的类型转换
int num = 11101;
//int --> char
char letter = (char)num;
//a-97
Console.WriteLine(letter);
评论区请教大神!!!
- int和bool之间的类型转换
- 不能进行转换
- string与其他类型之间的转换
- 举例:
- “false”,“true”
- “10”,“3.14”
- “A”
- 转换方法
- System.Convert
- System.Convert.ToBoolean()
- System.Convert.ToInt32()
- System.Convert.ToSingle()
- System.Convert.ToDouble()
- System.Convert.ToChar()
- 数据类型.Parse()
- int.Parse()
- bool.Parse()
- float.Parse()
- char.Parse()
- System.Convert
- 其他类型能不能转换成字符串
- 其他类型的变量.ToString();
- 举例:
⌨关系运算符 & 逻辑运算符
- 关系运算符:>,<,>=,<=,==,!=
- 逻辑运算符:逻辑运算是bool与bool之间的运算
- &:与运算
- true & true : true
- true & false : false
- false & false : false
- 总结:一假则假
- |:或运算
- true | true : true
- true | false : true
- false | false : false
- 总结:一真则真
- !:非运算
- ! true : false
- ! false : true
- &&:短路与运算
- 普通的&与运算,无论第一个条件是真是假,都会继续判断第二条件
- 短路与运算&&,如果判断第一个条件已经是假,则不会继续判断第二个条件
- ||:短路或运算
- 普通的|与运算,无论第一个条件是真是假,都会继续判断第二条件
- 短路或运算||,如果判断第一个条件已经是真,则不会继续判断第二个条件
- 短路&&、||
- 优点:第一个条件已经得知整个逻辑运算的结果,就不会去判断第二个条件了
- 节约了运算量
- 缺点:如果判断中带有运算,如果不进行第二个条件的判断,那第二个条件中的运算也不能执行
- 优点:第一个条件已经得知整个逻辑运算的结果,就不会去判断第二个条件了
- &:与运算
练习:👍👍👍👍👍👍
- 手动计算下列表达式的值和number的值,并编写程序来验证结果是否正确
- ①number初值为6:number++ * ++number - number-- / --number
- ②number初值为5:number++ > 3 | --number == 0
- ③number初值为2:number-- > 2 && --number == 0
- ④number初值为12:number++ > 10 && --number != 2 && number-- == 0 && ++number != 3
- ⑤number初值为0:number++ > -1 || --number <= 2 && number-- != 10
- ⑥number初值为0:number++ < -1 || (–number >= 2 && number-- != 10)【小括号优先级最高】
🍺分支语句
⌨条件运算符
- 条件运算符(三元运算符)
- 条件表达式 ? 结果a : 结果b
- if的第一种形式
if(条件表达式){
语句1;
}
- if的第二种形式
if (条件表达式)
{
语句1;
}
else
{
语句2;
}
- if的第三种形式
if (条件表达式1)
{
语句1;
}
else if (条件表达式2)
{
语句2;
}
else
{
语句3;
}
- Switch
- 如果case 冒号后面没有任何语句,可以不加break;
- Switch()括号中是可以允许添加浮点型变量的,但不推荐
- 浮点型是有误差的
- 浮点型一般不做等于的判断
- 企业面试题:有一个浮点数,判断该浮点数是不是等于5
⌨循环语句
- while循环
while (条件表达式)
{
//循环内容
}
- break关键词
- 跳出本层循环(通常与if连用)
- continue关键词
- 结束本次循环(continue后面的代码不再执行),进入下次循环。(通常与if连用)
练习:👍👍👍👍👍👍
- 从键盘输入一个算数运算式,使用if语句实现正确的算数运算,并输出运算结果
- 例如:
- 请输入第一个数字:4
- 请输入运算符:+
- 请输入第一个数字:2
- 计算结果为:6
- 例如:
- 输入一个生日日期,输出其星座.
白羊座:3.21-4.19,金牛座:4.20-5.20,双子座:5.21-6.21,巨蟹座:6.22-7.22
狮子座:7.23-8.22,处女座:8.23-9.22,天秤座:9.23-10.23,天蝎座:10.24-11.22
射手座:11.23-12.21,魔羯座:12.22-1.19,水瓶座:1.20-2.18,双鱼座:2.19-3.20- 例如:
- 请输入月:12
- 请输入日:21
- 您是射手座
- 例如:
🐖数组
🉑🉑一维数组的初始化
- 动态初始化
- 数据类型[] 数组名=new 数据类型[数组长度];
- 此时数组中每一个元素都是为默认值
- int的默认值0
- float的默认值0
- bool的默认值false
- char的默认值‘\0’,表示空字符
- string的默认值""
- 此时数组中每一个元素都是为默认值
- 数据类型[] 数组名=new 数据类型[数组长度]{元素1,元素2…};
- 数据类型[] 数组名=new 数据类型[]{元素1,元素2…};
- 数据类型[] 数组名=new 数据类型[数组长度];
- 静态初始化
- 数据类型[] 数组名={元素1,元素2…};
🉑数组的访问
- 从0开始计数
- array.Length是数组的长度,只读的。
- 访问数组时,切记下标所对应的数组元素是存在的
- 如果不存在,就会引发数组越界异常
🉑引用数据类型
- 数据类型
- 值类型(存储在栈内存)
- 栈内存里存的是具体的值
- 前面学习过的值类型
- int、float、bool、char
- 引用类型(存储在堆内存)
- 栈内存里存的是地址(堆内存的地址)
- 目前学习过的引用类型
- string(特例)、数组
- 值类型(存储在栈内存)
🉑遍历数组
- 遍历数组:访问数组中的每一个元素
int[] array = { 1,8,4,8,7,45,456,789,78,76};
for (int i = 0; i < array.Length; i++)
{
Console.WriteLine(i + ":" + array[i]);
}
👂👂二维数组、结构体、枚举
👂👂冒泡排序
- 思想:
- 当前数组元素与后面的数字元素进行对比,如果前大后小,则进行交换
- 每轮可以确定一个最大值在数组的末位,几轮之后即可完成排序
- 冒泡排序当然也可以从大到小排序,那样则前小后大进行交互
代码:
for (int i = 0; i < array.Length; i++)
{
//立个flag,表示判断过程中是否发生了交换
bool IsjiaoHuan= false;
for (int j = 0; j < array.Length - i - 1; j++)
{
if (array[j] > array[j + 1])
{
//说明交换了
IsjiaoHuan= true;
//交换
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
if(!IsjiaoHuan)
{
//说明已经排好序了,后面的轮次就没有必要了
break;
}
for (int m = 0; m < array.Length; m++)
{
Console.Write(array[m] + "\t");
}
Console.WriteLine();
}
Console.WriteLine("排序结束");
for (int m = 0; m < array.Length; m++)
{
Console.Write(array[m] + "\t");
}
👂👂二维数组的定义
- 数据类型[,] 数组名;
👂int[,] array;
-
二维数组的初始化
-
动态初始化
-
数据类型[,] 数组名 = new 数据类型[第一维长度,第二维长度];
👂int[,] arr= new int[50,3];
-
数据类型[,] 数组名 = new 数据类型[,]{数组值};
👂int[,] arr= new int[,]{{1,0,1},{3,0,5}};
-
-
静态初始化
-
数据类型[,] 数组名 = {数组值};
👂int[,] arr= {{1,0,1},{3,0,5}};
-
-
-
二维数组的元素访问
-
数组名[第一维下标,第二维下标]
👂 arr[3,2]
-
谨防数组越界
-
-
二维数组的长度
- 总长度(二维数组的元素个数)
- array.Length
- 第一维的长度
- array.GetLength(0)
- 第二维的长度
- array.GetLength(1)
- 总长度(二维数组的元素个数)
-
二维数组的遍历
for (int i = 0; i < heroData.GetLength(0); i++)
{
for (int j = 0; j < heroData.GetLength(1); j++)
{
Console.Write(heroData[i,j] + "\t");
}
//换行
Console.WriteLine();
}
- foreach迭代遍历
- 迭代遍历是只读的,不能修改
//foreach性能消耗要大一点,所以能用for的尽量用for
foreach (var item in number)
{
Console.WriteLine(item);
//迭代遍历是只读的,不能写入
//item = 1;
}
👂👂枚举类型
- 枚举类型是一个自定义类型
- 枚举类型是一个值类型
- 枚举类型的创建
//装备类型
enum EquipType
{
Helmet = 100,//头盔
BodyArmor = 200,//防弹衣
Knapsack,//背包
Knife
}
- 枚举类型变量的定义与使用
//定义一个枚举类型的变量
EquipType myEquip = EquipType.Knapsack;
EquipType yourEquip = EquipType.Knapsack;
//判断枚举变量
if(myEquip == EquipType.BodyArmor) { }
switch (myEquip)
{
case EquipType.BodyArmor:
break;
case EquipType.Helmet:
break;
case EquipType.Knife:
break;
//case EquipType.
default:
break;
}
//枚举类型和整型之间的转换
//枚举类型可以强制转换为整型
int num = (int)myEquip;
Console.WriteLine(num);
//整型可以强制转换为枚举类型
myEquip = (EquipType)200;
Console.WriteLine(myEquip);
//既然枚举可以用整数去表示
Console.WriteLine(myEquip+2);
👂👂结构体类型
- 结构体类型是自定义类型
- 结构体类型是值类型
- 结构体类型的创建
//学生类型
struct Student
{
public string name;
public char sex;
public int age;
}
- 结构体类型变量声明及字段赋值
//定义一个学生变量
Student xiaoming;
//学生结构内变量赋值
xiaoming.name = "xiaoming";
xiaoming.age = 16;
xiaoming.sex = 'M';
- 结构体的构造函数
- 结构体默认的构造函数,开发者不能创建默认构造(即无参构造)
public Student()
{
}
- 结构体的自定义构造函数,方便创建结构体变量时给字段赋值
//自定义构造函数
public Student(string n,char s,int a)
{
//作用:快速给结构体字段赋初值
//而且必须给每一个字段都赋初值
name = n;
sex = s;
age = a;
}
- 初始化结构体变量
//有了自定义的构造函数后,如何新建结构体变量
Student xiaogang = new Student("xiaogang",'M',18);
👂👂结构体练习题
eg:
#region 结构体、枚举
//创建英雄装备结构体,包含名称,攻击力加成,法术强度加成,血量加成,装备类型
enum EquipType
{
AD,
AP,
HP,
Other
}
//英雄装备
struct HeroEquip
{
public string name;
public float adAddition;
public float apAddition;
public float hpAddition;
public EquipType equipType;
public HeroEquip(string name, float adBuff, float apBuff, float hpBuff, EquipType equipType)
{
//给所有字段赋初值
this.name = name;
adAddition = adBuff;
apAddition = apBuff;
hpAddition = hpBuff;
this.equipType = equipType;
}
}
#endregion
#region 结构体、枚举练习
//有5个装备保存在结构体数组当中,编程找出血量加成最高者
//对装备数组按照攻击力加成排序并使装备按照攻击力加成升序进行信息打印
HeroEquip wjzr = new HeroEquip(
"无尽之刃", 100, 0, 50, EquipType.AD);
HeroEquip yxj = new HeroEquip(
"饮血剑", 80, 0, 20, EquipType.AD);
HeroEquip ktkj = new HeroEquip(
"狂徒铠甲", 0, 0, 150, EquipType.AD);
HeroEquip dmj = new HeroEquip(
"兰德里的折磨", 20, 100, 0, EquipType.AD);
//声明结构体数组存储这些装备
HeroEquip[] heroEquips = { wjzr, yxj, ktkj, dmj };
//设置初始血量最大值
float maxHPBuff = heroEquips[0].hpAddition;
//设置初始血量最大值的装备名称
string maxHPEquipName = heroEquips[0].name;
HeroEquip maxHPEquip = heroEquips[0];
//找血量最大
for (int i = 0; i<heroEquips.Length; i++)
{
if (maxHPBuff<heroEquips[i].hpAddition)
{
//更新最大值
maxHPBuff = heroEquips[i].hpAddition;
//更新最大值的装备名称
maxHPEquipName = heroEquips[i].name;
}
//如果声明结构体
if (maxHPEquip.hpAddition<heroEquips[i].hpAddition)
{
maxHPEquip = heroEquips[i];
}
}
Console.WriteLine("装备列表中,血量加成最高的装备是\n"
+ maxHPEquip.name + ",最大值是"
+ maxHPEquip.hpAddition);
Console.Read();
#endregion
🍺🍺🍺面向对象
⌨⌨面向过程
- 重点关心解决问题的步骤
- 优点:
- 可以很清晰的看明白问题解决的步骤
- 代码的行数要少一些,性能消耗低一些
- 缺点:
- 不易维护、不易拓展、不易复用
⌨⌨面向对象
- 重点关心解决问题过程中参与的对象有哪些,分别有哪些特性和行为
- 优点:
- 易维护、易拓展、易复用
- 缺点:
- 代码是分散的,行数会多一些
- 性能消耗要高一些
⌨⌨类和对象
- 创建一个类
[访问修饰符] Class 类名//大驼峰命名法
- 创建一个类的对象
类名 对象名;
- 类的类型是一个引用类的型
- 类的类型是一个自定义类型
- 一个对象在创建后,需要进行实例化(初始化)才能使用
类名 对象名 = new 类名();
- 原理 : 对象在进行new操作后,才分配了内存。
- 字段
- 描述类的特性
- 要使用小驼峰命名法命名
- 可以有初值
- 帮助注释
- 帮助注释也是一种注释
- 谁可以添加帮助注释
- 类
- 方法
- 字段
- 属性
- 帮助注释的优点
- 在使用类、方法、字段、属性的时候,可以在提示框中显示帮助注释的内容
- 字段
- this关键词
- 表示当前对象
- 如果没有重名冲突,可以不写(省去)
- Random随机数类
- 第一步,先创建一个随机数对象
Random random = new Random();
-
第二步,调用随机数对象的Next方法
- 调用时要传入两个参数,即随机数字的取值范围
- 如传入的是4和8,则随机数字范围为4至7,不包含8(这个方法就是这么写的)
- 即取值范围为左闭右开,即[min,max)
- 方法返回的结果即随机到的数字,需要用整型变量去接收
- 调用时要传入两个参数,即随机数字的取值范围
int num = random.Next(0,array.Length);
- 方法
- 方法是描述一个类的某个特定行为
- 一个方法尽量就完成一件小的事情
- 如果要完成一件大事
- 先定义一些方法,完成这个大事拆分出来的小事
- 最后在按照流程,在大事方法中调用这些小事的方法
- 如果要完成一件大事
- 方法要用大驼峰命名法命名
- 方法的创建
[访问修饰符] 返回值类型 方法名(参数列表)
{
//方法体
//实现方法功能
return 结果;//最终别忘了返回方法结果,结果类型需与返回值类型保持一致
}
- 方法的调用
对象名.方法名(参数列表);//无论有无参数,小括号都要存在
- return关键词
- 返回结果
- 终止方法,即return后面的语句不会执行
🍺🍺属性、方法参数
⌨⌨属性
- 属性命名方式使用大驼峰
- 在属性访问器内写代码,切记一点
- Get访问器内不要写读取该属性的语句
- Set访问器内不要写写入该属性的语句
- 否则,会出现递归循环,死循环
- 属性简写:public string Name { get; set; } = “先生”;
⌨⌨引用参数ref
- 添加了ref关键词的参数
- 传递的就不是值了
- 而是地址
- 而如果没有赋初值,是没有地址的
- 所以ref参数一定是个变量
- 所以ref参数的实参一定是赋过初值
- 所以ref一般加在值类型参数的前面
- 使用应用参数,无论是形参还是实参前面都要加ref关键词
⌨⌨输出参数out
- 添加了out关键词的参数
- 参数就成了一个输出的通道
- 离开方法之前形参必须赋值
- 实参必须是一个变量
- 传递的实参一般是值类型
- 使用输出参数,无论是形参还是实参前面都要加out关键词
🍺 字符串
Contains | Contains |
---|---|
Remove | LastIndexOf |
public string Remove(( int startIndex)/ (int startIndex, int count )) | public int LastIndexOf( (string/char) value ) |
CopyTo | PadLeft |
从 string 对象的指定位置开始复制指定数量的字符到 Unicode 字符数组中的指定位置。 | 补充左边长度:.PadLeft(2, ‘0’) |
Format | PadRight |
把指定字符串中一个或多个格式项替换为指定对象的字符串表示形式。 | 补充右边长度:.PadRight(2, ‘0’) |
IndexOf | Replace |
返回指定 Unicode 字符在当前字符串中第一次出现的索引,索引从 0 开始。 | 替换文字:stringObj.replace(“终古”,“中国”); |
Insert | Split |
返回一个新的字符串,其中,指定的字符串被插入在当前 string 对象的指定索引位置。 | 返回一个字符串数组,包含当前的 string 对象中的子字符串,子字符串是使用指定的 Unicode 字符数组中的元素进行分隔的。 |
Join | ToLower |
连接一个字符串数组中的所有元素,使用指定的分隔符分隔每个元素。 | 把字符串转换为小写并返回。 |
Substring | ToUpper |
string substr = str.Substring(23); | 把字符串转换为大写并返回。 |
Trim | |
移除当前 String 对象中的所有前导空白字符和后置空白字符。 |
🍺🍺StringBuilder、重载、递归
⌨⌨StringBuilder
- 在System.Text命名空间下
- 所以使用时,需要先引入这个命名空间
using System.Text;
- 如果不引入,可以直接写:
System.Text.StringBuilder strB = new System.Text.StringBuilder();
- 使用时,先要new
StringBuilder stringB = new StringBuilder();
- 追加字符串
//追加字符串
stringBuilder.Append("天气好晴朗");
- 什么时候用StringBuilder而不用string
- 字符串需要经常修改的时候
⌨⌨⌨方法的重载
- 同一个类里面,方法名一样,但参数不一样
- 参数的数据类型不一样
- 参数的个数不一样
- 参数的数据类型不一样,个数也不一样
- 参数个数和类型都一样,但返回值类型不一样,不能算做重载❌
class Funs
{
public string Fun(string a, string b)
{
return "a+b";
}
public int Fun(int a, int b)
{
return a + b;
}
}
Funs funs = new Funs();
int a = funs.Fun(1, 1);
string b = funs.Fun("2", "2");
string c = string.Format("{0},{1}", a, b);
Console.WriteLine(c);
Console.Read();
⌨⌨递归
- 方法自己调用自己
- 多个方法之间来回调用
- 使用递归时一定要有出口
- 使用递归时,一定概要慎重慎重再慎重…
⌨⌨⌨构造函数
- 执行的时机
- new对象()的时候调用
- 构造函数一般都是public
- 因为一旦构造函数被设置为了private,那么外界就无法new这个
- 构造函数没有返回值
- 也不能写返回值类型
- 构造函数的名字必须和类名保持完全的一致
- 构造函数是不能像普通方法一样被直接调用的
- 关于系统会不会自动创建一个空参空方法体的构造给你
- 如果一个类没有构造函数
- 那么系统会自动写一个空参数空方法体的构造函数
- 如果有构造函数
- 那么系统就不会帮你自动创建
- 如果一个类没有构造函数
- 构造函数可以有参、也可以无参
- 构造函数是可以重载的
- 如果想在执行当前构造函数之前,先执行其他的构造函数
- 当前构造函数(…) : this(传实参)
- 如果想在执行当前构造函数之前,先执行其他的构造函数
class Person
{
private int age;
private string name;
public Person(int age)
{
this.age = age;
Console.WriteLine(age);
}
public Person(string name) : this(18)
{
this.name = name;
Console.WriteLine(name);
}
public Person(int age, string name) : this("xian")
{
Console.WriteLine("我是性别,年龄!");
}
}
static void Main(string[] args)
{
Person body = new Person(128, "alll");
Console.Read();
}
🍺🍺🍺多态
⌨关于父类空参数构造函数的调用说明
- 首先,先要明确一点
- 子类在实例化的时候,必会调用父类的构造函数
- 子类在声明构造函数的时候,要想办法调用父类的构造
- 如果父类是空参数的构造函数 : base()
- 可以不写:base()
- 系统会默认调用父类的空参数构造
- 如果父类是有参数的构造函数,那么一定概要通过:base的方式调用,传参
- 如果父类是空参数的构造函数 : base()
- 关于VS自动生成类构造函数
- 右键类名
- 点击快速修复或重构
- 点击生成构造
public class Person
{
public string name;
public Person(string name)
{
this.name = name;
Console.WriteLine(name);
}
public void Say()
{
Console.WriteLine("你在干什么!");
}
}
public class Person1:Person
{
public Person1(string name):base(name)
{
}
public new void Say()
{
Console.WriteLine("弄啥呢!");
}
}
public class Person2:Person1
{
public Person2(string name) : base(name)
{
}
public new void Say()
{
Console.WriteLine("搞啥呢!");
}
}
static void Main(string[] args)
{
Person p = new Person("1");
p.Say();
Person1 p1 = new Person1("2");
p1.Say();
Person2 p2 = new Person2("3");
p2.Say();
PersonFun(p2);
Console.ReadLine();
}
public static void PersonFun(Person person)
{
person.Say();
}
⌨子类方法的覆盖
- 前提条件:父类中有一个public函数,子类中没有该函数
- 因为子类中并没有该函数,所以调用必是父类的
- 前提条件:子类里已经有了该函数,父类里面也有该函数
- 此时,子类对象调用子类的该函数,父类对象调用父类的该函数
- 这种子类函数,可以称之为覆盖
- 子类在书写该函数的时候,规范的写法应该是:
- [访问修饰符] new 返回值类型 函数名(参数列表)
- 覆盖:子类也有该函数了,以后调用的时候就调用子类的该函数
⌨子类方法的重写【表现出多态】
- 如果父类想要子类可以重写该函数
- 那么父类的该函数必须是一个虚函数
- [访问修饰符] virtual 返回值类型 函数名(参数列表)
- 子类该怎么重写
- [访问修饰符] override 返回值类型 函数名(参数列表)
- 重写:把子类和父类的该函数都重新写了一遍,有的新的内容
- 此时,子类的对象,无论是不是转换成了父类的类型,都会执行重写后的该函数
- 关于VS自动生成类重写函数
- 右键类名
- 点击快速修复或重构
- 点击生成重写
public class Person
{
public string name;
public Person()
{
}
public Person(string name)
{
this.name = name;
}
public virtual void Say()
{
Console.WriteLine("我是父类的方法");
}
}
public class Person1 : Person
{
public Person1(string name)
{
}
public override void Say()
{
Console.WriteLine("我是字类的方法");
}
}
static void Main(string[] args)
{
Person1 p1 = new Person1("我是字类");
p1.Say();
Person p = new Person1("我是父类");
p.Say();
Console.ReadLine();
}
⌨⌨所有类的最终基类:Object
- 所以,所有类都可以重写Object类中的虚方法
- Object的虚方法有三个:
- Equals:描述对象与对象之间是否相等
- GetHashCode:将一个对象编程一串数字
- ToString:将一个对象转换为一个字符串
⌨⌨抽象方法
- 抽象方法的关键词abstruct
- abstruct 返回值类型(参数列表)
- 抽象方法必须要放在抽象类里面
- 抽象方法没有方法体: [访问修饰符] abstruct 返回值类型 方法名(形参列表);
- 抽象方法的访问修饰符不能是private
- 抽象类即可以放抽象方法,也可以放普通方法
- 抽象方法必须在子类中全部实现
- 除非子类也是一个抽象类,那么可以先不实现该抽象方法
- 抽象方法和虚方法最大的区别
- 抽象方法必须其派生类中得以实现
- 而虚方法不是一定要在其派生类去重写
- 无论是虚方法还是抽象方法,子类进行了重写【实现】
- 那么子类的子类依旧可以继续重写
- 抽象类不能用sealed关键词修饰
- 总结:override可以重写哪些方法呢?
- 带有virtual、abstruct、override关键词的方法
代码:
//抽象类
public abstract class Person
{
public void Fun(string name)
{
Console.WriteLine(name);
}
public abstract void Say();
}
public class Person1 : Person
{
public override void Say()
{
Console.WriteLine("asd");
}
}
static void Main(string[] args)
{
Person1 a = new Person1();
a.Say();
Console.Read();
}
⌨⌨sealed关键词
- 密封类
- sealed关键词修饰的类称之为密封类
- 语法:sealed class 类名
- 密封类是不能被别的类继承的
- 密封方法
- sealed关键词修饰的重写的方法,称之为密封方法
- 语法:sealed override 返回值类型 方法名(参数列表)
- 密封方法无法再次被其子类重写
代码:
public class Person
{
public virtual void Fun()
{
Console.WriteLine(1);
}
}
public class Person1 : Person
{
public sealed override void Fun()
{
Console.WriteLine(2);
}
}
public class Person2 : Person1
{
//这里报错 因为续承的Person1是密封函数
public override void Fun()
{
base.Fun();
}
}
static void Main(string[] args)
{
Person a = new Person1();
a.Fun();
Console.Read();
}
⌨⌨静态类
- 关键词 static
- 静态成员
- 成员:字段、属性、方法
- 静态:跟对象没有任何关系,只跟类有关系
- 静态成员在何时开辟的内存
- 第一次访问这个类的时候【第一次用到这个类的时候】
- 比如:用这个类名去实例化一个对象
- 比如:用这个类型去访问一个静态字段
- 第一次访问这个类的时候【第一次用到这个类的时候】
- 静态成员在何时释放内存
- 在程序结束的时候才会释放
- 普通的实例成员,每有一个对象,就有一个该成员
- 而静态成员,跟对象没有关系,所以无论有多少个对象,静态成员都只有一个
- 例: 实例成员【name】,每有一个人,就会有对应的一个名字
- 而静态成员【Population】,跟对象没有关系,无论有多少个实例对象,人口数量只有一个
- 静态方法中是不可以访问非静态的成员的
- 不能访问非静态的字段、属性
- 不能调用非静态的方法
- 非静态方法中是可以访问静态成员的
- 能访问静态的字段、属性
- 能调用静态的方法
- 静态方法是可以有重载
- 静态类
- 静态的成员可以放在静态类中,也可以放在非静态类中
- 静态类中只能存在静态成员,不能存在非静态的成员
- 静态类是不能进行实例化的
- 静态构造函数
- 只有一种写法
- static 类名()
- 静态构造函数必须无参数
- 静态构造函数在什么时候才会调用
- 静态构造函数在程序运行期间只会执行一次
- 在第一次访问该类的时候调用
- 用这个类去new一个对象
- 用这个类去访问某个静态成员
- 用这个类去调用某个静态方法
- 如果有继承关系
- 静态构造函数的执行顺序是:
- 先执行子类的静态构造,再执行父类的静态构造
- 先子后父
- 静态构造有什么作用
- 一般用于对静态成员进行初始化
- 只有一种写法
代码
class Person
{
public static float age=88;
public static void Fun()
{
Console.WriteLine("我是父静态类!");
}
static Person()
{
Console.WriteLine("我是基静态类!");
}
}
class Per : Person
{
static Per()
{
Console.WriteLine("我是子静态类!");
}
}
static void Main(string[] args)
{
Per p = new Per();
Console.WriteLine();
Console.ReadLine();
}
集合、栈、堆、队列、字典 ✍🔤🔤🔤
1、集合~范型(命名空间using System.Collections.Generic;)
1.1、ArrayList
代码:
//实例化动态数组
ArrayList score = new ArrayList();
//向动态数组中添加元素
score.Add(90);
score.Add(85.5f);
score.Add("English:100");
int[] array = { 90,80,70 };
//向动态数组中批量添加元素
score.AddRange(array);
//向动态数组中插入元素
score.Insert(2, "Math:80.5");
//删除动态数组中的元素
score.Remove(85.5f);
//删除的是单个约束,如果动态数组中没有该元素,就忽略
score.Remove(90);
//根据下标移除动态数组中的元素
score.RemoveAt(0);
score.AddRange(new string[] { "A", "B", "C", "A" });
//批量删除元素
score.RemoveRange(2, 3);
//如果数组中没有该下标所对应的元素,则也会出现越界异常
Console.WriteLine(score[1]);
//数组元素翻转
score.Reverse();
//一般想要删除某个元素,会先进行判断是否存在
bool containsA = score.Contains("A");
Console.WriteLine("containsA:" + containsA);
//判断数组中是否包含某个元素
if(score.Contains("A"))
{
score.Remove("A");
}
Console.WriteLine("Count:" + score.Count);
//给一个元素的值,查该值在动态数组中的下标
Console.WriteLine("IndexOf:" + score.IndexOf(790));
score.Clear();
score.AddRange(new float[] { 1.1f,5.7f,4.5f,9.8f,3,2,1});
//从小到大排序
score.Sort();
//清空动态数组
//score.Clear();
Console.WriteLine("-------------");
for (int i = 0; i < score.Count; i++)
{
Console.WriteLine(score[i]);
}
Console.WriteLine("-------------");
foreach (var item in score)
{
Console.WriteLine(item);
}
1.2、List
代码:
//初始化范型集合
List<int> list = new List<int>();
//添加元素
list.Add(200);
list.Add(250);
list.Add(280);
//批量添加元素
list.AddRange(new int[] { 1,3,5,6,7,9 });
//移除某个元素
list.Remove(280);
//通过下标移除某个元素
list.RemoveAt(0);
list.RemoveAt(0);
//批量删除
list.RemoveRange(0, 3);
int index = list.IndexOf(9);
Console.WriteLine(index);
Console.WriteLine("-----------");
foreach (var item in list)
{
Console.WriteLine(item);
}
2、栈(Stack )
- 说明:后进先出
代码:
eg:非范型为例😥
Object<=Stack stack = new Stack();
//进栈
stack.Push("我是第一个进去的");
stack.Push("我是第二个进去的");
stack.Push("我是第三个进去的");
//出栈
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.Read();
//返回栈顶的元素Peek();
-
常用方法
-
Stack.TryPeek(T) 方法
- 返回一个值,该值指示 Stack 的顶部是否有对象;如果有,则将其复制到 result 参数。 不从 Stack 中删除对象。
- public bool TryPeek (out T result);
- 返回
Boolean
如果 Stack 的顶部有对象,则为 true;如果 Stack 为空,则为 false。
- Stack.TryPop(T) 方法
- public bool TryPop (out T result);
- 返回一个值,该值指示 Stack 的顶部是否有对象;如果有,则将其复制到 result 参数,并从 Stack 中删除它。
- 返回
Boolean
如果 Stack 的顶部有对象,则为 true;如果 Stack 为空,则为 false。
3、堆(heaps)
😀😀😀略略略
4、队列(Queue)
Queue queue = new Queue();
//进列
queue.Enqueue("我是第一个进去的");
queue.Enqueue("我是第二个进去的");
queue.Enqueue("我是第三个进去的");
//出列
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
Console.Read();
常用方法:
5、字典(Dictionary<string,int>)
代码:
Dictionary<string, int> dis = new Dictionary<string, int>();
dis.Add("我是第一个进去的", 1);
dis.Add("我是第二个进去的", 2);
dis.Add("我是第三个进去的", 3);
Console.WriteLine(dis.Values);
Console.WriteLine(dis.ContainsValue(2));
Console.WriteLine(dis.ContainsValue(3));
Console.Read();
6、常见的接口
🍺🍺🍺🍺单例、接口和范型
1、单例
- 如果一个对象在声明时直接实例化【new】。
- 在访问这个类的时候调用
- 实例化的时间点最早,在静态构造之前就执行了
2、接口
- 接口相比类,最大的不同之处在于,只有定义没有实现
- 接口相当于一堆骨架,实现接口的类,用于填充骨架上的肉
- 接口不能进行实例化,只能被类或其他接口实现
- 如即继承类,又实现接口时,类要放在最前面,接口放在后面
eg:- class Person1:Person,jiankou1,jiekou2
2.1、接口和抽象类的对比
- 相同点
- 两者都不能被实例化
- 两者都包含了由其他类或结构继承或实现的抽象成员不同点
- 不同点
- 抽象类当中除了拥有抽象成员外还可以拥有非抽象成员;而接口中所有的所有成员都是抽象的
- 抽象成员可以使用修饰符修饰,接口当中接口成员访问级别是默认不可修改的,并且默认是public
- 接口当中不可以包含构造方法,析构方法,静态成员以及常量
- C#类只支持单继承,接口支持多支持
3、范型
- 范型类的构造函数不用写范型类
- 有些时候重载的方法只有参数类型不同,其他的都一样,这时就可以使用泛型。
- 泛型:需要用户自己传过来的一个数据类型
- 平时方法里传递的是参数,是变量,参数传递用的是小括号()。
而泛型传递的是数据类型,泛型传递用的尖括号<> - 泛型定义之后,一定要用,不然就没有意义
- 泛型都在方法的哪里用?
- 定义参数
- 在方法体呢使用泛型定义局部变量口
- 设置返回值类型是一个泛型。给泛型添加约束
- 泛型都在方法的哪里用?
- 给范型舔加约束
- 方法名(参数列表)where 泛型:约束内容
- 方法名(参数列表)where 泛型A:约束内容1,约束内容2 where 泛型B:约束内容3
- 关于泛型方法的重载
- 如果泛型的个数不同,可以重载
- 如果泛型的个数相同,但约束不同,不可以重载
- 关于泛型类和泛型接口
- class 类名<T,F>
- 类中的字段类型、方法参数、方法返回值,都可以使用类中的泛型
- interface 接口名< T >
- class 类名<T,F>
代码:
public class fanxing
{
}
public static void FX<T>(T sex, T age)
{
T temp = sex;
sex = age;
age = temp;
Console.WriteLine(age);
}
public static void FX<T>(T sex, T age, T a)
{
T temp = sex;
sex = age;
age = temp;
Console.WriteLine(age);
}
static void Main(string[] args)
{
FX<int>(7,8);
FX<float>(7,8);
Console.ReadLine();
}
- 范型方法的范型重载
eg 代码:
```代码
public static void faning<T>()
{
}
public static void faning<T,F>()
{
}
public static void faning<T,F,U>()
{
}
static void Main(string[] args)
{
faning<>
}
🍺🍺🍺🍺委托与事件
1、委托(delegate)
2.1常见委托类型
- 什么是委托:相当于中介
- 委托的别名:代理、句柄
- 委托是自定义类型
- 委托是引用类型
代码eg:
eg:非范型为例😥
//定义委托
delegate void weituo(float momey);
class p1
{
public void zhao1(float momey)
{
Console.WriteLine("中介一需要:"+momey+"$");
}
public void zhao2(float momey)
{
Console.WriteLine("中介二需要:"+ momey + "$");
}
public void zhao3(float momey)
{
Console.WriteLine("中介三需要:"+ momey + "$");
}
public void zhao4(float momey)
{
Console.WriteLine("中介四需要:"+momey + "$");
}
}
internal class Program
{
static void Main(string[] args)
{
p1 p = new p1();
weituo we;
we = p.zhao1;
we += p.zhao2;
we += p.zhao3;
we += p.zhao4;
we(1232143);
Console.Read();
}
}
2.2系统委托类型
- 无返回值系统委托 Action<>
代码eg:
eg:😥
class p1
{
public void wcs()
{
Console.WriteLine("我是无参数的中介需要:" + "18456456456456"+ "$");
}
public void zhao1(float momey)
{
Console.WriteLine("中介一需要:"+momey+"$");
}
public void zhao2(float momey)
{
Console.WriteLine("中介二需要:"+ momey + "$");
}
public void zhao3(float momey)
{
Console.WriteLine("中介三需要:"+ momey + "$");
}
public void zhao4(float momey)
{
Console.WriteLine("中介四需要:"+momey + "$");
}
}
internal class Program
{
static void Main(string[] args)
{
p1 p = new p1();
//使用系统委托
//无参数
Action action;
action = p.wcs;
action();
//参数
Action<float> action1;
action1 = p.zhao1;
action1 += p.zhao2;
action1 += p.zhao3;
action1 += p.zhao4;
action1(182);
Console.Read();
}
}
- 有返回值系统委托 Func<>
代码eg:
eg:😥
class Hout
{
}
class p1
{
public string Func1()
{
return "有返回值函数无参数:";
}
public Hout Func2(string name)
{
Console.WriteLine("有返回值函数有参数:"+name);
return null;
}
}
internal class Program
{
static void Main(string[] args)
{
p1 p = new p1();
Func<string> func;
func = p.Func1;
Console.WriteLine(func());
Func<string,Hout> func1;
func1 = p.Func2;
Hout a= func1("先生");
Console.Read();
}
}
2.3委托之匿名函数
代码eg:
eg:😥
class p1
{
public Action<string> action;
public string name;
public int age;
public p1(string name, int age)
{
this.name = name;
this.age = age;
}
public void isAction(string age)
{
Console.WriteLine("我是个中介:"+ age);
if (action != null)
{
action(age);
Console.WriteLine("我是个中介:" + age);
}
}
}
internal class Program
{
static void Main(string[] args)
{
p1 p = new p1("先生",18);
p.action = delegate (string a)
{
Console.WriteLine(a);
Console.WriteLine("我好蒙啊2");
};
p.action += read;
p.isAction("我好蒙啊1");
Console.Read();
}
public static void read(string name)
{
Console.WriteLine(name);
Console.BackgroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Black;
}
}
❌❌❌如何使用VS进行程序调试
- 程序是从Main函数开始,顺序执行的
- 调试就是在你觉得可能会发生问题的地方打下断点
- 什么是断点?
- 让程序执行到这个地方暂停下来的点
- 有什么好处,可以一句句或一段段让程序执行
- 什么是断点?
- 调试步骤
- 给想要暂停的代码行添加断点
- 开始调试
- 通过监视或局部变量窗口,去看此时你想观察的变量的值
- 如果想看变量or对象的内存地址
- 找到即时窗口
- &变量名
总结
C#进阶教程
Excerpt
简单数据类型ArrayListStackQueueHashtable泛型泛型泛型约束常用泛型数据结构类ListDictionary顺序存储与链式存储Linedlist委托和事件委托事件匿名函数Lambad表达式List排序协变逆变多线程预处理器指令反射和特性迭代器特殊语法排序进阶…
C#进阶
- 复杂数据类型
- 泛型
- 常用泛型数据结构类
- 委托和事件
- 匿名函数
- Lambad表达式
- List排序
- 协变逆变
- 多线程
- 预处理器指令
- 反射和特性
-
- 反射
-
- 什么是程序集
- 元数据
- 反射的概念
- 反射的作用
- 语法相关
- 总结
- 特性
- 迭代器
- 特殊语法
- 排序进阶
复杂数据类型
ArrayList
ArrayList的本质
ArrayList是C#为我们封装好的类,它的本质是一个object类型的数组,ArrayList类帮助我们实现了很多方法,比如数组的增删查改
ArrayList的申明
命名空间:using System.Collections;
ArrayList list = new ArrayList();
ArrayList的增删查改
增
- 增加一个 Add
list.Add(1);
list.Add("hello");
list.Add(true);
list.Add(new object());
- 两个ArrayList拼接 AddRange
ArrayList list2 = new ArrayList();
list.AddRange(list2);
- 插入一个元素 Insert
只能在ArrayList的中间或者末尾插入,如果一个ArrayList的长度为1,执行Insert操作传入的位置是2,则会报错。
例如:list.Insert(0, 1);表示在第一个元素的后面插入1
// 在第4个位置插入元素1
list.Insert(3, 1);
- 插入一组元素 InsertRange
ArrayList list3 = new ArrayList();
list3[0] = "a";
list.InsertRange(1, list3);
删
- 移除指定元素 Remove
原理:从头开始找,找到第一个符合的元素,删掉,然后把后面的元素往前挪。如果找不到元素就不会有任何效果。
如果list中有两个1,只会移除掉第一个1
list.Remove(1);
- 移除指定位置的元素 RemoveAt
如果list中找不到指定的位置,会报错。
// 删除第一个位置的元素
list.RemoveAt(0);
- 清空 Clear
list.Clear();
查
- 得到指定位置的元素
var item = list[1];
- 查看元素是否存在 Contains
```csharp
bool exist = list.Contains(1);
- 正向查找元素位置,返回的值是第一个符合条件的元素的位置。-1表示没有找到元素 indexOf
int index = list.indexOf(1);
- 反向查找元素位置,返回的值是从后面开始第一个符合元素的位置(位置是从头计数的),-1表示没有找到元素 LastIndexOf
int index = list.LastIndexOf(1);
改
list[0] = 111;
如果list中只有一个元素,只能修改第一个元素,如果执行list[1] = XXX,则会报错。
遍历
- for遍历
for(int i = 0; i < list.Count; i ++)
{
}
- foreach遍历
foreach(var item in list)
{
}
装箱拆箱
由于ArrayList装载object,当往ArrayList中存值类型时,是在装箱(将值类型转化为引用类型,将栈上的元素转到堆上),当将值类型从ArrayList中取出时,是在拆箱(将引用类型转化为值类型,将堆上的元素转到栈上)。
Stack 栈
Stack的本质
Stack,栈存储容器,是一个C#为我们封装好的类,它的本质也是object[]数组,只是封装了特殊的存储规则。
Stack是一种先进后出的数据结构,先存入的数据后获取,后存入的数据先获取。
Stack的申明
命名空间:System.Collections;
Stack s = new Stack();
增取查改
增 压栈 push
s.Push(object);
取 弹栈 Pop
// 栈遵循先进后出的原则,执行Pop即栈顶开始取,取出的数据自动从栈中删除,当栈中没有元素了,执行Pop操作,会报错
object o = s.Pop();
查
栈是不能查看指定位置的元素,只能查看栈顶的元素。
查看栈顶元素,但是不会删除 Peek
object o = s.Peek();
查看元素是否在栈中 Contains
if(s.Contains(object))
{
}
改
栈无法改变其中的元素,只能压栈和弹栈,如果实在要改,只能清空 (Clear)栈。
遍历
长度 Count
int len = s.Count;
foreach遍历
// 从栈顶开始
foreach(object item in s)
{
Console.WriteLine(item);
}
将Stack转化为数据 再遍历 ToArray
object[] ary = s.ToArray();
for(int i = 0; i < ary.Length; i ++)
{
Console.WriteLine(ary[i]);
}
循环弹栈 while + Pop
while(s.Count > 0)
{
object o = s.Pop();
Console.WriteLine(o);
}
在这里插入代码片
装箱拆箱
由于Stack装载object,所以也存在装箱拆箱的问题。
Queue 队列
Queue的本质
Queue,队列容器,是一个C#为我们封装好的类,它的本质也是object[]数组,只是封装了特殊的存储规则。
Queue是一种先进先出的数据结构,先存入的数据先取出,后存入的数据后取出。
Queue的申明
命名空间:System.Collections;
Queue q = new Queue();
增取查改
增 Enqueue
q.Enqueue(object);
取 Dequeue
// 队列遵循先进先出的原则,执行Dequeue即从头开始取,取出的数据自动从队列中删除,当队列中没有元素了,执行Dequeue操作,会报错
q.Dequeue(object);
查
队列是不能查看指定位置的元素,只能查看列队头部的元素。
查看队列头部元素,但不会删除 Peek
object o = q.Peek();
查看元素是否存在于队列中 Contains
if(q.Contains(object))
{
}
改
队列无法改变其中的元素,只能进出队列,如果实在要改,只有清空(Clear)队列。不能使用[]来访问元素
遍历
长度 Count
int len = q.Count;
foreach遍历
// 从队列第一个开始
foreach(object item in q)
{
Console.WriteLine(item);
}
将Queue转化为数据 再遍历 ToArray
object[] ary = q.ToArray();
for(int i = 0; i < ary.Length; i ++)
{
Console.WriteLine(ary[i]);
}
循环出列 while + Dequeue
while(q.Count > 0)
{
object o = q.Dequeue();
Console.WriteLine(o);
}
装箱拆箱
由于Queue装载object,所以也存在装箱拆箱的问题。
Hashtable
Hashtable的本质
Hashtable(又称散列表),是基于键的哈希代码组织起来的键值对。它主要作用是提高数据查询的效率,主要使用键来访问表中的元素。
Hashtable的键值都是object类型,即Hashtable可以包含所有类型的元素。
Hashtable的申明
Hashtable hashtable = new Hashtable();
Hashtable的增删查改
增 Add
hashtable.Add(1, "ABC");
hashtable.Add("ABC", 1);
注意:使用Add添加元素时,不能重复出现相同的键
删 Remove
只能通过Remove来删除
删除不存在的键,没反应
hashtable.Remove(1);
hashtable.Remove(2);
清空 Clear
hashtable.Clear();
查
通过键查看值
通过键查看值,找不到返回null
判断是否存在
- 通过键判断 Contains或者ContainsKey
if (hashtable.Contains(1))
{
Console.WriteLine("存在键为1的键值对");
}
- 通过值判断 ContainsValue
if (hashtable.ContainsValue("ABC"))
{
Console.WriteLine("存在值为ABC的键值对");
}
改
只能改键对应的值内容,无法修改键
没有该键则为新增,有该键则为修改
hashtable["ABC"] = 100;
Hashtable的遍历
遍历所有键
foreach (object key in hashtable.Keys)
{
Console.WriteLine(string.Format("{0}={1}", key, hashtable[key]));
}
遍历所有值
foreach (object value in hashtable.Values)
{
Console.WriteLine(value);
}
键值对一起遍历
foreach (DictionaryEntry item in hashtable)
{
Console.WriteLine(string.Format("{0}={1}", item.Key, item.Value));
}
迭代器遍历法
IDictionaryEnumerator enumerator = hashtable.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(string.Format("{0}={1}", enumerator.Key, enumerator.Value));
}
装箱拆箱
由于Hashtable是使用object来储存数据,自然存在装箱拆箱操作。当在存值时,装箱,取值时拆箱。
泛型
泛型
什么是泛型
泛型实现了类型参数化,达到代码重用的目的。通过类型参数化来实现一份代码上操作多种类型。
泛型相当于类型占位符,定义类或者方法时使用替代符代表变量类型,当真正使用类或者方法时再具体指定类型。
泛型的分类
- 泛型类和泛型接口
基本语法:
class 类名<泛型占位符>
interface 接口名<泛型占位符> - 泛型函数
基本语法:函数名<泛型占位符>(参数列表)
泛型占位符可以有多个,使用逗号隔开。
泛型类和接口
- 泛型类
class TClass<T, M>
{
public T t;
public M m;
}
// 使用时指定泛型占位符所表示的类型
TClass<int, string> tc = new TClass<int, string>();
tc.t = 100;
tc.m = "Hello";
- 泛型接口
interface TInterface<T>
{
T t
{
get;
set;
}
}
// 实现接口时需要指定泛型占位符代表的类型
class TInterfaceClass : TInterface<int>
{
public int t
{
get;
set;
}
}
泛型方法
普通类中的泛型方法
class TestClass
{
// 方法的参数使用泛型
public void TestFunc<T>(T p)
{
Console.WriteLine(p);
}
// 泛型不作为方法的参数,用于方法的逻辑中
public void TestFunc<T>()
{
T t = default(T);
// 逻辑
}
// 泛型作为方法的返回值
public T TestFunc1<T>()
{
return default(T);
}
}
泛型类中的泛型方法
class TClass<T>
{
// 这个不是泛型方法,因为这是定义泛型类时就定义好的,只能传入实例化TClass时指定的类型作为参数
public void Func(T t)
{
}
// 这个是泛型方法,可以接受任何类型的参数
public void Func<K>(K k)
{
}
}
TClass<int> tc = new TClass<int>();
// 这里只能传入int类型的参数
tc.Func(10);
// 泛型方法可以传入任何类型的参数
tc.Func<string>("Hello");
泛型的作用
- 不同类型对象的相同逻辑处理就可以选择泛型
- 使用泛型可以一定程度避免装箱拆箱
泛型约束
什么是泛型约束
泛型约束就是限制泛型占位符所表示的数据类型
关键字是where
各种泛型约束
泛型约束总共有6种:
- 值类型:where 泛型占位符:struct
class TClass<T> where T:struct
{
}
- 引用类型:where 泛型占位符:class
class TClass<T> where T:class
{
}
- 存在公共无参构造函数的非抽象类型:where 泛型占位符:new()
class TClass<T> where T:new()
{
}
- 某个类本身或者其他派生类:where 泛型占位符:类名
class BaseClass
{
}
class CustomClass:BaseClass
{
}
class TClass<T> where T:BaseClass
{
}
TClass<BaseClass> tc = new TClass<BaseClass>();
TClass<CustomClass> tc1 = new TClass<CustomClass>();
- 某个接口的派生类型:where 泛型占位符:接口名
interface TestInterface
{
}
interface CustomInterface : TestInterface
{
}
class CustomClass : CustomInterface
{
}
class TClass<T> where T:TestInterface
{
}
TClass<CustomClass> tc = new TClass<CustomClass>();
TClass<CustomInterface> tc1 = new TClass<CustomInterface>();
TClass<TestInterface> tc2 = new TClass<TestInterface>();
- 另一个泛型类型本身或者派生类型:where 泛型占位符:另一个泛型字母
class BaseClass
{
}
class CustomClass : BaseClass
{
}
// T与K是同一种类型或者T是K的派生类型
class TestClass<T, K> where T:K
{
}
TestClass<CustomClass, BaseClass> tc = new TestClass<CustomClass, BaseClass>();
泛型约束的组合使用
约束之间可以组合起来一起使用,各个约束之间使用都好隔开
// 约束T是引用类型且有公共的无参构造函数
class TestClass<T> where T:class, new()
{
}
多个泛型有约束
where关键字并列写
// 约束T是引用类型且有公共的无参构造函数 约束K是值类型
class TestClass<T, K> where T:class, new() where K:struct
{
}
常用泛型数据结构类
List
List的本质 对应ArrayList
List是C#为我们封装好的类,它的本质是一个可变类型的泛型数组,List类帮助我们实现了很多方法,比如泛型数组的增删查改
List的申明
命名空间:System.Collections.Generic
List<int> list = new List<int>();
List<string> list1 = new List<string>();
增删查改
增
增加一个元素 Add
list.Add(1);
list.Add(2);
拼接两个List
List<int> list2 = new List<int>();
list2.Add(100);
list2.Add(200);
遍历
Dictionary
Dictionary的本质
可以将Dictionary理解为拥有泛型的Hashtable。它也是基于键的哈希代码组织起来的键值对。键值对类型从Hashtable的object变为了可以自己定义的泛型。
Dictionary的申明
Dictionary<int, string> dict = new Dictionary<int, string>();
Dictionary的增删查改
增 Add
注意:使用Add添加元素时,不能重复出现相同的键
// 键不能有相同的,值可以有相同的
dict.Add(1, "ABC");
dict.Add(10, "ABC");
dict.Add(100, "ABCD");
删 Remove
只能通过键来删除
删除不存在的键,没反应
dict.Remove(1);
dict.Remove(99);
清空 Clear
dict.Clear();
查
通过键查
通过键查看值,找不到直接报错(与Hashtable不同)
可以通过TryGetValue方法,来避免报错。
string v;
if (dict.TryGetValue(10, out v))
{
Console.WriteLine("存在键为10的键值对");
}
判断是否存在
- 通过键判断 ConainsKey
if (dict.ContainsKey(99))
{
Console.WriteLine("存在键为99的键值对");
}
- 通过值判断 ContainsValue
if (dict.ContainsValue("ABC"))
{
Console.WriteLine("存在值为ABC的键值对");
}
改
通过键来修改,没有该键则为新增,有该键则为修改
dict[99] = "Hello";
Dictionary的遍历
遍历所有键
foreach (int key in dict.Keys)
{
Console.WriteLine(string.Format("{0}={1}", key, dict[key]));
}
遍历所有值
foreach (string value in dict.Values)
{
Console.WriteLine(value);
}
键值一起遍历
foreach (KeyValuePair<int, string> item in dict)
{
Console.WriteLine(string.Format("{0}={1}", item.Key, item.Value));
}
迭代器遍历法
Dictionary<int, string>.Enumerator dictEnumerator = dict.GetEnumerator();
while (dictEnumerator.MoveNext())
{
Console.WriteLine(string.Format("{0}={1}", dictEnumerator.Current.Key, dictEnumerator.Current.Value));
}
顺序存储与链式存储
Linedlist
委托和事件
委托
事件
什么是事件
事件是基于委托的存在,事件是委托的安全包裹,让委托的使用更具安全性,事件也是一种特殊的变量类型
事件的使用
语法:访问修饰符 event 委托类型 事件名
事件的使用方法跟委托基本一样,主要有以下几个区别:
- 事件只能作为类、接口、结构体的成员变量,不能在方法中定义为局部变量;委托可以
- 事件在类外部不能使用=赋值。 = null、= 方法都不行。但是可以使用+=、-=复合运算符;委托在类内部和外部的是方式没有区别
- 不能在类外部调用事件;委托在任何地方都可以调用。
class TestClass
{
public Action myDelegate;
public event Action myEvent;
public TestClass()
{
// 委托
myDelegate += MyFunc;
myDelegate += MyFunc1;
// 执行委托
myDelegate();
//myDelegate.Invoke();
// 清空委托
myDelegate = null;
// 事件
myEvent += MyFunc;
myEvent += MyFunc1;
// 执行事件
myEvent();
//myEvent.Invoke();
// 清空事件
myEvent = null;
}
private void MyFunc()
{
Console.WriteLine("MyFunc");
}
private void MyFunc1()
{
Console.WriteLine("MyFunc1");
}
}
// 类的外部委托和事件的区别
class TestClass1
{
public TestClass1()
{
TestClass tc = new TestClass();
// 正确!委托可以在类外部调用
tc.myDelegate();
// 正确!委托可以在类外部使用+=、-=等复合运算符
tc.myDelegate += MyFunc;
// 正确!委托可以在类外部使用赋值运算符
tc.myDelegate = null;
// 错误!事件不能在类外部调用
tc.myEvent();
// 正确!事件可以在类外部使用+=、-=等复合运算符
tc.myEvent += MyFunc;
// 错误!事件在类外部不能使用赋值运算符,虽然等价于tc.myEvent += MyFunc
tc.myEvent = tc.myEvent + MyFunc;
// 错误!事件在类外部能使用赋值运算符
tc.myEvent = null;
}
private void MyFunc()
{
}
}
为什么有事件
- 防止外部随意置空委托
- 防止外部随意调用委托
- 事件相当于对委托进行了一次封装,让其更加安全
匿名函数
什么是匿名函数
没有名字的函数叫匿名函数,匿名函数主要是配合委托和事件进行使用,脱离委托和事件,是不会使用匿名函数的。
基本语法
delegate(参数列表)
{
// 函数逻辑
};
在定义匿名函数的时候,必须使用委托变量来承接,不能直接定义,否则会报错。
// 正确写法
Action act = delegate(){
Console.WriteLine("Hello");
};
// 错误写法
delegate(){
Console.WriteLine("Hello");
};
无参无返回值的匿名函数
Action act = delegate() {
Console.WriteLine("Hello");
};
有参数无返回值的匿名函数
Action<string, int> act1 = delegate(string name, int age)
{
Console.WriteLine($"{name}的年龄为{age}");
};
无参有返回值的匿名函数
Func<int> rt = delegate()
{
return 100;
};
有参有返回值的匿名函数
// 两个参数,一个返回值
Func<int, int, int> sum = delegate(int a, int b)
{
return a + b;
};
匿名函数使用场景
- 当作函数的参数传递
- 作为函数返回值
class UnnameFuncTest
{
// 当作函数的参数传递
public void DoFunc(Action func)
{
func?.Invoke();
}
// 作为函数返回值
public Action GetFunc()
{
return delegate ()
{
Console.WriteLine("Hello!");
};
}
}
匿名函数的缺点
因为匿名函数没有名字,当一个委托中存有多个匿名函数,如果这些匿名函数没有被记录,无法从委托中删除指定的匿名函数。
Action act = null;
Action act1 = delegate()
{
Console.WriteLine("Action1");
};
Action act2 = delegate()
{
Console.WriteLine("Action2");
};
Action act3 = delegate()
{
Console.WriteLine("Action3");
};
act += act1;
act += act2;
act += act3;
act.Invoke();
Console.WriteLine("-------------------------");
// 只有匿名函数有被记录的情况下,才可以从委托中指定删除
act -= act2;
act.Invoke();
Lambad表达式
什么是Lambad表达式
可以将lambad表达式理解为匿名函数的简写,除了语法不同,使用上和匿名函数一模一样,都是和委托和事件配合使用的
Lambad表达式的语法
(参数列表) => {
// 函数逻辑
};
*lambad表达式的使用与匿名函数相同
闭包
内层的匿名(闭包)函数可以引用包含它外层的函数的变量,即使外层函数执行已经终止。该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
class ClosedFuncTest
{
private event Action func;
public ClosedFuncTest()
{
int index = 10;
// 正常情况下,index变量在执行完ClosedFuncTest的构造函数后会被回收,由于index变量被一个匿名函数包裹,在该匿名函数没有被销毁之前,index也不会销毁
func = () =>
{
Console.WriteLine(index);
};
for(int i = 0; i < 10; i ++)
{
// 当执行func时,输出的i的值为10个10,而不是从0-9
// 因为for循环中,i变量是变化的,当执行完for循环时,i的最终值为10
func += () => {
Console.WriteLine(i);
};
// 要想打印出0-9,可以这样处理。执行for循环会创建10个ii变量,交给闭包函数,每个ii变量是不同的。
int ii = i;
func += () => {
Console.WriteLine(ii);
};
}
}
public void ExecuteAction()
{
func();
}
}
ClosedFuncTest closedFuncTest = new ClosedFuncTest();
closedFuncTest.ExecuteAction();
List排序
List自带排序方法
使用List自带的Sort()方法,可以对int、float、double等List进行升序排序
List<int> list = new List<int>();
list.Add(1);
list.Add(100);
list.Add(-20);
list.Add(50);
// -20、1、50、100
list.Sort();
对自定义类的排序
如果List中的元素是自定义的类型,则不能直接使用Sort()方法进行排序,否则会报错。
之所以对于int这些List可以直接使用Sort进行排序,是因为int等这些结构体都实现了IComparable、IComparable接口,这些接口中的CompareTo方法实现了排序规则。因此,想要使用Sort()方法直接进行排序,只需将自定义类也实现这两个接口中的一个或多个,重写CompareTo方法即可。
class Item : IComparable<Item>
{
public int count;
public Item(int count)
{
this.count = count;
}
// 实现排序逻辑,返回-1排在前面,返回0位置不变,返回1排在后面。这里的逻辑实现的是升序排序。
public int CompareTo(Item other)
{
return this.count > other.count ? 1 : -1;
}
}
List<Item> list = new List<Item>();
list.Add(new Item(1));
list.Add(new Item(99));
list.Add(new Item(100));
list.Add(new Item(20));
list.Add(new Item(50));
// 升序排序
list.Sort();
通过委托进行排序
对于盛放自定义类的List,除了对自定义类实现IComparable接口外,还可以调用List的Sort重载,传入一个比较函数。
public void Sort(Comparison<T> comparison);
Comparison是一个两个参数一个返回值的委托:
public delegate int Comparison<in T>(T x, T y);
class Item
{
public int count;
public Item(int count)
{
this.count = count;
}
}
List<Item> list = new List<Item>();
list.Add(new Item(1));
list.Add(new Item(99));
list.Add(new Item(100));
list.Add(new Item(20));
list.Add(new Item(50));
// 匿名函数方式
list.Sort(delegate(Item a, Item b)
{
return a.count > b.count ? 1 : -1;
});
// Lambad表达式的方式
//list.Sort((a, b) => a.count > b.count ? 1 : -1);
协变逆变
什么是协变逆变
- 协变
协变:和谐的变化,自然的变化,因为里氏替换原则,父类可以装子类,所以子类变父类比如string变成object,感觉是和谐的。 - 逆变
逆常规的变化,不正常的变化。因为里氏替换原则,父类可以装子类,但是子类不能装父类,所以父类变为子类比如object变为string,感受是不和谐的。
协变和逆变是用来修饰泛型委托和泛型接口的,协变使用out关键字修饰泛型字符,逆变使用in关键字修饰泛型字符。
协变逆变的应用
规范泛型字母的作用
- 使用out修饰的泛型只能用作返回值
delegate T TestDelegateOut<out T>();
// !!!!!!错误
// delegate T TestDelegateIn<out T>(T t);
interface TestInterfaceOut<out T>
{
void T TestFunc();
// !!!!!!错误 T被out修饰,不能作为参数
// void T TestFunc(T t);
}
- 使用in修饰的泛型只能用作参数
delegate void TestDelegateIn<in T>(T t);
// !!!!!!错误
// delegate T TestDelegateIn<in T>(T t);
interface TestInterfaceIn<in T>
{
void TestFunc(T t);
// 错误,T被in修饰,不能作为返回值
// T TestFunc(T t);
}
父类、子类委托的承接
class Father
{
}
class Son : Father
{
}
// 协变
TestDelegateOut<Son> son = () =>
{
return new Son();
};
// 协变 父类委托承接子类委托(把子类当父类用)
TestDelegateOut<Father> father = son;
// 逆变 子类委托承接父类委托(把父类当子类用)
TestDelegateIn<Father> father1 = (Father f) =>
{
};
TestDelegateIn<Son> son1 = father1;
多线程
预处理器指令
反射和特性
反射
什么是程序集
程序集就是我们写的代码的一个集合,我们所写的所有代码最终都会被编译器翻译为一个程序集供别人使用。比如一个代码库文件(.dll)或者一个可执行文件(.exe)
元数据
程序中的类、类中的变量、属性、方法等信息就是程序的元数据。元数据保存在程序集中。
反射的概念
在程序运行时,通过反射可以得到其他程序集或者自己的程序集代码中的元数据,可以实例化他们、执行他们或者操作他们。
反射的作用
因为反射可以在程序编译后获得元数据信息,所以它提高了程序的拓展性和灵活性
- 在程序运行时可以获得所有的元数据,包括元数据的特性
- 程序运行时可以通过反射实例化对象、操作对象
- 程序运行时可以通过反射创建新对象,用这些对象执行任务
语法相关
Type
什么是Type
Type:表示类的信息类,它是反射功能的基础,是访问元数据的主要方式。使用Type的成员可以获取对应类型声明的信息,如构造函数、变量、属性、方法、事件等。
获取Type
// 方式1 通过对象的GetType方法获取Type 所有的对象都有GetType方法
int a = 100;
Type type = a.GetType();
// 方式2 通过typeof(类)方法获取Type
Type type2 = typeof(int);
// 方式3 通过Type.GetType(类名)的方式获取Type 注意需要带命名空间
Type type3 = Type.GetType("System.Int32");
通过这三种方式获取的同一个类型的Type指向的是同一个堆内存(即每一种类型的Type是唯一的)
通过Type获取元数据
通过Type获取类的程序集信息(Assembly) [type.Assembly]
int a = 100;
Type type = a.GetType();
Assembly assembly = type.Assembly;
通过Type获取类中所有的公共成员(MemberInfo) [type.GetMembers()]
public class ReflectionTestClass
{
private int i = 100;
public string j = "sss";
public ReflectionTestClass()
{
}
public ReflectionTestClass(int i)
{
this.i = i;
}
public ReflectionTestClass(int i, string j) : this(i)
{
this.j = j;
}
public void Speek()
{
Debug.Log("Speek");
}
public void Speek(string content)
{
Debug.Log($"Speek {content}");
}
}
// 获取ReflectionTestClass类的所有公共成员
Type type = typeof(ReflectionTestClass);
MemberInfo[] memberinfoList = type.GetMembers();
for(int i = 0; i < memberinfoList.Length; i ++)
{
Debug.Log(memberinfoList[i]);
}
通过Type获取类的所有构造函数(ConstructorInfo) [type.GetConstructors()]
Type type = typeof(RefrectionTestClass);
ConstructorInfo[] constructorList = type.GetConstructor();
通过Type获取类的其中一个构造函数并执行(ConstructorInfo)[type.GetConstructor(param)]
获取构造函数需要传入Type数组,数组中的内容是要获取的构造函数各个参数对应的类型
执行构造函数需要传入Object数组,数组中的内容是对应构造函数的各个参数值
// 获取ReflectionTestClass的无参构造函数
Type type = typeof(ReflectionTestClass);
ConstructorInfo constructor = type.GetConstructor(new Type[0]);
// 获取ReflectionTestClass的带一个参数的构造函数
ConstructorInfo constructor2 = type.GetConstructor(new Type[] { typeof(int) });
// 获取ReflectionTestClass的带两个参数的构造函数
ConstructorInfo constructor3 = type.GetConstructor(new Type[] { typeof(int), typeof(string) });
// 执行ReflectionTestClass的无参构造函数
ReflectionTestClass r = constructor.Invoke(null) as ReflectionTestClass;
// 执行ReflectionTestClass的带一个参数的构造函数
ReflectionTestClass r2 = constructor2.Invoke(new object[] { 1000 }) as ReflectionTestClass;
// 执行ReflectionTestClass的带两个参数的构造函数
ReflectionTestClass r3 = constructor3.Invoke(new object[] { 10000, "aaa" }) as ReflectionTestClass;
通过Type获取类中的公共成员变量
通过Type获取类中所有的公共成员变量(FieldInfo)[type.GetFields()]
Type type = typeof(ReflectionTestClass);
FieldInfo[] fieldList = type.GetFields();
通过Type获取指定名称的公共成员变量(FieldInfo)[type.GetField(变量名)]
Type type = typeof(ReflectionTestClass);
FieldInfo info = type.GetField("j");
通过Type获取类对象指定名称的公共成员变量并设置它的值
Type type = typeof(ReflectionTestClass);
FieldInfo info = type.GetField("j");
ReflectionTestClass cls = new ReflectionTestClass(1, "x");
// 获取属性值
string value = info.GetValue(cls) as string// value='x'
// 设置属性值
info.SetValue(cls, "w");// 设置为cls的j="w"
通过Type获取类的公共的成员方法
通过Type获取类的所有的公共成员方法(MethodInfo)[type.GetMethods()]
Type type = typeof(ReflectionTestClass);
MethodInfo[] methodList = type.GetMethods();
通过Type获取类的指定的公共成员方法(MethodInfo)[type.GetMethod(方法名, params)]
params需要对应方法的各个参数的类型
Type type = typeof(ReflectionTestClass);
// 获取不带参数的
MethodInfo method = type.GetMethod("Speek", new Type[0]);
// 获取带一个参数的
MethodInfo method2 = type.GetMethod("Speek", new Type[] { typeof(string) });
通过Type获取类对象指定的公共成员方法并执行(MethodInfo][Invoke]
ReflectionTestClass cls = new ReflectionTestClass(1, "x");
Type type = typeof(ReflectionTestClass);
MethodInfo method = type.GetMethod("Speek", new Type[0]);
MethodInfo method2 = type.GetMethod("Speek", new Type[] { typeof(string) });
// 执行不带参数的
method.Invoke(cls, null);
// 执行带一个参数的
method2.Invoke(cls, new object[] { "hello world" });
如果是静态方法,Invoke的第一个参数填null即可
其他
通过Type获取枚举(string) GetEnumNames GetEnumName
通过Type获取事件(EventInfo) GetEvents GetEvent
通过Type获取接口(Type) GetInterfaces GetInterface
通过Type获取属性(PropertyInfo) GetProperties GetProperty
Assembly
什么是Assembly
主要用来加载其他程序集,加载后,才能使用Type来获取其他程序集中的元信息。
加载Assembly
- 加载同一文件夹下的其他程序集
Assembly.Load(“程序集名称”) - 加载不同文件夹下的其他程序集
Assembly.LoadFrom(“程序集完全路径”)
或
Assembly.LoadFile(“程序集完全路径”)
通过Assembly获取所有Type
Assembly对象.GetTypes()
通过Assembly获取指定Type
Assembly对象.GetType(“包含命名空间的类型名”)
Activator
Activator的作用
用于将Type对象快捷实例化为对象
通过Activator创建类对象
无参构造 Activator.CreateInstance(Type)
Type type = typeof(ReflectionTestClass);
ReflectionTestClass cls = Activator.CreateInstance(type) as ReflectionTestClass;
有参构造 Activator.CreateInstance(Type, 可变长度参数)
Type type = typeof(ReflectionTestClass);
ReflectionTestClass cls = Activator.CreateInstance(type, 10, "hello") as ReflectionTestClass;
总结
通过Assembly可获取Type,Activator可创建Type类型的实例对象,Type可获取类型的所有公共成员并执行实例对象的公共方法
特性
什么是特性
特性是一种允许我们向程序的程序集添加元数据的语言结构,它用于保存程序结构信息的某种特殊类型的类。
特性提供功能强大的方法以将声明信息与C#代码(类型、方法、属性、变量)相关联。特性与程序实体关联后,即可在运行时使用反射查询特性信息。
特性的目的是告诉编译器把程序的某组元数据嵌入程序集中,它可以放置在几乎所有的声明中
通俗地说,特性的本质是一个类,我们可以利用特性类为元数据添加额外的信息。
比如为一个类、成员变量、成员方法、属性添加更多的额外信息。
之后可以通过反射来获取这些额外的信息。
自定义特性
所有的特性类都继承自Attribute类,类名以Attribute结尾,类中的内容根据具体需求来写。例如,下面写一个简单的描述自定义类信息的特性:
class DefineInfoAttribute : Attribute
{
private string info;
public DefineInfoAttribute(string info = "")
{
this.info = info;
}
public string GetInfo()
{
return info;
}
}
特性的使用
基本语法
[特性名(构造函数参数列表)],写在类、方法、变量、属性的上一行,参数的左边,表示他们具有该特性。特性名省略Attrbute字样。
本质上就是在调用特性类的构造函数。
使用定义个特性来修饰自定义类
[DefineInfo("这是一个自定义类,用来计算两个数的加法")]
class CustomClass
{
[DefineInfo("两个数相加的结果")]
public float result;
[DefineInfo("计算两个数相加的方法")]
public void Sum([DefineInfo("第一个数")]float a, [DefineInfo("第二个数")]float b)
{
result = a + b;
}
}
判断一个类是否被某个特性修饰 Type.IsDefined
CustomClass cls = new CustomClass();
Type clsType = cls.GetType();
// 第二个参数表示是否检查其父类
if (clsType.IsDefined(typeof(DefineInfoAttribute), false))
{
Console.WriteLine("类CustomClass被DefineInfoAttribute特性修饰");
}
获取一个类的所有特性 Type.GetCustomAttributes
CustomClass cls = new CustomClass();
Type clsType = cls.GetType();
if (clsType.IsDefined(typeof(DefineInfoAttribute), false))
{
Console.WriteLine("类CustomClass被DefineInfoAttribute特性修饰");
}
// 参数表示是否检查其父类
object[] allAttribute = clsType.GetCustomAttributes(true);
foreach (var item in allAttribute)
{
if (item is DefineInfoAttribute)
{
Console.WriteLine((item as DefineInfoAttribute).GetInfo());
}
}
同样的,除了类,属性、方法、变量也都可以获取到修饰他们的特性类,同样的调用其GetCustomAttributes方法即可获得。
限制自定义特性的使用范围 AttributeUsage
使用AttributeUsage特性来修饰特性,可以限制自定义特性的使用范围。Attributeusage也是一个特性。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
参数一:AttributeTargets【AttributeTargets】 特性能够用在什么地方
参数二:AllowMultiple【bool】 一个目标是否可以使用多个该特性修饰
参数三:Inherited 【bool】特性是否能被派生类重写或者成员继承
// 该特性只能修饰类或者结构体,一个目标可以使用该特性进行多次修饰,且可以继承
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
class DefineInfoAttribute : Attribute
{
private string info;
public DefineInfoAttribute(string info = "")
{
this.info = info;
}
public string GetInfo()
{
return info;
}
}
系统自带特性
过时特性 Obsolete
用于提示用户,使用的方法或者成员已经过时,建议使用新方法。
// 第一个参数用于显示调用该过时方法时提示的内容,第二个参数为false是,调用该过时方法时仅仅是警告,如果为true,调用该过时方法时会报错
[Obsolete("调用该过时方法时提示的内容", false)]
调用者信息特性 CallerFilePath、CallerLineNumber、CallerMemberName
提示函数被哪个文件[CallerFilePath]、哪一行的代码[[CallerLineNumber]]、哪个函数[[CallerMemberName]]调用
// 当使用特性时,filePath、line、target这三个参数填默认值即可。因为这三个值通过特性可以传递
public void Speak(string content, [CallerFilePath]string filePath = "", [CallerLineNumber]int line = 0, [CallerMemberName]string target = "")
{
Console.WriteLine(string.Format("该方法被文件{0}的第{1}行的{2}方法调用", filePath, line, target));
}
当在外部调用该方法时
条件编译特性 Conditional
它会和预处理器指令#define配合使用,主要可以用在一些调试代码上,有时想执行,有时不想执行的情况。
[Conditional("UNITY_EDITOR")]
public static void Func()
{
Console.WriteLine("function Func");
}
static void Main(string[] args)
{
Func();
Console.ReadKey();
}
当没有定义UNITY_EDITOR宏,就算是执行了Func方法也不会打印function Func
外部Dll包函数特性 DllImport
用来标记非.Net的函数,表明该函数在一个外部的Dll中定义。
一般用来调用C或者C++ Dll包写好的方法
需要使用static extern来修饰该方法。
// 把Test.dll中的Add方法引入到C#代码中,以达到可以在C#中调用非C#语言包中的方法。
[DllImport("Test.dll")]
public static extern int Add(int a, int b);
例如,在ToLua框架中,C#调用C包装中的方法:
迭代器
迭代器是什么
迭代器iterator有时又被称为光标cursor;是程序设计的软件设计模式,迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部标识。
从表现效果上看,是可以在容器对象(例如链表或者数组)上遍历访问接口,设计人员无需关心容器对象的内存分布的实现细节。
可以用foreach遍历的类,都是实现了迭代器的。
标准迭代器的实现方法
关键接口:IEnumerable、IEnumerator
命名空间:using System.Collections;
可以用过同时实现IEnumerable和IEnumerator这两个接口来实现标准迭代器。
class CustomList : IEnumerable, IEnumerator
{
private int[] list;
private int index = -1;
public CustomList(params int[] initlist)
{
list = initlist;
}
#region IEnumerable 如果写了这个方法,但是不继承IEnumerable,也是可以的
public IEnumerator GetEnumerator()
{
// 在这里重置index到初始位置
Reset();
return this;
}
#endregion
#region IEnumerator
public object Current
{
get
{
return list[index];
}
}
// 循环是否继续
public bool MoveNext()
{
return ++index < list.Length;
}
// 重置光标位置 一般在GetEnumerator方法中调用
public void Reset()
{
index = -1;
}
#endregion
}
CustomList list = new CustomList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// foreach的本质:
// 先通过GetEnumerator方法获取in后面这个对象的IEnumerator(获取一次)
// 每次foreach,执行得到这个IEnumerator对象中的MoveNext方法
// 只要MoveNext方法返回值为true,表示还有元素可以访问,就会去得到Current,然后赋值给item
foreach(int item in list)
{
Console.WriteLine(item.ToString());
}
用yield return语法糖实现迭代器
yield return是C#提供给我们的语法糖,所谓语法糖也称糖衣语法,主要作用是将复杂逻辑简单化,可以增加程序的可读性,从而减少程序代码出错的机会。
关键接口IEnumerable
命名空间:using System.Collections;
让想要通过foreach遍历自定义类实现接口中的方法GetEnumerator即可。
class CustomList1 : IEnumerable
{
private int[] list;
public CustomList1(params int[] initlist)
{
list = initlist;
}
public IEnumerator GetEnumerator()
{
foreach(int item in list)
{
// yield关键字配合迭代器使用,可以理解为暂时返回,保留当前的状态,外部的每次foreach执行一次yield return
yield return item;
}
}
}
CustomList1 list1 = new CustomList1(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 每次foreach的in操作执行一次yield return,从而能将当前的光标对应的值赋值给item
foreach(int item in list1)
{
Console.WriteLine(item.ToString());
}
用yield return语法糖为泛型类实现迭代器
class CustomList2<T> : IEnumerable
{
private T[] list;
public CustomList2(params T[] initlist)
{
list = initlist;
}
public IEnumerator GetEnumerator()
{
foreach(T item in list)
{
yield return item;
}
}
}
CustomList2<int> list2 = new CustomList2<int>(1, 2, 3, 4, 5, 6, 7, 8, 9);
foreach(int item in list2)
{
Console.WriteLine(item.ToString());
}
特殊语法
var隐式类型
var是一种特殊的变量类型,它可以用来表示任意类型的变量。
注意:
- var不能作为类成员,只能用于临时变量申明,也就是一般写在函数语句块中
- var必须初始化
- 使用var申明的变量类型在初始化后,其类型就确定了,后面不能被更改
设置对象初始值
申明对象时,可以通过直接写大括号的形式初始化对象部分或者全部公共变量和属性
class Person
{
private int id;
public Person()
{
}
public Person(int id)
{
this.id = id;
}
public string Name;
public int Age
{
get;
set;
}
}
// 初始化所有的公共成员
Person person = new Person(1) { Name = "小明", Age = 20 };
// 初始化部分的公共成员
Person person1 = new Person(2) { Name = "小红" };
// 如果通过不带参数的构造函数实例化对象,对象类型后可以不用加括号 Person person2 = new Person() { Name = "小李", Age = 22 };
Person person2 = new Person { Name = "小李", Age = 22 };
设置集合初始值
申明集合时,也可以通过大括号直接初始化
int[] ary = new int[]{ 1, 2, 3, 4, 5, 6 };
// 括号可以省略 List<int> list = new List<int>{ 1, 2, 3, 4, 5, 6 };
List<int> list = new List<int>(){ 1, 2, 3, 4, 5, 6 };
// 括号可以省略 Dictionary<int, string> dict = new Dictionary<int, string> { { 1, "1" }, { 2, "2" } };
Dictionary<int, string> dict = new Dictionary<int, string>() { { 1, "1" }, { 2, "2" } };
// 括号可以省略
Dictionary<int, Person> dict1 = new Dictionary<int, Person>() { { 1, new Person { Name = "小红", Age = 20 } }, { 2, new Person { Name = "小明", Age = 22 } } };
匿名类型
var可以申明为自定义的匿名类型
var v = new { age = 10 };
Console.WriteLine(v.age);
可空类型
值类型是不能赋值为空的
// 错误
int a = null;
申命值类型的时候在后面加上?,可以赋值为空
// 不会报错
int? a = null;
判断可空值类型是否为空
int? a = null;
if(a.HasValue)
{
// 或者Console.WriteLine(a.Value);
Consoloe.WriteLine(a);
}
安全获取可空值类型
int? a = null;
// 如果为空返回int的默认值
Console.WriteLine(a.GetValueOrDefault());
// 如果为空返回指定的值 这里并不会将50赋值给a,a的值还是为空
Console.WriteLine(a.GetValueOrDefault(50));
安全执行空的引用类型的方法
object o = null;
// 加了?之后,如果o为空,则不会执行其ToString()方法
Console.WriteLine(o?.ToString());
Action action = null;
if(action != null)
{
action();
}
// 等效于上面的写法
action?.Invoke();
空合并操作符
空合并操作符:??
左边值 ?? 右边值
如果左边值为空,则返回右边值,否则返回左边值
可以适用于可空值类型以及引用类型
// 可空值类型
int? a = null;
// 如果a为空,返回100,否则返回a
int b = a ?? 100;
// 引用类型
string str = null;
Console.WriteLine(str ?? "Hello World");
内插字符串
关键字:$
用$来构造字符串,让字符串中可以拼接变量
int age = 20;
string name = "小明";
Console.WriteLine($"{name}的年龄为{age}");
单句逻辑简略写法
if、while或者for等循环语句
如果后面只有一句代码时,可以省略{}
if(true)
Console.WriteLine("Hello");
for(int i = 0; i < 10; i ++)
Console.WriteLine("Hi");
while(true)
Console.WriteLine("Hello World!");
属性和方法
如果属性里面只有一句代码,可以省略{},使用=>代替,其中get满足缩略写法的时候不需要写return
如果方法中只有一句代码,可以省略{},使用=>代替。如果有返回值,不需要写return
Class Person
{
public Person()
{
}
private string name;
public string Name
{
// 注意:这里不能写return,否则会报错
get => name;
set => name = value;
}
// 不需要写return,否则会报错。
public string ToString() => $"名字叫{name}";
}