title: Plan-2 date: 2024-08-08 11:52:00 tags:


私企八股文

mindmap
Unity
	算法
	优化部分
	渲染 & Shader
	网络
	Lua 语言和 Xlua 热更新
	数据持久化 & 资源管理
	C# 基础
	Unity 基础
	物理系统
	动画系统
	UI & 2D
	协程

❤️C# 基础

1. 面向对象的三大特点

  1. 继承: 提高代码重用度,增强软件可维护性的重要手段,符合开闭原则。继承最主要的作用就是把子类的公共属性集合起来,便与共同管理,使用起来也更加方便。你既然使用了继承,那代表着你认同子类都有一些共同的特性,所以你把这些共同的特性提取出来设置为父类。继承的传递性:传递机制 a→b; b→c; c 具有 a 的特性。继承的单根性:在 C# 中一个类只能继承一个类,不能有多个父类。
  2. 封装: 封装是将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据的安全性,属性是 C# 封装实现的最好体现。就是将一些复杂的逻辑经过包装之后给别人使用就很方便,别人不需要了解里面是如何实现的,只要传入所需要的参数就可以得到想要的结果。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
  3. 多态性: 多态性是指同名的方法在不同环境下,自适应的反应出不同得表现,是方法动态展示的重要手段。多态就是一个对象多种状态,子类对象可以赋值给父类型的变量。

2. 简述值类型和引用类型

介绍

区别

  1. 值类型存储在内存栈中,引用类型数据存储在内存堆中,而内存单元中存放的是堆中存放的地址。
  2. 值类型存取快,引用类型存取慢。
  3. 值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。
  4. 栈的内存是自动释放的,堆内存是 .NET 中会由 GC 来自动释放。
  5. 值类型继承自 System.ValueType,引用类型继承自 System.Object。
  6. 值类型在栈中存储的是直接的值,引用类型数据本身实在堆中,栈中存放的是一个引用的地址。

3. 重载和重写的区别

  1. 封装、继承、多态所处位置不同,重载在同类中,重写在父子类中。
  2. 定义方式不同,重载方法名相同参数列表不同,重写方法名和参数列表都相同。
  3. 调用方式不同,重载使用相同对象以不同参数调用,重写用不同对象以相同参数调用。
  4. 多态时机不同,重载时编译时多态,重写是运行时多态。

4. Net 与 Mono 的关系?

​ .Net 是一个语言平台,Mono 为 .Net 提供集成开发环境,集成并实现了.NET 的编译器、CLR 和基础类库,使得 .Net 既可以运行在 windows 也可以运行于 linux,Unix,Mac OS 等。

5. C# 中所有引用类型的基类是什么?

​ 引用类型的基类是 System.Object 值类型的基类是 System.ValueType。

​ 同时,值类型也隐式继承自 System.Object。

6. 请简述 ArrayList 和 List 的主要区别?

​ ArrayList 存在不安全类型(ArrayList 会把所有插 ⼊其中的数据都当做 Object 来处理)装箱拆箱的 操作(费时)IList 是接口,ArrayList 是⼀个实现了 该接口的类,可以被实例化

​ List 类是 ArrayList 类的泛型等效类。它的大部分用法都与 ArrayList 相似,因为 List 类也继承了 IList 接口。最关键的区别在于,在声明 List 集合时,我们同时需要为其声明 List 集合内数据的对象类型。

7. GC 相关知识点

GC 的概念

  1. C# 内部有两个内存管理池:堆内存和栈内存。栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。C# 中的变量只会在堆栈或者堆内存上进行内存分配,变量要么存储在栈内存上,要么处于堆内存上。
  2. 只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。
  3. 一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。不再使用的内存只会在GC 的时候才会被回收。
  4. 垃圾回收主要是指堆上的内存分配和回收,C# 中会定时对堆内存进行 GC 操作。

GC 会带来的问题

  1. 游戏性能:GC 操作是一个极其耗费事件的操作,堆内存上的变量或者引用越多则导致遍历检查时的操作变得十分缓慢,使得游戏运行缓慢,例如当 CPU 处于游戏性能的关键时刻,任何一个操作就会导致游戏帧率下降,造成极大的影响。

  2. 游戏内存:(UnityGC 采用的是非分代非压缩的标记清除算法)GC 操作会产生“内存碎片化”。当一个单元内存从堆中分配出来,其大小取决于存储变量的大小。当内存被回收到堆上时,有可能被堆内存分割成碎片化的单元。(就是说总容量大小时固定的,但是单元内存较小。例如房子很大,房间很小,找不到合适的房间)即下次分配时找不到合适的储存单元,就会触发 GC 操作,或者堆内存扩容操作,导致 GC 频发发生和游戏内存越来越大。

GC 触发时机

  1. 在堆内存上进行内存分配操作,而内存不够的时候都会触发垃圾回收来利用闲置的内存;
  2. GC 会自动的触发,不同平台运行频率不—样;
  3. GC 可以被强制执行。

如何避免 GC?

  1. 减少临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外,用到容器的时候进行修改即可)
  2. 减少 new 对象的次数。
  3. 对于大量字符串拼接时,将 StringBuilder 代替 String。(string 不可修改性,修改即创建一个新的 string 对象,旧的直接抛弃等待 GC,但少量字符串拼接用 string,性能优于 stringbuilder)
  4. 使用扩容的容器时,例如:List,StringBuilder 等,定义时尽量根据存储变量的内存大小定义储存空间,减少扩容的操作。(扩容后,旧的容器直接抛弃等待 GC)
  5. 代码逻辑优化:例如计时器当大于 1s 后才进行文本修改,而不是每帧都修改,或者禁止在关键时候 GC,影响游戏性能,可以在加载页面或者进度条的时候 GC。
  6. 利用对象池:对象池是一种 Unity 经常用到的内存管理服务,针对经常消失生成的对象,例如子弹,怪物等,作用在于减少创建每个对象的系统开销。在我们想要对象消除时,不直接 Destory,而是隐藏起来 SetActive(false),放入池子中,当需要再次显示一个新的对象时,先去池子中看有没有隐藏对象,有就取出来(显示) SetActive(true),没有的话,再实例化。
  7. 减少装箱拆箱( 装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的过程)的操作
  8. 协程:yeild return 0 会产生装箱拆箱,可以替换为 yeild return null。

8. 结构体和类

结构体和类的区别

  1. 结构体是值类型,类是引用类型。
  2. 结构体存在栈中,类存在堆中。
  3. 结构体变量和类对象进行类型传递时,结构体变量进行的就是值传递,而类对象进行的是引用传递,或者说传递的是指针,这样在函数中改变参数值,结构体对象的值是不变的,而类对象的值是变化了。
  4. 在 C# 中结构体类型定义时,成员是不能初始化的,这样就导致了,定义结构体变量时,变量的所有成员都要自己赋值初始化。但对于类,在定义类时,就可以初始化其中的成员变量,所以在定义对象时,对象本身就已经有了初始值,你可以自己在重新给个别变量赋值。(注意在 C++ 中,类的定义中是不能初始化的,初始化要放在构造函数中)
  5. 结构体不能申明无参的构造函数,而类可以。
  6. 声明了结构类型后,可以使用 new 运算符创建构造对象,也可以不使用 new 关键字。如果不使用 new,那么在初始化所有字段之前,字段将保持未赋值状态且对象不可用。
  7. 结构体申明有参构造函数后,无参构造不会被顶掉。
  8. 结构体不能申明析构函数,而类可以。
  9. 结构体不能被继承,而类可以。
  10. 结构体需要在构造函数中初始化所有成员变量,而类随意。
  11. 结构体不能被静态 static 修饰(不存在静态结构体),而类可以。

使用情景

结构体:

  1. 结构体是值类型在栈中,栈的存取速度比堆快,但是容量小,适合轻量级的对象,比如点、矩形、颜色。
  2. 如果对象是数据集合时,优先考虑接结构体(位置,坐标)
  3. 在变量传值的时候,希望传递对象的是拷贝,而不是对象的引用地址,这个时候就可以使用结构体。

类:

  1. 类是引用类型,存储在堆中,堆的容量大,适合重量级的对象,栈的空间不大,大量的对应当存在于堆中。
  2. 如果对象需要继承和多态特征,用类(玩家、怪物)。

9. C# 中四种访问修饰符是哪些?各有什么区别?

10. 修饰符 简述 private,public,protected,internal 的区别

11. 堆和栈的区别?

​ 栈通常保存着我们代码执行的步骤。 而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。 ​ 栈也是如此,当一个方法(或类型)被调 用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的 是他们被调用完毕不会立即被清理掉。

  1. GC 方面:栈保持着先进后出的原则,是一片连续的内存域,有系统自动分配和维护,产生的垃圾系统自动释放。而堆是无序的,他是一片不连续的内存域,用户自己来控制和释放,如果用户自己不释放的话,当内存达到一定的特定值时,通过垃圾回收器(GC)来回收。

  2. 存储方面:栈通常保存着我们代码执行的步骤,如方法变量等等。而堆上存放的则多是对象,数据等。我们可以把栈想象成一个接着一个叠放在一起的盒子(越高内存地址越低)。当我们使用的时候,每次从最顶部取走一个盒子,当一个方法(或类型)被调用完成的时候,就从栈顶取走接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。

  3. 缓存方面:栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

  4. 存储方面:栈(Stack)是一种先进后出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为引用类型的实例(对象),分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)——也就是栈上的变量指向了堆上地址为 XXX 的实例(对象)。

12. 静态构造函数

  1. 静态构造函数既没有访问修饰符,也没有参数。
  2. 在创建第一个类实例或任何静态成员被引用时,.NET 将自动调用静态构造函数来初始化类。
  3. 一个类只能有一个静态构造函数。
  4. 无参数的构造函数可以与静态构造函数共存。
  5. 最多只运行一次。
  6. 静态构造函数不可以被继承。
  7. 如果没有写静态构造函数,而类中包含带有初始值设定的静态成员,那么编译器会自动生成默认的静态构造函数。
  8. 如果静态构造函数引发异常,运行时将不会再次调用该构造函数,并且在程序运行所在的应用程序域的生存期内,类型将保持未初始化。

在类的构造函数前加上 static 会报什么错?为什么? 构造函数格式为 public + 类名如果加上 static 会报错(静态构造函数不能有访问、型的对象,静态构造函数只执行一次); 运行库创建类实例或者首次访问静态成员之前,运行库调用静态构造函数; 静态构造函数执行先于任何实例级别的构造函数; 显然也就无法使用 this 和 base 来调用构造函数。 一个类只能有一个静态函数,如果有静态变量,系统也会自动生成静态函数。

13. C# string 类型比 stringBuilder 类型的优势是什么?

​ 如果是处理字符串的话,用 string 中的方法每次都需要创建一个新的字符串对象并且分配新的内存地址,而 stringBuilder 是在原来的内存里对字符串进行修改,所以在字符串处理方面还是建议用 stringBuilder 这样比较节约内存。但是 string 类的方法和功能仍然还是 stringBuilder 类要强。

​ string 类由于具有不可变性(即对一个 string 对象进行任何更改时,其实都是创建另外一个 string 类的对象),所以当需要频繁的对一个 string 类对象进行更改的时候,建议使用 StringBuilder 类,StringBuilder 类的原理是首先在内存中开辟一定大小的内存空间,当对此 StringBuilder 类对象进行更改时, 如果内存空间大小不够, 会对此内存空间进行扩充,而不是重新创建一个对象,这样如果对一个字符串对象进行频繁操作的时候,不会造成过多的内存浪费,其实本质上并没有很大区别,都是用来存储和操作字符串的,唯一的区别就在于性能上。

​ string 主要用于公共 API,通用性好、用途广泛、读取性能高、占用内存小。

​ StringBuilder 主要用于拼接 string,修改性能好。

​ 不过现在的编译器已经把 string 的 + 操作优化成 StringBuilder 了, 所以一般用 string 就可以了

​ string 是不可变的,所以天然线程同步。

​ StringBuilder 可变,非线程同步。

14. C# 函数 Func(string a, string b) 用 Lambda 表达式怎么写?

(a, b) => {};

15. 虚函数实现原理

​ 每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。

16. 指针和引用的区别?

  1. 引用不能为空,即不存在对空对象的引用,指针可以为空,指向空对象。
  2. 引用必须初始化,指定对哪个对象的引用,指针不需要。
  3. 引用初始化后不能改变,指针可以改变所指对象的值。
  4. 引用访问对象是直接访问,指针访问对象是间接访问。
  5. 引用的大小是所引用对象的大小,指针的大小,是指针本身大小,通常是 4 字节。
  6. 引用没有 const,指针有 const。
  7. 引用和指针的 + 自增运算符意义不同。
  8. 引用不需要分配内存空间,指针需要。

17. C# 中有哪些常用的容器类,各有什么特点?

List,HashTable,Dictionary,Stack,Queue

// 哈希表结构体
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 的集合

性能排序:

18. C# 中常规容器和泛型容器有什么区别,哪种效率高?

​ 不带泛型的容器需要装箱和拆箱操作速度慢所以泛型容器效率更高数据类型更安全

19. 有哪些常见的数值类?

​ 简单值类型:包括 整数类型、实数类型、字符类型、布尔类型

​ 复合值类型:包括 结构类型、枚举类型

20. 泛型是什么

​ 多个代码对【不同数据类型】执行【相同指令】的情况

​ 泛型:多个类型共享一组代码

​ 泛型允许类型参数化,泛型类型是类型的模板

​ 5 种泛型:类、结构、接口、委托、方法

​ 类型占位符 T 来表示泛型

​ 泛型类不是实际的类,而是类的模板

​ 从泛型类型创建实例

​ 声明泛型类型 → 通过提供【真实类型】创建构造函数类型 → 从构造类型创建实例 类 <T1,T2> 泛型类型参数

性能:泛型不会强行对值类型进行装箱和拆箱,或对引用类型进行向下强制类型转换,所以性能得到提高。

安全:通过知道使用泛型定义的变量的类型限制,编译器可以在一定程度上验证类型假设,所以泛型提高了程序的类型安全。

21. C# 中 unsafe 关键字是用来做什么的?什么场合下使用?

​ 非托管代码才需要这个关键字一般用在带指针操作的场合。项目背包系统的任务装备栏使用到。

22. C# 中 ref 和 out 关键字有什么区别?

ref 修饰引用参数。参数必须赋值,在内部可改可不改,带回返回值,又进又出 ​ out 修饰输出参数。参数可以不赋值,在内部必须修改该值,带回返回值之前必须明确赋值。

​ 引用参数和输出参数不会创建新的存储位置

​ 如果 ref 参数是值类型,原先的值类型数据,会随着方法里的数据改变而改变,

​ 如果 ref 参数值引用类型,方法里重新赋值后,原对象堆中数据会改变,如果对引用类型再次创建新对象并赋值给 ref 参数,引用地址会重新指向新对象堆数据。方法结束后形参和新对象都会消失。实参还是指向原始对象,值不够数据改变了。

23. For,foreach,Enumerator.MoveNext 的使用,与内存消耗情况

​ for 循环可以通过索引依次进行遍历,foreach 和 Enumerator.MoveNext 通过迭代的方式进行遍历。

​ 内存消耗上本质上并没有太大的区别。

​ 但是在 Unity 中的 Update 中,一般不推荐使用 foreach 因为会遗留内存垃圾。

24. foreach 迭代器遍历和 for 循环遍历的区别

​ 如果集合需要 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);
}

25. Foreach 循环迭代时,若把其中的某个元素删除,程序报错,怎么找到那个元素?以及具体怎么处理这种情况?(注:Try…Catch捕捉异常,发送信息不可行)

​ foreach 不能进行元素的删除,因为迭代器会锁定迭代的集合,解决方法:记录找到索引或者 key 值,迭代结束后再进行删除。

26. JIT 和 AOT 区别

27. 给定一个存放参数的数组,重新排列数组

void SortArray(Array arr) {
    Array.Sort(arr);
}

28. 当需要频繁创建使用某个对象时,有什么好的程序设计方案来节省内存?

​ 设计单例模式进行创建对象或者使用对象池。

29. C# 的委托是什么?有何用处?

​ 委托类似于一种安全的指针引用,在使用它时是 当做类来看待而不是一个方法,相当于对一组方法的列表的引用,可以便捷的使用委托对这个方法集合进行操作。委托是对函数指针的封装。

​ 用处:使用委托使程序员可以将方法引用封装在 委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调 用哪个方法。与 C 或 C++ 中的函数指针不同,委托 是面向对象,而且是类型安全的。

30. C# 中委托和事件的区别

​ 大致来说,委托是一个类,该类内部维护着一个字段,指向一个方法。事件可以被看作一个委托类型的变量,通过事件注册、取消多个委托或方法。

31. C# 中委托和接口有什么区别?各用在什么场合?

接口(interface) 是约束类应该具备的功能集合,约束了类应该具备的功能,使类从千变万化的具体逻辑中解脱出来,便于类的管理和扩展,同时又合理解决了类的单继承问题。

C# 中的委托是约束方法集合的一个类,可以便捷的使用委托对这个方法集合进行操作。

​ 在以下情况中使用接口:

  1. 无法使用继承的场合
  2. 完全抽象的场合
  3. 多人协作的场合

​ 以上等等

​ 在以下情况中使用委托:多用于事件处理中

32. 接口 Interface 与抽象类

概念

33. 函数中多次使用 string 的 += 处理,会产生大量内存垃圾(垃圾碎片),有什么好的方法可以解决。

​ 通过 StringBuilder 进行 append,这样可以减少内存垃圾。

34. C# 和 C++ 的区别?

​ 简单的说:C# 与 C++ 比较的话,最重要的特性就是 C# 是一种完全面向对象的语言,而 C++ 不是,另外 C# 是基于 IL 中间语言和 .NET Framework CLR 的,在可移植性,可维护性和强壮性都比 C++ 有很大的改进。C# 的设计目标是用来开发快速稳定可扩展的应用程序,当然也可以通过 Interop 和 Pinvoke 完成一些底层操作。

具体对比:

35. C# 引用和 C++ 指针的区别

36. 反射的实现原理?

​ 可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息反射即在运行期动态获取类、对象、方法、对象数据等的一种重要手段。

​ 主要使用的类库: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, 方法参数);

37. C# 中基本类型占用的字节数

类型 字节
bool true/false
byte, char 1 字节
char, short 2 字节
int, float 4 字节
long, double 8 字节

38. Mock 和 Stub 有何区别?

39. 为什么 dynamic font 在 unicode 环境下优于 staticfont(字符串编码)

​ Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。 使用动态字体时,Unity 将不会预先生成⼀个与所有字体的字符纹理。 ​ 当需要支持亚洲语言或者较大的字体的时候,若使用正常纹理,则字体的纹理将非常大。

40. 简述 StringBuilder 和 String 的区别?(字符串处理)

​ String 是字符串常量。StringBuilder 是字符串变量,线程不安全。

​ String 类型是个不可变的对象,当每次对 String 进行改变时都需要⽣成⼀个新的 String 对象,然后将指针指向⼀个新的对象,如果在⼀个循环里面,不断的改变⼀个对象,就要不断的生成新的对象,所以效率很低,建议在不断更改 String 对象的地方不要使用 String 类型。

​ StringBuilde r对象在做字符串连接操作时是在原来的字符串上进行修改,改善了性能。这⼀点我们平时使用中也许都知道,连接操作频繁的时候,使用 StringBuilder 对象。

41. string、stringBuilder

string 不变性,字符序列不可变,对原管理中实例对象赋值,会重新开一个新的实例对象赋值,新开的实例对象会等待被 GC。 ​ string 拼接要重新开辟空间,因为 string 原值不会改变,导致 GC 频繁,性能消耗大。

StringBuilder 是字符串可变对象。 ​ StringBuilder 是非线程安全,所以性能略好,一般用于单线程。

​ 性能比较 StringBuilder > string

  1. 如果要操作少量的数据:string
  2. 单线程操作字符串缓冲区下操作大量数据:StringBuilder

42. 字典 Dictionary 的内部实现原理

​ 泛型集合命名空间 using System.Collections.Generic; ​ 任何键都必须是唯一

​ 该类最大的优点就是它查找元素的时间复杂度接近 O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。

实现原理

  1. 哈希算法:将不定长度的二进制数据集给映射到一个较短的二进制长度数据集一个 Key 通过 HashFunc 得到 HashCode
  2. Hash 桶算法:对 HashCode 进行分段显示,常用方法是对 HashCode 直接取余
  3. 解决碰撞冲突算法(拉链法):分段会导致 key 对应的桶会相同,拉链法的思想就像对冲突的元素,建立一个单链表,头指针存储到对应的哈希桶位置。反之就是通过确定 hash 桶位置后,遍历单链表,获取对应的 value

43. using 的作用

​ 资源:实现了 IDisposable 接口的类或结构。

​ using 语句确保这些资源能够被适当的释放(Resource.Dispose)

​ using 原理:using(分配资源){ 使用资源 } → 释放资源 (隐式)

​ 使用资源(可能会导致异常)会被放进 Try 块里,释放资源(有无异常)都会放进在 finally 块

using(分配资源)
{
	try{ 使用资源 }
	finally{ Resource.Dispose}
}

​ using 指令,using+ 命名空间(或命名空间一个类型) 在源文件的顶端声明 调用成员方法时也可以不使用using,直接命名空间.类.成员方法

44. Mathf.Round 和 Mathf.Clamp 和 Mathf.Lerp 含义?

45. 能用 foreach 遍历访问的对象需要实现接口或声明方法的类型(C# 遍历)

IEnumerable;GetEnumerator

​ List 和 Dictionary 类型可以用 foreach 遍历,他们都实现了 IEnumerable 接口,申明了 GetEnumerator 方法。

46. 什么是里氏替换原则?(C# 多态)

​ 里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之⼀。

47. 想要在 for 循环中删除 List(或者 vector,都行)中的元素时,有可能出现什么问题,如何避免?

​ 当删除遍历节点后面的节点时,会导致 List.Count 进行变化,删除元素后,当根据 i++,遍历到删除的节点会发生异常。

​ 处理方法:可以从后往前遍历元素进行操作,即删除在访问的前面。

48. 概述 C# 中代理和事件?

​ 代理就是用来定义指向方法的引用。

​ C# 事件本质就是对消息的封装,用作对象之间的通信;发送方叫事件发送器,接收方叫事件接收器;

49. New 的实现逻辑

rPoint1 = new RefPoint(1);
  1. 在应用程序堆上创建一个引用类型对象的实例,并为它分配内存地址。
  2. 自动传递该实例的引用给构造函数(正因如此,在构造函数中才能使用 this 来访问这个实例)。
  3. 调用该类型的构造函数。
  4. 返回该实例的引用内存地址,复制给 rPoint1 变量,该rPoint1 引用对象保存的数据是指向在堆上创建该类型的实例地址。

50. 请简述关键字 Sealed 用在类声明和函数声明时的作用

​ 类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。

51. 下列代码在运行中会发生什么问题?如何避免?

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 循环遍历可以解决。

52. 什么是装箱拆箱,怎样减少操作

​ C# 装箱是将值类型转换为引用类型; ​ 拆箱是将引用类型转换为值类型。 ​ 牵扯到装箱和拆箱操作比较多的就是在集合中,例如:ArrayList 或者 HashTable 之类。

​ 值类型和引用类型互相转换:拆箱和装箱 ​ 装箱:值类型 → 引用类型 object

  1. 分配内存堆
  2. 值类型数据拷贝到新的内存堆中
  3. 栈中分配一个新的引用地址指向内存堆

拆箱:引用类型 object → 值类型

  1. 检查确保对象是给定值类型的一个装箱值
  2. 将该值数据复制到栈中的值类型

53. MVC

​ MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范。

​ 用一种业务逻辑、数据、界面显示分离的方法,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC 被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

54. 非托管代码与不安全代码

托管代码: 在公共语言运行时(CLR)控制下运行的代码。

非托管代码: 不在公共语言运行时(CLR)控制下运行的代码。

不安全(Unsafe)代码:不安全代码可以被认为是介于托管代码和非托管代码之间的。不安全代码仍然在公共语言运行时(CLR)控制下运行,但它将允许您直接通过指针访问内存。

55. Heap 与 Stack 有何区别?

  1. heap 是堆,stack 是栈。
  2. stack 的空间由操作系统自 动分配和释放,heap 的空间是手动申请和释放的, heap常用 new 关键字来分配。
  3. stack 空间有限,heap 的空间是很大的自由区。

56. 栈溢出一般是由什么原因导致

  1. 无限递归。函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果递归调用太深,就会造成栈溢出,这时递归无法返回。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。
  2. 无限循环。
  3. 大量局部变量分配。

57. Stack 栈和 Queue 队列

相同点:

  1. 都是线性结构。
  2. 插入操作都是限定在表尾进行。
  3. 都可以通过顺序结构和链式结构实现。
  4. 插入与删除的时间复杂度都是 O(1),在空间复杂度上两者也一样。
  5. 多链栈和多链队列的管理模式可以相同。
  6. 底层都是由泛型数组实现。

不同点:

  1. 栈先进后出,队列先进先出:删除数据元素的位置不同,栈的删除操作在表尾进行,队列的删除操作在表头进行。
  2. 顺序栈能够实现多栈空间共享,而顺序队列不能。
  3. 应用场景不同

常见栈的应用场景包括

  1. 括号问题的求解,
  2. 深度优先搜索遍历等;
  3. 函数调用和递归实现,
  4. 表达式的转换和求值

常见的队列的应用场景包括

  1. 计算机系统中各种资源的管理,
  2. 消息缓冲器的管理
  3. 广度优先搜索遍历等

58. 链表相关

单双向链表的区别:

单向链表优缺点:

双向链表优缺点:

59. 链表与数组的对比

  1. 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取,时间复杂度 O(1)。
  2. 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据 next 指针找到下一个元素。

​ 如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反,如果需要经常插入和删除元素就需要用链表数据结构了。

60. 二叉树相关

计算深度(高度)

​ 二叉树的高度是二叉树结点层次的最大值,也就是其左右子树的最大高度 +1。当树为空时,高度为 0;否则为其左右子树最大高度 +1。

遍历:(看根节点的位置)

61. 字典相关

介绍

  1. Dictionary 表示键和值的集合。
  2. Dictionary<object, object> 是一个泛型。
  3. 本身有集合的功能有时候可以把它看成数组。
  4. 结构是这样的:Dictionary<[key], [value]>。
  5. 特点是存入对象是需要与 [key] 值一一对应的存入该泛型,任何键都是唯一。
  6. 通过某一个一定的 [key] 去找到对应的值。查找元素的时间复杂度为 O(1)。

增删查改时间复杂度

底层实现原理

​ Dictionary 在构造的时候做了以下几件事:

  1. 初始化一个桶数组 this.buckets = new int[prime]
  2. 初始化一个 this.entries = new Entry<TKey, TValue>[prime]

​ Bucket 和 entries 的容量都为大于字典容量的一个最小的质数。其中 this.buckets 主要用来进行 Hash 碰撞,this.entries 用来存储字典的内容,并且标识下一个元素的位置。

详细过程

  1. 哈希表法:将不定长的二进制数据集映射到一个较短的二进制数据集,一个 Key 通过 HashFunc 得到 HashCode。
  2. Hash 桶算法:对 HashCode 进行分段显示,常用方法对 HashCode 直接取余。
  3. 拉链法:分段则会导致 key 对应的哈希桶相同,拉链法的基本思想就像对冲突的元素,建立一个单链表,头指针存储在对应哈希桶的位置。反之就是通过 hash 桶对应后,遍历单链表,获取 value 值。

62. 哈希表与字典对比

**字典:**内部用了 Hashtable 作为存储结构

哈希表:

63. 关于 List 与字典的遍历与查询效率

​ List 的底层,是一个泛型数组,连续且紧密的顺序存储,一般数据存储在缓存中。而字典是离散(散列)分布,由数组和哈希表共同组成,遍历的时候,会伴有换页的操作,且数组都存储在内存中。而读写速度是:缓存 > 内存 > 硬盘。因此 List 更适合遍历。

​ 字典的查询效率是通过元素的 key 值进行取余操作,找的对应的哈希桶,判定哈希桶对应的哈希表的头节点是不是该元素,若不是进行 next 操作,对哈希表进行遍历,这两个过程都是常数级别的操作。所以是 O(1)。而 List 的查询效率是先遍历,找到对应的值,因此是 O(n)。所以字典更适合查询。

🧡Unity 基础

💛Unity 进阶

💚热更新与 Lua 语言

🩵Unity 渲染 & Shader 相关

💙Unity 性能优化

💜网络相关

🩷数据结构 算法相关