转载请注明出处,谢谢。
以 C# 为主,协议 / 多线程 / WPF 穿插其中,适合上位机(Host/PC 端)开发岗位面试复习。
一、C# 基础
1, C# 中值类型和引用类型的区别?
值类型(int、double、struct、enum、bool)存于栈,赋值拷贝整个值;引用类型(class、数组、字符串、委托)存于堆,变量只持有引用地址。
上位机里寄存器结构体常用 struct (值类型、无 GC 压力),业务对象用 class。
2, 什么是装箱和拆箱?上位机场景要注意什么?
装箱:值类型转 object / 接口,会在堆上分配对象;拆箱:反向转换,有类型检查开销。
高频采集时避免 ArrayList(里面全是 object,频繁装箱),改用泛型 List<T>。
3, string 是值类型还是引用类型?为什么拼接要用 StringBuilder?
引用类型,但不可变(immutable) :每次 + 都生成新对象,旧对象等 GC。
协议组帧时大量拼接字节/字符串,用 StringBuilder 复用缓冲区,减少分配。
4, const 和 readonly 有什么区别?
const 编译期常量,只能用于基本类型,且默认 static;readonly 运行期赋值,可在构造函数里初始化,更灵活。
配置类里设备地址、端口这类可能变化的量用 readonly 或配置文件,别写死成 const。
5, == 和 Equals 的区别?
== 对值类型是值比较,对引用类型默认比较引用地址;Equals 可被重写做内容比较(string 已重写,比较字符内容)。
比较字节数组内容别用 ==,用 Enumerable.SequenceEqual 或 System.Buffers.Binary 比较。
6, null 相关运算符:?.、??、??= 是什么?
?.空条件访问:obj?.Name为 null 时返回 null,不抛异常。??空合并:a ?? b表示 a 为 null 取 b。??=空合并赋值:a ??= b仅当 a 为 null 才赋值。
解析下位机返回的字段时大量使用:var temp = data?.Temperature ?? 0;
7, 属性和字段(Field)有什么区别?为什么多用属性?
字段是类内部的数据存储;属性是带 get/set 的封装访问器,可在读写时加校验、通知。
MVVM 里 ViewModel 的属性要实现 INotifyPropertyChanged 来驱动 UI 刷新,必须用属性。
8, 什么是扩展方法?
静态类里的静态方法,第一个参数加 this,就能像实例方法一样调用:
csharp
public static class ByteArrayExt
{
public static ushort ToUInt16BE(this byte[] b, int offset)
=> (ushort)((b[offset] << 8) | b[offset + 1]);
}
// 使用
ushort v = buffer.ToUInt16BE(2);
常用于给 byte[] 加协议解析 helper。
9, yield return 是什么?有什么用?
惰性迭代器,按需生成序列,不一次性占内存:
arduino
IEnumerable<int> Range(int n)
{
for (int i = 0; i < n; i++)
yield return i;
}
解析大批量采样点、分批处理时用它省内存。
10, 接口(interface)和抽象类(abstract class)的区别?
接口只定义契约、多实现;抽象类可带实现、单继承。上位机常把不同设备抽象成 IDeviceDriver 接口,各自实现。
二、面向对象与泛型
11, 什么是泛型?为什么上位机通信爱用泛型?
泛型在编译期确定类型,避免装箱、提高类型安全与性能。
例如:Read<T>() where T : struct 直接把寄存器字节解析成 T 结构体。
12, 泛型约束 where 有哪些?
where T : class(引用类型)、struct(值类型)、new()(有无参构造)、interface(实现某接口)、基类约束。
13, 重载(overload)和重写(override)的区别?
重载:同方法名、不同参数列表,编译期决定;重写:子类用 override 覆盖父类 virtual 方法,运行期多态。
14, out 和 ref 参数有什么区别?
ref 传入前必须赋值、方法内可改;out 不必事先赋值、方法内必须赋值后返回。协议解析常用 out 返回解析出的结果并额外返回成功标志。
csharp
bool TryParseTemp(byte[] buf, out double temp) { ... }
三、委托、事件与 Lambda
15, 委托(delegate)是什么?
类型安全的函数指针,可把方法当参数传递:
csharp
public delegate void DataReceivedHandler(byte[] data);
上位机里通信层收到数据后通过委托回调上层处理逻辑。
16, 事件(event)和委托的区别?
事件是基于委托的发布-订阅 封装,外部只能 += / -=,不能直接 = 或调用,更安全。
csharp
public event DataReceivedHandler DataReceived;
17, 为什么要小心事件的"内存泄漏"?
订阅者若一直被事件持有引用,GC 无法回收。页面/窗口关闭时要 -= 解绑,或订阅者实现 IDisposable 时解绑。
18, Lambda 闭包(closure)是什么坑?
Lambda 会捕获外部变量,循环里用同一变量要注意(C# 5+ 的 foreach 已修复,但 for 仍要小心):
ini
for (int i = 0; i < 3; i++)
tasks.Add(() => Console.WriteLine(i)); // 可能全打印 3
// 修复:int local = i; 在 lambda 里用 local
19, Action、Func、Predicate 是什么?
内置泛型委托:Action 无返回值,Func 有返回值,Predicate<T> 返回 bool。写回调、过滤时不用每次都定义 delegate。
四、多线程与异步(上位机重中之重)
20, Thread、Task、async/await 分别什么时候用?
Thread:底层、重量级,基本不直接用。Task/Task.Run:基于线程池,推荐做后台工作。async/await:避免阻塞 UI 线程的关键,让等待 IO 时让出线程。
采集循环示例:
csharp
async Task ReadLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var data = await port.ReadAsync(ct);
await ProcessAsync(data);
}
}
21, async/await 底层大概是什么原理?
编译器把 async 方法编译成状态机;await 处把后续代码注册为续体(continuation),等待的任务完成后回到线程池/同步上下文继续执行。
22, 什么是 CancellationToken?为什么上位机必须用它?
协作式取消令牌,用于优雅停止后台循环 (程序退出、断开连接时)。否则线程卡在 ReadAsync 无法退出。
23, 跨线程更新 UI 怎么做?
WPF 用 Dispatcher.Invoke/BeginInvoke,WinForms 用 Control.Invoke:
ini
Application.Current.Dispatcher.Invoke(() => Temperature = v);
24, lock 和 Monitor 是什么?有什么注意点?
lock 是 Monitor.Enter/Exit 的语法糖,保证同一时刻只有一个线程进临界区。
注意:锁对象用 private readonly object _lock = new();,不要锁 this、字符串或 Type;锁内不要做 IO,避免死锁。
25, 并发集合有哪些?生产-消费模型怎么写?
ConcurrentQueue<T>、ConcurrentDictionary<TKey,TValue>、BlockingCollection<T>。
采集(生产者)入队、处理(消费者)出队,用 BlockingCollection 最省心:
scss
var q = new BlockingCollection<byte[]>(1024);
// 生产者
q.Add(data);
// 消费者
foreach (var item in q.GetConsumingEnumerable(ct)) Process(item);
26, volatile 关键字有什么用?
告诉编译器/CPU 不要缓存该字段的读写,保证多线程可见性。但它不保证原子性 (如 i++ 仍非线程安全),复杂场景还是用 Interlocked 或锁。
27, Interlocked 类做什么用?
提供原子操作:Increment、Decrement、Exchange、CompareExchange,无锁地做计数/标志切换,性能高于 lock。
28, async 里 ConfigureAwait(false) 是干嘛的?
await 默认会回到原同步上下文(UI 线程)。在库代码/后台逻辑里用 ConfigureAwait(false) 避免不必要的 UI 线程切换和潜在死锁。
五、内存、GC 与资源
29, .NET 的 GC 分代是怎么回事?
分 Gen0 / Gen1 / Gen2,新对象进 Gen0,回收频率高;存活的晋升到 Gen1、Gen2。大对象(>85KB)进 LOH,回收成本高。
30, 高频采集场景如何减轻 GC 压力?
- 复用缓冲区,别每帧
new byte[4096];用ArrayPool<byte>.Shared租借。 - 避免在热路径里装箱、拼接字符串。
- 用
struct代替小对象。
31, 什么是 IDisposable 和 using?
IDisposable 用于释放非托管资源(串口、Socket、文件句柄);using 自动调用 Dispose:
csharp
using var port = new SerialPort("COM3", 9600);
port.Open();
// 离开作用域自动 Dispose,关闭串口
32, using 和 try/finally 的关系?
using 就是 try/finally { Dispose() } 的语法糖,确保异常时也能释放资源。
六、反射、特性与序列化
33, 反射(Reflection)在上位机里有什么实际用途?
- 插件化驱动加载 :按配置文件动态加载不同设备的
IDeviceDriver实现。 - 序列化 / 配置映射 / 协议字段反射解析。
34, 什么是特性(Attribute)?
给代码元素附加的元数据,可在运行时用反射读取:
csharp
[ProtocolField(Address = 0x1000, Length = 2)]
public ushort Temperature { get; set; }
用特性标注寄存器地址/长度,再反射批量解析,省去手写大量重复代码。
35, JSON 序列化和二进制序列化在上位机怎么选?
- 配置、日志、与云端通信用
System.Text.Json/Newtonsoft.Json(可读、跨平台)。 - 设备内部高速二进制帧用
BinaryWriter/MemoryMarshal/BitConverter,追求性能与紧凑。
36, 如何让一个对象能被 JSON 序列化/反序列化?
类通常是普通 POCO 即可;用 [JsonPropertyName] 指定字段名,用 [JsonIgnore] 忽略敏感字段。
七、字节序、协议与底层 C# 技巧(上位机特色)
37, 什么是大小端(Endianness)?C# 怎么处理?
- 大端(Big-Endian):高位在前;小端(Little-Endian):低位在前。
BitConverter.IsLittleEndian可判断本机字节序;跨设备通信要按协议统一转换:
ini
ushort v = 0x1234;
byte[] b = BitConverter.GetBytes(v); // 小端机器得到 [0x34, 0x12]
if (!BitConverter.IsLittleEndian) Array.Reverse(b);
38, 字节数组和结构体如何高效互转?
协议解析常用 MemoryMarshal / BinaryPrimitives,避免逐字节手工拼:
ini
ushort temp = BinaryPrimitives.ReadUInt16BigEndian(buf.AsSpan(2));
int len = BinaryPrimitives.ReadInt32LittleEndian(buf.AsSpan(0));
39, Span<T> / Memory<T> 是什么?为什么上位机值得学?
连续内存的轻量视图,零分配切片,避免复制大字节数组 。解析长帧时切片读取,性能明显优于 SubArray + CopyTo。
40, 手写一个 Modbus CRC16(高频笔试题)
ini
ushort CRC16(byte[] data)
{
ushort crc = 0xFFFF;
foreach (var b in data)
{
crc ^= b;
for (int i = 0; i < 8; i++)
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
return crc;
}
41, TCP 为什么会有粘包?C# 怎么解决?
TCP 是字节流,多条报文可能粘在一起或拆开。常见方案:固定长度、长度前缀(先读 4 字节长度再读 body)、分隔符、协议头标记。
42, 串口通信要注意哪些 C# 细节?
SerialPort 的波特率、数据位、停止位、校验位必须与下位机一致;设 ReadTimeout/WriteTimeout 防卡死;485 注意收发方向(RTS)控制。
八、WPF 与 MVVM(高频)
43, 什么是 MVVM?为什么上位机 UI 用它的多?
Model-View-ViewModel:View 通过绑定对接 ViewModel,逻辑与界面解耦,便于测试和多人协作。
44, INotifyPropertyChanged 是干嘛的?
属性变化后通知 UI 刷新绑定。标准写法:
csharp
private double _temp;
public double Temperature
{
get => _temp;
set { if (_temp != value) { _temp = value; OnPropertyChanged(); } }
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? n = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
进阶可用 CommunityToolkit.Mvvm 的 [ObservableProperty] 源生成器。
45, 依赖属性(DependencyProperty)和普通属性区别?
依赖属性走 WPF 属性系统,支持绑定、样式、动画、继承、默认值,用 GetValue/SetValue 读写;普通属性只是字段封装。
46, 命令(ICommand)是什么?
把按钮行为封装成可绑定命令(如 RelayCommand),View 不直接写事件,UI 与逻辑解耦。
九、异常与调试
47, try/catch/finally 里 finally 一定会执行吗?
正常情况下一定会(用于释放资源);但进程被 Environment.FailFast 强杀或断电等极端情况不会。
48, 上位机程序怎么排查"读不到数据"?
先查物理链路(线、串口号、波特率)→ 再查协议(从站地址、功能码、寄存器地址偏移,很多设备 0-based 与 1-based 差 1)→ 最后看解析与字节序。
49, 日志记录用什么?
Serilog / NLog:分级、落盘、滚动。现场问题靠日志复盘,比打断点实用。
十、常见笔试题自测
50, 实现一个线程安全的单例?
csharp
public sealed class DriverManager
{
private static readonly Lazy<DriverManager> _inst
= new(() => new DriverManager());
public static DriverManager Instance => _inst.Value; // Lazy<T> 内置双重检查锁
private DriverManager() { }
}
51, List<T>、LinkedList<T>、Dictionary<TKey,TValue> 各自场景?
List<T>:随机访问多、尾部增删。LinkedList<T>:频繁中间插入/删除、不随机访问。Dictionary:按 key 快速查找;并发用ConcurrentDictionary。
52, 程序退出时如何优雅释放串口 / Socket / 线程?
using 释放串口与 Socket;后台循环用 CancellationTokenSource.Cancel() 通知退出;task.Wait() 或 await 等待收尾,最后 Dispose 资源。
持续更新中,转载请注明出处,谢谢。