零、特性 1 预处理器指令 编译器是一种翻译程序 它用于将源语言程序翻译为目标语言程序
源语言程序: 某种程序设计语言写成的, 比如 C#、C、C++、Java 等语言写的程序
目标语言程序: 二进制数表示的伪机器代码写的程序
预处理器指令指导编译器在实际编译开始之前对信息进行预处理 预处理器指令都是以#
开始 预处理器指令不是语句,所以它们不以分号 ;
结束
title:折叠代码 1 2 3 #region 折叠块名字 ... #endregion
1 2 3 4 5 #define //定义一个符号,类似一个没有值的变量 #undef //取消define 定义的符号,让其失效
1 2 3 4 5 6 #if #elif //即elseif #else #endif
title:案例 1 2 3 4 5 6 7 8 9 10 11 #define tag1 #define tag2 #undef tag2 #if tag1 Console.WriteLine("Hello world!" ); #elif tag2 Console.WriteLine("Hi world!" ); #else Console.WriteLine("Exit!" ); #endif
2 控制台方法 title:打印输入输出 1 2 3 4 5 6 7 8 9 10 Console.Write("xxx" ); Console.WriteLine("xxx" ); Console.ReadLine(); Console.ReadKey(); Console.ReadKey(true ).KeyChar; Console.KeyAvailable
title:其他方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Console.Clear(); Console.SetWindowSize(50 ,40 ); Console.SetBufferSize(1000 , 1000 ); Console.SetCursorPosition(10 ,5 ); Console.ForegroundColor = ConsoleColor.Red; Console.BackgroundColor = ConsoleColor.White; Console.CursorVisible = false ; Environment.Exit(0 );
3 Path 类 用于操作路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void Main (string [] args ){ string str = @"C:\Users\22625\Desktop\1.txt" ; Console.WriteLine(Path.GetFileName(str)); Path.GetExtension(str); Path.GetFileNameWithoutExtension(str) Path.GetDirectoryName(str); Path.GetFullPath(str); Path.Combine(@"C:\Users\22625\Desktop\" ,"1.txt" ) ...... }
4 垃圾回收 GC 垃圾回收, 英文简写 GC (Garbage Collector)
垃圾回收的过程是在遍历堆 (Heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用。 所谓的垃圾就是没有被任何变量,对象引用的内容。垃圾就需要被回收释放,
垃圾回收有很多种算法,比如
引用计数 (Reference Counting)
标记清除 (Mark Sweep)
标记整理 (Mark Compact)
复制集合 (Copy collection)
GC 只负责堆 (Heap)内存的垃圾回收,引用类型都是存在堆 (Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理 栈 (Stack)上的内存是由系统自动管理的,值类型在栈 (Stack)中分配内存的。他们有自己的生命周期,不用对他们进行管理,会自动分配和释放
CS 中内存回收机制的大概原理: 0 代内存 1 代内存 2 代内存代的概念:
代是垃圾回收机制使用的一种算法 (分代算法)
新分配的对象都会被配置在第 0 代内存中
每次分配都可能会进行垃圾回收以释放内存 (0 代内存满时)
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步
标记对象从根(静态字段、方法参数)开始检查引用对象(引用类型,比如类、数组),标记后为可达对象,未标记为不可达对象,不可达对象就认为是垃圾
搬迁对象压缩堆 (挂起执行托管代码线程), 释放未标记的对象,搬迁可达对象,修改引用地址
大对象总被认为是第二代内存,目的是减少性能损耗,提高性能
不会对大对象进行搬迁压缩(85080 字节 (83kb)以上的对象为大对象)
title:手动垃圾回收
5 类型转换 我们要求等号两边参与运算的操作数必须一致,如果不一致,满足下列条件会发生转换。
隐式类型转换
低精度可以转换成高精度
char→整数(有符号、无符号)→float→double $\nrightarrow$ decimal
char→整数(有符号、无符号)→decimal
string 和 bool 不参与隐式转换规则
有符号 $\nrightarrow$ 无符号
无符号→有符号(精度低到高)
显式(强制)类型转换 括号强转 用于将高精度转换位低精度
1 2 3 4 5 6 7 8 9 10 11 12 13 double a =(int )b;int n1 = 10 ;int n2 = 3 ;double d = n1 / n2;Console.WriteLine(d); double d = n1*1.0 / n2; Console.WriteLine(d);
Convert 类型转换
如果两个变量类型不兼容,比如 string 与 int 或 string 与 double,可以使用 Convert 函数进行转换。
string 字符串的内容必须为要转换的类型。
Convert.ToInt32()
Convert.ToDouble()
…
string a 1 2 3 string a = "123" ; int b = Convert.ToInt32(a);Console.WriteLine(b);
.Parse 类型转换 效果同 Convert:
int.Parse()
double.Parse()
1 2 3 string a = "123" ; int b = int .Parse(a);
.TryParse 类型转换 int.TryParse()
1 2 3 4 5 int a = 0 ;bool b = int .TryParse("123" , out a);Console.WriteLine(b); Console.WriteLine(a);
6 命名空间 概念: 命名空间是用来组织和重用代码的
作用: 就像是一个工具包,类就像是一件一件的工具,都是申明在命名空间中的
不同命名空间中相互使用需要引用命名空间(using namespace;
)或指明出处(namespace.test ()
)
不同命名空间中允许有同名类
命名空间可以嵌套命名
一、变量和类型 内置类型 值类型: 无符号整形,有符号整形,浮点数 char bool enum 结构体
引用类型: string 类, 自定义类, 集合类,object 类,接口,委托,数组
值类型和引用类型区别:
值类型和引用类型在内存上存储的地方不一样。
在传递值类型和传递引用类型的时候,传递的方式不一样。值类型我们称之为值传递,引用类型我们称之为引用传递。
值类型的值存储在内存的栈 中 (系统自动回收,小而快),引用类型的值存储在内存的堆 中(手动释放,大而慢)
@ 引用类型的数据存在堆中,栈中只存一个地址指向堆中存储的数据1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int a = 1 ; int [] arr1 = { 1 , 2 , 3 , 4 }; int b = a; int [] arr2 = arr1; b = 5 ; arr2[0 ] = 5 ; Console.WriteLine(a); Console.WriteLine(arr1[0 ]);
值类型
有符号整数类型
描述
范围
默认值
sbyte
8 位有符号整数类型
-128 到 127
0
short
16 位有符号整数类型
-32,768 到 32,767
0
int
32 位有符号整数类型
-2,147,483,648 到 2,147,483,647
0
long
64 位有符号整数类型
-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
0L
无符号整数
描述
范围
默认值
byte
8 位无符号整数
0 到 255
0
ushort
16 位无符号整数类型
0 到 65,535
0
uint
32 位无符号整数类型
0 到 4,294,967,295
0
ulong
64 位无符号整数类型
0 到 18,446,744,073,709,551,615
0
浮点数
描述
范围
默认值
float
32 位单精度浮点型
-3.4 x 1038 到 + 3.4 x 1038
0.0F
double
64 位双精度浮点型
(+/-) 5.0 x 10-324 到 (+/-) 1.7 x 10308
0.0D
decimal
128 位精确的十进制值,28-29 有效位数
(-7.9 x 1028 到 7.9 x 1028) / 100 到 28
0.0M
其他类型
描述
范围
默认值
char
16 位 Unicode 字符
U +0000 到 U +ffff
‘\0’
bool
8 位布尔值
True 或 False
False
c# 中的小数默认为 double 类型,所以声明 float 时末尾加 f (或大写 F)显示表示 :
sizeof()
返回值类型 变量的大小(字节)
保留指定小数位数 1 2 3 4 5 6 double n1 = 3.33333 ;Console.WriteLine($"{n1:0.00 } " ); 输出:3.33
引用类型
C# 类型关键字
. NET 类型
object
System. Object
string
System. String
dynamic
System. Object
在上表中,左侧列中的每个类型关键字(dynamic 除外)都是相应 .NET 类型的别名。它们是可互换的。例如,以下声明声明了相同类型的变量:
1 2 int a = 123 ;System.Int32 b = 123 ;
结构体 struct 1 2 3 4 5 6 7 8 9 10 11 public struct Person { public string _name; public int _age; public char _gender; void Speak () { ... } }
[!summary] 结构体和类的区别概述:
结构体和类最大的区别是在存储空间上的,因为结构体是值,类是引用,因此他们的存储位置一个在栈上,一个在堆上,
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。
结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,由于结构体不具备继承的特性,所以它不能够使用 protected 访问修饰符
特别的,结构体可以继承接口,因为接口是行为的抽象
细节:
结构体是值类型,类是引用类型
结构体存在栈中,类存在堆中
结构体成员不能使用 protected 访问修饰符,而类可以
结构体成员变量声明不能指定初始值,而类可以
结构体不能声明无参的构造函数,而类可以
结构体申明有参构造函数后,无参构造不会被顶掉
结构体不能申明析构函数,而类可以
结构体不能被继承, 而类可以
结构体需要在构造函数中初始化所有成员变量,而类随意
结构体不能被静态 static 修饰 (不存在静态结构体),而类可以
结构体不能在自己内部申明和自已一样的结构体变量, 而类可以
[!info] 如何选择结构体和类
想要用继承和多态时,使用类,比如玩家、怪物等等
对象是数据集合时,优先考虑结构体,比如位置、坐标等等
从值类型和引用类型赋值时的区别上去考虑,比如经常被赋值传递的对象,并且改变赋值对象,原对象不想跟着变化时,就用结构体。比如坐标、向量、旋转等等
string 1 2 3 4 5 6 7 string str = "test" ;Console.WriteLine(str[0 ]); char [] chars = str.ToCharArray();Console.WriteLine(chars[0 ]);
具有值类型特征 string 虽然是引用类型,但他有值类型的特征,每次重新赋值或拼接时会分配新的内存空间。
1 2 3 4 5 6 string str1 = "123" string str2 = str1; str2 = "321" ;
字符串类型拼接方式
“+” “+=”号,不能用其他运算符
string. Format ("待拼接的内容",内容 1,内容 2,......)
使用占位符 {数字}
控制拼接顺序1 2 3 4 5 string s = string .Format("我是{0},我今年{1}岁,我喜欢{2}" ,"小明" ,"16" ,"玩游戏" );Console.WriteLine(s); Console.WriteLine(string .Format("我是{0},我今年{1}岁,我喜欢{2}" ,"小明" ,"16" ,"玩游戏" ));
$
替代 string.format()
原先赋值需要占位符和变量,当需要拼接多个变量会造成语句过长等不易理解问题,$
可以把字符串中的变量 {}
包含起来达到识别变量的目的 $"{id}"
;也支持表达式,使用 $"{(你的表达式)}"
1 2 3 4 5 6 7 8 var k = "a" ; var a0 = "User" ; var a1 = "Id" ; var a2 = 5 ; var ccb = $"select * from {a0} where {a1} ={a2} " ;var ccc = string .Format("select * from {0} where {1} = {2}" , a0, a1, a2);
字符串方法 查找字符位置 title:IndexOf查找字符位置 1 2 3 4 5 6 7 8 9 string str = "这是一句话" ;int index1 = str.IndexOf("是" ); int index2 = str.LastIndexOf("是" );
移除字符 title:Remove移除指定位置后的字符 1 2 3 4 5 6 7 8 9 10 11 12 string str = "这是一句话" ;str = str.Remove(2 ); Console.WriteLine(str); str = str.Remove(1 ,3 ); Console.WriteLine(str);
替换字符 title:Replace替换字符 1 2 3 4 string str = "这是一句话" ;str = str.Replace("一句话" , "歌词" ); Console.WriteLine(str);
大小写转换 1 2 3 4 5 6 string str = "abcdefg" ;str = str.ToUpper(); Console.WriteLine(str); str = str.ToLower();
字符串截取 title:Substringj截取字符串 1 2 3 4 5 6 7 8 9 10 string str = "abcdefg" ;str = str.Substring(2 ); str = str.Substring(1 ,3 );
字符串切割 title:Split切割字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 string str = "a|b|c|d|e|f|g" ;string [] strs = str.Split(new char [] { '|' }); for (int i = 0 ; i < strs.Length; i++) { Console.WriteLine(strs[i]); } a b c d e f g
StringBuilder
用于处理字符串的公共类
主要解决的问题 :修改字符串而不创建新的对象, 需要频繁修改和拼接的字符串可以使用它,可以提升性能
使用前需要引用命名空间 using System.Text;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 using System.Text;StringBuilder str = new StringBuilder("0123456" ); Console.WriteLine(str.ToString()); Console.WriteLine(str.Capacity); Console.WriteLine(str.Length); str.Append(' ' ); str.AppendFormat("{0}{1}" , 7 , 8 ); str.Remove(0 , 10 ); str.Clear(); str[0 ] = 'a' ; Console.WriteLine(str[0 ]); str.Insert(0 , "test" ); str.Replace("1" , "A" ); if (str.Equals("A" )) { }
string 和 StringBuilder 的区别
string 相对 StringBuilder 更容易产生垃圾,每次修改拼接都会产生垃圾
string 相对 StringBuilder 更加灵活因为它提供了更多的方法供使用
如何选择?
需要频繁修改拼接的字符串可以使用 StringBuilder
需要使用 string 独特的一些方法来处理一些特殊逻辑时可以使用 string
[!NOTE] 如何优化内存 内存优化从两个方面去解答
如何节约内存
如何尽量少的 GC (垃圾回收)?
答案:
少 new 对象少产生垃圾
合理使用 static - 合理使用 string 和 stringbuilder
万物之父 Object 类 关键字:object
概念: object 是所有类型的基类 ,它是一个类 (引用类型)
作用:
可以利用里氏替换原则,用 object 容器装所有对象
可以用来表示不确定类型,作为函数参数类型
用法 title:object类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 Father f = new Son(); if (f is Son){ (f as Son).Speak(); } object o = new Son();if (o is Son){ (o as Son).Speak(); } object o2 = 10.0f ; float f2 = (float )o2; object ostr = "123123" ;string str1 = ostr.ToString();string str2 = ostr as string ; object oarr = new int [10 ];int [] arr1 = (int [])oarr;int [] arr2 = oarr as int [];public class Father { } public class Son : Father { public void Speak () { Console.WriteLine("Hello world!" ); } }
装箱拆箱 发生条件
用 object 存值类型(装箱)
再把 object 转为值类型 (拆箱)
装箱
把值类型用引用类型存储,如 object o = 10.0f;
栈内存会迁移到堆内存中
拆箱
把引用类型存储的值类型取出来,如 float f = (float)o;
堆内存会迁移到栈内存中
好处: 不确定类型时可以方便参数的存储和传递坏处: 存在内存迁移,增加性能消耗
object 的方法
静态方法
Equals
: 判断两个对象是否相等 最终的判断权,交给左侧对象的 Equals 方法, 不管值类型引用类型都会按照左侧对象 Equals 方法的规则来进行比较
ReferenceEquals
:比较两个对象是否是相同的引用,主要是用来比较引用类型的对象。值类型对象返回值始终是 false。
成员方法
普通方法 GetType
该方法在反射相关知识点中是非常重要的方法,之后我们会具体的讲解这里返回的 Type 类型。
该方法的主要作用就是获取对象运行时的类型 Type,
通过 Type 结合反射相关知识点可以做很多关于对象的操作。
普通方法 Memberwiseclone
该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象, 但是新对象中的引用变量会和老对象中一致。
虚方法
Equals
默认实现还是比较两者是否为同一个引用,即相当于 ReferenceEquals
。
但是微软在所有值类型的基类 system. ValueType
中重写了该方法, 用来比较值相等。
我们也可以重写该方法,定义自己的比较相等的规则
GetHashcode
该方法是获取对象的哈希码
一种通过算法算出的,表示对象的唯一编码,不同对象哈希码有可能一样,具体值根据
我们可以通过重写该函数来自己定义对象的哈希码算法,正常情况下,我们使用的极少
ToString
该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串规则,
该方法非常常用。当我们调用打印方法时,默认使用的就是对象的 Tostring 方法后打印出来的内容。
特殊类型 常量 1 2 const 变量类型 变量名 = 值const int a = 1 ;
? 常量是特殊的静态 static?
const (常量)可以理解为特殊的 static (静态)
相同点 他们都可以通过类名点出使用
不同点
const 必须初始化,不能修改, static 没有这个规则
const 只能修饰变量、static 可以修饰很多
const 一定是写在访问修饰符后面的,static 没有这个要求
随机数 1 2 3 4 5 6 Random r = new Random(); r.Next(); r.Next(100 ); r.Next(5 ,100 );
枚举 title:声明枚举 1 2 3 4 5 6 7 8 9 10 public enum 枚举名{ 值1 , 值2 , 值3 , ...... 值n }
枚举通常声明到 namespace 的下面,class 的外面,表示这个命名空间下,所有的类都可以使用这个枚举。
不可以在函数中声明
title:枚举搭配switch使用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace ConsoleApp1 { enum EPlayer { singer, writer, teacher, student } internal class Program { static void Main (string [] args ) { EPlayer Player1 = EPlayer.singer; switch (Player1) { case EPlayer.singer: ... break ; case EPlayer.student: ... break ; default : break ; } } } }
title:枚举类型转换 1 2 3 4 5 6 7 8 9 EPlayer Player = EPlayer.singer; int i = (int )Player; Player = 0 ; string str = Player.ToString(); Player = (EPlayer)Enum.Parse(typeof (EPlayer), "teacher" );
数组 数组声明后不可以改变长度 ,若想在原数组的基础上进行收缩,需要新建一个数组,将值复制到新数组。
一维数组 title:一维数组的声明 1 2 3 4 5 6 7 int [] nums; int [] nums = new int [5 ]; int [] nums = new int []{1 ,2 ,3 ,4 ,5 };int [] nums = new int [5 ]{1 ,2 ,3 ,4 ,5 };int [] nums = {1 ,2 ,3 ,4 ,5 };
title:一维数组方法
title:增加和减少数组中的元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int [] array = { 1 , 2 , 3 , 4 , 5 };int [] array1 = new int [10 ];for (int i = 0 ; i < array.Length; i++){ array1[i] = array[i]; } array = array1; int [] array = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 };int [] array2 = new int [5 ];for (int i = 0 ; i < array2.Length; i++){ array2[i] = array[i]; } array = array2;
二维数组 title:二维数组的声明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int [,] nums; int [,] nums = new int [3 ,3 ]; int [,] nums = new int [,]{{1 ,1 ,1 }, {2 ,2 ,2 }, {3 ,3 ,3 }}; int [,] nums = new int [3 ,3 ]{{1 ,1 ,1 }, {2 ,2 ,2 }, {3 ,3 ,3 }}; int [,] nums = {{1 ,1 ,1 }, {2 ,2 ,2 }, {3 ,3 ,3 }};
title:二维数组方法 1 2 nums.GetLength(0 ) nums.GetLength(1 )
交错数组 不常用,和二维数组的区别在于,每行的列数可以不同
title:交错数组的声明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int [][] arr1;int [][] arr2 = new int [3 ][];int [][] arr3 = new int [][] { new int [] { 1 }, new int [] { 1 , 2 }, new int [] { 1 , 2 , 3 } }; int [][] arr4 = new int [3 ][] { new int [] { 1 }, new int [] { 1 , 2 }, new int [] { 1 , 2 , 3 } }; int [][] arr5 = { new int [] { 1 }, new int [] { 1 , 2 }, new int [] { 1 , 2 , 3 } };
title:交错数组方法 1 2 nums.GetLength (0 ) nums[0 ].Length (1 )
集合类(Collection) 集合(Collection)类是专门用于数据存储和检索的类。这些类提供了对栈(stack)、队列(queue)、列表(list)和哈希表(hash table)的支持。大多数集合类实现了相同的接口。
集合(Collection)类服务于不同的目的,如为元素动态分配内存,基于索引访问列表项等等。这些类创建 Object 类的对象的集合 。在 C# 中,Object 类是所有数据类型的基类。
下面是各种常用的 System. Collection 命名空间的类:
ArrayList (动态数组) ArrayList 和数组的区别 ArrayList:长度可以随意改变,可以存储任意类型的数据 数组:长度不可变,类型单一
每次集合中实际包含的元素个数 (count)超过了可以包含的元素的个数 (capcity)的时候,集合就会向内存中申请多开辟一倍 的空间,来保证集合的长度一直够用。
ArrayList 中的元素都存储为 object
类型(可以存储任何类型数据),存在装箱拆箱的损耗,所以 ArrayList 尽量少用。用 List 即可!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ArrayList array = new ArrayList(); array.Add(1 ); array.Add(2.1 ); array.Add(true ); array.Add("张三" ); array.AddRange(new int [] { 1 , 2 , 3 , 4 , 5 }); array.insert(1 ,"李四" ); array.Remove("张三" ); array.RemoveAt(0 ); array.RemoveRange(0 ,n); array.Clear(); array[0 ]; array.Contains("张三" ); array.IndexOf("张三" ); array.LastIndexOf("张三" ); array[0 ] = "李四" ; array.Sort(); array.Reverse();
title:遍历 1 2 3 4 5 6 7 array.Count; foreach (var item in array){ Console.WriteLine(array[item]); }
Stack 栈,先进后出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Stack stack = new Stack(); stack.Push("1" ); stack.Pop(); s = stack.Peek(); stack.Contains("1" ); stack.Count; stack.Clear();
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 foreach (var item in stack){ Console.WriteLine(item); } object [] array = stack.ToArray();for (int i =0 ;i<array.Length;i++){ Console.WriteLine(array[i]); } while (stack.Count>0 ){ object p = stack.Pop(); }
Queue 队列,先进先出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Queue queue = new Queue(); queue.Enqueue("1" ); 、 queue.Dequeue(); queue.Peek(); queue.Contains("1" ); queue.Count; queue.Clear();
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 foreach (var item in queue){ Console.WriteLine(item); } object [] array = queue.ToArray();for (int i =0 ;i<array.Length;i++){ Console.WriteLine(array[i]); } while (queue.Count > 0 ) { object v5 = queue.Dequeue(); }
Hashtable 哈希表(又称散列表),键值对
title:增删查改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Hashtable hashtable = new Hashtable(); hashtable.Add(1 , "value1" ); hashtable.Add("key" , "value2" ); hashtable.Add(true , "value3" );、 hashtable.Remove(1 ); hashtable.Clear(); hashtable["2" ]; hashtable.ContainsKey("key" ); hashtable.ContainsValue("value4" ); hashtable.Count; hashtable["key" ] = "value4" ;
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ICollection keys = hashtable.Keys; foreach (var item in keys) { Console.WriteLine(item); } ICollection values = hashtable.Values; foreach (var item in values){ Console.WriteLine(item); } foreach (DictionaryEntry item in hashtable){ Console.WriteLine(item.Key + ":" + item.Value); } IEnumerator enumerator1 = hashtable.GetEnumerator(); while (enumerator1.MoveNext()){ DictionaryEntry item = (DictionaryEntry)enumerator1.Current; Console.WriteLine(item.Key + ":" + item.Value); } IDictionaryEnumerator enumerator2 = hashtable.GetEnumerator(); while (enumerator2.MoveNext()){ Console.WriteLine(enumerator2.Key + ":" + enumerator2.Value); }
泛型数据结构类 泛型数据结构类和集合类区别:
泛型数据结构类可以指定泛型类型 ,避免了装箱拆箱的性能损耗
集合类数据类型都是 Object 类型,有装箱拆箱的性能损耗
List<> 本质是一个可变类型的泛型数组 ,和 ArrayList 主要区别在于可以指定泛型类型,避免了装箱拆箱的性能损耗
title:增删查改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 List<int > list = new List<int >(); list.Add(1 ); list.AddRange(new int [] { 2 , 3 , 4 }); list.Insert(1 , 66 ); list.Remove("张三" ); list.RemoveAt(0 ); list.RemoveRange(0 ,n); list.Clear(); list[0 ]; list.Contains(1 ); list.Count; list.IndexOf(1 ); list.LastIndexOf(1 ); list[0 ] = 1 ; list.Reverse();
title:遍历 1 2 3 4 5 6 7 8 9 10 11 foreach (var item in list) { Console.WriteLine(item); } for (int i = 0 ; i < list.Count; i++) { Console.WriteLine(list[i]); }
List 的排序 内置变量一般通过 Sort 方法进行进行排序,自定义类则需要自己写方法。
自定义类的排序
继承 IComparable<Item>
接口
实现接口的方法 CompareTo(Item other)
方法
title:自定义类的排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class Item : IComparable <Item > { public int money; public Item (int money ) { this .money = money; } public int CompareTo (Item other ) { if (this .money > other.money) { return 1 ; } else if (this .money == other.money) { return 0 ; } else { return -1 ; } } } class Program { static void Main (string [] args ) { List<Item> itemList = new List<Item>(); itemList.Add(new Item(10 )); itemList.Add(new Item(25 )); itemList.Add(new Item(13 )); itemList.Add(new Item(40 )); itemList.Sort(); for (int i =0 ;i<itemList.Count;i++) { Console.WriteLine(itemList[i].money); } } }
通过委托函数排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class Item { public int money; public Item (int money ) { this .money = money; } } class Program { static void Main (string [] args ) { List<Item> itemList = new List<Item>(); itemList.Add(new Item(10 )); itemList.Add(new Item(25 )); itemList.Add(new Item(13 )); itemList.Add(new Item(40 )); itemList.Sort(SortItem); for (int i =0 ;i<itemList.Count;i++) { Console.WriteLine(itemList[i].money); } static int SortItem (Item a, Item b ) { if (a.money > b.money) { return 1 ; } else if (a.money == b.money) { return 0 ; } else { return -1 ; } } } }
Dictionary<> 字典,可以将 Dictionary
理解为拥有泛型的 Hashtable
,它也是基于键的哈希代码组织起来的键/值对, 键值对类型从 Hashtable 的 object 变为了可以自己制定的泛型
title:增删查改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Dictionary<int ,string > dictionary = new Dictionary<int , string >(); dictionary.Add(1 , "one" ); dictionary.Remove(1 ); dictionary.Clear(); string v1 = dictionary[1 ]; string v2;if (dictionary.TryGetValue(1 , out v2)){ } dictionary.ContainsValue("one" ) dictionary.ContainsKey(1 ) dictionary.Count dictionary[1 ] = "ONE" ;
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 foreach (int key in dictionary.Keys){ Console.WriteLine(key); } foreach (string value in dictionary.Values){ Console.WriteLine(value ); } foreach (KeyValuePair<int , string > item in dictionary){ int key = item.Key; string value = item.Value; } IEnumerator<KeyValuePair<int , string >> enumerator = dictionary.GetEnumerator(); while (enumerator.MoveNext()){ KeyValuePair<int , string > item = enumerator.Current; int key = item.Key; string value = item.Value; }
LinkedList<> 和 LinkedListNode<> LinkedList
本质是一个可变类型的泛型双向链表 LinkedListNode
是链表节点类
title:增删查改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 LinkedList<int > linkedList = new LinkedList<int >(); LinkedListNode<int > first = linkedList.First; LinkedListNode<int > last = linkedList.Last; linkedList.AddLast(1 ); linkedList.AddFirst(2 ); linkedList.AddAfter(first, 3 ); linkedList.AddBefore(first, 4 ); linkedList.Remove(1 ); linkedList.RemoveFirst(); linkedList.RemoveLast(); linkedList.Remove(first); linkedList.Clear(); linkedList.Contains(1 ); LinkedListNode<int > node1 = linkedList.Find(1 ); LinkedListNode<int > node2 = linkedList.FindLast(1 ); LinkedListNode<int > node3 = linkedList.Find(1 ); node3.Value = 2 ;
title:b遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 foreach (var item in linkedList){ Console.WriteLine(item); } LinkedListNode<int > first = linkedList.First; while (first != null ){ Console.WriteLine(node.Value); node = node.Next; } LinkedListNode<int > last = linkedList.Last; while (last != null ){ Console.WriteLine(node4.Value); node4 = node4.Previous; }
Stack<> 栈,先进后出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Stack<int > stack = new Stack<int >(); stack.Push("1" ); stack.Pop(); s = stack.Peek(); stack.Contains("1" ); stack.Count; stack.Clear();
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 foreach (var item in stack){ Console.WriteLine(item); } object [] array = stack.ToArray();for (int i =0 ;i<array.Length;i++){ Console.WriteLine(array[i]); } while (stack.Count>0 ){ object p = stack.Pop(); }
Queue<> 队列,先进先出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Queue<int > queue = new Queue<int >(); queue.Enqueue("1" ); 、 queue.Dequeue(); queue.Peek(); queue.Contains("1" ); queue.Count; queue.Clear();
title:遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 foreach (var item in queue){ Console.WriteLine(item); } object [] array = queue.ToArray();for (int i =0 ;i<array.Length;i++){ Console.WriteLine(array[i]); } while (queue.Count > 0 ) { object v5 = queue.Dequeue(); }
二、函数(方法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 pubilc static 返回值类型 函数名(参数列表) { 函数体; } public static int GetMax (int n1, int n2 ) { return n1> n2 ? n1 : n2; }
ref 和 out 参数 #ref #out 他们使用的方式和效果都是一样:
解决值类型和引用类型在函数内部改值
重新声明能够影响外部传入的变量,让其也被修改(使传入的参数在函数外也修改 )
ref 和 out 的区别:
ref 传入的变量 (参数) 必须初始化,out 不用。
out 传入的变量必须在内部赋值,ref 不用。
title:ref参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void ChangeValue (int a ){ a = 20 ; } int b = 10 ChangeValue(b); static void ChangeValue (ref int a ){ a=20 ; } int b = 10 ChangeValue(b);
如果你在一个方法中,返回多个相同类型的值的时候,可以考虑返回一个数组。 但是,如果返回多个不同类型的值的时候,返回数组就不行了,那么这个时候, 我们可以考虑使用 out 参数。out 参数就侧重于在一个方法中可以返回多个不同类型的值。
title:out参数 1 2 3 4 5 6 7 8 9 10 11 public static void Test (int []nums,out int max.out int min,out int sum,out float avr ){ max = nums[0 ]; min = nums[1 ]; sum = 0 ; avr = sum / nums.Length; } Test(nums,out max,out min,out sun,out avr);
params 可变参数
可以输入不定的多个参数,并把这些参数存入数组。将实参列表中跟可变参数数组类型一致的元素都当做数组的元素去处理。
在函数参数中只能最多出现一个 params
关键字且一定在形参列表最后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static void Main (string [] args ) { int [] s = { 100 , 80 , 95 }; Test("张三" , s); Console.ReadKey(); } public static void Test (string name, int [] score ){ int sum = 0 ; for (int i=0 ;i<score.Length;i++) { sum += score[i]; } Console.WriteLine($"{name} 这次考试总成绩是{sum} " ); } static void Main (string [] args ) { Test("张三" , 100 , 80 , 95 ); Console.ReadKey(); } public static void Test (string name, params int [] score ){ int sum = 0 ; for (int i=0 ;i<score.Length;i++) { sum += score[i]; } Console.WriteLine($"{name} 这次考试总成绩是{sum} " ); }
可选参数 有参数默认值的参数一般称为可选参数 作用是当调用函数时可以不传入参数,不传就会使用默认值作为参数的值
1 2 3 4 static void Speak (string str == "hello" ){ Console.WriteLine (str); }
函数重载 概念:方法的重载指的就是方法的名称相同,但是参数不同 。参数不同,分为三种情况
如果参数的个数相同,那么参数的类型就不能相同。
果参数的类型相同,那么参数的个数就不能相同。
参数顺序不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void M (int n1,int n2 ){ int result = n1 + n2; } public static double M (double d1,double d2 ){ return d1 + d2; } public static void M (int n1,int n2,int n3 ){ int result = n1 + n2 + n3; } public static string M (string s1,string s2 ){ return s1 + s2; }
三、表达式和运算符 转义字符
换行 \n
:(windows 操作系统只认识 \r\n
,不认识 \n
)
英文半角双引号:\"\"
中文半角可以直接打印出
一个 Tab 空格: \t
警报音:\a
退格:\b
斜杠:\\
@
:取消转义符的作用(用来存路径)/将字符串按照原格式输出
1 string a = @"C:\mycode\a\文件.txt" ;
逻辑运算符 逻辑与:&& 逻辑或:|| 逻辑非:!
运算符优先级:
非 优先级最高
逻辑与 优先级大于逻辑或
逻辑与 、逻辑或 优先级低 于算术运算符和条件运算符
位运算符 位运算符主要用数值类型进行计算,将数值转换为 2 进制,在进行位运算
位与:& 对位运算,有 0 则 0
1 2 3 4 int a = 1 ; int b = 5 ; int c = a & b;
位或:| 对位运算,有 1 则 1
异或:^ 对位运算,相同为 0,不同为 1
位取反:~ 对位运算,0 变 1,1 变 0
左移:<<
右移:>> 让一个数的 2 进制数进行左移和右移
左移几位,右侧就加几个 0
右移几位,右侧去掉几个数
运算符重载
[!success] 可重载运算符 算数运算符: + - * / % ++ –
逻辑运算符: !
位运算符: & | ^ ~ << >>
条件运算符: < <= > >= == !=
[!error] 不可重载运算符
逻辑运算符 : && || [ ] () . = ?:
作用 让自定义类和结构体,能够使用运算符
使用关键字 operator
特点
一定是一个公共的静态方法 public static
返回值写在 operator
前
逻辑处理自定义
注意
条件运算符需要成对实现(比如 ==
和 !=
必须成对实现)
一个符号可以多个重载
不能使用 ref
和 out
title:语法 1 public static 返回类型 operator 运算符(参数列表)
title:案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Point p1 = new Point(); p1.x =1 ; p1.y =1 ; Point p2 = new Point(); p2.x= 2 ; p2.y = 2 ; Point p3 =p1 + p2; Console.WriteLine(p3.x); class Point { public int x; public int 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; } }
四、语句(控制流) 异常捕获 增加代码健壮性:哪行代码有可能出现异常,就 try 它。
语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 try { } catch { } finally { }
我们使用编辑器自动实现一些函数时,IDE 会自动添加
1 2 3 4 private void Awake (){ throw new NotImplementedException(); }
意思是说明该方法未实现,如果运行会抛出异常。
foreach 循环 foreach 循环用于列举出集合中所有的元素,foreach 语句中的表达式由关键字 in 隔开的两个项组成。
in 右边的项是集合名,in 左边的项是变量名,用来存放该集合中的每个元素。
该循环的运行过程 如下:每一次循环时,从集合中取出一个新的元素值。放到只读变量中去,如果括号中的整个表达式返回值为 true,foreach 块中的语句就能够执行。
一旦集合中的元素都已经被访问到,整个表达式的值为 false,控制流程就转入到 foreach 块后面的执行语句。
foreach 语句经常与数组一起使用 ,在 C# 语言中提供了 foreach 语句遍历数组中的元素 ,具体的语法形式如下。
1 2 3 4 5 foreach (数据类型 变量名 in 数组名){ }
这里变量名的数据类型必须与数组的数据类型相兼容。
在 foreach 循环中,如果要输出数组中的元素,不需要使用数组中的下标,直接输出变量名即可。
foreach 语句仅能用于数组、字符串或集合类数据类型。
【实例】在 Main 方法中创建一个 double 类型的数组,并在该数组中存入 5 名学生的考试成绩,计算总成绩和平均成绩。 根据题目要求,使用 foreach 语句实现该功能,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Program { static void Main (string [] args ) { double [] points = { 80 , 88 , 86 , 90 , 75.5 }; double sum = 0 ; double avg = 0 ; foreach (double point in points) { sum = sum + point; } avg = sum / points.Length; Console.WriteLine("总成绩为:" + sum); Console.WriteLine("平均成绩为:" + avg); } }
在计算平均成绩时,通过数组的 Length 属性即可得到数组中元素的个数,使用总成绩除以元素的个数即为结果。
执行上面的语句,效果如下图所示。
从上面的执行效果可以看出,在使用 foreach 语句时可以免去使用下标的麻烦,这也给遍历数组中的元素带来很多方便。
表达式主体成员 => 通过表达式主体定义,可采用非常简洁的可读形式提供成员的实现。只要任何支持的成员(如方法或属性)的逻辑包含单个表达式,就可以使用表达式主体定义 。表达式主体定义具有下列常规语法:
表达式主体定义可用于以下类型成员:
return 单句时可以用 =>
代替 1 2 3 4 5 6 7 8 9 10 public string Name{ get => "lk" ; set => Name = value ; } public int Add (int a, int b ) => a + b;(参数列表)=>{ 函数逻辑 }
只读属性 只读属性可以将 get
访问器作为 expression-bodied 成员实现。在这种情况下,既不使用 get
访问器关键字,也不使用 return
关键字。
1 2 3 public string Name => locationName;public string Name { get =>locationName; }
五、面向对象 OOP 1 类 class 1 2 3 4 5 6 7 8 9 语法: public class 类名{ 字段; 属性; 方法; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Person { public string _name; public int _age; public string _gender; public void CHLSS () { Console.WriteLine($"我叫{this ._name} ,我今年{this ._age} 岁了,我性别是{this ._gender} 。" ); } } static void Main (string [] args ){ Person sunQuan = new Person(); sunQuan._name = "孙权" ; sunQuan._age = 23 ; sunQuan._gender = "男" ; sunQuan.CHLSS(); Person p1; Person p2 = null ; }
可以在类中声明一个和自己相同类型的成员变量,但不能对类内部它进行实例化
1 2 3 4 5 6 7 class person { Person girlfriend; Person girlfriend = new Person(); Person[] boyfriend; }
成员变量的默认初始值
值类型,数字类型默认为 0,bool 类型默认为 false
引用类型,默认为 null
default(类型)
得到该类型的默认值
嵌套类 内部类,类中的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Person p = new Person(); Person.Body body = new Person.Body(); class Person { public int age; public string name; public Body body; public class Body { Arm leftArm; public class Arm { } } }
分部类 partial 把一个类分成几部分申明
关键字 :partial
作用
分部描述一个类
增加程序的拓展性
注意
分部类可以写在多个脚本文件中,数据共享
分部类的访问修饰符要一致
分部类中不能有重复成员
title:分布类 1 2 3 4 5 public partial class Person { } public partial class Person { }
分部方法 :将方法的声明和实现分离
特点
不能加访问修饰符,默认private
只能在分部类中声明
返回值只能是 void
可以有参数但不用 out 关键字1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public partial class Person { public bool sex; partial void Speak () ; } public partial class Person { public int number; partial void Speak () { } }
密封类 sealed 关键字 :sealed
密封类不能被继承,但可以继承其他父类
加强面向对象程序设计的规范性、结构性、安全性
1 2 public sealed class Person : Test { }
密封方法 概念 :用密封关键字 sealed
修饰的重写函数作用 :让虚方法或者抽象方法之后不能再被子类重写特点 :和 override
一起出现
1 2 public sealed overide void Eat (){ }
2 访问修饰符
不显式声明访问修饰符,则默认为 private
分类:public
:公开的,可被类的内部外部访问(可访问 可以理解为可读写 )
private
:私有的,只能在当前类的内部访问
protected
:受保护的,只能在当前类的内部以及该类的子类中访问
internal
:只能在当前项目中访问,在本项目中和 public 权限一样
protected internal
:protected+internal
能够修饰类的访问修饰符:public,internal
子类的访问权限不能高于父类的访问权限,会暴露父类的成员
3 成员属性 get set #get #set
用于保护成员变量
为成员属性的获取和赋值添加逻辑处理
解决访问修饰符的局限性
访问修饰符只能同时控制读写,不能单独控制
通过令属性的 get 或 set 为 private,可以让成员变量在外部只能读不能写 或只能写不能读
get 和 set 可以只有一个
既有 get ()也有 set ()我们诚之为可读可写属性。
只有 get ()没有 set ()我们称之为只读属性
没有 get ()只有 set ()我们称之为只写属性1 2 3 4 5 6 7 8 9 10 11 public void set_Name (string value ){ this ._name = value ; } public string get_Name{ return this ._name; }
title:用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Person { private int _age; public int Age { get { return _age; } set { if (value < 0 || value > 100 ) { value = 0 ; } _age = value ; } } public void CHLSS () { Console.WriteLine($"我今年{this .Age} 岁了" ); } } static void Main (string [] args ){ Person sunQuan = new Person(); sunQuan.Age = "10" ; sunQuan.CHLSS(); }
title:新写法 1 public int Age { get => _age; set => _age = value ; }
get 和 set 可以加访问修饰符
默认不加,会使用属性声明时的访问权限
加的访问修饰符要低于属性的访问权限
不能让 get 和 set 的访问权限都低于属性的权限1 2 3 4 5 6 public int age{ get { return _age } private set { _age = value } }
自动属性 1 2 3 4 5 public int age{ get ; private set ; }
没有在 get 和 set 中写逻辑的需求时,可以使用自动属性。
get set 仍可以添加 private。一般用于外部能读不能写的情况
4 静态 static #static
在非静态类中,既可以有实例成员(非静态),也可以有静态成员。
在调用实例成员的时候,需要使用对象名. 实例成员 ;
在调用静态成员的时候,需要使用类名. 静态成员名 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Person { public void M1 () { Console.WriteLine("非静态" ); } public static void M2 () { Console.WriteLine("静态" ); } } Person p = new Person(); p.M1(); Person.M2();
总结:
静态函数中,只能访问静态成员,不允许访问实例成员。
实例函数中,既可以使用静态成员,也可以使用实例成员。
静态类 中只允许有静态成员,不能被实例化(适合作为工具类)。
静态构造函数 ,用于初始化静态变量
静态类和普通类中都可以有静态构造函数
不能使用访问修饰符
不能有参数
只会自动调用一次title:静态构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 class Test { public static int a = 100 ; static Test () { a = 200 ; } public Test () {} }
什么时候使用: 1)、如果你想要你的类当做一个”工具类”去使用,这个时候可以考虑将类写成静态的。 2)、静态类在整个项目中资源共享。静态类存放在堆栈静态存储区域 ,只有在程序全部结束之后,静态类才会释放资源。
const (常量)可以理解为特殊的 static (静态)
相同点 他们都可以通过类名点出使用
不同点
const 必须初始化,不能修改, static 没有这个规则
const 只能修饰变量、static 可以修饰很多
const 一定是写在访问修饰符后面的,static 没有这个要求
5 拓展方法 概念:为现有非静态变量类型 添加新方法
作用
提升程序拓展性
不需要再对象中重新写方法
不需要继承来添加方法
为别人封装的类型写额外的方法
特点
一定是写在静态类中
一定是个静态函数
第一个参数为拓展目标
第一个参数用 this 修饰
title:语法 1 访问修饰符 static 返回值 函数名(this 拓展类名 参数名,参数类型 参数名,参数类型 参数名....)
1 2 3 4 5 6 7 8 9 10 11 12 13 int i =10 ;i.SpeakValue(); static class Tools { public static void SpeakValue (this int value ) { Console.WriteLine( value ); } }
也可以为类类型添加拓展方法,当拓展方法名和类成员函数重名时,只会调用类成员函数。
例:Unity 拓展 Transfrom 类判断朝向
h:8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static class ExtensionMethod { private static float s_DotThreshold = 0.5f ; public static bool IsFacingTarget (this Transform transform, Transform target ) { Vector3 direction = (target.position - transform.position).normalized; float dot = Vector3.Dot(direction, transform.forward); return dot>s_DotThreshold; } } transform.IsFacingTarget(m_attackTarget.transform)
6 构造函数 作用 :在实例化对象时(new 时),会调用用于初始化的函数,如果不写默认存在一个无参构造函数。
构造函数是一个特殊的方法: 1)、构造函数没有返回值,连 void 也不能写。 2)、构造函数的名称必须跟类名一样。 3)、没有特殊需求时,修饰符一般是 public
构造函数是可以有重载的。
重载之后会失去默认的无参构造函数,如果需要可以显式声明以下
特殊写法 (构造函数的继承)较少使用
在构造函数后添加 : this (指定的重载参数)
可以实现执行该构造函数前执行 this 指定的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class Person { public Person () { ... } public Person (string name,int age ) { this .Name = name; this .Age = age; } public Person (string name,int age,string gender ) { this .Name = name; this .Age = age; this .Gender = gender; } public Person (string name,int age,string gender ):this () { this .Name = name; this .Age = age; this .Gender = gender; } ~Person() { } private string _name; public string Name { get { return _name; } set { _name = value ; } } private int _age; public int Age { get { return _age; } set { _age = value ; } } private string _gender; public string Gender { get { return _gender; } set { _gender = value ; } } } class Program { static void Main (string [] args ) { Person p1 = new Person("孙权" ,10 ,"男" ); Person p2 = new Person("孙尚香" ,10 ); } }
类当中会有一个默认的无参数的构造函数 ,当你写一个新的构造函数之后,不管是有参数的还是无参数的,那个默认的无参数的构造函数都被干掉了。
new 关键字 Person p=new Person ();
new 帮助我们做了 3 件事儿: 1)、在内存中开辟一块空间 2)、在开辟的空间中创建对象 3)、调用对象的构造函数进行初始化对象
this 关键字 1)、代表当前类的对象 2)、在类当中显示的调用本类的构造函数 : this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public Person (string name,int age,string gender ){ this .Name = name; this .Age = age; this .Gender = gender; } public Person (string name,int age ):this (name,age,""){ }
7 索引器 作用: 让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写。语法:
title:语法 1 2 3 4 5 6 访问修饰符 返回值 this [参数类型 参数名, 参数类型 参数名, ......] { get {} set {} }
title:用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Person { private string _name; private int _age; private Person[] _friend; public Person this [int index] { get { return _friend[index]; } set { _friend[index] = value ; } } } Person p = new Person(); p[0 ] = new Person(); p[1 ] = new Person();
索引器 this 函数中的 get 和 set 可以写逻辑,this 函数支持重载
8 继承 1 2 3 4 5 public class Person public class Student : Person
特性
子类继承了父类的属性和方法,不能继承父类 private
字段和构造函数。
单根性 (子类只能有一个父类)和传递性 (子类可以间接继承父类的父类))
子类成员函数和父类的同名 时,会把父类的隐藏掉。(不建议写同名成员)
加 new 之后不再警告
1 2 public new void SayHello () {}
子类对象可以调用父类中的成员,但是父类对象永远都只能调用自己的成员。
当申明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数
父类的无参构造很重要,子类实例化时默认自动调用的是父类的无参构造,所以如果父类无参构造被顶掉,会报错!
子类可以通过 base
关键字代表父类调用父类构造
base 关键字 #base 子类写的成员函数和父类的同名 时,会把父类的隐藏掉。
base
关键字用于从派生类中访问基类的成员:
调用基类上已被 overide的方法。
指定创建派生类实例时应调用的基类构造函数。
基类访问只能在构造函数、实例方法或实例属性访问器 中进行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class BaseClass { protected string _className = "BaseClass" ; public virtual void PrintName () { Console.WriteLine("Class Name: {0}" , _className); } } class DerivedClass : BaseClass { public string _className = "DerivedClass" ; public override void PrintName () { Console.Write("The BaseClass Name is {0}" ); base .PrintName(); Console.WriteLine("This DerivedClass is {0}" , _className); } } public class BaseClass { int num; public BaseClass () { Console.WriteLine("in BaseClass()" ); } public BaseClass (int i ) { num = i; Console.WriteLine("in BaseClass(int {0})" , num); } } public class DerivedClass : BaseClass { public DerivedClass () : base () { } public DerivedClass (int i ) : base (i ) {
里氏替换原则 概念: 任何父类出现的地方,子类都可以替代
语法表现 :父类容器装子类对象, 因为子类对象包含了父类的所有内容
作用: 方便进行对象存储和管理
1 2 3 4 5 6 7 8 9 10 static void Main (string [] args ) { Person p = new Student(); Student ss = (Student)p; ss.StudentSayHello(); }
is as 关键字 #is #asis
:判断一个对象是否是指定的类对象,如果能够转换,则返回一个 true,否则返回一个 falseas
:将一个对象转换为指定的类对象,如果能够转换则转换为指定的类对象,否则返回一个 null
title:is和as的用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void Main (string [] args ){ Person p = new Student(); if (p is Teacher) { Student ss = (Student)p; ss.StudentSayHello(); } else { Console.WriteLine("转换失败" ); } Student ss = ss as Student; ss.StudentSayHello(); (ss as Student).StudentSayHello(); }
title:游戏中的应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Gameobject player = new Player(); Gameobject monster = new Monster(); Gameobject boss = new Boss(); Gameobject[] objects = new Gameobject[] { new Player(), new Monster(), new Boss() }; for (int i = 0 ; i < objects.Length; i++){ if ( objects[i] is Player ) { (objects[i] as Player) . PlayerAtk(); } else if ( objects[i] is Monster ) { (objects[i] as Monster). MonsterAtk() ; } else if (objects[i] is Boss) { ... } }
10 多态 多态按字面的意思就是“多种状态” 让继承同一父类的子类们在执行相同方法时有不同的表现 (状态)
主要目的 同一父类的对象执行相同行为 (方法)有不同的表现
解决的问题 让同一个对象有唯一行为的特征
多态有两种:
编译时多态(函数重载,开始就写好的)
运行时多态(重写父类虚函数、抽象函数、接口)
虚函数 virtual 当父类中的方法需要实现, 将父类的方法标记为虚方法,使用关键字 virtual
,这个函数 可以 被子类重写。 **子类的方法使用关键字 override
**。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Program { static void Main (string [] args ) { ReadDuck rd = new ReadDuck(); WoodDuck wd = new WoodDuck(); XPDuck xd = new XPDuck(); ReadDuck[] ducks = { rd, wd, xd }; for (int i = 0 ;i < ducks.Length;i++) { ducks[i].jiao(); } } } public class ReadDuck { public virtual void jiao () { Console.WriteLine("真的鸭子嘎嘎叫" ); } } public class WoodDuck : ReadDuck { public override void jiao () { Console.WriteLine("木头鸭子吱吱叫" ); } } public class XPDuck : ReadDuck { public override void jiao () { Console.WriteLine("橡皮鸭子唧唧叫" ); } }
抽象类 abstract 被抽象关键字 abstract
修饰的类当父类中的方法不知道如何去实现的时候,可以考虑将父类写成抽象类,将方法写成抽象方法。
特点:
不能被实例化,其他封装特性都有
可以包含抽象方法(即纯虚函数)
继承抽象类必须重写其抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 static void Main (string [] args ){ Animal dog = new Dog(); dog.jiao(); Animal cat = new Cat(); cat.jiao(); } public abstract class Animal { public abstract void jiao () ; } public class Dog : Animal { public override void jiao () { Console.WriteLine("狗会叫" ); } } public class Cat : Animal { public override void jiao () { Console.WriteLine("猫也会叫" ); } }
抽象成员必须标记为 abstract
, 并且不能有任何实现。
抽象成员必须在抽象类中。
抽象类不能被实例化
子类继承抽象类后,必须把父类中的所有抽象成员都重写。(除非子类也是一个抽象类,则可以不重写)
抽象成员的访问修饰符不能是 private
在抽象类中可以包含实例成员。并且抽象类的实例成员可以不被子类实现
抽象类是有构造函数的。虽然不能被实例化。
如果父类的抽象方法中有参数,那么。继承这个抽象父类的子类在重写父类的方法的时候必须传入对应的参数。
如果抽象父类的抽象方法中有返回值,那么子类在重写这个抽象方法的时候也必须要传入返回值。
使用时机:
如果父类中的方法有默认的实现,并且父类需要被实例化,这时可以考虑将父类定义成一个普通类,用虚方法来实现多态。
如果父类中的方法没有默认实现,父类也不需要被实例化,则可以将该类定义为抽象类。
[!summary] 抽象类和接口 相同点:
都可以被继承
都不能直接实例化
都可以包含方法申明
子类必须实现未实现的方法
都遵循里氏替换原则
不同点:
抽象类中可以有构造函数; 接口中不能
抽象类只能被单一继承; 接口可以被继承多个
抽象类中可以有成员变量; 接口中不能
抽象类中可以申明成员方法,虚方法,抽象方法,静态方法; 接口中只能声明没有实现的抽象方法
抽象类方法可以使用访问修饰符; 接口中建议不写,默认 public
[!summary] 如何选择抽象类和接口
表示对象的用抽象类,表示行为拓展的用接口
不同对象拥有的共同行为,我们往往可以使用接口来实现
11 面向对象七大原则
七大原则总体要实现的目标是: 高内聚、低耦合 ,使程序模块的可重用性、移植性增强
高内聚低耦合:
从类角度来看,高内聚低耦合要求减少类内部对其他类的调用
从功能块来看,高内聚低耦合要求减少模块之间的交互复杂度
单一职责原则 SRP (Single Responsibility Principle) 类被修改的几率很大,因此应该专注于单一的功能。如果把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另个功能。举例: 假设程序、策划、美术三个工种是三个类,他们应该各司其职,在程序世界中只应该做自己应该做的事情。
开闭原则 OCP (Open-Closed Principle) 对拓展开放,对修改关闭 拓展开放: 模块的行为可以被拓展从而满足新的需求 修改关闭: 不允许修改模块的源代码(或者尽量使修改最小化) 举例: 继承就是最典型的开闭原则的体现,可以通过添加新的子类和重写父类的方法来实现
里氏替换原则 LSP (Liskov Substitution Principle) 任何父类出现的地方,子类都可以替代 举例: 用父类容器装载子类对象,因为子类对象包含了父类的所有内容
依赖倒转原则 DIP (Dependence Inversion Principle) 要依赖于抽象,不要依赖于具体的实现
玩家对象的开枪不依赖于具体种类的开枪,而是依赖于抽象的接口
迪米特法则 LoP (Law of Demeter)又称最少知识原则 一个对象应当对其它对象尽可能少的了解不要和陌生人说话 举例: 一个对象中的成员,要尽可能少的直接和其它类建立关系,目的是降低耦合性
接口分离原则 ISP (Interface Segregation Principle) 不应该强迫别人依赖他们不需要使用的方法。 一个接口不需要提供太多的行为,一个接口应该尽量只提供一个对外的功能,让别人去选择需要实现什么样的行为,而不是把所有的行为都封装到一个接口当中 举例: 飞行接口、走路接口、跑步接口等等虽然都是移动的行为但是我们应该把他们分为一个一个单独的接口,让别人去选择使用
合成复用原则 CRP (Composite Reuse Principle) 尽量使用对象组合,而不是继承来达到复用的目的。继承关系是强耦合,组合关系是低耦合 举例: 脸应该是眼镜、鼻子、嘴巴、耳朵的组合,而不是依次的继承,角色和装备也应该是组合,而不是继承 注意: 不能盲目的使用合成复用原则,要在遵循迪米特原则的前提下
如何使用这些原则 在开始做项目之前,整理 UML 类图时先按自己的想法把需要的类整理出来再把七大原则截图放在旁边,基于七大原则去,优化整理自己的设计 整体目标就是: 高内聚,低耦合
六、接口 interface 接口是行为的抽象规范
关键字 :interface
接口声明的规范
不包含成员变量
只包含方法、属性索引器、事件
成员不能被实现
成员可以不用写访问修饰符,不能是私有的
接口不能继承类,但是可以继承另一个接口
接口的使用规范
类可以继承多个接口
类继承接口后,必须实现接口中所有成员
特点:
它和类的声明类似
接口是用来继承的
接口不能被实例化,但是可以作为容器存储对象
title:语法 1 2 3 4 public interface 接口名称(通常以I 开头,如ICompute ){ 接口成员; }
接口的使用 类可以继承 1 个类,多个接口 继承了接口后,必须实现其中的内容,并且必须是 public 的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 interface IFly { string name { get ; set ; } int this [int index] { get ; set ; } event Action doSomthing; void Fly () ; } public class Animal { }public class Person : Animal , IFly { public string name { get ; set ; } public int this [int index] { get { return 0 ; } set { } } public event Action doSomthing; public virtual void Fly () { } static void Main (string [] args ){ IFly f = new Person(); }
并不是所有动物都会飞,所以 Fly 放在动物父类中不合适,可以单独作为一个接口。
接口可以作为容器存储所以继承 Fly 的子类
接口可以继承接口 相当于将接口行为合并
接口继承接口时,不需要实现
待类继承接口后,类自己去实现所有内容
隐式实现接口 隐式实现接口成员是将接口的所有成员以 public 访问修饰符修饰。
使用隐式方式来实现接口 ICompute 的成员,以计算机专业的学生类 (ComputerMajor) 实现 ICompute 接口,为其添加英语 (English)、编程 (Programming)、数据库 (Database) 学科成绩属性,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ComputerMajor : ICompute { public int Id { get ; set ; } public string Name { get ; set ; } public double English { get ; set ; } public double Programming { get ; set ; } public double Database { get ; set ; } public void Avg () { double avg = (English + Programming + Database) / 3 ; Console.WriteLine("平均分:" + avg); } public void Total () { double sum = English + Programming + Database; Console.WriteLine("总分为:" + sum); } }
在 Main 方法中调用该实现类的成员,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Program { static void Main (string [] args ) { ComputerMajor computerMajor = new ComputerMajor(); computerMajor.Id = 1 ; computerMajor.Name = "李明" ; computerMajor.English = 80 ; computerMajor.Programming = 90 ; computerMajor.Database = 85 ; Console.WriteLine("学号:" + computerMajor.Id); Console.WriteLine("姓名:" + computerMajor.Name); Console.WriteLine("成绩信息如下:" ); computerMajor.Total(); computerMajor.Avg(); } }
执行上面的代码,效果如下图所示。
显式实现接口 显式实现接口是指在实现接口时所实现的成员名称前含有接口名称作为前缀。
主要用于实现不同接口中的同名函数的不同表现
使用显式实现接口的成员不能再使用修饰符修饰 ,即 public、abstract、virtual、 override 等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class ComputerMajor : ICompute { public double English { get ; set ; } public double Programming { get ; set ; } public double Database { get ; set ; } int ICompute.Id { get ; set ; } string ICompute.Name { get ; set ; } void ICompute.Total() { double sum = English + Programming + Database; Console.WriteLine ("总分数:" + sum); } void ICompute.Avg () { double avg = (English + Programming + Database) / 3 ; Console.WriteLine("平均分为:" + avg); } }
从上面的代码可以看出,在使用显式方式实现接口中的成员时,所有成员都会加上接口名称 ICompute 作为前缀,并且不加任何修饰符。
在 Main 方法中调用实现类中的成员, 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Program { static void Main (string [] args ) { ComputerMajor computerMajor = new ComputerMajor(); ICompute compute = computerMajor; compute.Id = 1 ; compute.Name = "李明" ; computerMajor.English = 80 ; computerMajor.Programming = 90 ; computerMajor.Database = 85 ; Console.WriteLine("学号:" + compute.Id); Console.WriteLine("姓名:" + compute.Name); Console.WriteLine("成绩信息如下:" ); compute.Total(); compute.Avg(); } }
执行上面的代码,效果与上图一致。从调用的代码可以看出,在调用显式方式实现接口的成员时,必须使用接口的实例来调用,而不能使用实现类的实例来调用。
七、泛型 generic
泛型实现了类型参数化,达到代码重用目的
通过类型参数化来实现同一份代码上操作多种类型
泛型相当于类型占位符
定义类或方法时使用替代符代表变量类型/当真正使用类或者方法时再具体指定类型
不同类型对象的相同逻辑处理就可以选择泛型
使用泛型可以一定程度避免装箱拆箱title:举例:优化ArrayList 1 2 3 4 5 6 7 8 9 10 11 class ArrayList <T >{ private T[] array; public void Add (T value ) { } }
1 泛型类和泛型接口 title:语法 1 2 class 类名<泛型占位字母>interface 接口名<泛型占位字母>
title:泛型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class TestClass <T >{ public T value ; } class Program { static void Main (string [] args ) { TestClass<int > t1 = new TestClass<int >(); t1.value = 1 ; TestClass<string > t2 = new TestClass<string >(); t2.value = "test" ; } } class TestClass2 <T1 , T2 , T3 , T4 >{ public T1 valu1; public T2 valu2; public T3 valu3; public T4 valu4; }
title:泛型接口 1 2 3 4 5 6 7 8 9 interface IInterface <T > { T value { get ; set ; } } class Test : IInterface <int > { public int value { get ; set ; } }
2 泛型方法(函数) title:语法
普通类中的泛型方法 title:普通类中的泛型方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Test { public void Func1 <T >(T value ) { Console.WriteLine(value ); } public void Func2 <T >(T value ) { T t = default (T); } public void Func3 <T >(T value ) { return default (T); } public void Func3 <T1 ,T2 ,T3 >(T1 value1,T2 value2,T3 value3 ) { } } class Program { static void Main (string [] args ) { Test t = new Test(); t.Func1<int >(1 ); } }
泛型类中的泛型方法 title:泛型类中的泛型方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Test <T > { public T value ; public void Func1 (T t ) { value = t; Console.WriteLine(value ); } public void Func2 <K >(K k ) { Console.WriteLine(k); } } class Program { static void Main (string [] args ) { Test<int > t = new Test<int >(); t.Func1(1 ); t.Func2<string >("test" ); } }
3 泛型约束 #where让泛型的类型有一定的限制 关键字:where
泛型约束一共有 6 种
值类型where 泛型占位字母:struct
引用类型where 泛型占位字母:class
存在无参公共构造函数where 泛型占位字母: new ()
某个类本身或者其派生类where 泛型占位字母: 类名
某个接口的派生类型where 泛型占位字母: 接口名
另一个泛型类型本身或者派生类型where 泛型占位字母: 另一个泛型字母
这里泛型约束使用了值类型,当泛型使用 string(引用类型)时报错
title:可以同时指定多个约束,使用逗号 1 2 3 4 class Test <T > where T : class , new (){ ... }
title:多个泛型同时指定约束 1 2 3 4 class Test <T ,K > where T : class where K : struct { ... }
八、委托 delegate
委托是函数的容器
可以理解为表示函数的变量类型
用来存储、传递函数
委托的本质是一个类,用来定义函数 (方法)的类型 (返回值和参数的类型)
不同的函数必须对应和各自”格式”—致的委托
支持泛型
关键字 delegate
写在哪里? 可以申明在 namespace 和 class 语句块中,更多的写在 namespace 中
title:语法: 1 2 访问修饰符 delegate 返回值 委托名 (参数列表);
1 委托的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public delegate void MyDelegate (string message ) ;class Program { static void Main (string [] args ) { MyDelegate del = new MyDelegate(DelegateMethod); MyDelegate del = DelegateMethod; del.Invoke("Hello World" ); del("Hello World" ); } static void DelegateMethod (string message ) { Console.WriteLine(message); } }
2 委托作为类的成员/函数参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public delegate void MyDelegate (string message ) ; public class DelegateClass { public MyDelegate del; public void TestFunc (MyDelegate del ) { string str = "Hello World" ; del(str); } } class Program { static void Main (string [] args ) { DelegateClass dc = new DelegateClass(); dc.del = DelegateMethod; dc.del.Invoke("Hello World" ); dc.del("Hello World" ); } static void DelegateMethod (string message ) { Console.WriteLine(message); } }
3 多播委托 多播的意思是委托变量可以存储多个函数
+=
追加委托-=
移除委托= null
清空委托
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public delegate void MyDelegate (string message ) ; class Program { static void Main (string [] args ) { MyDelegate del = null ; del += DelegateMethod1; del += DelegateMethod2; if (del != null ) { del("Hello World" ); } } static void DelegateMethod1 (string message ) { Console.WriteLine("第一个" +message); } static void DelegateMethod2 (string message ) { Console.WriteLine("第二个" +message); } }
4 内置委托类型 Action 和 Func 的区别是有无返回值 Action 无返回值 Func 有返回值
Action Action
:无参无返回值。 Action<>
:有参无返回值 ,支持 0~16 个参数
title:源码 1 2 public delegate void Action () ;public delegate void Action <in T >(T obj ) ;
title:用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Program { static void Main (string [] args ) { Action action1 = TestFunc1; Action<string > action2 = TestFunc2; } static void TestFunc1 () { } static void TestFunc2 (string s ) { } }
Func Func<>
:无参有返回值 Func<,>
:有参有返回值 ,支持 0~16 个参数
title:源码 1 2 3 public delegate TResult Func <out TResult >() ; public delegate TResult Func <in T , out TResult >(T arg ) ;
title:用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Program { static void Main (string [] args ) { Func<int > func1 = TestFunc1; Func<int , string > func2 = TestFunc2; Console.WriteLine(func1()); Console.WriteLine(func2(100 )); } static int TestFunc1 () { return 1 ; } static string TestFunc2 (int i ) { return i.ToString(); } }
5 案例 一家三口,妈妈做饭,爸爸妈妈和孩子都要吃饭 用委托模拟做饭—>开饭—>吃饭的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 namespace MyNamespace ;public abstract class Person { public abstract void Eat () ; } public class Mother : Person { public Action beginEat; public override void Eat () { Console.WriteLine("妈妈吃饭" ); } public void Cook () { Console.WriteLine("妈妈做饭" ); Console.WriteLine("饭做好了" ); if (beginEat != null ) { beginEat(); } } } public class Father : Person { public override void Eat () { Console.WriteLine("爸爸吃饭" ); } } public class Child : Person { public override void Eat () { Console.WriteLine("孩子吃饭" ); } } class Program { static void Main (string [] args ) { Mother mother = new Mother(); Father father = new Father(); Child child = new Child(); mother.beginEat += father.Eat; mother.beginEat += child.Eat; mother.beginEat += mother.Eat; mother.Cook(); } }
6 协变逆变(不常用) 协变 out
: 遵循里氏替换原则,父类的泛型委托可以装子类的泛型委托 **逆变 in
**:逆着来,子类的泛型委托可以装父类的泛型委托
协变和逆变是用来修饰泛型的
用于在泛型中修饰泛型字母的,只能在泛型接口和泛型委托中使用
作用 用 out
修饰的泛型只能作为返回值 用 in
修饰的泛型只能作为参数 `
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 delegate T MyDelegate1 <out T >() ;delegate void MyDelegate2 <in T >(T t ) ; class Father { public Father () { Console.WriteLine("Father" ); } } class Son : Father { public Son () { Console.WriteLine("Son" ); } } class Program { static void Main (string [] args ) { MyDelegate1<Son> son1 = () => { return new Son(); }; MyDelegate1<Father> fathrer1 = son1; Father f = fathrer1(); MyDelegate2<Father> fathrer2 = (value ) => { }; MyDelegate2<Son> son2 = fathrer2; Son s = new Son(); } }
九、事件 event 使用事件将逻辑和视觉代码分离 使用事件意味着说我可以让一件事发生而不关心是谁订阅了它,事件模型中有 publishers 和 subscribers,其中 publishers 触发事件,所有的 subscribers 都会收到事件被触发的通知。因为 publishers 并不关心是谁订阅了它,之后又发生了什么,所以使用事件模型可以使我们的代码解耦
通常我们不希望逻辑代码与视觉代码耦合在一起,我们希望不管有没有视觉组件,逻辑都能够单独运行,而视觉组件只关心逻辑代码运行时造成的具体的视觉变化
事件的使用
事件是基于委托的存在
事件是委托的安全包裹
让委托的使用更具有安全性
事件是一种特殊的变量类型
语法:
事件的使用:
事件是作为成员变量存在于类中
委托怎么用,事件就怎么用
事件相对于委托的区别:
不能在类外部 使用 =
赋值,但可以在类外追加减少 +=
-=
委托
不能在类外部调用
事件只能作为成员存在于类和接口以及结构体中,而委托可以作为临时变量在函数中使用。
[!question] 为什么使用事件?
防止外部随意置空委托
防止外部随意调用委托
事件相当于对委托进行了一次封装让其更加安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 namespace MyNamespace ;class Test { public event Action myEvent; public Test () { myEvent = null ; myEvent += TestFunc1; myEvent += TestFunc2; myEvent(); } public void TestFunc1 () { Console.WriteLine("TestFunc1" ); } public void TestFunc2 () { Console.WriteLine("TestFunc2" ); } } class Program { static void Main (string [] args ) { Test t = new Test(); t.myEvent = null ; t.myEvent += t.TestFunc1; t.myEvent(); } }
EventHandler 是一个多播委托类型
1 2 3 4 5 6 7 public delegate void EventHandler (object sender, EventArgs e ) ;public event EventHandler m_event;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class TestingEvents : MonoBehaviour { public event EventHandler OnSpacePressed; private void Start () { OnSpacePressed += Testing_OnSpacePressed; } private void Testing_OnSpacePressed (object sender, EventArgs e ) { Debug.Log("Space Pressed" ); } private void Update () { if (Input.GetKeyDown(KeyCode.Space)) { OnSpacePressed?.Invoke(this , EventArgs.Empty); } } }
行游戏,按下空格,我们就可以看到该事件触发的函数。
现在我们都是在同一个脚本、同一个类中去触发和监听事件,但使用事件模型的好处是我们可以从其他地方去监听 ,所以接下来我们新创建一个脚本 TestingEventSubscriber. cs,将上面的监听事件的过程放到这个脚本中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using System;using UnityEngine;public class TestingEventSubscriber : MonoBehaviour { private void Start () { TestingEvents testingEvents = GetComponent<TestingEvents>(); testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed; } private void TestingEvents_OnSpacePressed (object sender, EventArgs e ) { Debug.Log("Space Pressed" ); } } using System;using UnityEngine;public class TestingEvents : MonoBehaviour { public event EventHandler OnSpacePressed; private void Update () { if (Input.GetKeyDown(KeyCode.Space)) { OnSpacePressed?.Invoke(this , EventArgs.Empty); } } }
将脚本挂载到同一个物体上,运行游戏,按下空格,和之前的效果相同
参数 EventArgs e
EventHandler 的另一个参数 EventArgs e 可以通过事件传递更多信息,要使用 EventArgs,我们首先需要使用泛型,然后定义一个派生自 EventArgs 的类 ,比如这里我们想要传递一个 int 类型的 spaceCount 记录按下空格的次数,在调整了 EventArgs 之后两个脚本如下
h:25-30,41 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using System;using UnityEngine;public class TestingEventSubscriber : MonoBehaviour { private void Start () { TestingEvents testingEvents = GetComponent<TestingEvents>(); testingEvents.OnSpacePressed += TestingEvents_OnSpacePressed; } private void TestingEvents_OnSpacePressed (object sender, TestingEvents.OnSpacePressEventArgs e ) { Debug.Log("Space Pressed" + e.spaceCount); } } using System;using UnityEngine;public class TestingEvents : MonoBehaviour { public event EventHandler<OnSpacePressEventArgs> OnSpacePressed; public class OnSpacePressEventArgs : EventArgs { public int spaceCount; } private int _spaceCount; private void Update () { if (Input.GetKeyDown(KeyCode.Space)) { spaceCount++; OnSpacePressed?.Invoke(this , new OnSpacePressEventArgs { spaceCount = _spaceCount }); } } }
运行游戏,可以看到控制台显示出了按下空格的次数
十、匿名函数
顾名思义,就是没有名字的函数
匿名函数的使用主要是配合委托和事件进行使用
脱离委托和事件是不会使用匿名函数的
title:语法 1 2 3 4 5 6 7 8 9 10 11 delegate (参数列表){ } (参数列表)=>{ 函数逻辑 }
何时使用?
函数中传递委托参数时
作为函数返回值
委托或事件赋值时
匿名函数的缺点: 添加到委托或事件容器中后不记录,无法使用 -=
指定移除
使用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Action action1 = delegate () { Console.WriteLine("Hello World!" ); }; Action action1 = () => { Console.WriteLine("Hello World!" ); }; action1(); Action<string > action2 = delegate (string name) { Console.WriteLine("Hello " + name); }; Action<string > action2 = (string name) => { Console.WriteLine("Hello " + name); }; action2("World!" ); Func<string > func1 = delegate () { return "Hello World!" ; }; Func<string > func1 = ()=> { return "Hello World!" ; }; Console.WriteLine(func1()); Func<string , string > func2 = delegate (string name) { return "Hello " + name; }; Func<string , string > func2 = (string name) => { return "Hello " + name; }; Console.WriteLine(func2("World!" ));
title:作为参数传递\作为函数返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Test { public Action action; public void Dosomething (int a, Action fun ) { Console.WriteLine(a); fun(); } public Action GetFun () { return delegate { Console.WriteLine("Hello World!" ); }; } } class Program { static void Main (string [] args ) { Test t = new Test(); t.Dosomething(100 ,delegate { Console.WriteLine("Hello World!" ); }); Action ac = t.GetFun(); ac(); t.GetFun()(); } }
闭包 闭包:内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止
注意:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Test { public event Action action; public Test () { int value = 10 ; action = () => { Console.WriteLine(value ); }; for (int i =0 ;i<10 ;i++) { action += () => { Console.WriteLine(i); }; } } public void DoSomthing () { action(); } } class Program { static void Main (string [] args ) { Test t = new Test(); t.DoSomthing(); } }
十一、反射和特性 程序集和元数据 程序集 是经由编译器编译得到的,供进一步编译执行的那个中间产物,在 windows 系统中,它一般表现为后缀为 .dll
(库文件)或者是 .exe
(可执行文件) 的格式 程序集就是我们写的一个代码集合,我们现在写的所有代码最终都会被编译器翻译为一个程序集供别人使用,比如一个代码库文件 (d11)或者一个可执行文件 (exe)
元数据(metadata) 就是用来描述数据的数据,这个概念不仅仅用于程序上,在别的领域也有元数据。程序中的类,类中的函数、变量等等信息就是程序的元数据 ,有关程序以及类型的数据被称为元数据,它们保存在程序集中 。
反射 Type 程序正在运行时,可以查看其它程序集或者自身的元数据。一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射 在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息/类,函数,变量,对象等等,实例化它们,执行它们,操作它们
反射的作用 因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
程序运行时得到所有元数据,包括元数据的特性
程序运行时,实例化对象,操作对象
程序运行时创建新对象,用这些对象执行任务
Type (类的信息类)
它是反射功能的基础!
它是访问元数据的主要方式。
使用 Type 的成员获取有关类型声明的信息
有关类型的成员(如构造函数、方法、字段 、属性和类的事件)
[!question] 字段 字段 (Field) 就是类的成员变量!
[!info] Title 反射常用于跨文件获取数据,此案例只是为了演示功能,所以将所有代码放在一个文件中
title: 获取Type 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 using System.Reflection;namespace MyNamespace ;class Test { private int i = 1 ; public int j = 0 ; public string str = "123" ; public Test () { } public Test (int i ) { this .i = i; } public Test (int i, string str ) : this (i ) { this .str = str; } public void Speak () { Console.WriteLine(i); } } class Program { static void Main (string [] args ) { int a = 42 ; Type t1 = a.GetType(); Type t2 = typeof (int ); Type t3 = Type.GetType("System.Int32" ); Console.WriteLine(t1.Assembly); Console.WriteLine(t2.Assembly); Console.WriteLine(t3.Assembly); ... } }
获取类中的所有成员 title:获取类中的所有公共成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Type t4 = typeof (Test); MemberInfo[] members = t4.GetMembers(); for (int i = 0 ; i < members.Length; i++){ Console.WriteLine(members[i]); }
获取构造函数 title:获取类的公共构造函数并调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ConstructorInfo[] ctors = t4.GetConstructors(); for (int i = 0 ; i < ctors.Length; i++){ Console.WriteLine(ctors[i]); } ConstructorInfo info1 = t4.GetConstructor(new Type[0 ]); Test obj = info1.Invoke(null ) as Test; Console.WriteLine(obj.j); ConstructorInfo info2 = t4.GetConstructor(new Type[] {typeof (int )}); obj = info2.Invoke(new object [] { 2 }) as Test; Console.WriteLine(obj.str); ConstructorInfo info3 = t4.GetConstructor(new Type[] {typeof (int ), typeof (string )}); obj = info3.Invoke(new object [] { 3 , "456" }) as Test;
获取类的公共成员变量 title:获取类的公共成员变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FieldInfo[] fields = t4.GetFields(); for (int i = 0 ; i < fields.Length; i++){ Console.WriteLine(fields[i]); } FieldInfo infoJ = t4.GetField("j" ); Console.WriteLine(infoJ); Test test = new Test(); test.j = 99 ; test.str = "222" ; Console.WriteLine(infoJ.GetValue(test)); infoJ.SetValue(test, 100 ); Console.WriteLine(test.j);
获取类的成员方法 title:获取类的成员方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Type strType = typeof (string ); MethodInfo[] methods = strType.GetMethods(); for (int i =0 ; i < methods.Length; i++){ Console.WriteLine(methods[i]); } MethodInfo subStr = strType.GetMethod ("Substring" ,new Type[] { typeof (int ), typeof (int ) }); string str = "Hello World" ;object result = subStr.Invoke(str, new object [] { 0 , 5 }); Console.WriteLine(result);
其他 得枚举GetEnumName
GetEnumNames
得事件GetEvent
GetEvents
得接口GetInterface
GetInterfaces
得属性GetProperty
GetPropertys
判断一个类型的对象是否可以让另一个类型为自己分配空间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Type fatherType = typeof (Father); Type sonType = typeof (Son); if (fatherType.IsAssignableFrom(sonType)){ print("可以装" ); Father f = Activator.CreateInstance(sonType) as Father; print(f); } else { Debug.Log("不可以装" ); }
通过反射获取泛型类型 1 2 3 4 5 6 7 8 9 10 11 List<string > list = new List<string >(); Type listType = list.GetType(); Type[] types = listType.GetGenericArguments(); print(types[0 ]); Dictionary<string , float > dic = new Dictionary<string , float >(); Type dicType = dic.GetType(); types = dicType.GetGenericArguments(); print(types[0 ]); print(types[1 ]);
Activator 动态实例化 1 2 3 4 5 6 7 8 9 10 11 12 13 var className = " (命名空间 namespace).ClassName" ;var classType = Type.GetType (className);var classType = typeof (className);var args = new object [] { object1, object2, object3...};var classInstance = Activator.CreateInstance (classType, args);
Activator.CreateInstance
方法的第一个参数是要创建的类型,第二个参数是可选的,用于指定构造函数的参数。如果要创建的类型没有默认构造函数,那么必须传递构造函数所需的参数。如果要创建的类型有默认构造函数,那么第二个参数可以为空。 此外,Activator.CreateInstance
方法返回的是 object
类型,需要进行强制类型转换。
1 2 3 var classInstance = classType.InvokeMember ("" , BindingFlags. CreateInstance, null , null , null );
InvokeMember
方法的第一个参数是空字符串,因为我们要调用的是构造函数,而不是方法、属性或字段。 第二个参数是 BindingFlags. CreateInstance
标志,表示创建对象实例; 第三个参数是绑定器,用于指定成员查找的方式;第四个参数是目标对象,因为我们要创建的是对象实例,所以目标对象为 null;第五个参数是构造函数参数,用于传递给构造函数的参数
1 2 3 4 5 6 7 var method = classType.GetMethod ("MathodName" );return method.Invoke (classInstance, null );return classType .InvokeMember ("MathodName" , BindingFlags. InvokeMethod | BindingFlags. Public | BindingFlags. Instance, null , classInstance , null );
Invoke
和 InvokeMember
都是反射中用于调用方法的方法,但它们有一些区别:
参数列表不同:Invoke
方法的第二个参数是 object[]
类型的数组,用于传递方法的参数;而 InvokeMember
方法的第三个参数是 BindingFlags
枚举类型,用于指定方法的访问权限、搜索方式等信息。
访问权限不同:Invoke
方法可以调用 public、protected、private 等所有访问权限的方法,而 InvokeMember
方法需要指定对应的 BindingFlags
,才能调用对应访问权限的方法。
安全性不同:Invoke
方法可以执行非托管代码,因此需要受到安全性限制;而 InvokeMember
方法只能执行托管代码,因此相对更安全。
特性 Attribute
特性是一种允许我们向程序的程序集添加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类
特性提供功能强大的方法以将声明信息与代码 (类型、方法、属性等)相关联。特性与程序实体关联后,即可在运行时使用反射查询特性信息
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集中 ,它可以放置在几乎所有的声明中 (类、变量、函数等等申明)
说人话:
特性本质是个类
我们可以利用特性类为元数据添加额外信息 ,比如一个类、成员变量、成员方法等等为他们添加更多的额外信息
之后可以通过反射来获取这些额外信息
基本语法:
本质上就是在调用特性类的构造函数
可以写在类、函数、变量、函数参数前,表示为他们添加了额外的信息
自定义特性和使用 h:1,20,49 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class MyCustomAttribute : Attribute { public string info; public MyCustomAttribute (string info ) { this .info = info; } public void TestFun () { Console.WriteLine("特性的方法" ); } } [MyCustom("用于计算的类" ) ] class MyClass { [MyCustom("成员变量" ) ] public int value ; [MyCustom("成员函数" ) ] public void TestFun ([MyCustom("函数参数" )]int a) { } } class Program { static void Main (string [] args ) { MyClass mc = new MyClass(); Type t = mc.GetType(); if (t.IsDefined(typeof (MyCustomAttribute),false )) { Console.WriteLine("该类型应用了MyCustom特性" ); } object [] array = t.GetCustomAttributes(true ); for (int i=0 ; i<array.Length; i++) { MyCustomAttribute mca = array[i] as MyCustomAttribute; if (mca!=null ) { Console.WriteLine(mca.info); mca.TestFun(); } } } }
限制自定义特性的使用范围 通过为特性类加特性 限制其使用范围
参数一: AttributeTargets
特性能用在哪些地方 参数二: AllowMultiple
是否允许多个特性实例用在同一个目标上 参数三: Inherited
特性是否能被派生类和重写成员继承
h:3 e:21,24,26,27 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 [AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct,AllowMultiple = true,Inherited = true) ] class MyCustomAttribute : Attribute { public string info; public MyCustomAttribute (string info ) { this .info = info; } public void TestFun () { Console.WriteLine("特性的方法" ); } } [MyCustom("用于计算的类1" ) ] [MyCustom("用于计算的类2" ) ] class MyClass { [MyCustom("成员变量" ) ] public int value ; [MyCustom("成员函数" ) ] public void TestFun ([MyCustom("函数参数" )]int a) { } }
内置特性 过时特性 关键字: Obsolete
用于提示用户使用的方法等成员已经过时,建议使用新方法
一般加在函数前 的特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class TestClass { [Obsolete("该方法已经过时,请使用Speak方法" ,false) ] public void OldSpeak () { Console.WriteLine("OldSpeak" ); } public void Speak () { Console.WriteLine("Speak" ); } }
使用过时方法会报错或者警告:
调用者信息特性 哪个文件调用? CallerFilePath
特性
哪一行调用? CallerLineNumber
特性
哪个函数调用? CallerMemberName
特性
需要引用命名空间 using System. Runtime. CompilerServices;
一般作为函数参数的特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class TestClass { public void SpeakCall ( string str, [CallerFilePath]string filePath ="" , [CallerLineNumber]int linNumber = 0 , [CallerMemberName]string memberName = "" ) { Console.WriteLine(str); Console.WriteLine(filePath); Console.WriteLine(linNumber); Console.WriteLine(memberName); } } class program { static void Main (string [] args ) { TestClass testClass = new TestClass(); testClass.SpeakCall("hello world" ); } }
条件编译特性 关键字: Conditional
它会和预处理指令 #define
配合使用 需要引用命名空同 using System. Diagnostics
;
主要可以用在一些调试代码上 有时想执行有时不想执行的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define Func //没有这个宏定义,Func()函数不会被编译 using System.Diagnostics;namespace MyNamespace ;class Program { [Conditional("Func" ) ] static void Func () { Console.WriteLine("Hello World!" ); } static void Main (string [] args ) { Func(); } }
外部 DLL 包函数特性 关键字: DllImport
用来标话非.Net (C#)的函数,表明该函数在一个外部的 DLL 中定义. 一般用来调用 c 或者 c++的 DLL 包写好的方法 需要引用命名空间 `using System. Runtime. InteropServices
1 2 [DllImport("Test.dll" ) ] public static extern int Add (int a, int b ) ;
十二、枚举器和迭代器
迭代器 (iterator)有时又称光标(cursor)是程序设计的软件设计模式
迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的标识
在表现效果上看
是可以在容器对象 (例如链表或数组)上遍历访问的接口
设计人员无需关心容器对象的内存分配的实现细节
可以用 foreach 遍历的类,都是实现了迭代器的
标准迭代器的实现方法 关键接口: IEnumerator
, IEnumerable
命名空间: using system. collections;
可以通过同时继承 IEnumerable
和 IEnumerator
实现其中的方法
foreach
本质:
title:使用迭代器实现List数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class CustomList : IEnumerable ,IEnumerator { private int [] list; public CustomList () { list = new int [] { 1 , 2 , 3 , 4 , 5 }; } private int position = -1 ; public IEnumerator GetEnumerator () { Reset(); return this ; } public bool MoveNext () { ++position; return position < list.Length; } public void Reset () { position = -1 ; } public object Current { get { return list[position]; } } } class Program { static void Main (string [] args ) { CustomList list = new CustomList(); foreach (int item in list) { Console.WriteLine(item); } } }
用 yield return 语法糖实现迭代器 yield return
是 cs 提供的一个语法糖 ,也称糖衣语法 语法糖主要作用就是将复杂逻辑简单化,可以增加程序的可读性从而减少程序代码出错的机会 关键接口: IEnumerable
命名空间: using System. collections;
让想要通过 foreach
遍历的自定义类实现接口中的方法 GetEnumerator
即可
使用 yield return 实现和上一节相同的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class CustomList : IEnumerable { private int [] list; public CustomList () { list = new int [] { 1 , 2 , 3 , 4 , 5 }; } public IEnumerator GetEnumerator () { for (int i = 0 ; i < list.Length; i++) { yield return list[i]; } } } class Program { static void Main (string [] args ) { CustomList list = new CustomList(); foreach (int item in list) { Console.WriteLine(item); } } }
title:泛型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class CustomList <T > : IEnumerable { private T[] list; public CustomList (params T[] list ) { this .list = list; } public IEnumerator GetEnumerator () { for (int i = 0 ; i < list.Length; i++) { yield return list[i]; } } } class Program { static void Main (string [] args ) { CustomList<int > list = new CustomList<int >(1 ,2 ,3 ,4 ,5 ); foreach (int item in list) { Console.WriteLine(item); } } }
十三、特殊语法 1 var 隐式类型 var 是一种特殊的变量类型,它可以用来表示任意类型的变量
注意:
var 不能作为类的成员,只能用于临时变量声明 ,也就是一般写在函数语句块 中
var 必须初始化
1 2 3 var i = 5 ;var array = new int [] { 1 , 2 , 3 , 4 , 5 };var list = new List<int >();
2 匿名类型 匿名类型 : var 变量可以声明为自定义的匿名类型
title:匿名类型 1 2 3 var v = new {age =10 ,name="John" };Console.WriteLine(v.age); Console.WriteLine(v.name);
3 设置对象初始值 声明对象时,可以通过直接写**大括号 {}
的形式初始化公共成员变量和属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Person { public int money; public string Name { get ; set ; } public int Age { get ; set ; } } class Program { static void Main (string [] args ) { Person p = new Person { money = 100 , Age = 10 , Name = "Tom" }; } }
4 设置集合初始值 申明集合对象时,也可以通过大括号 直接初始化内部属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Person { public int money; public string Name { get ; set ; } public int Age { get ; set ; } } class Program { static void Main (string [] args ) { int [] array2 = new int [3 ]{1 ,2 ,3 }; List<int > list = new List<int >() { 1 , 2 , 3 , 4 , 5 }; List<Person> people = new List<Person>() { new Person{Age = 100 }, new Person{Age = 200 ,Name = "Test" }, new Person { money = 100 , Age = 10 , Name = "Tom" } }; } }
5 可空类型
值类型是不能赋值为空 (null)的
声明时在值类型后面加 ?
可以赋值为空
判断是否为空 .HasValue
1 2 3 4 5 6 7 8 9 if (c.HasValue){ Console.WriteLine(c); Console.WriteLine(c.Value); } else { Console.WriteLine("null" ); }
安全获取可空类型值
1 2 3 4 5 int ? d = null ;Console.WriteLine(d.GetValueOrDefault()); Console.WriteLine(d.GetValueOrDefault(5 ));
语法糖:自动判断是否为null
1 2 3 4 5 6 7 8 object o = "hello world" ;if (o!=null ){ Console.WriteLine(o.ToString()); } Console.WriteLine(o?.ToString());
6 空合并操作符 ?? 左边值 ?? 右边值
如果左边值为 null 就返回右边值,否则返回左边值
只要是可以为 null 的类型都能用1 2 3 4 int ? b = null ;int a = b ?? 100 ; int a = b == null ? 100 : b.value ;
7 内插字符串 $ 关键符号: $
用 $
来构造字符串,让字符串中可以拼接变量
1 2 string name = "Hello world!" ;Console.WriteLine($"好好学习,{name} " );
十四、多线程 进程是资源分配的最小单位,线程是 CPU 调度的最小单位
进程 (Process) 是计算机中的程序关于某数据集合上的一次运行活动 是系统进行资源分配和调度的基本单位,是操作系统结构的基础
说人话:
打开一个应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立运行,互不干扰
进程之间也可以相互访问、操作
线程 是操作系统能够进行CPU运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
我们目前写的程序都在主线程中
简单理解线程: 就是代码从上到下运行的一条“管道”
什么是多线程? 我们可以通过代码开启新的线程 可以以同时运行代码的多条“管道”就叫多线程
线程类 Thread 需要引用命名空间 using System.Threading;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Thread t = new Thread(TestFunc); t.Start(); t.IsBackground = true ; t.Abort(); t = null ; Thread.Sleep(1000 );
多个线程使用的内存是共享的,都属于该应用程序 (进程) 所以要注意,当多线程同时操作同一片内存区域时可能会出问题,可以通过加锁 的形式避免问题
当我们在多个线程当中想要访问同样的东西进行逻辑处理时,为了避免不必要的逻辑顺序执行的差错lock (引用类型对象)
1 2 3 4 5 6 7 8 9 10 11 static object obj = new object (); lock (obj){ } lock (obj){ }
多线程对于我们的意义 可以用多线程专门处理一些复杂耗时,影响主线成流畅度的逻辑,比如寻路、网络通信等等,副线程算完再拿到主线程使用