秋招笔记-8.4
昨天花了太多时间在语言上,我还是希望能够更均匀地学习,所以让我们不仅仅学习语言。
C#
- IList接口的用法和意义?
IList 是 C# 中表示有序集合的接口,并且是所有非泛型集合的集合,继承自 ICollection 和 IEnumerable,当然如果需要实现泛型的话还有我们的IList<T>接口,比如我们的List就是实现了IList<T>的数据容器。
- 关于四种委托
我们知道C#中有四种委托,分别是delegate,event,action和func。
在C#中,委托(Delegate)本质上是一种类型安全的函数指针封装,它允许我们将方法作为参数传递,实现回调函数的效果。委托的核心优势在于类型安全性和灵活性,但它的缺点是在声明它的类外部也可以被直接调用和修改,这可能导致封装性被破坏。为了解决这个问题,C#引入了事件(Event)机制,事件是基于委托实现的,但通过编译器生成的add和remove访问器进行了封装,使得事件只能在声明它的类内部被触发(具体来说就是把委托字段的访问权限修改为private),外部代码只能通过+=和-=操作符来订阅或取消订阅,从而保证了更好的封装性和安全性。Action和Func是.NET框架提供的两种泛型委托,它们通过泛型参数避免了为不同数据类型重复创建相似委托的开销。其中Action用于表示没有返回值的方法(最多支持16个输入参数),而Func用于表示有返回值的方法(最后一个泛型参数表示返回类型)。
- 关于property
在C#中,属性(Property)的本质是通过get
和set
访问器对字段进行封装,从而实现对类成员访问权限的精细控制。我们可以通过组合或省略这些访问器来实现四种典型配置:可读可写属性(同时包含get
和set
)、只读属性(仅包含get
)、只写属性(仅包含set
)以及完全不可访问的属性(省略访问器或设为私有)。
- 关于前台线程和后台线程
什么是前台线程和后台线程?
在C#中,前台线程(Foreground Thread)和后台线程(Background Thread)是两种线程类型,它们的核心区别在于对应用程序生命周期的影响以及任务执行优先级。
在C#中,线程分为前台线程和后台线程两种类型,它们的核心区别在于对应用程序生命周期的影响。默认情况下,通过new Thread()创建的线程都是前台线程,只要程序中还存在任何一个前台线程在运行,应用程序就不会退出,即使主线程已经结束;而后台线程需要显式设置IsBackground属性为true来指定,它的执行不会阻止程序终止,当所有前台线程都结束时,无论后台线程是否完成任务,都会被系统强制终止。前台线程适合用于必须完成的关键任务(如数据持久化),而后台线程则适用于允许中断的非关键任务(如日志记录)。需要注意的是,通过ThreadPool或Task创建的线程默认为后台线程,无需手动设置IsBackground属性。
- 关于Task和Thread和ThreadPool
我们在C#中创建线程主要就是通过这三种方式:
我们主要来聊聊Task:
在 C# 中,Task
是 System.Threading.Tasks
命名空间下的核心类,用于表示一个异步操作。它是对线程池的高级抽象,相比直接使用 Thread
,Task
更轻量且高效,支持返回值、异常处理、任务取消和并行调度。
讲清楚Task的内容之前,我得先说明一点:关于什么是异步。
异步,就是非阻塞,允许你在某个计算任务执行的同时去执行另外的任务,这样可以大大地提高运算的效率。大体上来说,非阻塞主要通过线程和回调实现。
- Mutex和Lock
这两个都是锁,但是Mutex是进程与进程之间的锁而Lock是线程与线程之间的锁,Mutex需要手动释放锁而Lock可以自动释放锁。
操作系统
- 什么是硬链接和软链接?
首先看看二者的区别:
inode(索引节点) 是文件系统中描述文件元数据和数据位置的核心数据结构,每个文件/目录对应唯一的 inode,存储其权限、所有者、大小、时间戳、数据块指针等信息。它相当于文件的“身份证”,操作系统通过 inode 定位文件的实际内容,并管理文件的访问控制。
硬链接 是文件系统中同一文件的多个别名,所有硬链接共享相同的 inode 和数据块,删除任一链接不影响文件数据,直到最后一个链接被删除。它适用于同一文件系统内的多路径访问,但不支持目录和跨文件系统。
软链接(符号链接) 是一个独立文件,存储目标文件的路径字符串,拥有自己的 inode。它像“快捷方式”,可跨文件系统、链接目录,但依赖原文件存在(原文件删除则链接失效)。软链接灵活但需额外存储空间,常用于简化路径或跨分区引用。
- 中断和轮询是什么?
中断(Interrupt) 是硬件或软件主动触发的事件通知机制,当特定条件(如键盘输入、定时器到期)发生时,强制CPU暂停当前任务,转而执行中断处理程序,实现异步响应(如网卡收到数据后中断CPU)。
轮询(Polling) 是CPU主动反复检查设备或状态的机制,通过循环查询(如不断读取寄存器)判断事件是否发生,属于同步等待(如持续检查串口是否有数据)。
- 什么是IO多路复用?
I/O多路复用(I/O Multiplexing) 是一种通过单线程监听多个I/O流事件的高效并发模型,核心思想是让操作系统(如通过epoll
、select
、kqueue
)批量通知程序哪些I/O操作已就绪(如可读、可写),避免为每个I/O分配独立线程阻塞等待。
- poll、epoll、select的区别是什么?
select,poll,epoll都是实现轮询的手段,但是区别在于select是一个有最大fd(File Descriptor,操作系统对打开的资源(如文件、套接字、管道等)的抽象引用,表现为一个非负整数)限制的无差别轮询而poll是一个无最大fd限制的轮询,二者的时间复杂度都为ON,epoll通过维护红黑树管理fd,这样查询的复杂度为O1。
- 什么是用户级线程和内核级线程?
用户级线程(ULT) 由用户空间的线程库(如pthread
或Go的调度器)直接管理,无需内核介入,创建和切换速度快(仅修改用户态栈和寄存器),但内核将其视为单线程,因此 无法利用多核并行,且一旦某个ULT阻塞(如I/O操作),整个进程会被阻塞。而 内核级线程(KLT) 由操作系统内核直接管理,线程的创建、调度和销毁需通过系统调用,开销较大,但支持 多核并行执行,单个线程阻塞不会影响其他线程,且崩溃隔离性更好(除非共享资源被破坏)。简言之,ULT轻量但受限,适合高并发I/O任务;KLT重量但强大,适合计算并行和稳定性要求高的场景。
- 为什么虚拟空间切换比较费时?
虚拟地址空间切换速度较慢的核心原因在于,切换过程中需要更新页表基址寄存器(如x86的CR3)并刷新TLB(快表)和CPU缓存,这会导致以下开销:首先,页表切换需要从内存中加载新进程的多级页表结构(如Linux的四级页表),而内存访问本身就有较高延迟;其次,TLB和缓存中缓存的旧进程地址映射会全部失效(因为虚拟地址相同但物理地址已变),后续内存访问必须重新通过页表走一遍地址转换流程,引发大量缓存未命中(Cache Miss)和TLB未命中(TLB Miss),尤其是当新进程刚切换后首次访问内存时,这种"冷启动"效应会显著增加延迟。
简单地说,就是要更新页表和原来的TLB失效。
计算机网络
- 什么是Cookie和Session?
Cookie 是服务器发送到浏览器并保存在客户端的小段数据(如用户ID),每次请求自动附带,用于无状态HTTP的简单身份识别,但安全性较低(可篡改)。
Session 是服务器端存储的用户会话数据(如购物车内容),通过唯一的Session ID关联客户端(通常该ID通过Cookie传递),安全性高但占用服务器资源。
- 从输入网址到最后网页正常显示中间经历了哪些过程?
Unity
- 关于Awake,OnEnable和Start
我们都知道执行顺序就是Awake,OnEnable和Start,但是具体来说,Awake在对象初始化时执行,OnEnable在脚本激活时执行,而Start则是在Update之前执行,所以一个生命周期中OnEnable可以执行多次。
- Unity脚本的执行顺序
我们都知道Unity是通过挂载脚本来实现内容的驱动的,但是具体每个脚本的执行顺序是怎么回事呢?
事实上,针对同一场景中的不同脚本,具体的执行顺序并不固定,我们可以在Edit->Project Settings->Script Exection Order中进行修改。
- CharacterController vs Rigidbody
二者都可以实现驱动角色的移动,但是底层的原理不同:前者通过直接计算并修改挂载的角色的几何坐标来实现移动,而后者完全基于物理模拟来实现角色移动。
- 烘焙(Baking)是什么?
烘焙指预计算并存储复杂数据以减少运行时开销,Unity 中主要应用在两类资源:
- Unity的几种坐标系
- Unity必须的几个文件夹:
这里引出一个问题就是在不同的工程文件中如何安全地转移资产文件(asset):
- Unity动态加载资源的方式?
- Unity的PlayerPrefs进行保存和读取的方法?
保存是以Set开头的函数,读取是以Get为首的函数,比如我们的数据类型是Int时:
- 场景中有多个摄像机同时激活会怎样?
默认按Depth
值从低到高渲染(低Depth先渲染,高Depth覆盖)。
- .Net和Mono的关系
- image和rawimage的区别
这里需要补充一下的是,我们外部的图片无论JPG或者PNG格式进入Unity之后默认是Texture格式,我们需要手动转换成Sprite格式才能被image使用。
- Transform的父类?
Component类
- 如何理解动态合批和静态合批?
在Unity中,场景中的每个物体本质上由网格(Mesh)和材质(Material)构成:网格定义了物体的几何形状(顶点、法线、UV坐标等),而材质则通过着色器(Shader)和引用的纹理(Texture)共同决定物体表面的视觉表现(如颜色、光泽、透明度),此时纹理可理解为“贴在网格表面的皮肤”。
静态合批针对标记为Static
的物体(不可移动/旋转/缩放),在场景构建(Build)或初始化阶段,将使用相同材质的静态物体的网格顶点和索引数据合并成一个持久化的大网格(若顶点数超64k则拆分为子网格)。此过程通过单次Draw Call提交整个合并网格,显著减少渲染调用次数,但需付出高内存占用的代价(存储合并后的大网格),且合并后子物体失去独立变换能力。
动态合批则针对运行时可移动的小型动态物体(顶点数≤300且使用完全相同的材质实例),在每帧实时检测符合条件的物体,将其顶点数据从本地空间变换到世界空间,并填充至公共顶点缓冲区(不生成新网格),最终通过单次Draw Call提交。此机制减少Draw Call但增加CPU开销(顶点变换计算),且受严格限制(如统一缩放、顶点属性总数≤900)。
- Unity的客户端和服务器交互的方式有哪些?
基于TCP或者UDP的Socket,或者是Http或者基于Http的一系列协议等等。
- Camera的ClearFlags的含义是什么?
Camera组件的Clear Flags
属性用于控制每一帧渲染开始时如何清除屏幕的帧缓冲区(包括颜色缓冲区和深度缓冲区)。
- Unity跨平台的原理?
Unity实现跨平台的底层原理本质上是构建了一个三层架构体系:开发者编写的C#代码首先被编译为与平台无关的中间语言(IL),这种符合CLI标准的字节码不依赖具体硬件或操作系统,为跨平台提供了基础;然后Unity通过两种后端方案处理这些IL代码——Mono虚拟机在支持动态编译的平台(如Windows、Android开发期)采用即时编译(JIT)技术,在运行时将IL动态转换为机器码并执行,而IL2CPP工具链则在构建阶段将IL静态转换为C++代码,再调用各平台的原生编译器(如iOS的Clang、Android的NDK)生成机器码,这种预编译(AOT)模式尤其适用于iOS等禁止JIT的平台,兼顾性能与安全;最后,Unity强大的平台抽象层统一封装了不同操作系统的底层差异,例如将DirectX、Metal、Vulkan等图形API抽象为统一的渲染管线接口,将鼠标、触摸屏、手柄等输入设备抽象为Input
类,并通过路径接口(如Application.dataPath
)屏蔽文件系统差异,同时资源处理系统(如纹理压缩格式转换、AssetBundle打包)也会按目标平台自动适配,确保开发者只需关注业务逻辑,而无需为每个平台重写底层实现。