C# 高级语法
前言
关于C#高级语法在重新拾起后发现很多有趣以及好用的特性与语法糖
本篇关于高级语法包括: 属性 常量 字段 泛型 委托 事件 匿名函数 lambda 协变逆变 预处理指令 反射 特性 多线程 迭代器
基础
属性 get set
定义 :属性Property—-看起来像变量的函数 如下
1 |
|
这边定义了一个Person的类
1 | class Person { |
对于get set 语句块是可以在返回之前添加一点逻辑规则的(毕竟是
语句块可以类似函数来执行一些操作)
1 | public string Name{ |
对于get set 的语法块的特性可以将数据进行加密和解密处理
这边可以使用加密解密的算法
1 | public int Money{ |
自动属性,如果代码中没有特殊处理可以使用自动属性
1 | public int age { get; set;} |
索引器
可以像数组一样用索引访问其中元素,使程序看起来更直观,更容易编写,也写在get set 语句块中。
定义类
1 | class Person { |
其他概念与总结
- 成员属性概念:一般是用来保护成员变量的
- 成员属性的使用和变量一样 外部用对蒙点出
- get中需要return内容;set中用value表示传入的内容
- get和set语句块中可以加逻辑处理
- get和set可以加访问修饰符,但是要按照一定的规则进行添加
- get和set可以只有一个
- 自动属性是属性语句块中只有get和set,一般用于外部能得不能政这种情况
常量 Const static
常量的基本概念
常量的特点
常量的值必须在编译时确定,简单说就是在定义是设置值,以后都不会被改变了,她是编译常量。
常量只能用于简单的类型,因为常量值是要被编译然后保存到程序集的元数据中,只支持基元类型,如int、char、string、bool、double等。
常量在使用时,是把常量的值内联到IL代码中的,常量类似一个占位符,在编译时被替换掉了。正是这个特点导致常量的一个风险,就是
不支持跨程序集版本更新
;
const
和 readonly
的区别
const
和 readonly
都可以用来标识常量,但它们有以下主要区别:
- 初始化位置不同
- **
const
**:必须在声明的同时赋值。 - **
readonly
**:可以在声明处赋值,也可以在构造函数中赋值。
- 常量类型不同
- **
const
**:是编译时常量,值在编译时确定。 - **
readonly
**:是运行时常量,值在运行时确定。
- 静态性不同
- **
const
**:默认是静态的(static
),无需显式声明。 - **
readonly
**:如果需要设置为静态,必须显式声明(static readonly
)。
- 修饰引用类型时的限制
**
const
**:只能修饰string
或值为null
的其他引用类型。**
readonly
**:可以修饰任何类型。const
适用于编译时已知且不会改变的值,且只能用于基本类型或string
。readonly
更灵活,适用于运行时确定的值,且可以修饰任意类型。
字段
在C#中,字段(Field)是类、结构体或枚举的一个成员,它用于存储某种类型的数据。字段可以是任何有效的C#数据类型,包括基本数据类型(如int、float、bool等)、用户定义的类型(如类、结构体等)、枚举类型或数组等。字段是类的状态的一部分,它们用于存储与类的实例或类型本身相关的信息。
字段通常定义在类的主体内部,作为类的成员。它们可以是静态的(static),这意味着字段属于类本身而不是类的任何特定实例;或者它们可以是实例字段,这意味着每个类的实例都有其自己的字段副本。
下面是一个简单的C#类,其中包含两个字段的示例:
1 | public class Person |
在这个例子中,Person
类有两个实例字段:name
和 age
,它们分别存储人的名字和年龄。还有一个静态字段 totalPersons
,用于跟踪创建的 Person
对象总数。
字段通常是私有的(private),这意味着它们只能在类的内部被访问。如果需要从类的外部访问字段的值,通常会使用公共属性(public property)来提供对字段的访问,如上例中的 Name
和 Age
属性。
字段的初始化通常发生在构造函数中,或者通过字段声明时的初始值来完成。例如:
1 | public class Example |
在这个例子中,exampleField
字段在声明时被初始化为42。
字段是面向对象编程中封装概念的一部分,它们允许将数据与操作这些数据的方法一起封装在类中,从而隐藏实现细节并暴露有限的接口给类的用户。
特性
在C#中,特性(Attributes)是用于为程序中的类型和成员提供元数据(metadata)的一种机制。特性允许开发人员在源代码中附加声明性信息,以供编译器、运行时和其他工具使用。
以下是关于C#特性的一些重要概念和用法:
声明特性:特性本身是一种自定义的类,通过在类、方法、属性等声明前面使用方括号并提供特性类的名称来使用特性。例如:
1
2[ ]
public class MyClass { }内置特性:C#提供了一些内置的特性,例如
[Serializable]
、[Obsolete]
等。这些特性可以用来标记类型和成员的属性,告诉编译器或其他工具如何处理它们。自定义特性:开发人员可以根据需要创建自定义特性。自定义特性类通常派生自
System.Attribute
类,可以包含字段、属性和方法等。例如:1
2
3
4
5[ ]
public class CustomAttribute : Attribute
{
// 构造函数、属性和方法等
}应用特性:可以将特性应用于程序中的各种元素,如类、方法、属性、参数等。特性可以使用单个实例应用于多个元素,也可以使用多个实例应用于单个元素。
获取特性信息:可以使用反射机制在运行时获取特性信息。通过检查类型和成员上的特性,可以动态地获取元数据并做出相应的决策。
特性参数:特性可以具有参数,这些参数可以在特性的使用中提供。例如:
1
2[ ]
public class MyClass { }使用条件:特性可以具有使用条件,指定了特性可以应用于的元素类型、范围和其他限制条件。
通过使用特性,开发人员可以为代码添加元数据,以提供更多的信息给编译器、运行时和其他工具,从而实现更灵活、更健壮的程序设计和开发。
语法糖
- var隐式类型:
var i=5;
var不能作为类的成员只能用于临时变量声明。只能存在于函数语句块中且必须初始化
- 设置对象初始值
Person p=new Person{sex=true,Age=18,name=“Frag”};
Person p=new Person(){sex=true,Age=18,name=“Frag”};
初始化可以不用写全,且是不用调用构造函数的初始化
小括号可以加只不过是调用了构造函数
- 设置集合初始值
ListlistPerson=new List {
new Person(200),
new Person(100){Age=18},
new Person{sex=true,Age=18,name=“Frag”}
};
Dictionary<int,string>=new Dictionary<int,string>{
{1,”2333”},
{2,”6666”}
};
- 匿名类型
var变量可以声明为自定义的匿名类型
var v=new {age=10,money=11,name=“Frag”};
Console.WriteLine(v.age);
- 可空类型
值类型不能为空
int c=null;(error)(可以赋值为空)
int? c=null;判断是否为空
if(c.HasValue){
Console.WriteLine(c);
Console.WriteLine(c.value);
}安全获取可空类型值
Console.WriteLine(c.GetValueOrDefault());
//0指定默认值
Console.WriteLine(c.GetValueOrDefault(100));
//100
自动判断是否为空
object o=null;
o?.ToString();
等价于
if(o!=null) o.ToString();
arrayInt?[0].ToString();
action?.Invoke();
- 空合并操作符
return left??right;
if(left!=null){
return left;
}else{
return right;
}
- 内插字符串$
-Console.WriteLine(“普通字符串{0},value”);
Console.WriteLine($”内插入字符串{value}”);
- 逐字字符串@
要实现这一切仅需要在字符串常量的值前加一个符号“@”,以这种形式赋值的字符串叫做逐字字符串,它后面的所有字符都被逐个地收录到字符串的值中!
因此,如果你需要类似“所见所得”效果的赋值,逐字字符串赋值方式会是你的首选!
此外,需要注意的是,当使用符号 “@” 为字符串赋值时,被赋值的所有字符将不需要经过转义——只有双引号这个本身作为界限的字符需要经过转义,此时它的转义输入方法是两个放在一起的双引号:””
在以这种方式给变量赋值时,也只有这唯一一个转义是合法的。如果希望诸如换行、制表符此类字符在字符串体现出来,也可以直接将带有换行与制表符的字面量字符串赋给字符串量;不过这样,就不能在换行前的前一行写注释或其他语句,否则这些语句将被当作字符串值的一部分。
在下面例子中的语句,可以实现不完成输入一系列不经转义的特殊字符:
(1) 逐字:
string NoEscapeFullFileName = @”C:\inetpub\ciznxcom"; // (此句中的反斜扛被直接赋值)
(2)双引号:
string StringWithDbQoute = @”Jim says,””he can reach home in about six minutes””.” //(此句中双引号被转义)
(3)换行: 相当于加一个回车\r 而不是\n
string StringWithNextLineChar = @”We can set a string value for a string variable
with such a “”NextLine”” char.”; // (此句中给字符串变量赋予了回车符,且未经任何转义;并且保持所有空格)
泛型
1 | class TestClass<T,U> |
泛型约束
让泛型的类型有一定的限制
关键字:where
1 | class Test<T> where T: ‘不同约束的替换区域 ’{ |
泛型约束 | 语法 | 其他代码块 | Main.cs | 注释 |
---|---|---|---|---|
值类型 | class Test1 |
Test1 Test1 |
int float 是值类型sturct | |
引用类型 | class Test2 |
Test2 |
Random是引用类型class | |
公共无参构造约束:存在无参公共构造函数非抽象类型 | class Test3 |
class Test0{ public Test0(){}} class Test1{ public Test1(int a){ }} class Test2 { private Test2(){}} |
正确:Test3 错误: Test3 |
Test0-2只有Test0满足构造函数是公共且无参,这边Test0 也可以省略写构造函数 int作为值类型的构造函数都是公共无参所以可以使用 注意这边不能实例化虚类 |
类约束:某个类本身或者其派生类(子类) | class Test4 |
class child : parent{} | 正确:Test 4 错误:Test4 |
对于Test 1来说 Test2是它的子类可以使用,object是它的父类不能使用 |
某个接口的派生类型 | class Test5 |
interface IFly{} class Fly:IFly{} interface IFlyChild :IFly{} |
Test 5 对于value的实例化: 错误:t.value=new Test5 正确t.value=new Test5 |
在IFly接口中可以使用本身,子接口,子类,但是在实例化中,接口不能被实例化所以用里式替换,子类Fly代替了IFly; |
另一个泛型类型本身或者派生类型 | class Test6 <T,U> where T :U{ } | class child : parent{} | Test 6<parent,child>=new Test6<parent,child>(); Test6<parent,parent>=new Test6<parent,parent>(); |
在约束中只能使用本身或者子类 |
多个约束 | 代码 | |
---|---|---|
约束的组合使用 | class Test 7 where T:class,new(){} | 一起使用的排列组合有可能会报错 |
多个泛型有约束 | class Test8<T,K> where T: class , new() where K: new(){} |
多个约束:
可以通过在 where 子句中使用逗号来组合多个约束。例如,可以要求泛型类型参数既实现一个接口又继承一个基类。
1 |
|
委托和事件
委托Delegate
什么是委托?简单来说,委托类似于 C或 C++中的函数指针,允许将方法作为参数进行传递。
C#中的委托都继承自System.Delegate类型;
委托类型的声明与方法签名类似,有返回值和参数;
委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的
- delegate :可以保存多个函数指针 用+= -=进行操作
- Action :无返回值的delegate
- Func: 有返回值的delegate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30//定义委托类CalcFunc
delegate float CalcFunc(float val1 ,float val2);
// 初始化listenerFunc为 Add 函数
CalcFunc listenerFunc = Add;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
// 3+4=7(Add函数)
// 将 listenerFunc 加一个 Sub 函数
listenerFunc += Sub;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
//3+4=7 (Add函数)
//3-4=-1(Sub函数被加入)
// 将 listenerFunc 减去一个 Sub 函数
listenerFunc -= Sub;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
//3+4=7 (此时只运行了Add函数)
// Add 函数
static int Add(int a, int b) =>Console.WriteLine($“{a}+{b}={a+b}”);
// Sub 函数
static int Sub(int a, int b) => Console.WriteLine($”{a}-{b}={a-b}”);
}
1 | Action<string>myPrint= (info)=>{ |
事件event
- 事件是基于委托的存在
- 事件的委托的安全包裹
- 让委托更具有安全性
- 事件是一种特殊的变量类型
申明语法:
访问修饰符 event 委托类型 事件名;
事件的使用:
- 事件是作为 成员变量存在于类中
- 委托怎么用 事件就怎么用
事件相对于委托的区别: - 不能在类外部 赋值
- 不能在类外部 调用
注意:
只能作为成员存在与类和接口以及结构体中
1 | class Test { |
1 | class Program{ |
为什么有事件
- 防止外部随意调用委托
- 防止外部随意置空委托
- 事件相当于对委托进行了一次封装,让其更安全
我自己认为事件是一种类似私有类的委托
委托和事件区别
大致来说,委托是一个类,该类内部维护着一个字段,指向一个方法。事件可以被看作一个委托类型的变量,通过事件注册、取消多个委托或方法。
委托可以用“=”来赋值,事件不可以。
委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用。
委托是一个类型,事件修饰的是一个对象。
委托就是一个类,也可以实例化,通过委托的构造函数来把方法赋值给委托实例
触发委托有2种方式: 委托实例.Invoke(参数列表),委托实例(参数列表)
事件可以看作是一个委托类型的变量
通过+=为事件注册多个委托实例或多个方法
通过-=为事件注销多个委托实例或多个方法
EventHandler就是一个委托
闭包
匿名函数
顾名思义,就是没有名字的函数
匿名函数的使用主要是配合委托和事件进行使用
//脱离委托和事件 是不会使用匿名函数的
知识点二 基本语法
//delegate(参数列表)
//
//函数逻辑
//};
何时使用?
- 函数中传递委托参数时
- 委托或事件赋值时
这里声明了一个委托Action(无返回值的委托) 然后赋值(初始化)为匿名函数
1 | //Action.cs |
lambda表达式
使用上和匿名函数一样的
1 | Action a1 =()=>{ Console.WriteLine(“a1”);}; |
闭包:
内层函数可以引用包含在它外层的函数的变量
即使外层函数的执行已经终止
注意:
该变量提供的值并非变量创建时候的值,而是在父函数范围内的最终值
1 | // 定义一个整数变量 |
- 共享的局部变量i被提升为公共字段(static)
- 变量i的生命周期延长了;
- for循环结束后字段i的值是10了;
- 后面再次调用委托方法,肯定就是输出10了;
1 | Action action; |
那该如何修正呢?很简单,委托方法使用一个临时局部变量就OK了,不共享数据:
1 |
|
关于OOP
预处理指令
C# 中并没有像 C 或 C++ 中的预处理指令(如 #include
, #define
, #ifdef
等)那样的功能。C# 是一种更高级的语言,其编译器不处理预处理步骤,而是在编译过程中直接处理源代码。
但是,C# 提供了一些与预处理指令类似的功能或概念,尽管它们的工作方式与传统的预处理指令不同。以下是一些与预处理相关的 C# 功能和概念:
- #define 和 #undef: C# 提供了
#define
和#undef
预处理指令,但它们主要用于条件编译。这些指令通常在项目属性或编译器的命令行参数中设置,而不是在源代码文件中。
1 |
|
然而,要注意的是,与 C 和 C++ 中的 #define
不同,C# 中的 #define
并不创建宏或替换文本。它仅仅是一个编译时的标志。
2. 条件编译: 通过 #if
, #else
, #elif
, #endif
指令,你可以在编译时基于某些条件包含或排除代码块。这通常用于调试、平台特定的代码或配置特定的功能。
3. 元数据: C# 使用元数据来描述类型、方法和属性等。这与 C 或 C++ 中的预处理指令不同,但提供了类似的功能,允许在编译时检查类型信息。
4. 编译常量和条件属性: 你可以在项目属性中设置编译常量,或在编译器的命令行参数中设置它们。此外,你还可以使用条件属性(如 [Conditional("DEBUG")]
)来标记方法,以便它们仅在特定的编译条件下被调用。
5. 区域指令: #region
和 #endregion
指令允许你在 Visual Studio 等 IDE 中折叠和展开代码块,这对于组织大型代码文件非常有用。
总的来说,尽管 C# 没有与 C 或 C++ 完全相同的预处理指令,但它提供了一套功能强大的工具和技术,允许你在编译时根据条件包含或排除代码。
反射
- 什么是程序集
程序集是经由编译器编译得到的,更进一步编译执行的那个中间产物
在WINDOWS系统中,它一般表现为后缀为.dll(库文件)或者是/exe(可执行文件) 的格式
说人话:
程序集就是我们与的一个代码集合,我们现在写的所有代码
最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件 (dll)或者一个可执行文件(exe)
- 元数据
元数据就是用来描述数据的数据
这个概念上不仅仅用于程序上那个在别的领域也有元数据
说人话:
程序中的类,类中的函数,变量等等信息就是 程序的 元数据
有关程序以及类型的数据被称为 元数据,它们保存在程序集中
- 反射的概念
程序正在运行时,可以查看其它程序集或者自身的元数据。
一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射
在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
类,函数,变量,对象等等,实例化它们,执行它们,操作它们
- 反射的作用
因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
- 程序运行时得得到所有元数据,包括元数据的特性
- 程序运行时,实例化对象,操作对象
- 程序运行时创建新对象,用这些对象执行任务
方法 | 描述 | 示例代码 |
---|---|---|
使用对象的GetType() 方法 |
在任何对象中,可以通过调用GetType() 方法来获取该对象的类型。 |
csharp int a = 42; Type type = a.GetType(); Console.WriteLine(type); |
使用typeof 关键字 |
通过typeof 关键字并传入类名,可以直接获取到对应类型的Type。 |
csharp Type type2 = typeof(int); Console.WriteLine(type2); |
通过类的全名获取类型 | 通过Type.GetType() 方法,传入包含命名空间的完整类名,可以获取到对应的Type。 |
csharp Type type3 = Type.GetType("System.Int32"); Console.WriteLine(type3); |
在这上面三个例子实际栈中实际指向的堆地址都是一样的
得到类的程序集信息(了解)
Console.WriteLine(type.Assembly);
获取类中所有的公共成员
Type t=typeof(Test);//本文件常用,如果是不同文件的话就用类名找
MemberInfo[]infos=t.GetMembers();
- 获取类的公共构造函数并调用
你的文字描述中存在一些语法错误和拼写错误,我会尝试将其整理并修改成合理的C#代码。根据你的描述,你想要执行一个无参构造方法和一个有参构造方法,并且打印出构造出来的对象的某个属性。
首先,我们整理并修改无参构造的部分:
1 | // 2-1 得到无参构造 |
然后,我们整理并修改有参构造的部分。这里假设Test
类有一个接受int
和string
类型参数的构造方法,并且你想要构造一个对象并打印它的str
属性(假设Test
类有这个属性):
1 | // 2-2 得到有参构造(一个int参数) |
请注意,t
应该是一个Type
对象,代表你想要反射的类。同时,Test
类应该已经定义,并且包含你尝试访问的i
和str
属性或字段。此外,请确保反射的类已经被加载到当前的执行上下文中,否则GetConstructor
方法可能会返回null
。
获取指定名称的公共成员变量
获取类的公共成员方法
https://www.zhihu.com/tardis/zm/art/493738261?source_id=1005
Activator
用于快速实例化对象的类
用于将Type对象快捷实例化为对象
先得到Type
然后 快速实例化一个对象
Type testType=typeof(Test);
//无参构造
Test testObj=Activator.CreateInstance(testType)
多线程
进程、线程、协程
线程与进程
知识点— 了解线程前先了解进程
进程(Process) 是计算机程序中关于某数据集合上的一次运行活动
是系统进行资源分配和调度的基本单位,是操作系统结构的基础
说人话:打开应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立运行,互不干扰
进程之间也可以相互访问、操作
什么是线程
- 操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位 - 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
- 我们目前写的程序 都在主线程中
简单理解线程:
- 就是代码从上到下运行的一条“管道”
什么是多线程
- 我们可以通过代码开启新的线程
- 可以同时运行代码的多条“管道”就叫多线程
在C#中,你可以通过多种方式创建和管理线程。以下是一些基本的线程创建和操作的示例代码:
区别
协程:即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程实际上是在一个线程中,只不过每个协程对CPU进行分时,协程可以访问和使用unity的所有方法和component。同一时间只能执行某个协程。开辟多个协程开销不大。协程适合对某任务进行分时处理。
线程:多线程是阻塞式的,每个IO都必须开启一个新的线程,但是对于多CPU的系统应该使用thread,尤其是有大量数据运算的时刻,但是IO密集型就不适合;而且thread中不能操作unity的很多方法和component。同一时间可以同时执行多个线程。开辟多条线程开销很大。线程适合多任务同时处理。
线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只在必要时才会被挂起。
进程、线程、协程
进程
- 保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,不同进程间可以进行进程间通信,上级挂靠单位是操作系统。一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),进程是资源分配的最小单位。
线程
线程从属于进程,也被称为轻量级进程,是程序的实际执行者。线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条- 线程并行执行不同的任务。一个线程只有一个进程。
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程
协程是伴随着主线程一起运行的一段程序。
协程与协程之间是并行执行,与主线程也是并行执行,同一时间只能执行一个协程提起协程,自然是要想到线程,因为协程的定义就是伴随主线程来运行的。
一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
协成是单线程下由应用程序级别实现的并发。
unity 协程
在Unity中,协程是一种轻量级的线程,用于在单一线程上执行多个任务,实现异步等待和延迟执行等效果。协程不是取代线程,而是抽象于线程之上,同一时间只有一个协程拥有运行权,相当于单线程的能力。协程的执行基于迭代器,通过yield语句来实现暂停和恢复执行,从而更加灵活地控制协程的执行过程。
协程可以在不创建新线程的情况下实现异步等待和延迟执行,避免了线程切换和同步等问题,从而提高了程序的性能和效率。在Unity中,协程通常用于处理一些异步任务,比如等待一段时间、播放动画、加载资源等。协程的执行过程可以通过yield语句来控制,比如yield return new WaitForSeconds(5)可以让协程等待5秒钟后再继续执行。此外,协程还可以通过yield return null等方式来控制协程的执行过程。
需要注意的是,协程并不是多线程,协程的执行是在主线程上进行的,因此协程中的操作不应该阻塞主线程的执行。如果协程需要执行一些耗时的操作,可以将其放到后台线程中执行,然后通过yield return null等方式来控制协程的执行过程。
在Unity中只有主线程才能访问Unity3D的对象、方法、组件。当主线程在执行一个对资源消耗很大的操作时,在这一帧我们的程序就会出现帧率下降,画面卡顿的现象!
那这个时候我们就可以利用协程来做这件事,因为协程是伴随着主线程运行的,主线程依旧可以丝滑轻松的工作,把脏活累活交给协程处理就好了!简单来说:协程是辅助主线程的操作,避免游戏卡顿。
迭代器
迭代器是什么
详解C# 迭代器 - Cat Qi - 博客园 (cnblogs.com)
- 迭代器(iterator)有时又称光标(cursor)
- 是程序设计的软件设计模式
- 迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素
- 而又不暴露其内部的标识
- 在表现效果上看
- 是可以在容器对象(例如链表或数组)上遍历访问的接口
- 设计人员无需关心容器对象的内存分配的实现细节
- 可以用foreach遍历的类,都是实现了迭代器的
使用迭代器
IEnumerator,IEnumerable
命名空间: using System.Collections;
可以通过同时继承IEnumerable和IEnumerator实现其中的方法
底层
协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义一个迭代方法。
StartCoroutine 接受到的是一个 IEnumerator ,这是个接口,并且是枚举器或迭代器的意思。
yield 是 C#的一个关键字,也是一个语法糖,背后的原理会生成一个类,并且也是一个枚举器,而且不同于 return,yield 可以出现多次。
yield 实际上就是返回一次结果,因为我们要一次一次枚举一个值出来,所以多个 yield 其实是个状态模式,第一个 yield 是状态 1,第二个 yield 是状态 2,每次访问时会基于状态知道当前应该执行哪一个 yield,取得哪一个值。
1 | using System.Collections; |
- foreach 本质
- 先获取in 后面的对象的IEnumerator
- 会调用其中的GetEnumerator 方法
1 | class IterationSampleEnumerator : IEnumerator |
通过yield语句简化迭代
1 | public IEnumerator GetEnumerator() |
总结
从程序的角度讲,协程的核心就是迭代器。
想要定义一个协程方法有两个因素,第一:方法的返回值为 IEnumerator 。第二,方法中有 yield关键字。
当代码满足以上两个条件时,此方法的执行就具有了迭代器的特质,其核心就是 MoveNext方法。
方法内的内容将会被分成两部分:yield 之前的代码和 yield 之后的代码。yield之前的代码会在第一次执行MoveNext时执行, yield之后的代码会在第二次执行MoveNext方法时执行。
而在Unity中,MoveNext的执行时机是以帧为单位的,无论你是设置了延迟时间,还是通过按钮调用MoveNext,亦或是根本没有设置执行条件,Unity都会在每一帧的生命周期中判断当前帧是否满足当前协程所定义的条件,一旦满足,当前帧就会抽出CPU时间执行你所定义的协程迭代器的MoveNext。
注意,只要方法中有yield语句,那么方法的返回值就必须是 IEnumerator ,不然无法通过编译