C#进阶
CLR中简单数据结构类
命名空间:System.Collections;
ArrayList
语法:new ArrayList();
建议:本质是一个自动扩容的object数值,存在装箱拆箱,尽量少用;
C# 动态数组(ArrayList) |菜鸟教程 (runoob.com)
Stack(栈)
语法:new Stack();
建议:存在装箱拆箱;
C# 堆栈(Stack) | 菜鸟教程 (runoob.com)
Queue(队列)
语法:new Queue();
建议:存在装箱拆箱;
C# 队列(队列) |菜鸟教程 (runoob.com)
Hashtable(哈希表)
定义:又叫散列表,是基于键的哈希代码组织起来的键值对,主要用来提高数据查询效率;
语法:new Hashtable();
迭代器遍历法:
IDictionaryEnumerator me=哈希表对象名.GetEnumerator();
while(me.MoveNext()){
}
建议:存在装箱拆箱;
C# 哈希表(Hashtable) |菜鸟教程 (runoob.com)
CLR中的泛形
命名空间:System.Collections.Generic
泛形
定义:通过类型参数来实现代码操作多种类型;
原理:相当于占位符,定义类或者方法是用替代符代替变量类型,当真正使用时再指定具体类型;
泛形约束
关键字:where,可以多个使用;
种类:6种如下:
1.值类型 where 泛形字母:struct;
2.引用类型 where 泛形字母:class;
3.存在无参公共构造函数 where 泛形字母:new();
4.某个类本身或者派生类 where 泛形字母:类名;
5.每个接口的派生 where 泛形字母:接口名;
6.另一个泛形类型或者派生类型 where 泛形字母:另一个泛形字母;
List
定义:表示可通过索引访问的对象的强类型列表。 提供用于对列表进行搜索、排序和操作的方法。
语法:new List<>();
List 类 (System.Collections.Generic) | Microsoft Docs
Dictionary
定义:表示键和值的集合。
语法:new Dictionart<键,值>();
Dictionary 类 (System.Collections.Generic) | Microsoft Docs
LinkedList
定义:表示双重链接列表。
语法:new LinkedList<>();
LinkedList 类 (System.Collections.Generic) | Microsoft Docs
泛形队列
定义:表示对象的先进先出泛形集合。
语法:new Queue<>();
Queue 类 (System.Collections.Generic) | Microsoft Docs
泛形栈
定义:表示相同指定类型的实例可变大小的后进先出 (LIFO) 集合。
语法:new Stack<>();
Stack 类 (System.Collections.Generic) | Microsoft Docs
委托和事件
委托
定义:C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针,本质时一个类。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变,专门用来装载函数的容器。
语法:
//定义
访问修饰符 delegate 返回值类型 委托名<泛形类型>(参数列表)
//实例化
委托名 变量名=new 委托名();
//赋值,参数要一致
变量名[+或-]=方法名()
//使用
委托变量名(对应参数):
系统自带的委托:Action<泛形类型,……泛形类型>和Func<泛形类型,…..返回值>;(d都支持16个泛形类型)
事件
定义:让委托使用更加安全,事件是一种特殊的变量类型
语法:访问修饰符 event 委托类型 事件名
和委托区别:不能再类外部赋值(可以加减),不能在类外部调用,只能在类内部封装调用;
匿名函数
定义:没有名字的函数,要配合委托和事件使用;
缺点:没有名字,无法通过+-移除;
语法:
Action a=delegate(参数列表){
}
Lambad表达式
定义:可以理解成是匿名函数的简写;
(参数列表)=>{
}
多线程和任务
前台线程:主程序必须等待线程执行完毕后才可退出程序。Thread默认为前台线程,也可以设置为后台线程
后台线程:主程序执行完毕后就退出,不管线程是否执行完毕。ThreadPool默认为后台线程
线程消耗:开启一个新线程,线程不做任何操作,都要消耗1M左右的内存
多线程(Thread)
命名空间:using System.Threading
语法:
//启动线程,将要执行的代码逻辑封装到一个函数语句块中
Thread 线程名=new Thread(委托方法);
//启动线程
线程名.start();
//设置为后台线程,当进程结束后台线程结束
线程名.IsBackground=true;
//关闭释放一个死循环进程,有俩中方法
//为线程死循环加一个标志变量
//通过线程提供的方法(在.net core版本会无法中止报错),可以加异常捕捉
线程名.Abort();
//线程休眠
线程名.Sleep(时间/ms);
共享数据问题:多线程操作同一内存区域可能出现问题,可以通过加锁的形式避免问题
lock(同一引用类型变量){
}
线程池(ThreadPoll)
命名空间:using System.Threading
定义:ThreadPoll是线程池(享元设计模式),其目的是为了减少开启新线程消耗的资源(使用线程池中的空闲线程,不必在开启新线程,以及统一管理线程(线程池中的线程执行完毕后,回归到线程池里,等待新任务).
优缺点:ThreadPoll性能优于Thread,但是Thread和ThreadPoll对线程的控制都不是很好,例如线程等待(线程执行一段时间无响应后,直接停止线程,释放资源 等 都没有直接的API来控制 只能通过硬编码来实现,同时ThreadPool使用的是线程池全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能。
语法:
//快速启动
ThreadPool.QueueUserWorkItem(委托方法);
//获取线程池中辅助线程的最大数量(workerThreadsMax)和线程池中异步I/O线程的最大数量(completionPortThreadsMax)
ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);
//获取线程池中辅助线程的最小数量(workerThreadsMin)和线程池中异步I/O线程的最小数量(completionPortThreadsMin)
ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);
//设置最大线程数量 和 设置最小线程数量,在进程内是全局的。在一个地方设置了,后面所有的请求中都是这个数量了
//委托异步调用、Task、Parallel、async/await 都使用的是线程池的线程; new
Thread()不受限制,但是会占用线程池的数量。
ThreadPool.SetMaxThreads(12, 12);//不能低于当前电脑的线程数;比如四核八线程,就不能低于8,否则无效
ThreadPool.SetMinThreads(1, 1);
//线程等待,需要使用ManualResetEvent来完成
ManualResetEvent mre = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem((obj) => {
DoSomething("");
mre.Set();
} );
mre.WaitOne();
任务(Task)
命名空间:using System.Threading.Tasks
特点:Task是基于任务的异步编程模型,Task的背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll,因为它使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的资源竞争减少。同时Task提供了丰富的API来管理线程、控制。但是相对前面的两种耗内存,Task依赖于CPU对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。
建议:使用Task的时候应该尽量结合async和await关键字来使用。避免使用.Result 和 .Wait()来阻塞等待;.Result 和 .Wait()会占用线程资源,直到任务完成;而await的基于异步回调的,不会浪费CPU资源;async和await是语法糖,本质上其实是ContinueWith()。
基本语法:
1.创建无返回值Task的三种方式(加上泛形就有返回值)
//(1).通过new一个Task对象传入委托函数并启动
Task<int> t1 = new Task<int>(() =>
{
//用一个参数来控制暂停
while (isRuning){}//todo
return 1;
});
t1.Start();
//(2).通过Task中的Run静态方法传入委托函数(返回值,后面可以不写)
Task t2 = Task.Run<string>(() =>
{
return "你好";
});
//(3).通过Task.Factory中的StartNew静态方法传入委托函数(返回值,可以都不写)
Task t3 = Task.Factory.StartNew(() =>
{
return 4.5f;
});
//获取返回值
t3.Result;
//注意:Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,也就是说 执行到这句代码时 由于我们的Task中是死循环,所以主线程就会被卡死
2.同步执行Task(只做了解)
//如果你希望Task能够同步执行
//只需要调用Task对象中的RunSynchronously方法
//注意:需要使用 new Task对象的方式,因为Run和StartNew在创建时就会启动
Task t = new Task(() =>
{
Thread.Sleep(1000);
});
t.RunSynchronously();
3.Task中线程阻塞的方式(任务阻塞)
//(1).Wait成员方法,主线程只有等调用方线程执行完毕才能继续执行
t3.Wait();
//(2).WaitAny静态方法:传入任务中任意一个任务结束就继续执行
Task.WaitAny();
//(3).WaitAll静态方法:任务列表中所有任务执行结束就继续执行
Task.WaitAll();
4.Task完成后继续其它Task(任务延续)
//(1).WhenAll静态方法 + ContinueWith方法:传入任务完毕后再执行某任务
Task.WhenAll(t1, t2).ContinueWith((t) =>{});
//(2).WhenAny静态方法 + ContinueWith方法:传入任务只要有一个执行完毕后再执行某任务
Task.WhenAny(t1, t2).ContinueWith((t) =>{});
5.取消Task执行
//(1).方法一:通过加入bool标识 控制线程内死循环的结束
//(2).方法二:通过CancellationTokenSource取消标识源类 来控制
//CancellationTokenSource对象可以达到延迟取消、取消回调等功能
CancellationTokenSource c = new CancellationTokenSource();
//取消执行
c.Cancel();
//延迟取消
c.CancelAfter(5000);
//取消回调
c.Token.Register(() =>{});
//任务声明
Task.Run(() =>
{
//设置任务取消参数
while (!c.IsCancellationRequested){}
});
异步方法async/await关键字
1.同步和异步说明:需要处理的逻辑严重影响主线程的执行流畅时,我们需要使用异步编程,比如复杂逻辑计算、网络下载、网络通讯、资源加载,同步和异步主要用来修饰方法:
- 同步方法:同一个方法被调用,调用者需要等待该方法执行完毕后才能继续执行;
- 异步方法:当一个方法被调用时立即返回,并且获取一个线程执行该方法内部的逻辑,调用者不用等待该方法执行完毕;
2.async和await关键字说明:类似Unity的协程,async和await一般需要配合Task进行使用,async用于修饰函数、lambda表达式、匿名函数,await用于在函数中和async配对使用,主要作用是等待某个逻辑结束,此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑,在一个async异步函数中可以有多个await等待关键字,如下使用建议说明:
- async关键字(修饰异步方法):
- 在异步方法中使用await关键字(不使用编译器会给出警告但不报错),否则异步方法会以同步方式执行 ;
- 异步方法名称建议以Async结尾;
- 异步方法的返回值只能是void、Task、Task<>;
- 异步方法中不能声明使用ref或out关键字修饰的变量;
- await关键字(等待异步内容执行完毕):遇到await关键字时如下流程;
- (1).异步方法将被挂起;
- (2).将控制权返回给调用者;
- (3).当await修饰内容异步执行结束后,继续通过调用者线程执行后面内容;
3.async和await关键字案例
1.复杂逻辑计算(利用Task新开线程进行计算 计算完毕后再使用 比如复杂的寻路算法)
public async void CalcPathAsync(GameObject obj, Vector3 endPos)
{
print("开始处理寻路逻辑");
int value = 10;
await Task.Run(() =>
{
//处理复杂逻辑计算 我这是通过 休眠来模拟 计算的复杂性
Thread.Sleep(1000);
value = 50;
//是多线程 意味着我们不能在 多线程里 去访问 Unity主线程场景中的对象
//这样写会报错
//print(obj.transform.position);
});
print("寻路计算完毕 处理逻辑" + value);
obj.transform.position = Vector3.zero;
}
2.计时器或者资源加载(Addressables的资源异步加载是可以使用async和await的)
public async void Timer()
{
UnityWebRequest q = UnityWebRequest.Get("");
source = new CancellationTokenSource();
int i = 0;
while (!source.IsCancellationRequested)
{
print(i);
await Task.Delay(1000);
++i;
}
}
4.Unity使用建议:Unity中大部分异步方法是不支持异步关键字async和await的,我们只有使用协同程序进行使用,但是存在第三方的工具(插件)可以让Unity内部的一些异步加载的方法 支持 异步关键字,或者使用.Net 库中提供的一些API时,可以考虑使用异步方法,一般.Net 提供的API中 方法名后面带有 Async的方法 都支持异步方法:
- Web访问:HttpClient;
- 文件使用:StreamReader、StreamWriter、JsonSerializer、XmlReader、XmlWriter等等;
- 图像处理:BitmapEncoder、BitmapDecoder;
svermeulen/Unity3dAsyncAwaitUtil: A bunch of code to make using async-await easier in Unity3D (github.com)
预处理器指令
什么是编译器
源语言程序:某种程序设计语言写的,像c#、c、c++、java等;
目标语言程序:计算机可以识别的二进制数程序;
编译器:是一种翻译程序(编译原理 ),将源语言程序翻译成目标语言程序;
什么是预处理器指令
定义:指导编译器,在实际编译开始时对信息进行预处理,都是以#开始,不是指令,所以不以;结束;
常见的预处理器指令
//写在脚本最前面,配合if指令或特性使用
1.#define:定义一个符号,类似一个没有值的变量
2.#undef:取消define定义的符号
3.#if、#elif、#else、#endif:和if语法规则一样,用于告诉编译器进行编译代码的流程控制;
4.#warning、#error:告诉编译器是报警告还是报错;
反射和特性
程序集:由编译器编译(.exe或者.dll),供编译执行的中间产物;
元数据:数据的数据;
反射
概念:程序正在运行时可以查看其他程序集或者自己的元数据这就叫反射;
type定义:类的信息类,是访问元数据的主要方式,反射的技术;
语法:获取的type指向的内存都是一样的
//1.通过object.GetType()获取对象的Type
Type t1=object.GetType();
//2.通过typeof关键字获取类的Type
Type t2=typeof(int);
//3.通过类名获取,但是必须有命名空间
Type p3=Type.GetType("System.Int32");
//程序集获取
t1.Assembly;
//获取类的所有公共成员
MemberInfo[] infos=t1.GetMembers();
//获取类所有构造函数并调用
ConstructorInfo[] ctors=t1.GetConstructors();
//获取构造函数传入 Type数组 数组中按顺序是参数类型,执行构造函数传入 object数组 表示按顺序传入的参数;
//得到无参构造
ConstructorInfo info =t1.GetConstructor(new Type[]);
info.Invoke(null) as 类名;
//得到有参构造
ConstructorInfo info =t1.GetConstructor(new Type[]{typeof(int)});
info.Invoke(new object[]{2}) as 类名;
//得到所有成员变量
FieldInfo[] fieldINfos=t1.GetFields();
//得到指定名称的公共成员变量
FieldInfo infoj=t1.GetField("变量名");
//通过反射获取变量值
infoj.GetValue(类实例);
//通过反射设置变量值
infoj.SetValue(类实例,要赋的值);
//获得类的公共成员方法
MethodInfo[] methods=t1.GetMethods();
//获取指定方法
MethodInfo method=t1.GetMethod("方法名",new Type[]{反射参数类型同上});
method.Invoke(参数);
特性
定义:本质是个类,可以利用特性类为元数据添加额外信息,之后可以用反射获取这些额外信息,类、变量、函数前都可以添加;
自定义特性:继承特性基类 Attribute(使用自定义特性时类名后面会省略Attribute这几个字);
语法
//1.定义特性
class 特性类名:Attribute{
}
//2.特性使用
[特性名(参数列表)]
//类、函数、变量上一行
//3.判断是否使用了某个特性
//参数一:特性的类型 参数二:是否搜索继承链(属性和时间忽略)
if(类type类型.IsDefind(typeof(特性类型),false)){}
//4.获取Type元数据中的所有特性
t.GetCustomAttributes(ture);
//5.为特性类加特性,限制自定义特性
//参数一:AttributeTargets--特性可以用在哪里
//参数二:AllowMultiple--是否允许多个特性在同一个目标上
//参数三:Inherited--特性是否能被派生类和重写成员继承
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct,AllowMultiple=true,Inherited=true)]
//6.系统自带特性
//6.1过时特性
[Obsolete("过时特性,后面是ture直接报错,否则警告",false)]
//6.2调用者信息特性
用处不大,省略,用工具就可以了
//6.3条件编译特性
//和#define配套使用,必须有该参数名的预指令符号才会执行修饰成员
using System.Runtime.CompilerServices
[Conditional("Fun")]
//6.4外部Dll包函数特性
//用来标记非.Net的函数,表明在一个外部的DLL中定义,用来调用c或者c++得DLL包写好的方法
using System.Runtime.InteropServices
[DllImport("程序集全名")]
public static extern int 外部DLL元数据函数名()
其他补充
lock和using语句块
lock
定义:确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
注意:
1.lock不能锁定空值,但Null是不需要被释放的。
2.lock不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR“暂留”。即整个程序中任何给定字符串都只有一个实例,具有相同内容的字符串都代表着同一个实例。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中与该字符串具有相同内容的字符串。因此,最好锁定不会被暂留的私有或受保护成员。
using
作用:
1.自动释放,避免缓存,内存溢出
2.简化try catch 得到在此定义域内自动释放所新建的对象,以简化代码;
协变逆变
协变:out
逆变:in
作用一:用out修饰的泛形只能作为返回值,用in修饰的泛形只能作为参数
作用二:用out和in修饰的泛形委托可以i互相装载(有父子关系的委托)
//结合里氏替换原则理解
//协变 父类总是能被子类替代
TestOut<Son> os=()=>{
}
TestOut<Father> of=os;//参数必须声明out
of();//实际上os装的函数返回的是Son的
//逆变 子类装父类委托
TestIn<Father> of=()=>{
}
TestIn<Son> os=of;//参数必须声明in
os();//实际上os装的函数返回的是Father的
迭代器(iterator)
概念:又称为光标(cursor),是程序设计的软件设计模式,是可以在遍历访问的接口,设计人员无需关心容器的内存细节,能用foreach遍历的类,都实现了迭代器;
关键接口:IEnumerator(迭代器实现),IEnumerable(foreach实现,有方法即可);
命名空间:using System.Collections;
yield return:是c#提供的语法糖(糖衣语法),可以将复杂逻辑简单化,增加程序可读性,yield关键字可以理解为暂时返回,保留当前状态;
特殊语法
# 隐藏类型var
可以用来表示任何类型的变量,一般用来临时变量,但是不能再更改类型。
# 设置对象初始值
可以直接通过大括号来进行赋值。
# 设置集合初始值
可以直接通过大括号来进行赋值。
# 匿名类型
var变量可声明为自定义的匿名类型,但是只能有变量。
语法
var v=new {age=10,name="小明};
# 可空类型
//1.数值类型使用,声明时再值类型后面加?可以赋值为空
int? lue=null;
//又如
int[] os=null;
int? x=os?[0];
//2.引用类型使用,相当于一种语法糖,自动判断是否为空,例如判断委托是否为不为空才执行
o?Invoke();
//3.判断值类型是否为空
值变量名.HasValue;
//4.安全获取值类型默认值(可以指定默认值)
值变量名.GetValueOrDefault(默认值);
# 空和并操作符
定义:左边值??右边值,左边为null返回右边值,否则左边;
# 内插字符串
用关键字$,类似于Format拼接字符串。
语法
Console.WriteLine($"你好,{name}”);
# 单句逻辑简略写法
逻辑语句只有一句代码的话可以省略{},而属性只有一句getset可以写成get=>返回值;set=>变量名=要赋的值,方法同理。