Unity / C# 面试题复盘7


1. C# 中如何让一个类不能再被其他类所继承?

标准答案

C# 中可以使用 sealed 关键字修饰类,例如:

public sealed class Test
{
}

sealed 修饰的类不能再作为父类被其他类继承,如果强行继承会编译报错。

在项目中,sealed 通常用于不希望被扩展或修改继承行为的类,比如某些工具类、核心管理类,或者设计上已经不希望继续派生的类型。


2. C# 中使用泛型的好处是什么?

标准答案

C# 中泛型的好处主要有三个。

第一是提高代码复用性,可以把类型作为参数传入,让同一套类或方法适配不同类型。

第二是类型安全,泛型会在编译期检查类型,避免运行时类型转换错误。

第三是性能更好,尤其是处理值类型时,相比使用 object 或非泛型集合,可以避免装箱和拆箱。

比如 List<int> 相比 ArrayList 存储 int,既更安全,也更高效。

在 Unity 项目中,像 List<T>Dictionary<TKey, TValue>、对象池 ObjectPool<T> 都是泛型的常见应用。


3. C# 中元组对于我们的作用是什么?

标准答案

C# 中元组的作用是把多个值临时组合成一个整体,常见用途是方法返回多个结果,或者在局部逻辑中临时组织一组数据。

比如一个方法既要返回是否成功,又要返回伤害值,就可以写成:

(bool success, int damage)

元组可以减少额外定义类或结构体的成本,也可以通过命名字段和解构提高使用便利性。

但它更适合临时数据结构,如果数据有长期业务含义、字段较多,或者需要封装行为,还是应该定义专门的 classstruct


4. 请说明 Thread、ThreadPool、Task 分别是什么?并简单说明彼此的区别。

标准答案

Thread 是 C# 中比较底层的线程对象,可以显式创建和启动一个新线程,开发者可以直接控制线程的生命周期,但管理成本也比较高。

ThreadPool 是线程池,它内部维护了一组可复用线程,适合执行大量短时间的后台任务,避免频繁创建和销毁线程带来的性能开销。

Task 是更高级的任务抽象,它不等同于线程。使用 Task.Run 时,通常会把任务提交到线程池执行;但在异步 I/O 场景中,Task 也可能只是表示一个异步操作的状态,并不一定新开线程。

Task 还可以方便地配合 async/await 获取结果、处理异常和组织异步流程。

在 Unity 中,不管是 ThreadThreadPool 还是 Task.Run,都要注意不能在子线程直接调用大部分 Unity API,通常只把耗时计算或 I/O 放到后台线程,最后回到主线程更新游戏对象。


5. 请简述 GC 垃圾回收产生的原因,并至少说出避免 GC 发生的三种方式。

标准答案

GC 产生的根本原因是程序运行过程中会在托管堆上分配对象,比如 new 引用类型、字符串拼接、集合扩容、装箱、闭包、LINQ 等。

当这些对象不再被任何可达引用持有时,就会变成垃圾对象。GC 会在合适时机扫描并回收这些对象。

在 Unity 中,GC 可能带来卡顿风险,所以我们通常不是完全避免 GC,而是减少运行时 GC Alloc。常见方式包括:

第一,使用对象池复用对象,避免频繁 InstantiateDestroy

第二,减少字符串拼接,高频拼接可以使用 StringBuilder

第三,集合提前设置容量,并复用 ListDictionary 等容器,避免频繁扩容和临时 new。

第四,避免装箱、闭包、LINQ 等隐式分配。

第五,使用 Unity 的 NonAlloc API,比如 RaycastNonAllocOverlapSphereNonAlloc

总结来说,优化 GC 的核心是减少托管堆分配,尤其要避免在 Update、战斗逻辑、寻路检测、UI 高频刷新等场景中产生临时对象。


6. Unity 中动态加载资源的方式有哪些?

标准答案

Unity 中动态加载资源常见方式有几种。

第一种是 Resources.Load,使用简单,但 Resources 目录下的资源都会被打进包体,资源管理不够灵活,一般不建议在大型项目中大量使用。

第二种是 AssetBundle,可以把资源打包成 AB 包,支持本地或远程加载,常用于资源分包、按需加载和热更新,但需要自己管理依赖、版本和卸载。

第三种是 Addressables,它是 Unity 更高层的资源管理方案,可以通过地址加载资源,并自动处理依赖、异步、缓存和释放,底层通常仍然可以基于 AssetBundle。

另外,也可以从 StreamingAssetspersistentDataPath 这些路径读取资源。StreamingAssets 常用于存放随包发布的初始资源,persistentDataPath 常用于存放下载后的热更资源、存档或缓存文件。

读取时一般会结合 UnityWebRequest、文件流,或者 AssetBundle.LoadFromFile

实际项目中还要注意异步加载和资源释放,避免加载过程造成卡顿或内存泄漏。


7. Unity 中的光照贴图的作用是什么?

标准答案

Unity 中光照贴图的作用是把静态场景中的光照信息提前烘焙到贴图中,比如直接光、间接光、阴影和全局光照效果。

运行时物体不需要每帧进行复杂的实时光照计算,而是通过 UV 去采样光照贴图,从而提升性能。

它主要适用于静态物体,比如场景建筑、地面、墙体和固定装饰物。对于动态角色或移动物体,通常不能直接完全依赖光照贴图,而是会结合 Light Probe、Reflection Probe 或实时光照来获得更自然的效果。

总结来说,光照贴图的核心作用就是:用更低的运行时开销表现更复杂、更真实的静态光照效果。


8. Unity 场景中有两个点连成了一条线,想要旋转这条线,应该怎么做?

标准答案

这题要先看这条线是什么。

如果它是一个实际的 GameObject,比如一根细长的模型或 Sprite,那么直接修改它的 transform.rotation 或调用 transform.Rotate 就可以。

如果这条线是通过两个点绘制出来的,比如使用 LineRenderer,那就要用向量方式处理。

两个点 startend 可以得到方向向量:

Vector3 dir = end - start;

如果要绕起点旋转,就用四元数旋转这个方向向量:

Vector3 newDir = rotation * dir;

然后新的终点就是:

Vector3 newEnd = start + newDir;

最后重新设置 LineRenderer 的两个点。

如果要绕中心点旋转,就先求中心点:

Vector3 center = (start + end) / 2f;

然后分别旋转 start - centerend - center,再加回 center

所以核心是:先确定旋转中心和旋转轴,再用 Quaternion 旋转端点相对于旋转中心的向量。


9. LOD 多细节层次和 MipMap 纹理图的作用是什么?

标准答案

LOD 是多细节层次,主要用于模型层面的优化。它会根据物体距离摄像机的远近,切换不同精度的模型。近处使用高模保证效果,远处使用低模减少三角面和顶点数量,从而降低渲染开销。

MipMap 是纹理的多级缩小版本。Unity 在纹理导入时可以生成多级 MipMap,运行时 GPU 会根据物体在屏幕上的大小选择合适的纹理层级。

远处物体会采样更小的纹理版本,这样可以减少纹理采样压力,同时降低远处纹理闪烁和摩尔纹问题。

简单来说,LOD 主要优化模型复杂度,MipMap 主要优化纹理采样和显示稳定性。它们都常用于远景优化,在视觉影响较小的情况下提升性能。


10. 游戏开发中,客户端和服务端交互数据,程序中常用方式是什么?

标准答案

游戏客户端和服务端交互数据,常见方式要从通信协议和数据格式两个角度看。

普通业务接口一般使用 HTTP/HTTPS,在 Unity 中可以通过 UnityWebRequest 发送 GET 或 POST 请求,常用于登录、公告、配置、背包、商城等请求。数据格式通常可以用 JSON,因为可读性好、调试方便。

如果需要长连接或服务端主动推送,比如聊天、好友状态、实时通知,可以使用 WebSocket 或 TCP Socket。

如果是对实时性要求很高的玩法,比如动作同步、射击游戏同步,也可能使用 UDP、KCP 或自定义网络协议。

数据格式方面,JSON、XML 属于文本格式;Protobuf、MessagePack 或自定义协议属于二进制格式。项目中会根据性能、带宽、可维护性来选择。