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)
元组可以减少额外定义类或结构体的成本,也可以通过命名字段和解构提高使用便利性。
但它更适合临时数据结构,如果数据有长期业务含义、字段较多,或者需要封装行为,还是应该定义专门的 class 或 struct。
4. 请说明 Thread、ThreadPool、Task 分别是什么?并简单说明彼此的区别。
标准答案
Thread 是 C# 中比较底层的线程对象,可以显式创建和启动一个新线程,开发者可以直接控制线程的生命周期,但管理成本也比较高。
ThreadPool 是线程池,它内部维护了一组可复用线程,适合执行大量短时间的后台任务,避免频繁创建和销毁线程带来的性能开销。
Task 是更高级的任务抽象,它不等同于线程。使用 Task.Run 时,通常会把任务提交到线程池执行;但在异步 I/O 场景中,Task 也可能只是表示一个异步操作的状态,并不一定新开线程。
Task 还可以方便地配合 async/await 获取结果、处理异常和组织异步流程。
在 Unity 中,不管是 Thread、ThreadPool 还是 Task.Run,都要注意不能在子线程直接调用大部分 Unity API,通常只把耗时计算或 I/O 放到后台线程,最后回到主线程更新游戏对象。
5. 请简述 GC 垃圾回收产生的原因,并至少说出避免 GC 发生的三种方式。
标准答案
GC 产生的根本原因是程序运行过程中会在托管堆上分配对象,比如 new 引用类型、字符串拼接、集合扩容、装箱、闭包、LINQ 等。
当这些对象不再被任何可达引用持有时,就会变成垃圾对象。GC 会在合适时机扫描并回收这些对象。
在 Unity 中,GC 可能带来卡顿风险,所以我们通常不是完全避免 GC,而是减少运行时 GC Alloc。常见方式包括:
第一,使用对象池复用对象,避免频繁 Instantiate 和 Destroy。
第二,减少字符串拼接,高频拼接可以使用 StringBuilder。
第三,集合提前设置容量,并复用 List、Dictionary 等容器,避免频繁扩容和临时 new。
第四,避免装箱、闭包、LINQ 等隐式分配。
第五,使用 Unity 的 NonAlloc API,比如 RaycastNonAlloc、OverlapSphereNonAlloc。
总结来说,优化 GC 的核心是减少托管堆分配,尤其要避免在 Update、战斗逻辑、寻路检测、UI 高频刷新等场景中产生临时对象。
6. Unity 中动态加载资源的方式有哪些?
标准答案
Unity 中动态加载资源常见方式有几种。
第一种是 Resources.Load,使用简单,但 Resources 目录下的资源都会被打进包体,资源管理不够灵活,一般不建议在大型项目中大量使用。
第二种是 AssetBundle,可以把资源打包成 AB 包,支持本地或远程加载,常用于资源分包、按需加载和热更新,但需要自己管理依赖、版本和卸载。
第三种是 Addressables,它是 Unity 更高层的资源管理方案,可以通过地址加载资源,并自动处理依赖、异步、缓存和释放,底层通常仍然可以基于 AssetBundle。
另外,也可以从 StreamingAssets 或 persistentDataPath 这些路径读取资源。StreamingAssets 常用于存放随包发布的初始资源,persistentDataPath 常用于存放下载后的热更资源、存档或缓存文件。
读取时一般会结合 UnityWebRequest、文件流,或者 AssetBundle.LoadFromFile。
实际项目中还要注意异步加载和资源释放,避免加载过程造成卡顿或内存泄漏。
7. Unity 中的光照贴图的作用是什么?
标准答案
Unity 中光照贴图的作用是把静态场景中的光照信息提前烘焙到贴图中,比如直接光、间接光、阴影和全局光照效果。
运行时物体不需要每帧进行复杂的实时光照计算,而是通过 UV 去采样光照贴图,从而提升性能。
它主要适用于静态物体,比如场景建筑、地面、墙体和固定装饰物。对于动态角色或移动物体,通常不能直接完全依赖光照贴图,而是会结合 Light Probe、Reflection Probe 或实时光照来获得更自然的效果。
总结来说,光照贴图的核心作用就是:用更低的运行时开销表现更复杂、更真实的静态光照效果。
8. Unity 场景中有两个点连成了一条线,想要旋转这条线,应该怎么做?
标准答案
这题要先看这条线是什么。
如果它是一个实际的 GameObject,比如一根细长的模型或 Sprite,那么直接修改它的 transform.rotation 或调用 transform.Rotate 就可以。
如果这条线是通过两个点绘制出来的,比如使用 LineRenderer,那就要用向量方式处理。
两个点 start 和 end 可以得到方向向量:
Vector3 dir = end - start;
如果要绕起点旋转,就用四元数旋转这个方向向量:
Vector3 newDir = rotation * dir;
然后新的终点就是:
Vector3 newEnd = start + newDir;
最后重新设置 LineRenderer 的两个点。
如果要绕中心点旋转,就先求中心点:
Vector3 center = (start + end) / 2f;
然后分别旋转 start - center 和 end - 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 或自定义协议属于二进制格式。项目中会根据性能、带宽、可维护性来选择。
Comments
评论区
欢迎在这里留言交流。