C#入门篇
EanoJiang/CSharp-: C#入门教程,自用
程序思维题:
两根不均匀的香,烧完一根是1h,怎么用来计时15min呢?
思路:一根香从两头同时点燃烧完是30min,只需再对半即可,那么怎么对半呢?可以在第一根香两端同时点燃的时候也点燃第二根香的一端,这样,当第一根香烧完的时候第二根香还剩30min可以烧,这时候再点燃第二根香的另一端,开始计时,烧完则是在30min可烧的时间再次对半,即为15min。
C#基本结构
- /// /// 引用命名空间(工具包),相当于头文件/// using System;/// /// 命名空间(工具包),相当于自定义的头文件/// namespace lesson1;/// /// 类/// class Program{ /// /// 主函数 /// /// static void Main(string[] args) { ////WriteLine(),Write() //Console.WriteLine("自带换行的print"); //Console.Write("不带换行的print"); //Console.Write("你看就没有换行"); ////ReadLine():输入完需要按回车 //Console.WriteLine("请输入:"); //Console.ReadLine(); //Console.WriteLine("over"); //ReadKey():输入一个键,自动回车 Console.ReadKey(); Console.WriteLine("\nover"); }}
复制代码 注释
三杠注释:用来注释 类、函数其他注释和c++一样
控制台函数
控制台输入:Console.ReadLine()、Console.ReadKey()
控制台输出:Console.WriteLine()、Console.Write()
//WriteLine():打印信息后换行
//Write():打印信息后不换行- Console.WriteLine("自带换行的print");Console.Write("不带换行的print");Console.Write("你看就没有换行");
复制代码 //ReadLine():检测用户的一系列输入,回车结束
输入完需要按回车才结束- Console.WriteLine("请输入:");Console.ReadLine();Console.WriteLine("over");
复制代码 //ReadKey():检测用户的一键输入,立刻结束
输入一个键,就结束- //ReadKey():输入一个键,自动回车Console.ReadKey();Console.WriteLine("\nover");
复制代码 变量
- //折叠代码# region 这是一段折叠的代码# endregion
复制代码- #region 变量//有符号(signed)的整型变量//sbyte -2^7 ~ 2^7-1 1字节//short -2^15 ~ 2^15-1 2字节//int -2^31 ~ 2^31-1 4字节//long -2^63 ~ 2^63-1 8字节//无符号(unsigned)的整型变量//byte 0 ~ 2^8-1 1字节//ushort 0 ~ 2^16-1 2字节//uint 0 ~ 2^32-1 4字节//ulong 0 ~ 2^64-1 8字节//浮点型变量//float 4字节,精度7位有效数字//不加f后缀,默认是double类型float f1 = 1.221312f;//如果是一个整数也可以用float定义,且可以不写f后缀float myHeight = 183;//double 8字节,精度15位有效数字double d1 = 1.2213124211249;//decimal 16字节,精度28位有效数字//其他类型变量//bool 1字节,true或falsebool b1 = true;bool b2 = false;//可以与字符串相加Console.WriteLine(b1 + "and" +b2);//char 2字节,表示一个字符,用''char c1 = 'a';//string 字符串,没有上限,用""string str1 = "hello world";//注意修改变量直接再赋值就行了#endregion
复制代码- #region 潜在知识点//拼接输出int num = 1;Console.WriteLine("num is :" + num);#endregion
复制代码
- 变量初始化:声明完变量最好立刻赋值。
- 内存空间大小:
- // sizeof()不能用来计算string类型,其他都可以
复制代码 题目:
- //有符号整型 字节 (位数=字节数*8)sbyte 1short 2int 4long 8//无符号整型byte 1ushort 2uint 4ulong 8//浮点数float 4double 8decimal 16//其他bool 1char 2string
复制代码- 驼峰命名:变量int playerName;帕斯卡命名:类,函数public void PlayerJump(){}class PlayerJumpState{}
复制代码 常量
- #region 常量//关键字:const//声明的时候要带上类型//必须初始化//常量不能修改const float PI = 3.1415926f;const string userName = "飞舞";#endregion
复制代码 常量:用来声明一些不想被修改的变量
转义字符
- #region 转义字符// \' 单引号// " 双引号Console.WriteLine("123\'123");Console.WriteLine("123"123");// \\ 反斜杠Console.WriteLine("123\\123");// \n 换行Console.WriteLine("123\n123");// \t 制表符Console.WriteLine("123\t123");// \b 退格Console.WriteLine("123\b123");// \f 换页 就是上下行文本错开Console.WriteLine("123\f123");// \0 空字符Console.WriteLine("123\0123");// \a 系统警报音Console.WriteLine("123\a123");// 取消转义字符Console.WriteLine(@"123\a123");#endregion
复制代码 类型转换
1. 隐式转换
规则:大范围装小范围- #region 隐式转换————同一大类型之间 //有符号 long l = 1; int i = 1; short s = 1; sbyte sb = 1; //下面用隐式转换:大范围装小范围的类型 long -> int -> short -> sbyte l = i; l = s; l = sb; //如果反过来装,则会数据溢出,报错 //比如:i = l;错误 //无符号 ulong ul = 1; uint ui = 1; ushort us = 1; byte b = 1; //也是大范围装小范围的类型 ulong -> uint -> ushort -> byte //浮点数 decimal de = 1.1m; double d = 1.1; float f = 1.1f; //decimal类型不能隐式转换,不能用来存储double和float //比如:de = d; 错误 //但是float和double可以隐式转换 double -> float d = f; //特殊类型 bool char string //不是同一大类型,不存在隐式转换 #endregion
复制代码- #region 隐式转换————不同大类型之间 #region 无符号和有符号之间 //无符号 byte b2 = 1; ushort us2 = 1; uint ui2 = 1; ulong ul2 = 1; //有符号 sbyte sb2 = 1; short s2 = 1; int i2 = 1; long l2 = 1; //无符号装有符号 装不了,因为无符号不存在符号位 //比如:b2 = sb2; 错误 //有符号装无符号 能隐式转换的前提是有符号的范围要更大,才能装下无符号的范围 i2 = b2; l2 = i2; // 比如:i2 = ui2; 错误 #endregion #region 整型和浮点型之间 //浮点数 float f2 = 1.1f; double d2 = 1.1; decimal de2 = 1.1m; //浮点数装整数 浮点数可以装任意整数 还是大范围装小范围 //decimal虽然不能隐式存储double和float,但是可以隐式存储整形 f2 = i2; de2 = i2; /*总结*/ // double -> float -> 所有整形(有无符号都行) // decimal -> 所有整形(有无符号都行) //整数装浮点数 不行,因为整数的范围比浮点数的范围小,而且整数也没小数位置 #endregion #region 特殊类型和其他类型之间 bool bo2 = true; char c2 = 'a'; string str2 = "hello"; //bool 不能和其他类型 相互隐式转换 // i2 = bo2; // bo2 = i2; // 均报错 //char 不能隐式转换成其他类型,但是可以隐式转换成整形浮点型大范围的类型 // c2 = i2; 报错 i2 = c2; f2 = c2; //string 不能和其他类型 相互隐式转换 // str2 = i2; // i2 = str2; // 均报错 #endregion #endregion
复制代码 题目:
哈哈,这里出现了一个搞心态的markdown bug,就是如果图片地址有特殊符号,比如这个md文件名有#符号,那粘贴过来的图片会显示地址不存在。- //作业: int tang = '唐'; int lao = '老'; int shi = '狮'; Console.WriteLine("名字:"+tang+lao+shi);//前面是字符串,后面相连也就是字符串拼接
复制代码 2.显式转换
2.1 括号强转
(目标类型)源类型变量名- #region 显式转换————括号强转 //用于:将高精度的类型强制转换为低精度的类型 // 低精度 装 高精度, 大范围存小范围 //语法: (目标类型) 源类型变量名 // long l1 = 1; // int i1 = (int) l1; //long l1 = (long) i1; 错误,低精度不能强转高精度,也就是高精度不能存放低精度 //注意:精度问题(浮点数) 范围问题 //相同大类的整形 sbyte sb1 = 1; short s1= 1; int i1 = 1; long l1 = 1; s1 = (short)i;//小存大会因为范围产生异常,但不会报错 //浮点数 float f1 = 1.1f; double d1 = 1.1124234213f; decimal de1 = 1.1m; f1 = (float)d1; //小存大会精度丢失,但不会报错 Console.WriteLine(f1); //无符号和有符号 uint ui1 = 1; i1 = (int)ui1; Console.WriteLine(i1); i1 = -1; ui1 = (uint)i1; //无符号存有符号,会因为缺少符号位产生异常,但不会报错 Console.WriteLine(ui1); //浮点和整形 i1 = (int)f1;//整形存浮点会精度丢失 Console.WriteLine(i1); f1 = (float)i1;//浮点存整形肯定没问题 Console.WriteLine(f1); //char和数值类型 i2 = 'a'; char c = (char)i2;//对应ASCII码转字符,来回都能转 Console.WriteLine(c); f1 = 97.2f; c = (char)f1;//char存浮点数,会自动舍去小数位后映射到ASCII码 Console.WriteLine(c); //bool 和 string 都不能通过括号强转 bool bo1 = true; // i1 = (int)bo1;//报错 string str1 = "123"; // i1 = (int)str1;//报错 #endregion
复制代码 2.2 Parse 法
目标类型.Parse(字符串)- #region 显式转换————Parse法 //作用: 把string转换成其他类型(前面有提到,string不能括号强转) //语法: 目标类型.Parse(string类型变量名) // 目标类型.Parse("字符串") //注意:字符串必须能够转换成对应类型才行,否则会报错 //整形 int i4 = int.Parse("123"); Console.WriteLine(i4); //i4 = int.Parse("123.45"); //异常了,报错,编译不通过 //Console.WriteLine(i4); // short s4 = short.Parse("6666666"); //超出范围,报错 // Console.WriteLine(s4); //浮点型 和上面一样 // bool 字符串必须是true或false,否则会报错 bool b5 = bool.Parse("true"); Console.WriteLine(b5); // char 字符串必须是单个字符,否则会报错 char c5 = char.Parse("a"); Console.WriteLine(c5); #endregion
复制代码 2.3 Convert 法
Convert.To目标类型(源类型变量名/常量名)- #region 显式转换————Convert法 //作用: 更准确地在各个类型之间转换 //语法: Convert.To目标类型(源类型变量名/常量名) // Convert.ToInt32() // Convert.ToInt16() 相当于short // Convert.ToInt64() 相当于long // Convert.ToSingle() Single就是单精度,相当于float // Convert.ToDouble() 相当于double // Convert.ToDecimal() 相当于decimal // Convert.ToSByte() // Convert.ToByte() // Convert.ToBoolean() // Convert.ToChar() // Convert.ToString() //注意: 填写的变量/常量必须是可以转换的类型,否则会报错 //Conver.ToInt32(string) int i3 = Convert.ToInt32("123"); Console.WriteLine(i3); //Conver.ToInt32(浮点数) 会四舍五入 i3 = Convert.ToInt32(1.5f); Console.WriteLine(i3); //Conver.ToInt32(bool) i3 = Convert.ToInt32(true); Console.WriteLine(i3); i3 = Convert.ToInt32(false); Console.WriteLine(i3); //其他类型也能转 bool b3 = Convert.ToBoolean(312); Console.WriteLine(b3); #endregion
复制代码 2.4 ToString()
其他类型转成字符串
源类型变量.toString()- #region 显式转换————其他类型转string //作用:拼接打印 //语法: 源变量.ToString() string str3 = 1.ToString(); Console.WriteLine(str3); string str4 = true.ToString(); Console.WriteLine(str3); string str5 = 1.2f.ToString(); Console.WriteLine(str3); //下面两个是等价的 Console.WriteLine("1"+true+1.2f);//实际运行的时候自动调用toString()方法 Console.WriteLine(str3+str4+str5); #endregion
复制代码题目:
4种:
括号强转 int i1 = (int)"123";
Parse法 i1 = int.Parse("123");
Convert法 i1 = Convert.ToInt32(1.2f);
ToString()法 string st1 = 1.ToString();
注意是转成字符,不是字符串- //char只能隐式转换成其他大范围的类型,而不能隐式存放其他类型 char c1 = (char)24069; Console.WriteLine(c1); c1 = Convert.ToChar(24069); Console.WriteLine(c1);
复制代码- //题目3: Console.WriteLine("请按语文数学英语的顺序,输入三门成绩:"); Console.WriteLine("输入语文成绩:"); int chinese = Convert.ToInt32(Console.ReadLine()); //或者:int chinese = int.Parse(Console.ReadLine()); Console.WriteLine("输入数学成绩:"); int math = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("输入英语成绩:"); int english = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("Chinese: {0}\nMath: {1}\nEnglish: {2}",chinese,math,english); //或者:Console.WriteLine("Chinese:"+chinese+"\n"+"Math:"+math+"\n"+"English:"+english);
复制代码 异常捕获
- #region 语法 try{ Console.WriteLine("请输入:"); string str1 = Console.ReadLine(); int i1 = int.Parse(str1); Console.WriteLine(i1); } catch{ Console.WriteLine("你输入的不合法"); } finally{ Console.WriteLine("请输入合法数字!!!"); } #endregion
复制代码练习题:
- try{ Console.WriteLine("请输入一个数字:"); string str2 = Console.ReadLine(); int i2 = int.Parse(str2); } catch{ Console.WriteLine("你输入的不合法"); }
复制代码- try{ Console.WriteLine("请输入姓名:"); string str3 = Console.ReadLine(); Console.WriteLine("请输入成绩1:"); string str4 = Console.ReadLine(); int i4 = int.Parse(str4); Console.WriteLine("姓名:" + str3 + " 成绩1:" + i4 + "\n"); } catch{ Console.WriteLine("你输入成绩1不合法"); } try{ Console.WriteLine("请输入成绩2:"); string str5 = Console.ReadLine(); int i5 = int.Parse(str5); Console.WriteLine(" 成绩2:" + i5 + "\n"); } catch{ Console.WriteLine("你输入成绩2不合法"); }
复制代码
可以知道哪一步不合法
运算符
算术运算符
除法:/
整形的除法运算会丢失小数部分,要用浮点数存储要在运算时有浮点数特征,比如其中一个数加上f后缀⬇️取余数:%
整数和浮点数可以取余数,bool等其他类型不能- float a = 4.11f % 3.11f; Console.WriteLine(a);
复制代码 优先级
先乘除取余 后 加减
自增自减
- int a1 = 1, a2 = 1; //先用后变 Console.WriteLine(a1++ +" "+ a2--); int a3 = 1, a4 = 1; //先变后用 Console.WriteLine(++a3 +" "+ --a4);
复制代码
练习题:
法1:中间商- int a = 1,b = 2;int temp = a;a = b;b = temp;
复制代码 法2:加减法(节省一个变量)- int a = 1,b = 2;a = a + b;b = a - b;a = a - b;
复制代码- #region 练习题 int seconds = 987652; int one_day = 60 * 60 * 24; int one_hour = 60 * 60; int one_minute = 60; int one_second = 1; Console.WriteLine(seconds/one_day+"天"+seconds%one_day/one_hour+"小时"+seconds%one_hour/one_minute+"分"+seconds%one_minute+"秒"); #endregion
复制代码 字符串拼接
只用+和+=
- #region 字符串拼接 string str3 = "1"; str3 += "2"; Console.WriteLine(str3); str3 += 1; Console.WriteLine(str3); str3 += 1 + 2; Console.WriteLine(str3); str3 += "" + 3 ; Console.WriteLine(str3); #endregion
复制代码- str3 = ""; str3 += 1 + 2 + "" + 2 + 3; Console.WriteLine(str3);
复制代码
先计算""前面的,再和""以及后面的拼接
string.Format()
string.Format("待拼接的内容",内容0,内容1, ...);- #region 拼接法2 //语法:string.Format("格式化字符串",参数1,参数2,参数3...) //格式化字符串里想要拼接的内容用占位符{i}替代,从0开始依次往后 string str4 = string.Format("我是{0},今年{1}岁,身高{2}cm","sb",18,180); Console.WriteLine(str4); #endregion
复制代码 控制台打印拼接
- #region 控制台打印拼接 Console.WriteLine("我是{0},今年{1}岁,身高{2}cm", "sb", 18, 180); #endregion
复制代码 注意:后面的内容0,1,...可以多填(只是不拼接),但不能少填(会报错)
条件运算符
特殊类型char string bool只能同类型== 或 !=
char可以和自己或数值类型比较(ascii码)
逻辑运算符
- #region 逻辑与 && bool result1 = true && false; Console.WriteLine(result1); // false #endregion #region 逻辑或 || bool result2 = true || false; Console.WriteLine(result2); // true #endregion #region 逻辑非 ! bool result3 = !true; Console.WriteLine(result3); // false #endregion
复制代码 优先级:! > 算数运算符 > && > ||
逻辑运算符短路规则
- #region 短路规则 int i3 = 1; bool result = i3 > 0 || ++i3 >0; Console.WriteLine(i3); // 1 result = i3 < 0 && ++i3 >0; Console.WriteLine(i3); // 1 #endregion
复制代码 ||是有真则真,如果左边就是真,就跳过后面。
&&是有假则假,如果左边就是假,就跳过后面。
位运算符
按位与&,按位或|,按位取反~,异或^,左移
也就是换成二进制后进行位运算,最后结果再转回十进制
异或^:不同为1,相同为0- Console.WriteLine(1 ^ 5);// 001//^101// 100 结果4
复制代码 按位取反~
补码和原码是互逆的,操作都是反码+1
反码:负数除符号位按位取反,正数不变- #region 位运算符 // 位取反 ^ int a = 5; // 0000 0000 0000 0000 0101 // 1111 1111 1111 1111 1010 这样按位取反得到的是补码,还需要找到其原码 // 1000 0000 0000 0000 0101 + 1 // 1000 0000 0000 0000 0110 而最高位符号位是1,所以是-6 Console.WriteLine(~a); // -6 #endregion
复制代码 按位左移右移>
左移几位 右侧加几个0
右移几位 右侧去掉几个数
三目运算符
条件语句 ? 条件为真返回内容1 : 条件为假返回内容2;
注意:和c语言不一样的地方
三目运算符有返回值,必须使用(也就是必须赋值给变量)
返回值的类型必须一致- #region 三目运算符 string str4 = (1>0)?"大于0":"小于等于0"; Console.WriteLine(str4); // 大于0 // str4 = (1>0)?"大于0":1; 错误,返回值类型不统一 #endregion
复制代码 if语句
习题
打印语句写进if里面或者在if前面定义b才行,因为b这个变量是在if里面定义的
switch语句
- // switch(变量){ // case 值1: // //变量==值1则执行代码块1; // break; // case 值2: // //变量==值2则执行代码块2; // break; // default: // //默认代码块; // break; // }
复制代码 注意:switch语句中的值1,2,...必须是常量,不能条件语句
可以自定义常量
用const自定义常量,然后把这个常量名放在值1,2,...中- char c = 'a'; const char c2 = 'a'; switch(c) // switch语句 { case c2: Console.WriteLine("c等于c2"); break; }
复制代码 贯穿
如果某几个case所要执行的语句一样,可以只在最后一个case中写即可- int c = 0; switch(c) // switch语句 { case 1: case 2: case 3: Console.WriteLine("哈哈哈"); break; case 4: Console.WriteLine("呵呵呵"); break; default: Console.WriteLine("啥也没有"); break; }
复制代码 注意:和c语言不同的地方
写一个case必须跟一个break,不能省略
循环语句
while
注意:
在每个循环体/if语句内定义的变量是局部变量,在外面不能使用,因此每个循环体内的变量之间没有关系,可以重复变量名,但最好不要这样做
流程控制关键字:
break: 跳出while循环
continue: 跳回循环开始,也就是判断条件处继续执行- #region while // 流程控制关键字break和continue while(true){ Console.WriteLine("break前"); break; Console.WriteLine("break后"); } Console.WriteLine("循环体外"); #endregion
复制代码
注意break跳出的是while循环
但是嵌套语句里有for/switch的时候break是和for/switch配套的,这时候就不是跳出while了,但是continue是和while配套的- // 流程控制关键字break和continue int i = 0; while(true){ ++i; Console.WriteLine(i); if(i==3){ break; } } Console.WriteLine(i);
复制代码- while(true){ Console.WriteLine("continue前"); continue; Console.WriteLine("continue后"); } Console.WriteLine("循环结束");
复制代码 结果会一直输出"continue前"
题目
- // 题目:打印1~20的奇数 int i = 0; while(i = arr[i]) ? max : arr[i]; min = (min b? a : b; } #endregion #region 2 static int Max(params int[] numbers){ int max = numbers[0]; for(int i = 0; i < numbers.Length; i++){ max = (numbers[i] > max)? numbers[i] : max; } return max; } static double Max(params double[] numbers){ double max = numbers[0]; for(int i = 0; i < numbers.Length; i++){ max = (numbers[i] > max)? numbers[i] : max; } return max; } #endregion static void Main(string[] args) { Console.WriteLine(Max(1, 2)); Console.WriteLine(Max(1.0, 2.0)); Console.WriteLine(Max(1, 2, 3, 4, 5)); Console.WriteLine(Max(1.0, 2.0, 3.0, 4.0, 5.0)); }}
复制代码 递归
必须有结束掉用的条件- static void Fun(int n0 = 1,int n1 = 10){ if(n1 < n0)return; Console.WriteLine(n1); n1--; Fun(n0,n1);}Fun(1,10);
复制代码习题
 - #region 1static void PrintNum(int n0, int n1){ if(n1 < n0){ return; } Console.WriteLine(n1); // n1--; //这里要用前缀--n1,先减后用,不然会出现无限递归导致栈溢出 PrintNum(n0, --n1);}#endregion#region 2static int Factorial(int n){ if(n == 1) return n; return n * Factorial(n-1); // 5 * Fun2(4) = 5 * 4 * 3 * 2 * 1 // 4 * Fun2(3) = 4 * 3 * 2 * 1 // 3 * Fun2(2) = 3 * 2 * 1 // 2 * Fun2(1) = 2 * 1 // 1}#endregion#region 3static int SumOfFactorial(int n){ if(n == 1) return Factorial(n); return Factorial(n) + SumOfFactorial(n-1);}#endregion#region 4static float getFinalLength(float length,int days){ //从第0天开始,所以days要+1 if(days+1 == 0) return length; //这里要用前缀--days,先减后用,不然会出现无限递归导致栈溢出 return getFinalLength(length/2.0f,--days);}#endregion#region 5static bool PrintNum2(int n0, int n1){ // if(n1 < n0){ // return; // } Console.WriteLine(n1); // n1--; //这里要用前缀--n1,先减后用,不然会出现无限递归导致栈溢出 return n1 < n0 || PrintNum2(n0, --n1);}#endregionPrintNum(0, 10);Console.WriteLine("阶乘5= "+Factorial(5));Console.WriteLine("!1 + ... + !10 = "+SumOfFactorial(10));Console.WriteLine("100 的最终长度为: "+getFinalLength(100, 10));PrintNum(0, 200);
复制代码 结构体
结构体相当于一个人,他的变量相当于人的各个属性,方法相当于人的各个功能函数
语法
- 写在namespace语句块中
- 关键字struct
- 帕斯卡命名法
- struct 结构体名{ //1. 变量 int 变量名; //2. 构造函数 结构体名(int 变量名){ this.变量名 = 变量名; } //3. 方法 void 方法名(){ //... } }
复制代码 访问修饰符
修饰结构体中的变量和方法 是否能被外部使用
public 可以被外部访问
private 只能在内部使用
默认不写,就是private
结构体的构造函数
- 没有返回值
- 函数名和结构体名相同
- 必须有参数
- 如果申明了构造函数,那就必须在其中对所有变量数据初始化
使用
- namespace 结构体;class Program{ #region 语法 // struct 结构体名{ // //1. 变量 // int 变量名; // //2. 构造函数 // 结构体名(int 变量名){ // this.变量名 = 变量名; // } // //3. 方法 // void 方法名(){ // //... // } // } #endregion #region 示例 struct Student{ //1. 变量 //结构体申明的变量 不能直接在结构体里面初始化 //变量类型任意,包括结构体类型,但是只能是其他结构体类型,不能是自身结构体类型 public int age; public bool sex; //true表示男性,false表示女性 public string name; public Teacher teacher1; // Student student1; //错误,不能是自身结构体类型 //2. 构造函数 //用于在外部初始化结构体变量 public Student(int age, bool sex, string name, Teacher teacher1){ this.age = age; this.sex = sex; this.name = name; this.teacher1 = teacher1; } //3. 方法 //用来表现这个数据结构的行为,在结构体中不需要加static关键字 //函数中可以直接使用结构体申明的变量 public void Speak(){ Console.WriteLine("Hi, my name is {0}, I am {1} years old.", name, age); } } struct Teacher{ } #endregion static void Main(string[] args) { #region 结构体的使用 Student s1; s1.age = 20; s1.sex = true; s1.name = "Tom"; s1.Speak(); //用构造函数的方式初始化结构体变量 Student s2 = new Student(25, false, "Jerry", new Teacher()); s2.Speak(); #endregion }}
复制代码习题
 - namespace 结构体习题{ class Program { #region 1 struct Student{ public string name; public int age; public bool isMale; public int classNum; public string subject; public Student(string name, int age, bool isMale, int classNum, string subject){ this.name = name; this.age = age; this.isMale = isMale; this.classNum = classNum; this.subject = subject; } public void PrintInfo(){ Console.WriteLine("Name:{0}, Age:{1}, Gender:{2}, Class:{3}, Subject:{4}", name, age, isMale? "Male" : "Female", classNum, subject); } } #endregion #region 3 struct Rectangle{ public int height; public int width; public Rectangle(int height, int width){ this.height = height; this.width = width; } public void PrintInfo(){ Console.WriteLine("Rectangle length: {0}, width: {1}, area: {2}, perimeter: {3}", height, width, height * width, 2 * (height + width)); } } #endregion #region 4 enum Occupation{ /// /// 战士 /// warrior, /// /// 法师 /// mage, /// /// 猎人 /// hunter, } struct Player{ public string playerName; public Occupation playerOccupation; public Player(string playerName, Occupation playerOccupation){ this.playerName = playerName; this.playerOccupation = playerOccupation; } public void PrintAtkInfo(){ string occupation = ""; string skill = ""; switch(playerOccupation){ case Occupation.warrior: occupation = "战士"; skill = "冲锋"; break; case Occupation.mage: occupation = "法师"; skill = "奥术攻击"; break; case Occupation.hunter: occupation = "猎人"; skill = "假死"; break; } Console.WriteLine("{0}{1}释放了{2}", occupation, playerName,skill); } } #endregion #region 5 struct Monster{ public string name; public int attack; public Monster(string name, int attack){ this.name = name; this.attack = attack; } public void PrintInfo(){ Console.WriteLine("{0}的攻击力为{1}", name, attack); } } #endregion #region 7 struct OutMan{ public string name; public int attack; public int hp; public int defence; public OutMan(string name, int attack, int hp, int defence){ this.name = name; this.attack = attack; this.hp = hp; this.defence = defence; } public void PrintInfo(){ Console.WriteLine("{0}的攻击力为{1}", name, attack); } public void Attack(ref Boss boss) { if (boss.attack > defence) { boss.hp -= (attack - boss.defence); Console.WriteLine("{0}攻击了{1}, 造成{2}点伤害, {3}剩余{4}点血量", name, boss.name, attack - boss.defence, boss.name, boss.hp); } else { Console.WriteLine("{0}闪避了{1}的攻击", name, boss.name); } } } struct Boss{ public string name; public int attack; public int hp; public int defence; public Boss(string name, int attack, int hp, int defence){ this.name = name; this.attack = attack; this.hp = hp; this.defence = defence; } public void PrintInfo(){ Console.WriteLine("{0}的攻击力为{1}", name, attack); } public void Attack(ref OutMan outman) { if (outman.attack > defence) { outman.hp -= (attack - outman.defence); Console.WriteLine("{0}攻击了{1}, 造成{2}点伤害, {3}剩余{4}点血量", name, outman.name, attack - outman.defence, outman.name, outman.hp); } else { Console.WriteLine("{0}闪避了{1}的攻击", name, outman.name); } } } #endregion static void Main(string[] args) { //1 Student s1 = new Student("John", 18, true, 101, "Math"); s1.PrintInfo(); //2 // private只能在类内部访问 // public可以在类外部访问 //3 Rectangle r1 = new Rectangle(5, 10); r1.PrintInfo(); //4 Player p1 = new Player("唐老师", Occupation.hunter); p1.PrintAtkInfo(); //6 Monster[] monstersName = new Monster[10]; Random r = new Random(); for(int i = 0; i < 10; i++){ //用结构体构造函数初始化每个怪物的名字: // monstersName[i].name 、 monstersName[i].attack monstersName[i] = new Monster("怪物" + i, r.Next(100)); monstersName[i].PrintInfo(); } //7 OutMan outMan = new OutMan("路飞", 50, 100, 55); Boss boss = new Boss("索隆", 60, 200, 30); while(outMan.hp > 0 && boss.hp > 0){ outMan.Attack(ref boss); boss.Attack(ref outMan); } } }}
复制代码 排序
冒泡排序
- //冒泡排序static int[] BubbleSort(int[] arr){ // 数组几个数就需要进行n轮冒泡 for(int n=0;narr[i+1]){ arr[i] = arr[i] + arr[i+1]; arr[i+1] = arr[i] - arr[i+1]; arr[i] = arr[i] - arr[i+1]; } } } return arr;}int[] arr = { 3, 5, 8, 6, 2, 7, 1, 4};BubbleSort(arr);Console.WriteLine(string.Join(",", arr));
复制代码- //优化后的冒泡排序static int[] BubbleSort2(int[] arr){ // 数组几个数就需要进行n轮冒泡 for(int n=0;narr[i+1]){ int tmp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = tmp; //每次交换后,isSwap置为true isSwap = true; } } //如果本轮没有发生交换,说明已经排序好了,即刻退出循环 if(!isSwap)break; } return arr;}int[] arr2 = { 3, 5, 8, 6, 2, 7, 1, 4};BubbleSort2(arr2);Console.WriteLine(string.Join(",", arr2));
复制代码习题
- #region 1static void BubbleSortUp(int[] arr){ for(int n = 0; n < arr.Length; n++){ bool isSwap = false; for(int i = 0; i < arr.Length - 1 - n; i++){ //大于后面的就换位置,也就是大的放后面 if(arr[i] > arr[i+1]){ int temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = temp; isSwap = true; } } if(!isSwap) break; }}static void BubbleSortDown(int[] arr){ for(int n = 0; n < arr.Length; n++){ bool isSwap = false; for(int i = 0; i < arr.Length - 1 - n; i++){ //小于后面的就换位置,也就是小的放后面 if(arr[i] < arr[i+1]){ int temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = temp; isSwap = true; } } if(!isSwap) break; }}int[] arr1 = new int[20];Random r1 = new Random();for(int i = 0; i < arr1.Length; i++){ arr1[i] = r1.Next(0, 101);}Console.WriteLine("Before Sort:"+string.Join(",", arr1));BubbleSortUp(arr1);Console.WriteLine("After Sort Up:"+string.Join(",", arr1));BubbleSortDown(arr1);Console.WriteLine("After Sort Down:"+string.Join(",", arr1));#endregion
复制代码- #region 2static void BubbleSort_UpOrDown(int[] arr,bool isUp){ for(int n = 0; n < arr.Length; n++){ bool isSwap = false; for(int i = 0; i < arr.Length - 1 - n; i++){ if(isUp){ //大于后面的就换位置,也就是大的放后面 if(arr[i] > arr[i+1]){ int temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = temp; isSwap = true; } } else{ //小于后面的就换位置,也就是小的放后面 if(arr[i] < arr[i+1]){ int temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = temp; isSwap = true; } } } if(!isSwap) break; }}int[] arr2 = new int[20];Random r2 = new Random();for(int i = 0; i < arr2.Length; i++){ arr2[i] = r2.Next(0, 101);}BubbleSort_UpOrDown(arr2,true);Console.WriteLine("After Sort Up:"+string.Join(",", arr2));BubbleSort_UpOrDown(arr2,false);Console.WriteLine("After Sort Down:"+string.Join(",", arr2));#endregion
复制代码 选择排序
步骤:
- 新建中间商
- 每轮依次比较,更新中间商
- 找出极值
- 中间商与目标位置互换位置
- n轮比较
详细步骤:
- 新建一个中间商,索引为0
- 中间商与数组的值比较,从索引0开始向后依次比较,每次比较后更新中间商的索引为较大值(或较小值)的索引,找到极值(MAX/min),把极值与目标位置(arr.Length-n-1)互换位置(如果是升序排列,就把MAX放在末尾)
- 这样比较n轮,每轮比较完重置中间商的索引为0,再继续比较,后续每轮的比较只需i从1遍历到数组长度-n即可(第0个不需要和自己比较,末尾的已经排序完不需要再比较)
- // 选择排序//升序, 中间商:maxIndex,目标位置:arr[arr.Length - 1 - n]static void SelectionSort(int[] arr){ //n轮 for(int n = 0; n < arr.Length - 1; n++){ int maxIndex = 0; //用中间商找出每轮的最优元素maxIndex //只需要从1到arr.Length - n - 1遍历 // 不需要和第0个比较(因为中间商就是索引0),不需要和末尾的元素比较,因为arr[arr.Length - 1 - n]后面的元素在前面n轮已经排好序了 for(int i = 1; i < arr.Length - n; i++){ //更新中间商的索引为较大值的索引 if(arr[i] > arr[maxIndex]){ maxIndex = i; } } //交换极值和目标位置(末尾)的元素 //交换条件:中间商不是目标位置 if(maxIndex!= arr.Length - 1 - n){ int tmp = arr[arr.Length - 1 - n]; arr[arr.Length - 1 - n] = arr[maxIndex]; arr[maxIndex] = tmp; } }}int[] arr = {5, 3, 8, 6, 2, 7, 1, 4};SelectionSort(arr);Console.WriteLine(string.Join(" ", arr));
复制代码习题
- static void SelectionSort(int[] arr,bool isUp){ for(int n = 0; n < arr.Length - 1; n++){ int bestIndex = 0; for(int i = 1; i < arr.Length - n; i++){ if(isUp){ if(arr[i] > arr[bestIndex]){ bestIndex = i; } }else{ if(arr[i] < arr[bestIndex]){ bestIndex = i; } } } if(bestIndex!=arr.Length-n-1){ int temp = arr[arr.Length-n-1]; arr[arr.Length-n-1] = arr[bestIndex]; arr[bestIndex] = temp; } }}int[] arr = new int[20];Random r = new Random();for (int i = 0; i < arr.Length; i++){ arr[i] = r.Next(0, 101);}Console.WriteLine("Before Sort:"+string.Join(",",arr));SelectionSort(arr,true);Console.WriteLine("After Sort Up:"+string.Join(",",arr));SelectionSort(arr,false);Console.WriteLine("After Sort Down:"+string.Join(",",arr));
复制代码 C#核心篇
Github仓库:https://github.com/EanoJiang/CSharp_core
面向对象的概念
封装(类)、继承,多态
类
类的基本概念
- 具有相同特征、相同行为、一类事物的抽象
- 类是对象的模板,可以通过类创建出对象
- 关键词class
类的申明
申明在nameplace语句块中——也就是要写在class Program 的外面,如果在类(class)里面申明类,那就是内部类
语法
- namespace 面向对象; #region 类申明语法 // 命名:帕斯卡命名法 // 同一个语句块中的不同类不能重名 //访问修饰符 class 类名{ // //特征——成员变量 // //行为——成员方法(函数) // //保护特征——成员属性 // //构造函数、析构函数 // //索引器 // //运算符重载 // //静态成员 // } #endregionclass Program{ static void Main(string[] args) { }}
复制代码 使用
- namespace 面向对象;class Person{}class Machine{}class Program{ static void Main(string[] args) { #region 实例化对象示例(类创建对象) //类对象都是引用类型的 //语法: 类名 对象名 = new 类名(); //在栈上开辟了一个空间存放地址,但是不开辟 堆内存空间,也就是null Person p; Person p1 = null; //分配堆内存空间 //创建的每个对象只是模板都是同一个类,但是里面的信息都是不同的————类似造人 Person p2 = new Person(); Person p3 = new Person(); #endregion }}
复制代码习题
- namespace 类和对象习题;class Person{}class Animal{}class Machine{}class Plant{}class Astro{}class Program{ static void Main(string[] args) { // 1 Machine robot = new Machine(); Machine machine = new Machine(); Person people = new Person(); Animal cat = new Animal(); Person aunt = new Person(); Person uncle_Wang = new Person(); Machine car = new Machine(); Machine plane = new Machine(); Plant sunflower = new Plant(); Plant chrysanthemum = new Plant(); Astro sun = new Astro(); Astro star = new Astro(); Plant lotus = new Plant(); }}
复制代码- A指向一个地址指向一块堆内存B指向一个地址,地址拷贝自A的地址,所以也指向A的堆内存B = null :把B的地址与堆内存之间的指向关系断开所以,A的堆内存没变
复制代码 成员变量——类的特征
- 申明在类语句块中
- 用来描述对象的特征
- 任意变量类型
- 数量不限
- 赋不赋值都行
- namespace 成员变量;//性别枚举enum E_SexType{ Male, Female,}//位置结构体struct Position{}//宠物类class Pet{}class Person{ //特征——成员变量 public string name = "Eano";//可以初始化也可以不初始化 public int age; public E_SexType sex; public Position position; //可以申明任意类的对象,包括自身类 // (这点和结构体就不同,结构体如果申明自身结构体的变量就会无限循环导致报错 // 而在类里申明自身类的对象则没有问题,因为类是引用类型,只是声明一个对该对象的引用,也就是开辟了一个地址空间 // 不能实例化自身类的对象,因为这样的话在后面创建对象的时候就会陷入无限循环) public Person girlfriend; //不能实例化自身类的对象,初始化为null是可以的 public Person[] friends; public Pet pet; //可以实例化其他类的对象}class Program{ static void Main(string[] args) { //创建对象 Person p = new Person(); #region 成员变量的使用与初始值 //值类型的默认值 都是0 // 相应的bool——false , char——'' ,string——"" //引用类型的默认值 都是null //调用defalut()方法可以查看默认值 Console.WriteLine(default(int)); Console.WriteLine(default(bool)); Console.WriteLine(default(char)); //如果不申明,那么这个成员变量就是默认值 Console.WriteLine(p.age); p.age = 25; Console.WriteLine(p.age); #endregion }}
复制代码 总结:
- 访问修饰符——3P
- 在类里面申明自身类的对象的时候,不能实例化
- defalut()方法得到数据类型的默认值
习题
- 3P:privatepublicprotected
复制代码- namespace 成员变量习题;class Student{ public string name; public int age; public string num; public Student deskmate;}class Classroom{ public string major; public int capacity; public Student[] students; public Classroom(int capacity) { this.capacity = capacity; students = new Student[capacity]; }}class Program{ static void Main(string[] args) { //3 Student s1 = new Student(); Student s2 = new Student(); //4 Classroom c1 = new Classroom(5); }}
复制代码- p2.age 是引用类型,拷贝的时候拷贝的是p.age的地址,改变p2.age的值,p.age也会改变p.age = 20
复制代码- age是值类型,只是拷贝了s.age的值,不指向同一地址,所以s.age不变s.age = 10
复制代码 成员方法——类的行为
不要加static关键字- namespace 成员方法;class Person{ //成员方法 public void Speak(string message){ Console.WriteLine("{0}说{1}",name,message); } public bool IsAdult(){ return age>=18; } public void AddFriend(Person p){ if(friends==null) friends = new Person[]{p}; else{ Person[] temp = new Person[friends.Length+1]; for(int i=0;if.name))); }}
复制代码习题
- namespace 成员方法习题;class Student{ public void Speak(string message){ Console.WriteLine("{0} says: {1}",name,message); } public void Eat(Food food){ Console.WriteLine("{0} is eating {1},calories: {2}",name,food.name,food.calories); } public string name;}class Food{ public string name; public int calories;}class Program{ static void Main(string[] args) { Student student = new Student(){name="Alice"}; Food apple = new Food(){name="apple",calories=50}; student.Eat(apple); }}
复制代码 构造、析构函数、垃圾回收机制
构造函数——初始化时调用
- 在类里面用于调用时快速初始化的函数
- 没有构造函数的时候默认存在一个无参构造函数
也就是 Person p = new Person();
写法:
和结构体一样,构造函数名要和类名相同- namespace 构造_析构函数;class Person{ public string name; public int age; //构造函数 //类中允许申明无参构造函数,结构体则不允许 public Person(){ name = "eano"; age = 18; } //构造函数可以被重载 public Person(string name, int age){ this.name = name; this.age = age; }}class Program{ static void Main(string[] args) { //现在有了3种申明并初始化对象的方式 Person p = new Person(); Console.WriteLine("Name: " + p.name); Person p2 = new Person("eano", 18); Console.WriteLine("Name: " + p2.name); Person p3 = new Person(){name = "eano", age = 18}; Console.WriteLine("Name: " + p3.name); }}
复制代码 注意:
- 有参构造函数 会顶掉 默认的无参构造函数。
- 想要保留无参构造函数,需要重载出来
- this用来区分类内成员变量和外部传入参数
构造函数的特殊写法
:this(可选参数)复用代码
先进入无参构造函数
作用:复用先进入的构造函数代码- class Person{ public string name; public int age; //构造函数 //类中允许申明无参构造函数,结构体则不允许 public Person(){ name = "eano"; age = 18; } // //构造函数可以被重载 // public Person(string name, int age){ // this.name = name; // this.age = age; // } //构造函数的特殊写法,在构造函数后:this(可选参数) public Person(string name, int age) : this(){ Console.WriteLine("先进入无参构造函数"); } public Person(string name, int age) : this(name){ Console.WriteLine("先进入string类型参数的构造函数"); } }
复制代码 :this(可选参数)可以指定先进入的构造函数
可选参数可以写死,比如
:this(int类型参数名)就是先进入参数为int类型的构造函数
:this(string类型参数名)就是先进入参数为string类型的构造函数
习题
- namespace 构造_析构函数习题;class Person{ public string name; public int age; //构造函数 public Person(){ name = "eano"; age = 25; } //重载 public Person(string name, int age){ this.name = name; this.age = age; } //特殊的构造函数 public Person(string name):this(){ Console.WriteLine("有参构造函数里的name:"+name); }}class Ticket{ uint distance; float price; //构造函数 public Ticket(uint distance){ this.distance = distance; //price是通过GetPrice()方法计算出来的 price = GetPrice(); } //成员方法 public float GetPrice(){ if(distance > 300){ return distance * 0.8f; } else if(distance > 200){ return distance * 0.9f; } else if(distance > 100){ return distance * 0.95f; } else{ return distance * 1.0f; } } public void PrintPrice(){ Console.WriteLine("距离{0}的票价为:{1}",distance,GetPrice()); }}class Program{ static void Main(string[] args) { //1 //先进入无参构造函数,再进入有参构造函数 Person p1 = new Person("John"); Console.WriteLine(p1.name+" "+p1.age); //3 Ticket t1 = new Ticket(250); t1.PrintPrice(); }}
复制代码 析构函数——释放时调用
当引用类型的堆内存真正被回收时,调用析构函数
C++需要手动管理内存,所以才需要在析构函数中做内存回收处理
C#有自带的自动垃圾回收机制,所以不太需要析构函数,除非想在某个对象被垃圾回收时做一些特殊处理
要写在类里面垃圾回收机制GC
原理:遍历堆(Heap)上动态分配的所有对象,通过识别是否被引用来确定哪些对象是垃圾,然后回收释放
垃圾回收的算法:
堆(Heap)内存由GC垃圾回收,引用类型
栈(Stack)内存由系统自动管理,值类型在栈中分配内存,有自己的申明周期,自动分配和释放
C#中内存回收机制的原理:
分代算法
0代内存 1代内存 2代内存
新分配的对象都被配置在0代内存中,(0代内存满时)触发垃圾回收
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,进行以下两步:
- 标记对象:从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,被标记的为不可达对象——不可达对象就是垃圾
- 搬迁对象压缩堆:(挂起执行托管代码线程)释放未标记的对象,搬迁可达对象到一代内存中,修改可达对象的引用地址为连续的地址
大对象:
大对象是第二代内存,目的是减少性能损耗以提高性能
不会对大对象进行搬迁压缩,85000字节(83kb)以上的对象是大对象
这个机制有点像三级缓存
速度:0 > 1 > 2
容量:0 < 1 < 2
手动进行GC
GC.Collect()
一般在Loading过场动画的时候调用
小节
- class 类名{//特征——成员变量//行为——成员的方法//初始化时调用——构造函数//释放时调用——析构函数}
复制代码 成员属性——保护成员变量
- 通过在get和set里面写逻辑,来保护成员变量
- 解决3p的局限性
- 用来让成员变量在外部:只能获取不能修改 / 只能修改不能获取
语法:
- //访问修饰符 属性类型 属性名{ // get{} // set{} //}
复制代码 使用:
- namespace 成员属性; //访问修饰符 属性类型 属性名{ // get{} // set{} //} class Person{ private string name; private int age; private int money; private bool sex; //成员属性 public string Name{ get{ //返回之前可以写逻辑规则 return name; } set{ //设置之前可以写逻辑规则 //value用来接收外部传入的值 name = value; } } public int Money{ get{ //加密处理 return money - 5; } set{ //逻辑处理 if(value < 0){ value = 0; Console.WriteLine("金额不能为负数"); } //加密处理 //这一部分涉及到加密算法,这里省略 money = value + 5; } } }class Program{ static void Main(string[] args) { Person p = new Person(); p.Name = "eano";//调用的是set语句块 Console.WriteLine(p.Name);//调用的是get语句块 p.Money = -999; Console.WriteLine(p.Money); p.Money = 1000; Console.WriteLine(p.Money); }}
复制代码 get和set前可以加访问修饰符
- #region get和set前可以加访问修饰符 //1. 默认不加,会使用成员属性的访问修饰符(这里就是public) //2. 加的修饰符要低于成员属性的访问修饰符,否则会报错 //3. 不能让get和set的访问权限都低于成员属性的权限 public int Age{ private get{ return age; } set{ age = value; } } #endregion
复制代码 get和set可以只有一个
- #region get和set可以只有一个 //一般只会出现 只有get的情况,只能获取值,不能修改值————只读属性 //只有一个的时候,不要加修饰符 public bool Sex{ get{ return sex; } } #endregion
复制代码 自动属性
- #region 自动属性 //作用:外部只读不写的特性 //使用场景:一个特征是只希望外部只读不可写,也不加别的特殊处理 public float Height { get; private set; } //只可以在类内部set #endregion
复制代码习题
 - namespace 成员属性习题;class Student{ private string name; private string sex; private int age; private int csGrade; private int unityGrade; public string Name{get; private set;} public string Sex{ get{ return sex; } private set{ if(value != "男" && value != "女") sex = "unknown"; else sex = value; } } public int Age{ get{ return age; } private set{ if(value < 0) age = 0; else if(value > 150) age = 150; else age = value; } } public int CsGrade{get; private set;} public int UnityGrade{ get{ return unityGrade; } private set{ if(value < 0) unityGrade = 0; else if(value > 120) unityGrade = 120; else unityGrade = value; } } public Student(string name, string sex, int age, int csGrade, int unityGrade){ Name = name; Sex = sex; Age = age; CsGrade = csGrade; UnityGrade = unityGrade; } public void Saymyself(){ Console.WriteLine("My name is {0}, I am {1} years old, a {2}.", Name, Age, Sex); } public void SayGrade(){ int sum = CsGrade + UnityGrade; float average = (float)sum / 2; Console.WriteLine("My sum grade is {0}, my average grade is {1}.", sum, average); }}class Program{ static void Main(string[] args) { Student student1 = new Student("Tom", "男", 18, 90, 80); student1.Saymyself(); student1.SayGrade(); Student student2 = new Student("Jerry", "女", 160, 100, 90); student2.Saymyself(); student2.SayGrade(); }}
复制代码 索引器——像数组一样访问元素
让对象可以像数组一样通过索引访问元素
注意:结构体中也支持索引器
语法
- class Person{ private string name; private int age; private Person[] friends; #region 索引器语法 //访问修饰符 返回值 this[数据类型 参数名1,数据类型 参数名2,...]{ // 和属性的写法相同: // get{ // } // set{ // } // } public Person this[int index]{ get{ return friends[index]; } set{ friends[index] = value; } } #endregion }
复制代码 用法
- namespace 索引器;class Person{ private string name; private int age; private Person[] friends; private int[,] array; public string Name{get;private set;} public int Age{get;private set;} public Person[] Friends{get;private set;} public int[,] Array{get;private set;} public Person(string name, int age){ Name = name; Age = age; friends = new Person[5]; Friends = friends; array = new int[3, 4]; Array = array; } #region 索引器语法 //访问修饰符 返回值 this[数据类型 参数名1,数据类型 参数名2,...]{ // 和属性的写法相同: // get{ // } // set{ // } // } public Person this[int index]{ get{ #region 索引器里也能写逻辑 if(friends == null || index < 0 || index >= friends.Length){ return null; } else{ return friends[index]; } #endregion } set{ if(friends == null){ friends = new Person[]{value}; } //如果越界,顶掉最后一个元素 else if(index < 0 || index >= friends.Length){ friends[friends.Length - 1] = value; } else friends[index] = value; } } #endregion #region 索引器可以重载 //参数不同 public int this[int row, int col]{ get{ return array[row, col]; } set{ array[row, col] = value; } } public string this[string str]{ get{ switch(str){ case "name": return Name; case "age": return Age.ToString(); default: return "Invalid index"; } } } #endregion}class Program{ static void Main(string[] args) { Person p1 = new Person("Alice", 25); p1.Friends[0] = new Person("Bob", 20); p1[1] = new Person("Charlie", 22); Console.WriteLine(p1[0].Name); p1[2, 3] = 10; Console.WriteLine(p1[2, 3]); Console.WriteLine("{0}的年龄是{1}, 朋友是{2}", p1["name"],p1["age"],p1[0]["name"]); }}
复制代码索引器就相当于给对象加一个属性,用中括号[参数]调用这个属性的内容
习题
- namespace 索引器习题;class IntArray{ public int[] arr; public int length; public IntArray(int size){ length = 0; arr = new int[size]; } //增 public void Add(int index, int value){ if(index < 0 || index > length){ Console.WriteLine("索引超出范围"); return; } else{ if(length < arr.Length){ arr[length] = value; length++; } else{ int[] newArr = new int[arr.Length + 1]; for(int i=0;i=index;i--){ arr[i+1] = arr[i]; } arr[index] = value; length++; } } } //删 public void Remove(int index){ if(index > length-1 || index < 0){ Console.WriteLine("索引超出范围"); return; } else{ //后面元素前移 for(int i = index;i 0)?n:-n; Console.WriteLine("{0}绝对值为{1}", n, n1); return n1; } static MathCalc(){ Console.WriteLine("静态构造函数执行"); }}class Program{ static void Main(string[] args) { MathCalc.CircleArea(5); MathCalc.CirclePerimeter(5); MathCalc.RectangleArea(5, 10); MathCalc.RectanglePerimeter(5, 10); MathCalc.Abs(-5); }}
复制代码 拓展方法
为现有非静态变量类型 添加新方法
作用:
- 提升程序拓展性
- 不需要在对象中重新写方法
- 不需要继承来添加方法
- 为别人封装的类写额外的方法
特点:
- 一定写在静态类中
- 一定是一个静态函数
- 第一个参数是拓展目标(想要拓展方法的类型),要用this修饰
语法
- 访问修饰符 static 返回值类型 函数名(this 拓展类名 参数名,参数数据类型 参数, ...){}
复制代码- namespace 拓展方法; #region 语法 //访问修饰符 static 返回值类型 函数名(this 拓展类名 参数名,参数数据类型 参数, ...){ // //} #endregion #region 示例 static class Tools{ public static void Print(this string str){ Console.WriteLine("为string拓展方法:"+str); } } #endregionclass Program{ static void Main(string[] args) { string str = "Hello World"; str.Print(); //调用拓展方法 }}
复制代码 使用
- namespace 拓展方法; #region 语法 //访问修饰符 static 返回值类型 函数名(this 拓展类名 参数名,参数数据类型 参数, ...){ // //} #endregion #region 示例 static class Tools{ public static void Print(this string str){ Console.WriteLine("为string拓展方法:"+str); } public static void PrintInfo(this string str, string str1, int num){ Console.WriteLine("拓展方法的对象:"+str); Console.WriteLine("传入的参数:"+str1 + " " + num); } public static void PrintInfo(this Test t){ Console.WriteLine("为Test类拓展方法:"+t.i); } //如果拓展的方法名和类里面的方法重名,优先使用类的方法 public static void Func(this Test t){ Console.WriteLine("为Test类拓展同名方法:"); } } #endregion #region 为自定义的类型拓展方法 class Test{ public int i = 10; public void Func(){ Console.WriteLine("Test类自己的Func方法"); } } #endregionclass Program{ static void Main(string[] args) { string str = "Hello World"; str.Print(); //调用拓展方法 str.PrintInfo("你好", 123); //调用拓展方法 //为自定义的类型拓展方法 Test t = new Test(); t.PrintInfo(); //调用拓展方法 t.Func(); //重名,优先调用类自己的方法 }}
复制代码 注意:
如果拓展的方法名和类里面的方法重名,优先使用类的方法
习题
 - namespace 拓展方法习题;//1//平方static class Test{ public static int Square(this int n){ Console.WriteLine("Square of " + n + " is " + (n*n)); return n*n; } public static void Suicide(this Player player){ Console.WriteLine("Player " + player.name + " is suiciding!"); }}//2//玩家class Player{ public string name; public int hp; public int atk; public int def; public Player(string name, int hp, int atk, int def){ this.name = name; this.hp = hp; this.atk = atk; this.def = def; } public void Attack(Player target){ Console.WriteLine(this.name + " attacks " + target.name + "!"); target.hp -= this.atk - target.def; Console.WriteLine(target.name + " now has " + target.hp + " HP."); if(this.atk - target.def > 0){ Hurted(target); } } public void Move(int x, int y){ Console.WriteLine(this.name + " moves to (" + x + ", " + y + ")."); } public void Hurted(Player target){ Console.WriteLine(target.name + " is hurt!"); }}class Program{ static void Main(string[] args) { //1 int num = 3; num.Square(); //2 Player player1 = new Player("player1", 100, 10, 5); Player player2 = new Player("player2", 100, 13, 2); player1.Attack(player2); player1.Move(1, 2); player1.Suicide(); player2.Attack(player1); player2.Suicide(); }}
复制代码 运算符重载——自定义对象能够运算
让自定义的类和结构体对象 能够使用运算符
关键字: operator
特点:
- 必须是公共的静态方法
- 返回值写在operator前
注意:
- 条件运算符需要成对实现
- 一个符号可以多个重载
- 不能使用ref和out
语法
- //语法 //public static 类名 返回类型 operator 运算符(参数类型1 参数名1, 参数类型2 参数名2){ //}
复制代码 用法实例
- namespace 运算符重载;class Program{ //语法 //public static 类名 返回类型 operator 运算符(参数类型1 参数名1, 参数类型2 参数名2){ //} //实例 class Point { public int x, y; public static Point operator +(Point p1, Point p2) { Point p = new Point(); p.x = p1.x + p2.x; p.y = p1.y + p2.y; return p; } //重载 public static Point operator +(Point p1, int num) { Point p = new Point(); p.x = p1.x + num; p.y = p1.y + num; return p; } } static void Main(string[] args) { Point p1 = new Point(); p1.x = 1; p1.y = 2; Point p2 = new Point(); p2.x = 3; p2.y = 4; Point p3 = p1 + p2; Console.WriteLine("p3.x = " + p3.x); Point p4 = p1 + 2; Console.WriteLine("p4.x = " + p4.x); //可以连续使用 p4 = p1 + p2 + 3; Console.WriteLine("p4.x = " + p4.x); }}
复制代码 可重载和不可重载的运算符
- #region 可重载的运算符 //算数运算符:+ - * / % ++ -- // (自增自减的参数只有一个) //逻辑运算符:! // ( &&和||不能重载 ) //位运算符:~ & | ^ > // (~只有一个参数) // (左移右移的参数Point p,int num) //条件运算符:> < >= 和=和= persons.Length){ Console.WriteLine("车子已满"); return; } persons[num] = person; num++; } public void RemovePerson(Person delPerson){ if(delPerson is Driver){ Console.WriteLine("驾驶员不能下车"); return; } //只有乘客下车 else{ for(int i = 0; i < persons.Length; i++){ //结束循环的条件是找到空位 if(persons[i] == null){ break; } // 找到要删除的对象 else if(persons[i] == delPerson){ // 找到了要删除的对象,将其后面的元素前移一位 for(int j = 0; j < num - 1; j++){ persons[j] = persons[j+1]; } //最后一个位置清空 persons[num-1] = null; num--; break; } } } } public void Move(){ } public void Accident(){ if(speed > maxSpeed) Console.WriteLine("发生事故"); else Console.WriteLine("正常"); } public void PrintNum(){ Console.WriteLine("当前车子乘客数量:" + num); }}class Person{}class Driver:Person{}class Passenger:Person{}class Program{ static void Main(string[] args) { Person d1 = new Driver(); Person p1 = new Passenger(); Person p2 = new Passenger(); Car car = new Car(60, 80, 5); car.AddPerson(d1); car.AddPerson(p1); car.AddPerson(p2); car.PrintNum(); car.RemovePerson(d1); car.PrintNum(); car.RemovePerson(p1); car.PrintNum(); car.Accident(); car.speed = 100; car.Accident(); }}
复制代码 多态
多态的基本概念
让继承同一父类的子类们,在执行相同方法的时候有不同的表现
解决问题:让同一个对象有唯一行为的特征- class Father{ public void SpeakName() { Console.WriteLine("Father类"); }}class Son : Father{ public new void SpeakName() { Console.WriteLine("Son类"); }}class Program{ static void Main(string[] args) { //如果用里氏替换原则 Father s = new Son(); s.SpeakName(); //这里会打印父类的方法 //只有用as转换成子类对象才会调用子类的方法 (s as Son).SpeakName(); }}
复制代码 这样写很容易造成混乱,于是就有了多态
多态的实现
前面学过函数重载,这是编译时候的多态,也叫做静态的多态,意思是在程序编译阶段,编译器根据参数的类型和数量来决定调用哪个具体的重载函数版本。
下面介绍的是运行时的多态(vob、抽象函数、接口)
vob
v:virtual(虚函数)
o:override(重写)
b:base(父类)
父类的虚函数,在子类用override重写该函数,可以选择用/不用base。
用里氏替换或者其他方法声明对象,new什么就调用什么的方法ß- namespace 多态_vob;class GameObject{ public string name; public GameObject(string name) { this.name = name; } public virtual void Atk() { Console.WriteLine("GameObject对象的攻击"); }}class Player : GameObject{ //子类默认找的是父类的无参构造函数,但是上面父类中已经有参构造,顶掉了无参构造 //所以需要:base()继承构造函数 public Player(string name) : base(name) { } //虚函数可以被子类重写 public override void Atk() { //base.Atk(); //保留父类的虚函数行为,可以写在这个override方法的任何需要的地方 Console.WriteLine("Player对象的攻击"); }}class Program{ static void Main(string[] args) { GameObject p1 = new Player("sb"); p1.Atk(); //这就和原来用as的方式结果一样,但是可读性更强 // (p1 as Player).Atk(); }}
复制代码 什么时候需要base?
需要保留父类行为就base.方法名()
习题
 - using System.Drawing;namespace 多态_vob习题;//1class Duck{ public virtual void Speak() { Console.WriteLine("嘎嘎叫"); }}class woodenDuck : Duck{ public override void Speak() { Console.WriteLine("吱吱叫"); }}class rubberDuck : Duck{ public override void Speak() { Console.WriteLine("唧唧叫"); }}//3class Graph{ public virtual float area() { return 0; } public virtual float perimeter() { return 0; }}class Rectangle : Graph{ public float width, length; public Rectangle(float width, float length) { this.width = width; this.length = length; } public override float area() { return width * length; } public override float perimeter() { return 2 * (width + length); }}class Square : Graph{ public float sideLength; public Square(float sideLength) { this.sideLength = sideLength; } public override float area() { return sideLength * sideLength; } public override float perimeter() { return sideLength * 4; }}class Sphere : Graph{ public float radius; public Sphere(float radius) { this.radius = radius; } public override float area() { return radius * 3.14f * 3.14f; } public override float perimeter() { return radius * 2 * 3.14f; } }class Program{ static void Main(string[] args) { //1 Duck d1 = new Duck(); d1.Speak(); Duck w1 = new woodenDuck(); w1.Speak(); Duck r1 = new rubberDuck(); r1.Speak(); //3 Graph rect1 = new Rectangle(1, 2); Console.WriteLine(rect1.area()); Console.WriteLine(rect1.perimeter()); Graph square1 = new Square(1); Console.WriteLine(square1.area()); Console.WriteLine(square1.perimeter()); Graph sphere1 = new Sphere(1); Console.WriteLine(sphere1.area()); Console.WriteLine(sphere1.perimeter()); }}
复制代码 抽象类和抽象函数
抽象类
关键字:abstract
特点:
- 不能被实例化,但是可以用里氏替换原则作为容器存储对象
- 可以包含抽象方法
- 继承抽象类必须重写他的抽象方法
- namespace 多态_抽象类和抽象函数;abstract class Thing{ public string name; //抽象函数}class Water : Thing{ }class Program{ static void Main(string[] args) { //抽象类不能被实例化 // Thing t = new Thing(); // 错误 //抽象类的子类可以用里氏替换原则用父类装子类 Thing t = new Water(); //抽象类的子类可以被实例化 Water w = new Water(); }}
复制代码 抽象函数
又名:纯虚方法
关键字:abstarct
特点:
- 只能在抽象类中申明
- 没有函数体,就是不要写花括号{}
- 不能是私有的
- 继承后必须用override重写
- abstract class Thing{ public string name; //抽象函数 public abstract void Show(); //虚函数 public virtual void Test() { }}class Water : Thing{ //继承一个有抽象函数的抽象类,必须要实现抽象函数 public override void Show() { } //虚函数可以选择是否要覆盖 public override void Test() { }}
复制代码 抽象函数和虚函数的区别
抽象函数:父类里面一定不能有函数体,只能在抽象类里申明,必须要在其子类里实现,但在子类的子类就可以不用实现了
虚函数:可以选择在父类中写不写函数体,可以在任意类申明,可以选择在子类是否重写
抽象函数和虚函数的共同点
都可以在子类/子类的子类无限重写
习题
 - namespace 多态_抽象类和抽象函数习题;//1.abstract class Animal{ public abstract void Speak();}class Person : Animal{ public override void Speak() { Console.WriteLine("人叫"); }}class Dog : Animal{ public override void Speak() { Console.WriteLine("狗叫"); }}class Cat : Animal{ public override void Speak() { Console.WriteLine("猫叫"); }}//2.abstract class Graph{ public abstract float area(); public abstract float perimeter();}class Rectangle : Graph{ public float width, length; public Rectangle(float width, float length) { this.width = width; this.length = length; } public override float area() { return width * length; } public override float perimeter() { return 2 * (width + length); }}class Square : Graph{ public float sideLength; public Square(float sideLength) { this.sideLength = sideLength; } public override float area() { return sideLength * sideLength; } public override float perimeter() { return 4 * sideLength; }}class Circle : Graph{ public float radius; public Circle(float radius) { this.radius = radius; } public override float area() { return 3.14f * radius * radius; } public override float perimeter() { return 2 * 3.14f * radius; }}class Program{ static void Main(string[] args) { //1. Animal[] animals = new Animal[3] { new Dog(), new Cat(), new Dog() }; foreach (Animal a in animals) { a.Speak(); } //2. Graph[] graphs = new Graph[3] { new Rectangle(1, 2), new Square(2), new Circle(2) }; foreach (Graph g in graphs) { Console.WriteLine(g.GetType().Name + "面积:" + g.area()); Console.WriteLine(g.GetType().Name + "周长:" + g.perimeter()); } }}
复制代码 接口
——接口就是抽象出来的一种行为父类,不同类的子类都可以继承这个接口
比如鸟和飞机,分别是动物类的子类和机器的子类,但是都有飞这个行为,就可以抽象出来一个接口:IFly
关键字:interface
接口是行为的抽象规范,是一种自定义类型。
特点:
- 接口和类的申明相似
- 接口是用来继承的
- 接口不能被实例化,但是可以用里氏替换原则作为容器存储对象
接口的申明
接口是抽象行为的父类
接口命名:帕斯卡命名法前加一个“ I "
接口申明的规范:
- 不包含成员变量
- 只包含方法、属性、索引器、事件
- 成员不能被实现
- 成员可以不用写访问修饰符,但绝对不能是私有的
- 接口不能继承于类,但是可以继承另一个接口
- interface IFly{ //接口不能包含成员变量 // int a; //错误 //方法 void Fly(); //属性 string Name { get; set; } //索引器 int this[int index] { get; set; } //事件 event Action doSomething;}
复制代码 接口的使用
接口的使用规范:
- 接口是用来继承的
- 类可以继承1个类,n个接口
- 接口本身可以不用写访问修饰符,默认就是public
- 继承了接口后,必须实现接口中的所有成员,并且必须用public(如果用protected,那就必须显示实现)
- //接口的使用class Animal{}class Person : Animal, IFly{ //实现接口中的函数,可以申明为虚函数virtual,在子类中重写 public virtual void Fly() { } public string Name { get; set; } public int this[int index] { get { return 0; } set { } } public event Action doSomething;}
复制代码 接口遵循里氏替换原则- class Program{ static void Main(string[] args) { //接口不能实例化 // IFly f = new IFly(); //错误 //接口可以作为容器,里氏替换原则 IFly f = new Person(); }}
复制代码 接口可以继承接口
- 接口继承接口后,不需要实现
- 类继承接口后,类必须实现所有内容
- //接口继承接口interface IWalk{ void Walk();}//接口继承接口,不需要实现interface IMove : IWalk, IFly{}//类继承接口,必须实现所有成员class Test : IMove{ public string Name { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public int this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public event Action doSomething; public void Fly() { } public void Walk() { } }
复制代码 里氏替换,接口作为容器- IMove im = new Test(); IFly ifly = new Test(); IWalk iw = new Test(); //用什么接口装,其对象就只能是该接口含有的方法 im.Walk(); im.Fly(); //IFly只有Fly() ifly.Fly(); //IWalk只有Walk() iw.Walk();
复制代码 显示实现接口
当一个类继承2个接口,但是接口存在同名方法时
显示实现接口不能写访问修饰符- //显示实现接口interface IAtk{ void Atk();}interface ISuperAtk{ void Atk();}class Player : IAtk, ISuperAtk{ //继承接口的方法 //类继承接口时,不加public,就必须要显示实现接口中的方法 //接口名.方法名 void IAtk.Atk() { } void ISuperAtk.Atk() { } //玩家自身的方法 public void Atk() { }}
复制代码- //显示实现的使用 IAtk ia = new Player(); ISuperAtk isa = new Player(); ia.Atk(); isa.Atk(); Player p = new Player(); (p as IAtk).Atk(); (p as ISuperAtk).Atk(); p.Atk();
复制代码 总结
- 继承类:是对象间的继承
- 继承接口:行为间的继承,继承接口里的行为规范去实现内容
接口可以作为容器装对象
接口的引入,可以实现装载各种不同类但是有相同行为的对象
特别注意:
- 接口包含 成员方法、属性、索引器、事件,并且都不实现,都没有访问修饰符
- 可以继承多个接口,但只能继承一个类
- 接口可以继承接口,这就相当于行为的合并,在子类继承的时候再去实现具体的行为
- 接口可以被显示实现,用来解决不同接口中的同名函数的不同实现
- 接口方法在子类实现的时候可以加virtual申明为虚函数,然后在之后的子类中重写
习题
 - namespace 多态_接口习题;//1.interface IRegister{ void Register();}class Person : IRegister{ public void Register() { Console.WriteLine("在派出所登记"); }}class Car : IRegister{ public void Register() { Console.WriteLine("在车管所登记"); }}class House : IRegister{ public void Register() { Console.WriteLine("在房管局登记"); }}//2.abstract class Animal{ public abstract void Walk();}interface IFly{ void Fly();}interface ISwim{ void Swim();}class helicopter : IFly{ public void Fly() { Console.WriteLine("直升机开始飞"); }}class sparrow : Animal, IFly{ public override void Walk() { } public void Fly() { }}class Ostrich : Animal{ public override void Walk() { }}class Penguin : Animal, ISwim{ public void Swim() { } public override void Walk() { }}class Parrot : Animal, IFly{ public void Fly() { } public override void Walk() { }}class Swan : Animal, ISwim,IFly{ public void Swim() { } public void Fly() { } public override void Walk() { }}//3.interface IUSB{ void ReadData();}class Computer{ public IUSB usb1;}class StorageDevice : IUSB{ public string name; public StorageDevice(string name) { this.name = name; } public void ReadData() { Console.WriteLine(name + "读取数据"); }}class Mp3 : IUSB{ public void ReadData() { Console.WriteLine("mp3读取数据"); }}class Program{ static void Main(string[] args) { //1. IRegister[] arr = new IRegister[] { new Person(), new Car(), new House() }; foreach (IRegister item in arr) { item.Register(); } //3. Computer cp1 = new Computer(); cp1.usb1 = new Mp3(); cp1.usb1.ReadData(); cp1.usb1 = new StorageDevice("硬盘"); cp1.usb1.ReadData(); }}
复制代码 密封方法
关键字:consealed
子类对虚函数和抽象函数override的时候加上了关键字sealed,那么在这个子类的子类就不能再重写了。
特点:
- 密封方法可以让虚函数和抽象函数不能再次被子类重写
- 和override一起出现
- abstract class Animal{ public string name; public abstract void Atk(); public virtual void Fuck() { Console.WriteLine("fuck"); }}class Person : Animal{ public sealed override void Atk() { } public sealed override void Fuck() { Console.WriteLine("fuck me"); }}// class Test : Person// {// //后续子类就不能重写了// public override void Atk()// {// }// public override void Fuck()// {// }// }
复制代码 命名空间
作用:用来组织和复用代码
命名空间:工具包,类:工具包里面的一个个工具(申明在命名空间中)
使用
命名空间可以同名,也就是分段写,也可以分文件写- namespace MyGame{ class GameObject { }}namespace MyGame{ class Player : GameObject { }}
复制代码 不同命名空间中互相使用:需要引用命名空间/指明出处
- using MyGame;namespace 命名空间{ class Program { static void Main() { //不同命名空间中相互使用,需要引用命名空间或指明出处 GameObject g = new GameObject(); } }}
复制代码- //不同命名空间中相互使用,需要引用命名空间或指明出处 MyGame.GameObject g = new MyGame.GameObject();
复制代码 不同命名空间中允许有同名类
- namespace MyGame{ class GameObject { }}//不同命名空间允许有同名的类namespace MyGame2{ class GameObject { }}
复制代码 如果要在另一个命名空间调用不同命名空间的同名类,只能必须指明出处- MyGame.GameObject g = new MyGame.GameObject(); MyGame2.GameObject g2 = new MyGame2.GameObject();
复制代码 命名空间可以包裹命名空间
也就是命名空间里细分命名空间- namespace MyGame{ namespace UI { } namespace Game { }}
复制代码 调用的时候一层层点就行,或者引用命名空间using MyGame.UI
修饰类的访问修饰符
public 默认公开
internal 只能在该程序集里用
abstract 抽象类
sealed 密封类
partial 分部类
万物之父中的方法
object中的静态方法
Equals()
作用:判断两个对象是否相等,比较的是二者指向的内存地址是否一样
最终判断权交给左侧对象的Equals方法- //Equals() //比较的是二者指向的内存地址是否一样 //最终判断权交给左侧对象的Equals方法 //值类型 Console.WriteLine(Object.Equals(1, 1)); Console.WriteLine(1.Equals(1)); //引用类型 Test t1 = new Test(); Test t2 = new Test(); Console.WriteLine(Object.Equals(t1, t2)); Console.WriteLine(t1.Equals(t2)); t2 = t1; Console.WriteLine(Object.Equals(t1, t2)); Console.WriteLine(t1.Equals(t2));
复制代码 ReferenceEquals()
作用:比较两个对象是否是相同的引用(内存地址)- //ReferenceEquals() //比较两个对象是否是相同的引用(内存地址) //值类型:返回值永远是flase Console.WriteLine(Object.ReferenceEquals(1, 1)); //引用类型: Test t3 = new Test(); Test t4 = new Test(); Console.WriteLine(Object.ReferenceEquals(t3, t4)); t4 = t3; Console.WriteLine(Object.ReferenceEquals(t3, t4));
复制代码 object可以省略,因为是万物之父,只要在类里,这个类肯定继承于Object,所以也包含这个方法
object中的成员方法
普通方法GetType()
作用:获取对象运行时的类型Type,返回一个Type类型的对象
通过Type结合后面的反射相关特性,可以做很多关于对象的操作- //普通方法Type() Test t5 = new Test(); Type type = t5.GetType();
复制代码 普通方法MemberwiseClone()
作用:获取对象的浅拷贝对象,返回一个新的对象。
但是新对象(克隆体)的引用变量改了之后,老对象相应的引用变量也会改变- class Test{ //值类型成员变量 public int i = 1; //引用类型成员变量 public Test2 ttt = new Test2(); public Test Clone() { return MemberwiseClone() as Test; }}class Test2{ public int i = 2;}
复制代码- //普通方法MemberwiseClone() Test t_2 = t.Clone(); Console.WriteLine("克隆对象后"); Console.WriteLine("t.i = " + t.i); Console.WriteLine("t.ttt.i = " + t.ttt.i); Console.WriteLine("t_2.i = " + t_2.i); Console.WriteLine("t_2.ttt.i = " + t_2.ttt.i); Console.WriteLine("改变克隆对象的信息"); t_2.i = 99; t_2.ttt.i = 100; Console.WriteLine("t.i = " + t.i); Console.WriteLine("t.ttt.i = " + t.ttt.i); Console.WriteLine("t_2.i = " + t_2.i); Console.WriteLine("t_2.ttt.i = " + t_2.ttt.i);
复制代码
object中的虚函数方法
虚函数Equals()
默认实现还是比较二者是否为同一个引用,等同于ReferenceEquals()
但是微软在所有值类型的基类System.ValueType中重写了Equals(),用来比较值相等
我们也可以对Equals()进行重写
虚函数GetHashCode()
作用:获取对象的哈希码
可以重写
虚函数ToString()
作用:返回当前对象的字符串
可重写
当调用打印方法,默认会使用ToString()
习题
 - namespace 万物之父中的方法习题;//1.class Player{ private string name; private int hp; private int atk; private int def; private int miss; public Player(string name, int hp, int atk, int def, int miss) { this.name = name; this.hp = hp; this.atk = atk; this.def = def; this.miss = miss; } public override string ToString() { return String.Format("姓名{0},血量{1},攻击{2},防御{3},闪避{4}", name, hp, atk, def, miss); }}//2.class Monster{ public Monster m; public string Name { get; set; } public int Hp{ get; set; } public int Atk{ get; set; } public int Def{ get; set; } public int SkillID{ get; set; } public Monster(string name, int hp, int atk, int def, int skillID) { Name = name; Hp = hp; Atk = atk; Def = def; SkillID = skillID; m = this; } public Monster Clone() { return MemberwiseClone() as Monster; }}class Program{ static void Main(string[] args) { //1. Player p = new Player("张三", 100, 10, 5, 5); Console.WriteLine(p); //2. Monster A = new Monster("A", 100, 10, 5, 5); Monster B = A.Clone(); B.Name = "B"; Console.WriteLine(A.Name); //因为是值类型的,所以改克隆体不会改变原来的值 B.m.Name = "B"; Console.WriteLine(A.m.Name); //引用类型的内容改变,改克隆体,原来的值也会改变 }}
复制代码 String
获取字符串指定位置字符
字符串本质是char的数组
可以直接用索引器,或ToCharArray()转成数组后再索引- //字符串获取指定位置字符 string str = "hello world"; Console.WriteLine(str[0]); //ToCharArray():转成char数组 char[] chars = str.ToCharArray(); Console.WriteLine(chars[0]);
复制代码 字符串拼接
string.Format()- //字符串拼接 // str = string.Format(str, "1"); //错误用法 str = string.Format("{0}111",str); //必须用占位符的形式 Console.WriteLine(str);
复制代码 正向查找字符位置
IndexOf()- //正向查找字符位置 //找不到返回-1 int index = str.IndexOf("o"); Console.WriteLine(index); //忽略大小写,StringComparison.OrdinalIgnoreCase index = str.IndexOf("o",StringComparison.OrdinalIgnoreCase); Console.WriteLine(index);
复制代码 反向查找字符位置
LastIndexOf()
返回值:最后一次出现的起始索引位置
这个索引值还是从前往后的
反向体现在返回值是最后一次出现的起始索引位置
- //反向查找字符位置 //返回值 最后一次出现的起始索引位置 int lastIndex = str.LastIndexOf("o"); Console.WriteLine(lastIndex); //找最后一次出现目标字符串的第一个字符的位置 lastIndex = str.LastIndexOf("d111"); Console.WriteLine(lastIndex);
复制代码 移除指定位置后的字符
Remove()- //移除指定位置后的字符(含指定位置一起移除) string str1 = str.Remove(2); Console.WriteLine(str1); //移除[开始位置,开始位置+count]的字符 //第二个参数,count str1 = str.Remove(2, 3); Console.WriteLine(str1);
复制代码 字符串替换
Replace()- //字符串替换 str = str.Replace("hello", "FUCK"); Console.WriteLine(str);
复制代码 大小写转换
ToUpper()
ToLower()- //大小写转换 str = str.ToUpper(); Console.WriteLine(str); str = str.ToLower(); Console.WriteLine(str);
复制代码 字符串截取
Substring()- //字符串截取 //截取指定位置开始之后的字符串(含指定位置) str1 = str.Substring(1); Console.WriteLine(str1); //截取[开始位置,开始位置+count] str1 = str.Substring(1, 3); Console.WriteLine(str1);
复制代码 字符串切割
Split()- //字符串切割 str = "1_1 | 1_2 | 1_3 | 1_4 | 1_5"; string[] strs = str.Split(" | "); for (int i = 0; i < strs.Length; i++) { //[1]:取切割符串后的字符串 strs[i] = strs[i].Split("_")[1]; Console.WriteLine(strs[i]); }
复制代码习题
- //1.// SubString// Replace//2.string str = "1|2|3|4|5|6|7";string[] strs = str.Split('|');str = "";for (int i = 0; i < strs.Length; i++){ str += int.Parse(strs[i]) + 1; if (i != strs.Length - 1) str += "|";}Console.WriteLine(str);//3.//别名//4.//3个堆空间//str = "123";//str2 = "321";//str2 += "123";//只要重新赋值string就会重新分配内存//5.string str2 = "hello";char[] str2s = str2.ToCharArray();for (int i = 0; i < str2.Length / 2; i++){ str2s[i] = (char)(str2s[i] + str2s[str2.Length - 1 - i]); str2s[str2.Length - 1 - i] = (char)(str2s[i] - str2s[str2.Length - 1 - i]); str2s[i] = (char)(str2s[i] - str2s[str2.Length - 1 - i]);}foreach (char c in str2s) Console.Write(c);Console.WriteLine();
复制代码 StringBuilder
一个用于处理字符串的公共类
作用:修改字符串而不用每次都创建新的对象
对于需要频繁修改和拼接的字符串可以使用它,用来提升性能,因为每次创建新的对象都会加快gc的到来
使用前需要using 命名空间:using System.Text;
申明
- using System.Text;//第二个参数:初始化容量,设定过大会浪费空间//在后续每次往里增加内容,会自动扩容StringBuilder str = new StringBuilder("123123",100);Console.WriteLine(str);Console.WriteLine(str.Capacity);
复制代码 增删查改替换
- //增删查改替换//增//不能用+=,用Append()和AppendFormat()str.Append("111");Console.WriteLine(str);Console.WriteLine(str.Length);Console.WriteLine(str.Capacity);str.AppendFormat("{0}{1}", "222", "333");Console.WriteLine(str);Console.WriteLine(str.Length);Console.WriteLine(str.Capacity);//插入str.Insert(0, "FUCK");Console.WriteLine(str);//删//Remove(开始位置,count)str.Remove(0, 4);Console.WriteLine(str);//清空// str.Clear();// Console.WriteLine(str);//查// 索引器Console.WriteLine(str[1]);//改// 之前的string是只读不可改的,现在的StringBuilder是可改的str[1] = 'f';Console.WriteLine(str);//替换// 只替换第一个匹配项str.Replace("123", "FUCK");Console.WriteLine(str);//Equals()str.Clear();str.Append("111");if (str.Equals("111")){ Console.WriteLine("相等");}
复制代码习题
//1.
string每次改动都会创建一个新的对象,也就更容易产生垃圾,更容易触发gc
stringbuilder因为有初始容量的存在,只有达到初始容量上限才会扩容
string更加灵活,内置方法更多:IndexOf()、LastIndexOf()、ToUpper()、ToLower()、Substring()、Split()
stringbuilder适合需要频繁改动的字符串
//2.
就目前已学知识
如何节约内存:少new对象、合理使用static
如何尽可能的减少gc:合理使用string和stringbuilder
结构体和类的区别(面试常考)
- 存储空间:结构体是值,在栈上;类是引用,在堆上
- 使用:结构体不具备继承和多态的特性,只有封装的思想,也不能使用protected保护成员变量
详细对比:
结构体类值类型引用类型栈内存堆内存不能用protected可以结构体成员变量的申明不能设定初始值可以结构体不能自己申明无参构造函数,因为自带可以结构体申明有参构造函数时,无参构造函数不会被顶掉会被顶掉结构体不能申明析构函数可以结构体不能被继承可以结构体需要在构造函数中初始化所有成员变量随意结构体不能被static修饰(不存在静态结构体),但是结构体可以有静态成员可以结构体不能在内部申明和自己一样的结构体变量可以对于最后一点:C# 的结构体是值类型,不允许结构体中直接包含自身类型字段
C / C++:允许使用指针实现自引用结构体- struct Node { int value; struct Node* next; // ✅ C / C++允许:使用指针实现自引用};
复制代码 不能直接嵌套自身类型:- struct Node { int value; struct Node next; // ❌ 错误:会导致无限大小的结构体};
复制代码 结构体的特别之处
结构体可以继承接口,但是结构体不能继承结构体和类
如何选择:结构体or类?
当需要继承和多态,只能用类(玩家、怪物)
当对象是数据集合,优先考虑结构体(位置、坐标)
从值和引用类型的赋值上的区别考虑:
因为结构体是值类型,所以当对象经常需要被赋值传递,但是又不希望原对象被改变,就用结构体。(坐标、向量、旋转)
抽象类和接口的区别(面试常考)
抽象类:abstract,不能实例化,抽象函数只能在抽象类里面申明,是个纯虚函数,必须在子类中实现
接口:interface,是行为的抽象,不能实例化,但是可以作为容器,不含成员变量,只有方法、属性、索引器、事件, 这些成员都不能实现,最好不要写访问修饰符,默认public,避免显示实现(接口名.方法名)
相同点
都可以被继承
都不能直接实例化
都可以包含方法等的申明
其子类必须实现
遵循里氏替换原则
不同点
抽象类接口可以有构造函数没有只能被单一继承
这是类的通性,只能继承一个父类和但是可以多个接口可以被继承多个有成员变量没有可以申明成员方法、虚函数、抽象函数、静态函数只能申明没有实现的函数可以使用访问修饰符最好不写,默认为public
(否则就要在子类中显示实现:接口名.方法名)如何选择抽象类和接口
抽象出来的对象,用抽象类
一个规范行为,用接口
不同对象的共有行为,用接口
OVER~
C#进阶篇
简单数据结构类
ArrayList
Object类型的数组
申明
- //ArrayList的申明 ArrayList arr1 = new ArrayList();
复制代码 增删查改
- //增删查改 // 增 //尾插法 //可以增任何类型 arr1.Add("张三"); arr1.Add(1); arr1.Add(true); arr1.Add(new object()); //批量增加,把另一个list容器里的所有元素都添加到当前容器的后面 arr1.AddRange(new ArrayList() { "张三", "李四", "王五" }); //在中间指定位置插入 arr1.Insert(2, "111111"); //批量插入 arr1.InsertRange(3, new ArrayList() { "123", "234", "345" }); // 删 //从前往后遍历,删除首个匹配的元素 arr1.Remove("张三"); //删除指定位置的元素 arr1.RemoveAt(0); //清空 // arr1.Clear(); // 查 //获取指定位置的元素 Console.WriteLine(arr1[0]); //查看元素是否存在 if (arr1.Contains("1")) Console.WriteLine("存在"); //正向查找元素位置,找不到返回-1 int index = arr1.IndexOf(true); Console.WriteLine(index); //反向查找元素位置,返回的索引还是从前开始计数的,找不到返回-1 index = arr1.LastIndexOf(true); Console.WriteLine(index); // 改 arr1[0] = "999"; Console.WriteLine(arr1[0]); Console.WriteLine(); //长度,数组的元素个数 Console.WriteLine(arr1.Count); //容量 //用来避免每次改动数组都产生垃圾,有了容量的存在,只有扩容的时候才产生垃圾 Console.WriteLine(arr1.Capacity); //遍历 //一般的遍历 for (int i = 0; i < arr1.Count; i++) { Console.WriteLine(arr1[i]); } //迭代器遍历 foreach (object obj in arr1) { Console.WriteLine(obj); }
复制代码 排序和反转
和数组一样- arr1.Sort();arr1.Reverse();
复制代码 装箱拆箱
- #region 装箱拆箱 //ArrayList本质是一个可以自动扩容的object数组 //装箱:进行值类型的储存 //拆箱:进行值类型的取出 //所以尽量选择其他的数据容器 int num = 1; arr1[0] = num; //装箱 num = (int)arr1[0]; //拆箱 #endregion
复制代码 ArrayList和数组的区别
ArrayList本质是object数组
功能数组ArrayList获取长度数组名.Length数组名.Count访问元素数组名[index](直接访问)数组名[index](需拆箱)修改元素数组名[index] = value数组名[index] = value排序Array.Sort(数组名)数组名.Sort()反转Array.Reverse(数组名)数组名.Reverse()查找索引Array.IndexOf(数组名, value)数组名.IndexOf(value)元素是否存在❌数组名.Contains(value)清空Array.Clear(数组名, startIndex, count)数组名.Clear()增删方法增删方法需要自己写内置添加元素❌ 固定大小,不能动态添加数组名.Add(value)插入元素❌ 不支持数组名.Insert(index, value)
数组名.InsertRange(index, 一个集合);批量添加❌ 不支持数组名.AddRange(一个集合)删除元素❌ 不支持数组名.Remove(value)
RemoveAt(index)自动扩容❌ 定长✅ 是类型安全✅ 是❌ 否(需手动强制转换)性能高(只要数组不是object数组就不存在装箱拆箱)相对较低(存在装箱/拆箱)习题
- using System.Collections;class Bag{ private ArrayList items; private int money; public Bag(int money) { this.money = money; items = new ArrayList(); } public void BuyItem(Item item) { //物品信息错误 if (item.num 0){ Console.WriteLine(queue.Dequeue()); //隔停100毫秒 Task.Delay(100).Wait();}
复制代码 Hashtable
哈希表/散列表,本质是一个字典
是基于键的哈希代码组织起来的键值对
用键来访问集合中的元素
哈希表的使用
- using System.Collections;Hashtable hashtable = new Hashtable();//增删查改//增//可以有相同value,但是不能有相同keyhashtable.Add(1, 123);hashtable.Add("123", 321);hashtable.Add(true, false);//或者直接用索引器加,用索引器加相同key的时候相当于改了对应valuehashtable[1] = 123;//删//1.只能通过key来删hashtable.Remove(1);//2.删除不存在的键,不会报错hashtable.Remove("1");//3.清空// hashtable.Clear();//查//1.通过key来查,找不到返回空Console.WriteLine(hashtable[1]);//2.通过key查是否存在键值对if (hashtable.Contains("123")) Console.WriteLine("存在");if (hashtable.ContainsKey("123")) Console.WriteLine("存在");//3.通过value查是否存在键值对if (hashtable.ContainsValue(321)) Console.WriteLine("存在");//改//只能改key对应的value,不能改keyhashtable[1] = 321;Console.WriteLine(hashtable[1]);//遍历//键值对数Console.WriteLine(hashtable.Count);//通过key遍历:可以遍历key和valueforeach (var key in hashtable.Keys){ Console.WriteLine("key:{0},value:{1}",key ,hashtable[key]);}//通过value遍历:只能遍历valueforeach (var value in hashtable.Values){ Console.WriteLine("value:{0}",value);}//迭代器遍历键值对foreach (var item in hashtable){ Console.WriteLine(item);}//迭代器遍历IDictionaryEnumerator enumerator = hashtable.GetEnumerator();bool flag = enumerator.MoveNext();while (flag){ Console.WriteLine("key:{0},value:{1}",enumerator.Key,enumerator.Value); flag = enumerator.MoveNext();}
复制代码注意:哈希表的键值对排列顺序,取决于 key 的哈希码和冲突处理机制,并不是按照插入顺序排列的
关于迭代器:
foreach底层调用的就是 GetEnumerator()
哈希表的装箱拆箱
本质是object容器,字典,所以必然存在装箱拆箱- Hashtable table = new Hashtable();// 装箱:int → objecttable.Add(1, 100); // key 和 value 都是值类型,会装箱// 拆箱:object → intint key = 1;int value = (int)table[key]; // 拆箱操作
复制代码用字典泛型Dictionary 来避免装箱拆箱:
- Dictionary dict = new Dictionary();dict.Add(1, 100);//直接加int value = dict[1]; // 直接取用
复制代码习题
- using System.Collections;for (int i = 0; i < 10; i++){ MonsterManager.Instance.AddMonster();}MonsterManager.Instance.RemoveMonster(1);MonsterManager.Instance.RemoveMonster(5);class MonsterManager{ //要让管理器是唯一的 所以用单例模式来实现 private static MonsterManager _instance = new MonsterManager(); public static MonsterManager Instance { get { return _instance; } } private Hashtable monsterTable = new Hashtable(); //不让在外面new private MonsterManager() { } private int monsterID = 0; public void AddMonster() { Monster monster = new Monster(monsterID); monsterTable.Add(monster.id, monster); (monsterTable[monsterID] as Monster).Generate(); monsterID++; } public void RemoveMonster(int id) { if (monsterTable.ContainsKey(id)) { (monsterTable[id] as Monster).Dead(); monsterTable.Remove(id); } }}class Monster{ public int id; public Monster(int id) { this.id = id; } public void Generate() { Console.WriteLine("生成怪物{0}", id); } public void Dead() { Console.WriteLine("怪物{0}死亡", id); }}
复制代码关于单例模式,这个在C# 核心里面提到过
这就是一个标准的单例模式书写,在外部不能实例化,只有类名.单例属性名.成员方法()才能调用
泛型
泛型的基本概念
- 泛型实现了类型参数化,用于代码复用
- 通过类型参数化来实现在同一份代码上操作多种类型
- 相当于类型占位符
- 定义类/方法的时候使用替代符来来代表变量类型
- 当真正使用类和方法时再具体制定类型
- 泛型占位符一般用大写字母
泛型的作用
- 不同类型对象的相同逻辑处理,可以选择泛型,提升代码的复用
- 使用泛型,可以一定程度避免装箱拆箱
- eg:自己写泛型类ArrayList 来解决ArrayList存在的装箱拆箱问题、Stack 、Queue 、用字典 Dictionary实现Hashtable
泛型分类
语法
泛型类: class 类名
泛型接口: interface 接口名
泛型函数: 函数名
泛型占位字母可以有多个,用逗号隔开
泛型类
- class TestClass{ public T value;}//重载——多个泛型占位字母class TestClass{ public T1 value; public T2 value2;}class Program{ static void Main(string[] args) { //类型占位符T可以用任意数据类型代替,这样就实现了类型的参数化 TestClass t = new TestClass(); t.value = 10; TestClass t2 = new TestClass(); t2.value = "hello world"; TestClass t3 = new TestClass(); t3.value = 10; t3.value2 = "111"; }}
复制代码 泛型接口
- #region 泛型接口interface TestInterface{ //接口只能有属性、方法、事件、索引器 T value { get; set; }}//在类中实现接口,因为是实现,所以必须在内注明数据类型class Test : TestInterface{ public int value { get; set; }}#endregion
复制代码 泛型方法(函数)
不确定泛型类型的时候可以用default(T)来获取默认值,然后在后面写函数逻辑
- #region 普通类中的泛型方法class Test2{ public void TestFunc(T value) { Console.WriteLine(value); } //无参 public void TestFunc() { T t = default(T); Console.WriteLine("{0}类型的默认值是{1}", typeof(T), t); } //占位符作为返回值类型 public T TestFunc(string v) { return default(T); } //多个占位符 public void TestFunc(T v1, T2 v2) { }}#endregionclass Program{ static void Main(string[] args) { //泛型方法 Test2 t4 = new Test2(); t4.TestFunc(10); t4.TestFunc("hello world"); t4.TestFunc(); Console.WriteLine(t4.TestFunc("1")); }}
复制代码 泛型类中的泛型方法
- #region 泛型类中的泛型方法class Test2{ public T value; //函数名后没有,不是泛型方法 // 调用函数的时候,参数类型T已经被类的T定死,无法重新指定其数据类型 public void TestFunc(T v) { } //函数名后有,才是泛型方法 // 括号里的参数类型T只与该函数的一致,和类的T无关 public void TestFunc(T v) { }}#endregionclass Program{ static void Main(string[] args) { //泛型类中的泛型方法 Test2 t5 = new Test2(); t5.TestFunc(10); t5.TestFunc("hello world"); t5.TestFunc("111"); //编译器会自动推算出T的类型为string,但最好写上,不然可读性不高 }}
复制代码习题
- namespace 泛型习题;class Program{ static void Main(string[] args) { Console.WriteLine(Test()); } static string Test() { if (typeof(T) == typeof(int)) { return String.Format("{0},{1}字节", typeof(T), sizeof(int)); } else if (typeof(T) == typeof(double)) { return String.Format("{0},{1}字节", typeof(T), sizeof(double)); } else if (typeof(T) == typeof(float)) { return String.Format("{0},{1}字节", typeof(T), sizeof(float)); } else if (typeof(T) == typeof(char)) { return String.Format("{0},{1}字节", typeof(T),sizeof(char)); } else if (typeof(T) == typeof(string)) { return String.Format("{0}", typeof(T)); } else { return String.Format("其他类型"); } }}
复制代码 泛型约束
泛型约束的基本概念
where 泛型字母:(约束的类型)
- 让泛型的类型有一定限制
- 关键字:where
- 泛型约束一共有6种
各泛型约束
值类型where 泛型字母:struct引用类型where 泛型字母:class存在无参公共构造函数where 泛型字母:new()类本身/子类where 泛型字母:类名接口本身/接口的子类where 泛型字母:接口名另一个泛型类型本身/其派生类型where 泛型字母:另一个泛型字母- namespace 泛型约束;#region 各个泛型类型约束//值类型约束class Test1 where T : struct{ public T value; public void TestFunc(K v) where K : struct { }}//引用类型约束class Test2 where T : class{ public T value; public void TestFunc(K v) where K : class { }}//公共无参构造约束class Test3 where T : new(){ public T value; public void TestFunc(K v) where K : new() { }}class Test1{}class Test2{ public Test2(int a) { }}class Test3{ private Test3() { }}abstract class Test4{}//类约束:某个类本身或其子类class Test4 where T : Test1{ public T value; public void TestFunc(K v) where K : Test1 { }}class Test1_ : Test1{}//接口约束:某个接口或者其子接口或其子类interface IFly{}interface IMove : IFly{ }class Test6 : IFly{ }class Test5 where T : IFly{ public T value;}//另一个泛型约束//前者必须是后者本身或其派生类型class Test7 where T : U{ public T value; public void TestFunc(K k) where K : V { }}#endregionclass Program{ static void Main(string[] args) { //值类型 Test1 t = new Test1(); t.TestFunc(true); // Test1 t2 = new Test1(); 错误 //引用类型 Test2 t2 = new Test2(); t2.TestFunc(new object()); //无参公共构造函数 Test3 t3 = new Test3(); // Test3 t3 = new Test3(); 错误,必须要有无参公共构造函数 // Test3 t3 = new Test3(); 错误,必须要有无参公共构造函数 // Test3 t3 = new Test3(); 错误,抽象类不行,因为抽象类不能new对象,只能在子类继承 Test3 t4 = new Test3(); //正确,所有的值类型实际上都默认有一个无参构造 //类约束:某个类本身或其子类 Test4 t5 = new Test4(); t5.TestFunc(new Test1()); //Test1_是Test1的子类 Test4 t6 = new Test4(); //接口约束 //接口本身 Test5 t7 = new Test5(); t7.value = new Test6(); //接口的实现类(子类) Test5 t8 = new Test5(); //接口的子接口 Test5 t9 = new Test5(); //另一个泛型约束 //同一类型 Test7 t10 = new Test7(); Test7 t11 = new Test7(); //前是后的派生类型 Test7 t12 = new Test7(); Test7 t13 = new Test7(); }}
复制代码 约束的组合使用
用 逗号连接两个约束,相当于多个约束条件
注意:
- 但不是每个都能组合起来使用,看报错
- new()一般写在最后
- #region 约束的组合使用//同时是引用类型且必须有无参构造函数class Test8 where T : class, new(){}class Test8_{ }#endregion
复制代码- #region 约束的组合使用 Test8 t14 = new Test8(); #endregion
复制代码 多个泛型有约束
每个泛型字母都要对应一个 where- #region 多个泛型有约束class Test9 where T : class, new() where U : struct{}#endregion
复制代码习题
- namespace 泛型约束习题;//1. 泛型实现单例模式class SingleBase where T : new(){ private static T _instance = new T(); public static T Instance { get { return _instance; } }}class Test : SingleBase{ }//2. 泛型实现一个不确定类型的ArrayListclass ArrayList{ private T[] array; public void Add(T value) { //... } public void RemoveAt(int index) { //... } public void Remove(T value) { //... } public T this[int index] { get { return array[index]; } set { array[index] = value; } }}
复制代码 常用泛型数据结构类型
List——列表,泛型ArrayList
本质:一个可变类型的泛型数组,也就是泛型实现的ArrayList
类型在申明时就确定好,所以不存在装箱拆箱
List的申明
- using System.Collections.Generic;//申明List list = new List();
复制代码 List的增删查改遍历
和ArrayList一样- using System.Collections.Generic;//申明List list = new List();//增删查改#region 增//单个加list.Add(1);list.Add(2);List list2 = new List();list2.Add(1);//范围加list.AddRange(list2);//在指定位置插入list.Insert(0, 999);#endregion#region 删//移除指定元素list.Remove(1);//移除指定位置元素list.RemoveAt(0);//清空list.Clear();#endregionlist.Add(1);list.Add(2);list.Add(3);#region 查//得到指定位置元素Console.WriteLine(list[0]);//元素是否存在Console.WriteLine(list.Contains(1));//正向查找元素位置//找不到返回-1Console.WriteLine(list.IndexOf(1));Console.WriteLine(list.IndexOf(0));//反向查找元素位置,返回的也是从左往右数的位置,只是从末尾开始遍历//找不到返回-1Console.WriteLine(list.LastIndexOf(1));Console.WriteLine(list.LastIndexOf(0));#endregion#region 改list[0] = 999;#endregionConsole.WriteLine();#region 遍历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);}#endregion
复制代码 List和ArrayList的区别
List就是在申明时就确定好类型的ArrayList
ListArrayList内部封装泛型数组
不存在装箱拆箱object数组Dictionary——字典,泛型哈希表
本质:泛型实现的Hashtable,也是基于键的哈希代码组织起来的键值对
键值对的类型在申明时就确定好,所以不存在装箱拆箱
Dictionary的申明
- using System.Collections.Generic;//申明Dictionary dictionary = new Dictionary();
复制代码 Dictionary的增删查改遍历
- using System.Collections.Generic;//申明Dictionary dictionary = new Dictionary();//增删查改#region 增dictionary.Add(1, "111");dictionary[2] = "222";#endregion#region 删//1.通过键删除//删除不存在的键,不报错dictionary.Remove(1);//2.清空dictionary.Clear();#endregiondictionary.Add(1, "111");dictionary.Add(2, "222");dictionary.Add(3, "333");#region 查//1.通过键查询//找不到键就报错,不返回空Console.WriteLine(dictionary[1]);//2.查看是否存在//根据keyConsole.WriteLine(dictionary.ContainsKey(1)); //根据valueConsole.WriteLine(dictionary.ContainsValue("222"));#endregion#region 改dictionary[2] = "9999";#endregion//遍历Console.WriteLine(dictionary.Count);//一起遍历foreach (var item in dictionary){ Console.WriteLine(item.Key + ":" + item.Value);}foreach (KeyValuePair item in dictionary){ Console.WriteLine(item);}//遍历keyforeach (var item in dictionary.Keys){ Console.WriteLine(item);}//遍历valueforeach (var item in dictionary.Values){ Console.WriteLine(item);}
复制代码 顺序存储和链式存储
顺序结构:数组、ArrayList、Stack、Queue、List
链式结构:链表(单向、双向、循环)
LinkedList——泛型双向链表
本质:一个可变类型的泛型双向链表
链表的节点类LinkedListNode
LinkedList申明
- //申明LinkedList linkedList = new LinkedList();LinkedList linkedList2 = new LinkedList();
复制代码 LinkedList的增删查改和遍历
- //申明LinkedList linkedList = new LinkedList();LinkedList linkedList2 = new LinkedList();//增删查改#region 增//头插linkedList.AddFirst(1);//尾插linkedList.AddLast(99);//在指定节点后插入LinkedListNode n = linkedList.Find(1);linkedList.AddAfter(n, 2);//在指定节点前插入linkedList.AddBefore(n, 0);#endregion#region 删//删除头节点linkedList.RemoveFirst();//删除尾节点linkedList.RemoveLast();//删除指定值的节点//无法通过位置删除,因为链表没有办法直接获取索引linkedList.Remove(99);//清空linkedList.Clear();#endregionlinkedList.AddLast(1);linkedList.AddLast(2);#region 查//获取头节点LinkedListNode first = linkedList.First;//获取尾节点LinkedListNode last = linkedList.Last;//获取指定值的节点LinkedListNode node = linkedList.Find(1);Console.WriteLine(node.Value);//判断是否存在Console.WriteLine(linkedList.Contains(1));#endregion#region 改Console.WriteLine(node.Value);node.Value = 3;Console.WriteLine(node.Value);#endregion#region 遍历//迭代器foreach (var item in linkedList){ Console.WriteLine(item);}//通过节点遍历:因为本质是双向链表,所以存在正序和倒序遍历//1.正序遍历LinkedListNode nowNode = linkedList.First;while (nowNode != null){ Console.WriteLine(nowNode.Value); nowNode = nowNode.Next;}//2.倒序遍历nowNode = linkedList.Last;while (nowNode != null){ Console.WriteLine(nowNode.Value); nowNode = nowNode.Previous;}#endregion
复制代码 泛型栈和队列
前面介绍栈和队列的时候有装箱拆箱的问题,在其解决方法中我已提过引入泛型来解决
泛型栈
Stack - Stack stack = new Stack();
复制代码 泛型队列
Queue queue- Queue queue = new Queue();
复制代码 其内置方法和之前的栈和队列完全一样
总结:上述各种数据容器的适用场景
数组、List、Dictionary, Stack, Queue, LinkedList
数据结构类型特点适用场景数组固定长度连续内存存储,支持下标访问,性能高数据量固定、频繁通过下标访问的场景List动态数组可变长度,支持下标访问,插入/删除效率较低需要频繁修改内容但又需要通过索引快速查找的场景LinkedList双向链表插入/删除效率高,不支持下标访问不确定长度,频繁在中间插入或删除元素的场景Stack后进先出入栈(Push)、出栈(Pop)、查看栈顶(Peek)实现递归算法、撤销/重做机制、UI面板显隐规则等Queue先进先出入队(Enqueue)、出队(Dequeue)消息队列、任务调度、事件处理等需按顺序处理的场景Dictionary键值对集合快速通过键查找值,不允许重复键存储具有唯一标识的数据,如ID-对象映射、配置项、资源管理等委托和事件
委托
委托是专门装载函数的容器,也就是函数的变量类型
用来存储、传递函数
本质:是一个类,用来定义函数的类型(返回值和参数的类型)
不同的函数对应和各自“格式"一致的委托
委托的申明和使用
关键字:delegate
位置:nameplace、class语句块中,一般写在nameplace中
访问修饰符:一般用public,默认不写就是public
语法:访问修饰符 delegate 返回值 委托名(参数列表)- namespace 委托;//委托的申明,统一语句块中不能重名delegate void MyFunc();public delegate int MyFunc2(int a);class Program{ static void Main(string[] args) { //委托是专门装载函数的容器 //把格式一样(无参无返回值)的方法Fun装进了MyFunc的对象f里面 //两种存放写法 MyFunc f = new MyFunc(Fun); MyFunc f2 = Fun; //调用委托对象f存放的方法 //两种调用写法 f.Invoke(); f2(); //注意:格式必须一样才能装载 MyFunc2 f3 = new MyFunc2(Fun2); Console.WriteLine(f3(1)); } static void Fun() { Console.WriteLine("Fun"); } static int Fun2(int value) { return value; }}
复制代码 泛型委托
- //泛型委托delegate T MyFunc3(T t, K k);
复制代码 使用定义好的委托——观察者设计模式
- #region 使用定义好的委托//委托常用在://1.作为类的成员//2.作为函数的参数class Test{ public MyFunc func; public MyFunc2 func2; public void TestFunc(MyFunc func, MyFunc2 func2) { //观察者设计模式 //先处理一些逻辑,后 存放/延迟执行 传入的函数 int i = 0; i++; //延迟执行传入的函数 //func(); //func2(i); //存放传入的函数 this.func = func; this.func2 = func2; }}#endregionclass Program{ static void Main(string[] args) { #region 使用定义好的委托 Test t = new Test(); t.TestFunc(Fun, Fun2); #endregion } static void Fun() { Console.WriteLine("Fun"); } static int Fun2(int value) { return value; }}
复制代码 委托变量存储多个函数——加、减、清空
- class Test{ public MyFunc func; public MyFunc2 func2; public void TestFunc(MyFunc func, MyFunc2 func2) { //观察者设计模式 //先处理一些逻辑,后 存放/延迟执行 传入的函数 int i = 0; i++; //延迟执行传入的函数 //func(); //func2(i); //存放传入的函数 this.func = func; this.func2 = func2; } #region 委托变量存储多个函数 //同样,需要格式一致才能装载 //增 += public void AddFunc(MyFunc func, MyFunc2 func2) { this.func += func; this.func2 += func2; } //删 -= public void RemoveFunc(MyFunc func, MyFunc2 func2) { this.func -= func; this.func2 -= func2; } #endregion}
复制代码- #region 委托变量存储多个函数 //同样,需要格式一致才能装载 //增 += // MyFunc ff = Fun; // ff += Fun3; // ff(); //或者:先赋值为null,再+= MyFunc ff = null; ff += Fun; ff += Fun3; ff(); t.AddFunc(Fun, Fun2); t.func(); //删 -= ff -= Fun; //多删不会报错 ff -= Fun; ff(); ff -= Fun3; //删完会报错 // ff(); 删完,ff为null,调用会报错 //清空委托容器 // ff = null; if (ff != null) ff(); #endregion
复制代码 系统定义好的委托
- #region 系统定义好的委托容器 //无参无返回 —— Action Action action = Fun; action += Fun3; action(); //n个参数无返回,最多支持传入16个参数 —— Action Action actions = Fun6; actions(1, "111"); //无参有返回的泛型委托 —— Func Func funcString = Fun4; Func funcInt = Fun5; //n个参数有返回,最多支持传入16个参数 —— Func //注意:参数的类型写前面,返回值的类型写后面 Func funcs = Fun7; //参数是int,返回值是string #endregion
复制代码- static void Fun() { Console.WriteLine("这是Fun方法"); } static void Fun3() { Console.WriteLine("这是Fun3方法"); } static int Fun2(int value) { return value; } static string Fun4() { return "这是Fun4方法"; } static int Fun5() { return 5; } static void Fun6(int value, string value2) { } static string Fun7(int value) { return "这是Fun7方法"; }
复制代码习题
后面再来做题巩固
事件
事件是委托的安全包裹,让委托的使用更加安全
这是一种特殊的变量类型
申明语法:访问修饰符 event 委托类型 事件名
作用:
- 作为成员变量存在于类、接口、结构体中
- 委托怎么用,事件就怎么用
事件和委托的区别:事件不能在类的外部赋值、调用
匿名函数
Lambda表达式
反射和特性
游戏常用查找(寻路)
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |