面试之语言篇 agile Posted on Jun 18 2023 c# > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/386306072) 一、OOP 语言 -------- **1.1 特性与原则** (1)特性 * 封装:将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息。 * 继承:让某个类型的对象获得另一个类型的对象的属性的方法。 * 多态:就是指一个类实例的相同方法在不同情形有不同表现形式。 (2)原则 * 开闭原则(Open Close Principle):对扩展开放,对修改关闭。 * 里氏代换原则(Liskov Substitution Principle):子类可以出现在基类使用的地方。 * 依赖倒转原则(Dependence Inversion Principle):依赖于接口而非具体实现。 * 接口隔离原则(Interface Segregation Principle):不要使用不依赖的接口。 * 迪米特法则(Demeter Principle):减少对象与外部的交互。限制字段、方法的作用域;两个对象的交互可通过第三方中转;减少对其他类的引用。 * 单一职责原则(Single responsibility principle):一个类只负责一项职责。 **1.2 值类型与引用类型** (1)堆栈 栈区 (Stack) 由编译器自动分配和释放,一般存放函数的参数值、局部变量的值等。 堆区 (heap)——由程序员分配及释放,若程序员不释放,程序结束后可能由 OS 回收。 **值类型与引用类型在内存中的分布** ![](https://pic3.zhimg.com/v2-98b9f3195320087b76ee0602cd113fae_r.jpg)![](https://pic3.zhimg.com/v2-2f1b25f5107c746f8708bf71a6d17a6e_r.jpg) 值类型和引用类型的引用 其实是不需要“垃圾回收器”来释放内存的,因为值类型和引用类型的引用都保存在栈中,出了作用域之后,其所占用内存会被自动释放。** 只有引用类型的引用所指向的实例对象才保存在堆(heap)中,而堆因为是一个自由的存储空间,所以它并没有向“栈” 那样有生存期(“栈"的元素弹出后就代表生存期结束,即所占用内存被释放);当然,需要注意的是:”垃圾回收机制”,只对堆内存起作用 **ref/out 传递值类型** ![](https://pic1.zhimg.com/v2-5d00d72d34bc55d570931691da6f3a40_r.jpg) ref/out 对值类型进行传递时,传递的是值类型的地址。在该地址进行数据获取与赋值时,进行了两步操作:a. 根据地址查找内存;b. 修改内存。 **1.3 装箱与拆箱** (1)装箱:值类型转换为引用类型时发生装箱。在托管堆上分配内存,用以存储装箱数据。 (2)拆箱:引用类型转换为值类型时发生拆箱。首先检查对象实例,确保能够转换为值类型,然后将实例复制到栈中。 **1.4 闭包** 闭包是有权限访问其他函数作用域内的变量的一个函数。 (1)闭包解决了什么问题? 正常的函数无法访问另一个函数作用域的变量,而闭包可以对其缓存访问。 (2)闭包是如何解决这一问题的? 函数中的变量在函数执行后,就会被清理、内存也随之回收。由于闭包是建立在一个函数内部的子函数,即使上级函数执行完,作用域也不会随之销毁,同时可以访问上级作用域中的变量的权限。 (3)存在什么问题? a. 占用上级函数内存 b. 外部变量赋值需要额外注意 **1.5 托管代码与 GC** 公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。CLR 的核心功能:垃圾回收、内存安全和类型安全、支持语言特性等等。 (1)垃圾回收 垃圾回收(GC)的意思是 “内存自动回收”。运行时会自动跟踪在 GC 堆内存上的所有内存引用,并且他会不时地遍历这些引用,判断这些内存是不是还会被程序所使用。所有不再被使用的内存就是 垃圾,它们可以被用于新的内存申请。 垃圾回收的优点非常明显: a. 极大简化了编程方式。在没有垃圾回收的编程中,需要手动分配内存,以及内存的回收。需要考虑谁来回收垃圾,什么时候回收垃圾。 b. 消除了一些常见的错误。当我们对一个变量的生存周期出现错误判断时,过早得销毁数据会使得引用失效,忘记删除数据则会导致内存泄漏。 但垃圾回收也带来了问题:CLR 需要几乎随时跟踪 GC 堆上的所有引用,造成较大的性能损耗(相比于没有 GC 处理)。 (2)托管代码 这种能够做到 “几乎随时” 报告所有仍然生效的 GC 引用的代码,就叫做“托管代码”(因为它由 CLR 进行“托管”)。 (3)常见的 GC 算法 a. 引用计数 堆上的每个对象都维护这一个内存字段来统计程序中有多少 “部分” 在引用。随着某些 “部分” 不再被使用,即对象不再被引用,对象计数为 0 时,对象就可以被删除了。但这种处理方式很难处理循环引用的问题。 b. 引用跟踪 引用跟踪只关心引用类型的变量。我们将所有的引用类型称为根,CLR 开始 GC 时,首先暂停进程中的所有线程。然后 CLR 进入标记阶段,CLR 遍历堆中所有对象。然后 CLR 遍历所有活动根,查看它们引用了哪些对象。CLR 会标记堆上的对象,如果对象已经标记则跳过。 对于没有标记的对象表示没有变量引用,对这些对象进行删除,然后进入 GC 的压缩阶段。由于删除对象后,会造成内存碎片,提升访问效率、内存利用率。 由于移动堆中的数据后,线程栈中引用的还是之前的内存地址。因此需要更新引用数据的地址:每个根减去引用对象在内存中偏移的字节数。压缩完成后,NextObjPtr 指向最后一个幸存对象之后的位置。 (4)代 CRL 的 GC 是基于代的垃圾回收(generational garbage collector),做出以下假设: * 对象越新,生存期越短;对象越老,生存周期越长 * 回收堆的一部分快于回收整个堆 CLR 只支持 0、1、2 代。CLR 初始化时,为每代分配了一定容量。如果分配的新对象超过预算,会触发 GC。通常会优先检查第 0 代的内存,并将处理后的内存归并到第 1 代。如果第一代过大,会同时检查第 0、1 代处理。 ![](https://pic4.zhimg.com/v2-dbb25b69c6be53fc8935c0c2e8662f6f_r.jpg) CLR 的垃圾回收是自调节的。若第 0 代存活的对象很少,就会减少第 0 代的预算;反之则增加。 (5)GC 触发条件 * 强制调用:显示调用 System.GC.Collect()(Microsoft 不推荐)。通常会在某个非重复性的事件导致大量就对象死亡时调用。由于强制调用会导致代的预算发生调整,所以它会影响程序的想要时间,更多的是为了减小进程的工作集。 * Windows 报告低内存。 * CLR 卸载 AppDomain。 * CLR 正在关闭。 (6)大对象 CLR 认为的大对象是指大于 85000 字节的对象。以上所说的处理方式只适用于小对象,大对象的处理方式略有不同: * 大对象在特定的进程地址空间分配,避免与小对象混合处理。 * 由于内存移动的代价过高,目前版本的 GC 不压缩大对象,因此可能造成地址空间碎片化。 * 大对象总是 2 代的,所以只能存活时间很长的资源才创建大对象。 二、C# ---- **2.1 String** (1)String 与字符串池 String 在内存中是不可变的,对 String 的操作通常会生成新的字符串。如果每次都赋值字符串都申请新的空间,会有很大开销。 CLR 在初始化时会在内部创建一个哈希表,key 为字符串,value 是托管堆中的 String 引用对象。当对字面值(literal)赋值时,会对该值留用。当再次赋值该值时,会调用 String.Intern(string) 获取存储的字符串。 (2)String 的 == 与 Equal 对于引用类型,== 操作符通常比较引用地址是否相同。String 重写了 == 操作符,其处理逻辑与 Equal 一致,比较的是 string 的内容是否相同。下面展示了两个字符的对比。 ``` String a = "1234"; String b = "123" + "4";// 调用String.Intern => a Console.WriteLine(Object.ReferenceEquals(a, b));// true Console.WriteLine(a == b);// true Console.WriteLine(a.Equals(b));// true ``` 上述例子中,a、b 变量内存地址相同,因此后续的比较也相同。 ``` String a = "1234"; char[] c = new char[] { '1', '2', '3', '4' }; String b = new string(c); Console.WriteLine(Object.ReferenceEquals(a, b));// false Console.WriteLine(a == b);// true Console.WriteLine(a.Equals(b));// true ``` 当申请新的内存空间存储 b 时,a、b 内存地址不同,但 == 与 Equals 仍然返回 true。 (3)StringBuilder StringBuilder 的优化原理与 List 类似,提前申请了 char[],避免多次创建 string。StringBuilder 只有在(1)动态字符串超过容量;(2)调用 ToString() 会分配新的内存。 **2.2 List 与 ArrayList => 如何避免拆箱与装箱** (1)使用泛型替代显式的装箱与拆箱 (2)避免类似 Directory 内部调用 IEquatable 的隐式装箱处理。 **2.3 Dictionary 与 HashTable** (1)异同 <table data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal"><tbody><tr><th></th><th>HashTable</th><th>Dictionary</th></tr><tr><td>线程安全</td><td>安全</td><td>不安全</td></tr><tr><td>功能</td><td>键值对(object-object)</td><td>键值对匹配,支持泛型</td></tr><tr><td>性能</td><td>需要进行装箱操作</td><td>无装箱操作,多线程中需要手动使用 lock 处理</td></tr></tbody></table> (2) 哈希冲突处理 HashTable 中使用了 bucket 来记录键值对,对于 hash 冲突使用了开放地址法处理。开放地址法:当发生冲突时 (p = H(key)),迭代得将 p 输入另一个方法,直到获得一个不再冲突的 index。 ![](https://pic4.zhimg.com/v2-33c53cfa81172128f608984cc00dd3eb_r.jpg) Dictionary 使用了 Entry 来记录键值对,采用链地址法的方式来处理哈希冲突的问题。具体处理如下:使用 entries 来存储实际的键值对数据,并记录其在桶中链表的下一个元素在 buckets 中的下标。buckets 为 int 数组,记录数值表示 entries 中的下标。 ![](https://pic3.zhimg.com/v2-b0c7c3b8fe1780ba659882a7366061fa_r.jpg) (3)扩容:以原有数组的两倍扩容(不能超过 2^31 + 1) 补充:Enum 作为 Dictionary 的 Key 的问题 Dictionary 在使用 Enum 作为 Key 时,查找数据时会有明显效率降低, 较好的处理方式为使用 int 替代枚举。Dictionary 查找数据的源码如下: ``` private IEqualityComparer<TKey> comparer; public Dictionary(int capacity, IEqualityComparer<TKey> comparer){ ... this.comparer = comparer ?? EqualityComparer<TKey>.Default; } private int FindEntry(Tkey key){ ... if(buckets == null) return -1; int hash = comparer.GetHashCode(key) & int.MaxValue; int length = bucket.Length; for(int i = buckets[hash % length]; i >= 0; i = entries[i].next){ var entry = entries[i]; if(entry.hashCode == num && comparer.Equals(entry.key, key)) return i; } return -1; } ``` comparer.Equals(entry.key, key) 较为耗时。EqualityComparer<TKey>.Default 获取 TKey 的 Equal 方法。若 TKey 存在 IEquatable<TKey > 接口,则返回其泛型比较对象,否则会使用具有装箱操作的比较对象。 这里没有使用 entry.key.Equals(key) 的原因在于,object.Equals(obj) 会进行装箱操作,而接口 IEqualityComparer 采用了泛型,避免装箱操作。但 enum 没有实现 IEquatable<T > 接口,必须进行装箱操作。 **2.5 协程** (1)迭代器 迭代器通过持有迭代状态获取当前迭代元素并且识别下一个需要迭代的元素,从而可以遍历集合中每一个元素而不用了解集合的具体实现。.Net 的迭代器通常由 IEnumerable 和 IEnumerator 接口来实现。 ``` public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { object Current { get; } // 获取当前状态的对象 bool MoveNext();// 移动到下一个状态 void Reset();// 重置状态 } ``` C# 中的可以使用 foreach 的类就是一种迭代器,foreach 要求对象实现 IEnumerable 接口。foreach 使用 GetEnumerator() 进行迭代处理:Reset() 进行初始化,然后循环调用 MoveNext() 直到返回 fasle,Current 为当前迭代器的元素数据。 C# 提供了 yield return 语句,便于直接枚举元素,而不必实现 IEnumerator 接口。yield break 用于终止迭代。示例如下: ``` public class Enumerator<T> : IEnumerable<T> { private T[] content; public Enumerator(params T[] arr) { content = arr; } public IEnumerator<T> GetEnumerator() { if (content == null) yield break; for(int i = 0; i < content.Length; i++) { Console.WriteLine("元素:" + content[i]);// 输出4、2、5 yield return content[i]; } yield break; } } class Program { static void Main(string[] args) { Enumerator<int> x = new Enumerator<int>(4, 2, 5); foreach (var t in x) { } } } ``` foreach 循环调用 GetEnumerator(),遇到 yield break 则结束循环,否则一直运行到 yield return。 (2)协程原理 协程的作用:控制代码在特定时机执行。使用方式如下: ``` public void Start() { StartCoroutine(s()); } IEnumerator s() { print(1); yield return 1; print(2); yield return 2; } ``` 协程的使用方式与迭代器一致,StartCoroutine 与 foreach 的作用类似:遍历迭代器。 **2.6 虚拟类与接口** 相同:(1)都无法实例化;(2)可以让派生类实现特定方法 不同:(1)继承:基类只能有一个,接口可以有多个;(2)成员:抽象类中可以定义抽象方法、非抽象方法、以及字段属性等;接口无法定义字段、实现方法、静态成员、访问修饰符;(3)应用:抽象类具有类的特性,而接口只是定义一系列方法的实现。 **2.7 多线程同步方式**:(1)Monitor;(2)lock Lock 是 Monitor 的语法糖: ``` try { Monitor.Enter(obj); dosomething(); } catch(Exception ex){} finally{ Monitor.Exit(obj); } ``` * Lock 只能针对引用类型加锁。Monitor 能够对值类型进行加锁,实质上是 Monitor.Enter(object) 时对值类型装箱。 * Monitor 能设置获得锁定的超时值,避免线程之间的死锁。 **2.8 委托与事件** 委托定义了一种函数类型变量。而事件可以理解为委托实例的数组。通常用于实现发布订阅模式。使用示例如下: ``` class Test { public delegate void X(); public event X m; public Test() { m += A; m += B; } public void A() { } public void B() { } } ``` **2.9 类型转换与内存** (1)装箱与拆箱 (2)数值 ``` static void Main(string[] args) { int a = 1; byte[] byteArray; byteArray = BitConverter.GetBytes(a); Console.WriteLine(BitConverter.ToString(byteArray));// 01-00-00-00 byteArray = BitConverter.GetBytes((float)a); Console.WriteLine(BitConverter.ToString(byteArray));// 00-00-80-3F byteArray = BitConverter.GetBytes((double)a); Console.WriteLine(BitConverter.ToString(byteArray));// 00-00-00-00-00-00-F0-3F } ``` * int 是有符号 32 位值(符号位 1 + 无符号位 31) * float 是 IEEE 32 位浮点数(符号位 1 + 指数位 8 + 尾数部分 23) * double 是 IEEE 64 位浮点数(符号位 1 + 指数位 11 + 尾数部分 52) **2.10 new 对象** CLR 要求所有对象的创建都必须使用 new 操作符。new 操作符做了一下的内容: (1)计算实例字段需要的大小 内存对齐:操作系统要求变量地址是某个数的整数倍。(32 位:4 字节,64 位:8 字节) Struct 默认的内存对齐方式为 LayoutKind.Sequential,会按照 struct 内最大成员长度进行依次对齐。 ``` [StructLayout(LayoutKind.Sequential)] struct StructA { public int num; public char ch; } void Start() { StructA stru = new StructA(); Debug.LogError("num的长度:" + Marshal.SizeOf(stru.num)); // 4 Debug.LogError("ch的长度:" + Marshal.SizeOf(stru.ch)); // 2 Debug.LogError("StructA的长度:" + Marshal.SizeOf(stru)); // 8 } ``` 创建引用类型时,除了成员变量,还会有开销成员:“类型对象指针” 和 “同步块索引”。类型对象指针用于标记内存的类型,保证内存的安全操作。CLR 开始在进程中运行时,利用 System.Type 类型创建一个特殊的类型对象,代码中的类型对象都是该类型的 “实例”,因此类型对象指针成员会初始化成对的 System.Type 类型对象的引用。 同步块索引用于线程同步功能。当线程使用对象同步时,会检查该对象的同步索引 (默认为 - 1)。如果索引为负数,则会在同步块数组中寻找或者新建一个同步块,并且把同步块的索引值写入该对象的同步索引中。否则,找到该对象的同步块并且检查是否有其他线程在使用该同步块,如果有则进入等待状态,如果没有则申明使用该同步块。 ![](https://pic1.zhimg.com/v2-6bd85096b0673bec3eb68b926c343a0c_r.jpg) (2)检测内存 如果托管堆有足够的空间,就在 NextObjPtr 指针指向的地址放入对象,为对象分配的字节会被清零。接着调用类型的构造器,符返回对象引用。如果托管堆没有足够的空间,则会触发垃圾回收,然后再分配内存。 返回对象引用之前,NextObjPtr 指针的值会加上对象占用的字节数,即下个对象放入托管堆时的地址。 三、Lua ----- **3.1 table 实现原理** lua 的 table 是开放地址法与拉链地址法的结合。Table 中存在 Array、HashNode 以及 lastfree,Array 中存储了 key 对应于 HashNode 中的下表。HashNode 中存储了 value 等相关数据。lastfree 用于标记空闲节点。 每次插入的节点时,若不存在冲突,则直接给对应的 node 赋值。若存在冲突,分为两种情况:原位置节点与插入节点的 mainposition 是否相同。若 mainposition 相同,则将 lastfree 对应的节点分配给插入节点;如果 mainposition 不同,则将原有节点迁移至 lastfree 位置,插入节点存入当前位置。 ![](https://pic2.zhimg.com/v2-4f06cf1c9a2e690d0588740cd180ea1d_r.jpg) **3.2 元表** (1)__index 如果在访问 table 时,键不存在则会查找元表的__index 字段。若__index 为 function, 则会执行 function;若__index 为 table, 则会在__index 中查找 key。 (2)__newindex 当对原始表中缺少的 key 进行赋值时,在设置了元表后会对其__newindex 字段进行处理。若__newindex 为 function, 则会执行 function;若__newindex 为 table, 则会为__newindex 进行赋值。 (3)其它操作符... 拓展:限制全局变量的使用 lua 的全局变量保存在_G 中,若希望限制全局变量的定义,可通过元表的方式来处理: ``` local __g = _G -- export global variable cc.exports = {} setmetatable(cc.exports, { __newindex = function(_, name, value) rawset(__g, name, value) end, __index = function(_, name) return rawget(__g, name) end }) -- disable create unexpected global variable setmetatable(__g, { __newindex = function(_, name, value) local msg = "USE 'cc.exports.%s = value' INSTEAD OF SET GLOBAL VARIABLE" error(string.format(msg, name), 0) end }) ``` **3.3 面向对象的实现(class)** (1)封装:对 class 封装,只给到特定接口 (2)继承:元表的__index (3)多态:在子类 table 中定义函数名 **3.4 Lua 与 C# 如何交互** 常见的做法是 C# 端创建 Lua 虚拟机,Lua 通过 C 接口调用 C# 的 Wrap 文件。Lua 虚拟机维护了一个公共内存堆栈,当 Lua 需要调用 C# 对象(GameObject)时,向栈顶放入对象的 index,C# 拿到 index 后,从维护的 Map 池中查找对象,并进行相关操作。 参考 -- * 《CLR via C#》 * [什么是 CLR ?](https://zhuanlan.zhihu.com/p/68158037) * [一文读懂 C# 的 堆、栈、值类型、引用类型](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_27825451/article/details/80574938) * [一文详解堆栈(二)——内存堆与内存栈](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_27825451/article/details/102572795) * [C# 的内存分配与管理](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_27825451/article/details/102581255) * [C# 深入理解堆栈、堆在内存中的实现](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/jearay/p/7692195.html) * [C# (CLR) 中内存分配解析](https://link.zhihu.com/?target=https%3A//blog.csdn.net/JianZuoGuang/article/details/91391005) * [同一进程中的线程究竟共享哪些资源](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/aipiaoborensheng/p/11826288.html) * [装箱与拆箱 / 复制内存](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/kuailewangzi1212/archive/2007/04/18/717765.html) * [C# 中的装箱与拆箱 值类型与引用类型 内存中的堆区与栈区](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_43461641/article/details/96436060) * [协程的注意问题](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/alongu3d/p/5318189.html) * [Unity 的 coroutine 问题](https://www.zhihu.com/question/34878524) * [【Lua 5.3 源码】table 实现分析](https://link.zhihu.com/?target=https%3A//blog.csdn.net/y1196645376/article/details/94348873) * [LUA 的 table 实现](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/xcw0754/p/11416545.html) 0613学习 排序