title: Plan-2 date: 2024-08-08 11:52:00 tags:
mindmap
Unity
算法
优化部分
渲染 & Shader
网络
Lua 语言和 Xlua 热更新
数据持久化 & 资源管理
C# 基础
Unity 基础
物理系统
动画系统
UI & 2D
协程
介绍
值类型:包含了所有简单类型(整数、浮点、bool、char)、struct、enum。
继承自 System.ValueTyoe
引用类型包含了 string,object,class,interface,delegate,array
继承自 System.Object
区别
.Net 是一个语言平台,Mono 为 .Net 提供集成开发环境,集成并实现了.NET 的编译器、CLR 和基础类库,使得 .Net 既可以运行在 windows 也可以运行于 linux,Unix,Mac OS 等。
引用类型的基类是 System.Object 值类型的基类是 System.ValueType。
同时,值类型也隐式继承自 System.Object。
ArrayList 存在不安全类型(ArrayList 会把所有插 ⼊其中的数据都当做 Object 来处理)装箱拆箱的 操作(费时)IList 是接口,ArrayList 是⼀个实现了 该接口的类,可以被实例化
List 类是 ArrayList 类的泛型等效类。它的大部分用法都与 ArrayList 相似,因为 List 类也继承了 IList 接口。最关键的区别在于,在声明 List 集合时,我们同时需要为其声明 List 集合内数据的对象类型。
GC 的概念
GC 会带来的问题
游戏性能:GC 操作是一个极其耗费事件的操作,堆内存上的变量或者引用越多则导致遍历检查时的操作变得十分缓慢,使得游戏运行缓慢,例如当 CPU 处于游戏性能的关键时刻,任何一个操作就会导致游戏帧率下降,造成极大的影响。
游戏内存:(UnityGC 采用的是非分代非压缩的标记清除算法)GC 操作会产生“内存碎片化”。当一个单元内存从堆中分配出来,其大小取决于存储变量的大小。当内存被回收到堆上时,有可能被堆内存分割成碎片化的单元。(就是说总容量大小时固定的,但是单元内存较小。例如房子很大,房间很小,找不到合适的房间)即下次分配时找不到合适的储存单元,就会触发 GC 操作,或者堆内存扩容操作,导致 GC 频发发生和游戏内存越来越大。
GC 触发时机
如何避免 GC?
结构体和类的区别
使用情景
结构体:
类:
属性修饰符
Serializable:按值将对象封送到远程服务器。
STATread:是单线程套间的意思,是一种线程模型。
MATAThread:是多线程套间的意思,也是⼀种线程模型。
存取修饰符
public:存取不受限制。
private:只有包含该成员的类可以存取。
internal:只有当前工程可以存取。
protected:只有包含该成员的类以及派生类可以存 取。
类修饰符
成员修饰符
abstract:指示该方法或属性没有实现。
sealed:密封方法。可以防止在派生类中对该方法的 override(重载)。不是类的每个成员方法都可以作为密封方法密封方法,必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed 修饰符总是和 override 修饰符同时使用。
delegate:委托。用来定义⼀个函数指针。C# 中的事件驱动是基于 delegate + event 的。
const:指定该成员的值只读不允许修改。
event:声明⼀个事件。
extern:指示方法在外部实现。
override:重载。对由基类继承成员的新实现。
readonly:指示⼀个域只能在声明时以及相同类的内部被赋值。
static:指示一个成员属于类型本身,而不是属于特定的对象。即在定义后可不经实例化,就可使用。
virtual:指示⼀个方法或存取器的实现可以在继承类中被覆盖。
new:在派生类中隐藏指定的基类成员,从而实现重写的功能。 若要隐藏继承类的成员,请使用相同名称在派生类中声明该成员,并用 new 修饰符修饰它。
栈通常保存着我们代码执行的步骤。 而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。 栈也是如此,当一个方法(或类型)被调 用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的 是他们被调用完毕不会立即被清理掉。
GC 方面:栈保持着先进后出的原则,是一片连续的内存域,有系统自动分配和维护,产生的垃圾系统自动释放。而堆是无序的,他是一片不连续的内存域,用户自己来控制和释放,如果用户自己不释放的话,当内存达到一定的特定值时,通过垃圾回收器(GC)来回收。
存储方面:栈通常保存着我们代码执行的步骤,如方法变量等等。而堆上存放的则多是对象,数据等。我们可以把栈想象成一个接着一个叠放在一起的盒子(越高内存地址越低)。当我们使用的时候,每次从最顶部取走一个盒子,当一个方法(或类型)被调用完成的时候,就从栈顶取走接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
缓存方面:栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
存储方面:栈(Stack)是一种先进后出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为引用类型的实例(对象),分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)——也就是栈上的变量指向了堆上地址为 XXX 的实例(对象)。
在类的构造函数前加上 static 会报什么错?为什么? 构造函数格式为 public + 类名如果加上 static 会报错(静态构造函数不能有访问、型的对象,静态构造函数只执行一次); 运行库创建类实例或者首次访问静态成员之前,运行库调用静态构造函数; 静态构造函数执行先于任何实例级别的构造函数; 显然也就无法使用 this 和 base 来调用构造函数。 一个类只能有一个静态函数,如果有静态变量,系统也会自动生成静态函数。
如果是处理字符串的话,用 string 中的方法每次都需要创建一个新的字符串对象并且分配新的内存地址,而 stringBuilder 是在原来的内存里对字符串进行修改,所以在字符串处理方面还是建议用 stringBuilder 这样比较节约内存。但是 string 类的方法和功能仍然还是 stringBuilder 类要强。
string 类由于具有不可变性(即对一个 string 对象进行任何更改时,其实都是创建另外一个 string 类的对象),所以当需要频繁的对一个 string 类对象进行更改的时候,建议使用 StringBuilder 类,StringBuilder 类的原理是首先在内存中开辟一定大小的内存空间,当对此 StringBuilder 类对象进行更改时, 如果内存空间大小不够, 会对此内存空间进行扩充,而不是重新创建一个对象,这样如果对一个字符串对象进行频繁操作的时候,不会造成过多的内存浪费,其实本质上并没有很大区别,都是用来存储和操作字符串的,唯一的区别就在于性能上。
string 主要用于公共 API,通用性好、用途广泛、读取性能高、占用内存小。
StringBuilder 主要用于拼接 string,修改性能好。
不过现在的编译器已经把 string 的 + 操作优化成 StringBuilder 了, 所以一般用 string 就可以了
string 是不可变的,所以天然线程同步。
StringBuilder 可变,非线程同步。
(a, b) => {};
每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。
List,HashTable,Dictionary,Stack,Queue
Stack 栈:先进后出,入栈和出栈,底层泛型数组实现,入栈动态扩容 2 倍
Queue 队列:先进先出,入队和出队,底层泛型数组实现,表头表尾指针,判空还是满通过 size 比较 Queue 和 Stack 主要是用来存储临时信息的
Array 数组:需要声明长度,不安全
ArrayList 数组列表:动态增加数组,不安全,实现了 IList 接口(表示可按照索引进行访问的非泛型集合对象),Object 数组实现
List 列表:底层实现是泛型数组,特性,动态扩容,泛型安全 将泛型数据(对值类型来说就是数据本身,对引用类型来说就是引用)存储在一个泛型数组中,添加元素时若超过当前泛型数组容量,则以 2 倍扩容,进而实现 List 大小动态可变。(注:大小指容量,不是 Count)
LinkList 链表:
HashTable 哈希表(散列表)
概念:不定长的二进制数据通过哈希函数映射到一个较短的二进制数据集,即 Key 通过 HashFunction 函数获得 HashCode
装填因子:α=n/m=0.72,存储的数据 N 和空间大小 M
然后通过哈希桶算法,HashCode 分段,每一段都是一个桶结构,一般是 HashCode 直接取余。
桶结构会加剧冲突,解决冲突使用拉链法,将产生冲突的元素建立一个单链表,并将头指针地址存储至 Hash 表对应桶的位置。这样定位到 Hash 表桶的位置后可通过遍历单链表的形式来查找元素。
// 哈希表结构体
private struct bucket {
public Object key; // 键
public Object val; // 值
public int hash_col; // 哈希码
}
// 字典结构体
private struct Entry {
public int hashCode; // 除符号位以外的 31 位 hashCode 值, 如果该Entry 没有被使用,那么为 -1
public int next; // 下一个元素的下标索引,如果没有下一个就为 -1
public TKey key; // 存放元素的键
public TValue value; // 存放元素的值
}
private int[] buckets; // Hash 桶
private Entry[] entries; // Entry 数组,存放元素
private int count; // 当前 entries 的 index 位置
private int version; // 当前版本,防止迭代过程中集合被更改
private int freeList; // 被删除 Entry 在 entries 中的下标 index,这个位置是空闲的
private int freeCount; // 有多少个被删除的 Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; // 比较器
private KeyCollection keys; // 存放 Key 的集合
private ValueCollection values; // 存放 Value 的集合
性能排序:
不带泛型的容器需要装箱和拆箱操作速度慢所以泛型容器效率更高数据类型更安全
简单值类型:包括 整数类型、实数类型、字符类型、布尔类型
复合值类型:包括 结构类型、枚举类型
多个代码对【不同数据类型】执行【相同指令】的情况
泛型:多个类型共享一组代码
泛型允许类型参数化,泛型类型是类型的模板
5 种泛型:类、结构、接口、委托、方法
类型占位符 T 来表示泛型
泛型类不是实际的类,而是类的模板
从泛型类型创建实例
声明泛型类型 → 通过提供【真实类型】创建构造函数类型 → 从构造类型创建实例 类 <T1,T2> 泛型类型参数
性能:泛型不会强行对值类型进行装箱和拆箱,或对引用类型进行向下强制类型转换,所以性能得到提高。
安全:通过知道使用泛型定义的变量的类型限制,编译器可以在一定程度上验证类型假设,所以泛型提高了程序的类型安全。
非托管代码才需要这个关键字一般用在带指针操作的场合。项目背包系统的任务装备栏使用到。
ref 修饰引用参数。参数必须赋值,在内部可改可不改,带回返回值,又进又出 out 修饰输出参数。参数可以不赋值,在内部必须修改该值,带回返回值之前必须明确赋值。
引用参数和输出参数不会创建新的存储位置
如果 ref 参数是值类型,原先的值类型数据,会随着方法里的数据改变而改变,
如果 ref 参数值引用类型,方法里重新赋值后,原对象堆中数据会改变,如果对引用类型再次创建新对象并赋值给 ref 参数,引用地址会重新指向新对象堆数据。方法结束后形参和新对象都会消失。实参还是指向原始对象,值不够数据改变了。
for 循环可以通过索引依次进行遍历,foreach 和 Enumerator.MoveNext 通过迭代的方式进行遍历。
内存消耗上本质上并没有太大的区别。
但是在 Unity 中的 Update 中,一般不推荐使用 foreach 因为会遗留内存垃圾。
如果集合需要 foreach 遍历,是否可行,存在一定问题。
foreach 中的迭代变量 item 是的只读,不能对其进行修改,比如 list.Remove(item) 操作。
foreach 只读的时候记录下来,在对记录做操作,或者直接用 for 循环遍历。
foreach 对 int[] 数组循环已经不产生 GC,避免对 ArrayList 进行遍历。
for 语句中初始化变量 i 的作用域,循环体内部可见。
通过索引进行遍历,可以根据索引对所遍历集合进行修改。
unity 中 for 循环使用 lambda 表达式注意闭包问题。
foreach 遍历原理:
任何集合类(Array)对象都有一个 GetEnumerator() 方法,该方法可以返回一个实现了 迭代器 IEnumerator 接口的对象。 这个返回的 IEnumerator 对象既不是集合类对象,也不是集合的元素类对象,它是一个独立的类对象。
通过这个实现了 IEnumerator 接口对象 A,可以遍历访问集合类对象中的每一个元素对象。
对象 A 访问 MoveNext 方法,方法为真,就可以访问 Current 方法,读取到集合的元素。
List<string> list = new List<string>() { "25", "哈3", "26", "花朵" };
IEnumerator listEnumerator = list.GetEnumerator();
while (listEnumerator.MoveNext())
{
Console.WriteLine(listEnumerator.Current);
}
foreach 不能进行元素的删除,因为迭代器会锁定迭代的集合,解决方法:记录找到索引或者 key 值,迭代结束后再进行删除。
Just-In-Time - 实时编译
执行慢安装快占空间小一点
Ahead-Of-Time - 预先编译
执行快安装慢占内存占外存大
void SortArray(Array arr) {
Array.Sort(arr);
}
设计单例模式进行创建对象或者使用对象池。
委托类似于一种安全的指针引用,在使用它时是 当做类来看待而不是一个方法,相当于对一组方法的列表的引用,可以便捷的使用委托对这个方法集合进行操作。委托是对函数指针的封装。
用处:使用委托使程序员可以将方法引用封装在 委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调 用哪个方法。与 C 或 C++ 中的函数指针不同,委托 是面向对象,而且是类型安全的。
大致来说,委托是一个类,该类内部维护着一个字段,指向一个方法。事件可以被看作一个委托类型的变量,通过事件注册、取消多个委托或方法。
接口(interface) 是约束类应该具备的功能集合,约束了类应该具备的功能,使类从千变万化的具体逻辑中解脱出来,便于类的管理和扩展,同时又合理解决了类的单继承问题。
C# 中的委托是约束方法集合的一个类,可以便捷的使用委托对这个方法集合进行操作。
在以下情况中使用接口:
以上等等
在以下情况中使用委托:多用于事件处理中
概念
抽象类
接口
当注重代码的扩展性跟可维护性时,应当优先采用接口。
接口与实现它的类之间可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活;
接口只关心对象之间的交互的方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多。
区别
使用情形
通过 StringBuilder 进行 append,这样可以减少内存垃圾。
简单的说:C# 与 C++ 比较的话,最重要的特性就是 C# 是一种完全面向对象的语言,而 C++ 不是,另外 C# 是基于 IL 中间语言和 .NET Framework CLR 的,在可移植性,可维护性和强壮性都比 C++ 有很大的改进。C# 的设计目标是用来开发快速稳定可扩展的应用程序,当然也可以通过 Interop 和 Pinvoke 完成一些底层操作。
具体对比:
C# 不支持指针,但可以使用 Unsafe,不安全模式,CLR 不检测
C# 可以定义指针的类型、整数型、实数型、struct 结构体
C# 指针操作符、C# 指针定义
使用 fixed,可以操作类中的值类型
相同点:都是地址
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
指针是个实体,引用是个别名。
sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
引用是类型安全的,而指针在不安全模式下
引用不能为空,即不存在对空对象的引用,指针可以为空,指向空对象。
引用必须初始化,指定对哪个对象的引用,指针不需要。
引用初始化后不能改变,指针可以改变所指对象的值。
引用访问对象是直接访问,指针访问对象是间接访问。
引用的大小是所引用对象的大小,指针的大小,是指针本身大小,通常是 4 字节。
引用没有 const,指针有 const
引用和指针的 + 自增运算符意义不同。
引用不需要分配内存空间,指针需要。
可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息反射即在运行期动态获取类、对象、方法、对象数据等的一种重要手段。
主要使用的类库:System.Reflection
核心类:
通过以上核心类可在运行时动态获取程序集中的类,并执行类构造产生类对象,动态获取对象的字段或属性值,更可以动态执行类方法和实例方法等。
审查元数据并收集关于它的类型信息的能力。
实现步骤:
using System.Reflection;
Assembly.Load("程序集") // 加载程序集,返回类型是⼀个 Assembly
foreach (Type type in assembly.GetTypes())
{
string t = type.Name;
}
// 得到程序集中所有类的名称
Type type = assembly.GetType("程序集.类名"); // 获取当前类的类型
Activator.CreateInstance(type); // 创建此类型实例
MethodInfo mInfo = type.GetMethod("⽅法名"); // 获取当前⽅法
mInfo.Invoke(null, 方法参数);
| 类型 | 字节 |
|---|---|
| bool | true/false |
| byte, char | 1 字节 |
| char, short | 2 字节 |
| int, float | 4 字节 |
| long, double | 8 字节 |
Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。 使用动态字体时,Unity 将不会预先生成⼀个与所有字体的字符纹理。 当需要支持亚洲语言或者较大的字体的时候,若使用正常纹理,则字体的纹理将非常大。
String 是字符串常量。StringBuilder 是字符串变量,线程不安全。
String 类型是个不可变的对象,当每次对 String 进行改变时都需要⽣成⼀个新的 String 对象,然后将指针指向⼀个新的对象,如果在⼀个循环里面,不断的改变⼀个对象,就要不断的生成新的对象,所以效率很低,建议在不断更改 String 对象的地方不要使用 String 类型。
StringBuilde r对象在做字符串连接操作时是在原来的字符串上进行修改,改善了性能。这⼀点我们平时使用中也许都知道,连接操作频繁的时候,使用 StringBuilder 对象。
string 不变性,字符序列不可变,对原管理中实例对象赋值,会重新开一个新的实例对象赋值,新开的实例对象会等待被 GC。 string 拼接要重新开辟空间,因为 string 原值不会改变,导致 GC 频繁,性能消耗大。
StringBuilder 是字符串可变对象。 StringBuilder 是非线程安全,所以性能略好,一般用于单线程。
性能比较 StringBuilder > string
泛型集合命名空间 using System.Collections.Generic; 任何键都必须是唯一
该类最大的优点就是它查找元素的时间复杂度接近 O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。
实现原理
资源:实现了 IDisposable 接口的类或结构。
using 语句确保这些资源能够被适当的释放(Resource.Dispose)
using 原理:using(分配资源){ 使用资源 } → 释放资源 (隐式)
使用资源(可能会导致异常)会被放进 Try 块里,释放资源(有无异常)都会放进在 finally 块
using(分配资源)
{
try{ 使用资源 }
finally{ Resource.Dispose}
}
using 指令,using+ 命名空间(或命名空间一个类型) 在源文件的顶端声明 调用成员方法时也可以不使用using,直接命名空间.类.成员方法
IEnumerable;GetEnumerator
List 和 Dictionary 类型可以用 foreach 遍历,他们都实现了 IEnumerable 接口,申明了 GetEnumerator 方法。
里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之⼀。
当删除遍历节点后面的节点时,会导致 List.Count 进行变化,删除元素后,当根据 i++,遍历到删除的节点会发生异常。
处理方法:可以从后往前遍历元素进行操作,即删除在访问的前面。
代理就是用来定义指向方法的引用。
C# 事件本质就是对消息的封装,用作对象之间的通信;发送方叫事件发送器,接收方叫事件接收器;
rPoint1 = new RefPoint(1);
类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。
List<int> ls = new List<int>(new int[]{ 1, 2, 3, 4, 5 });
foreach (int item in ls)
{
Console.WriteLine(item * item);
ls.Remove(item);
}
会产生运行时错误,因为 foreach 是只读的。不能一边遍历一边修改。
使用 For 循环遍历可以解决。
C# 装箱是将值类型转换为引用类型; 拆箱是将引用类型转换为值类型。 牵扯到装箱和拆箱操作比较多的就是在集合中,例如:ArrayList 或者 HashTable 之类。
值类型和引用类型互相转换:拆箱和装箱 装箱:值类型 → 引用类型 object
拆箱:引用类型 object → 值类型
MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范。
用一种业务逻辑、数据、界面显示分离的方法,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC 被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。
通常模型对象负责在数据库中存取数据。
View(视图)是应用程序中处理数据显示的部分。
通常视图是依据模型数据创建的。
Controller(控制器)是应用程序中处理用户交互的部分。
通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
托管代码: 在公共语言运行时(CLR)控制下运行的代码。
非托管代码: 不在公共语言运行时(CLR)控制下运行的代码。
不安全(Unsafe)代码:不安全代码可以被认为是介于托管代码和非托管代码之间的。不安全代码仍然在公共语言运行时(CLR)控制下运行,但它将允许您直接通过指针访问内存。
相同点:
不同点:
常见栈的应用场景包括
常见的队列的应用场景包括
单双向链表的区别:
单向链表优缺点:
优点:单向链表增加删除节点简单。遍历时候不会死循环;
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。
双向链表优缺点:
如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反,如果需要经常插入和删除元素就需要用链表数据结构了。
计算深度(高度)
二叉树的高度是二叉树结点层次的最大值,也就是其左右子树的最大高度 +1。当树为空时,高度为 0;否则为其左右子树最大高度 +1。
遍历:(看根节点的位置)
介绍
增删查改时间复杂度
底层实现原理
Dictionary 在构造的时候做了以下几件事:
Bucket 和 entries 的容量都为大于字典容量的一个最小的质数。其中 this.buckets 主要用来进行 Hash 碰撞,this.entries 用来存储字典的内容,并且标识下一个元素的位置。
详细过程
**字典:**内部用了 Hashtable 作为存储结构
哈希表:
List 的底层,是一个泛型数组,连续且紧密的顺序存储,一般数据存储在缓存中。而字典是离散(散列)分布,由数组和哈希表共同组成,遍历的时候,会伴有换页的操作,且数组都存储在内存中。而读写速度是:缓存 > 内存 > 硬盘。因此 List 更适合遍历。
字典的查询效率是通过元素的 key 值进行取余操作,找的对应的哈希桶,判定哈希桶对应的哈希表的头节点是不是该元素,若不是进行 next 操作,对哈希表进行遍历,这两个过程都是常数级别的操作。所以是 O(1)。而 List 的查询效率是先遍历,找到对应的值,因此是 O(n)。所以字典更适合查询。