C# 上位机(持续更新中)

转载请注明出处,谢谢。

以 C# 为主,协议 / 多线程 / WPF 穿插其中,适合上位机(Host/PC 端)开发岗位面试复习。

一、C# 基础

1, C# 中值类型和引用类型的区别?

值类型(intdoublestructenumbool)存于栈,赋值拷贝整个值;引用类型(class、数组、字符串、委托)存于堆,变量只持有引用地址。

上位机里寄存器结构体常用 struct (值类型、无 GC 压力),业务对象用 class

2, 什么是装箱和拆箱?上位机场景要注意什么?

装箱:值类型转 object / 接口,会在堆上分配对象;拆箱:反向转换,有类型检查开销。

高频采集时避免 ArrayList(里面全是 object,频繁装箱),改用泛型 List<T>

3, string 是值类型还是引用类型?为什么拼接要用 StringBuilder

引用类型,但不可变(immutable) :每次 + 都生成新对象,旧对象等 GC。

协议组帧时大量拼接字节/字符串,用 StringBuilder 复用缓冲区,减少分配。

4, constreadonly 有什么区别?

const 编译期常量,只能用于基本类型,且默认 staticreadonly 运行期赋值,可在构造函数里初始化,更灵活。

配置类里设备地址、端口这类可能变化的量用 readonly 或配置文件,别写死成 const

5, ==Equals 的区别?

== 对值类型是值比较,对引用类型默认比较引用地址;Equals 可被重写做内容比较(string 已重写,比较字符内容)。

比较字节数组内容别用 ==,用 Enumerable.SequenceEqualSystem.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, outref 参数有什么区别?

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, ActionFuncPredicate 是什么?

内置泛型委托:Action 无返回值,Func 有返回值,Predicate<T> 返回 bool。写回调、过滤时不用每次都定义 delegate。

四、多线程与异步(上位机重中之重)

20, ThreadTaskasync/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, lockMonitor 是什么?有什么注意点?

lockMonitor.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 类做什么用?

提供原子操作:IncrementDecrementExchangeCompareExchange,无锁地做计数/标志切换,性能高于 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, 什么是 IDisposableusing

IDisposable 用于释放非托管资源(串口、Socket、文件句柄);using 自动调用 Dispose

csharp 复制代码
using var port = new SerialPort("COM3", 9600);
port.Open();
// 离开作用域自动 Dispose,关闭串口

32, usingtry/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/finallyfinally 一定会执行吗?

正常情况下一定会(用于释放资源);但进程被 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 资源。

持续更新中,转载请注明出处,谢谢。

相关推荐
风雨_1 小时前
Git Worktree sourcetree完整使用指南
前端
半个落月1 小时前
React 组件通信进阶:useContext 与自定义 Hook 实战详解
前端·react.js
用户921080262861 小时前
ChatComposer 输入区改造:基于技能市场实现 Agent 输入框
前端
用户45989204565161 小时前
一套 KMP 多仓库版本治理工作流:用 Version Catalog + CI 把发版流水线自动化
前端·kotlin
小赵同学WoW1 小时前
1-4 TypeScript 中的 `number`、`bigint` 与 `string`
前端
小赵同学WoW1 小时前
1-3 `boolean` 与字面量类型
前端
光影少年1 小时前
react navite 长列表优化:FlatList / SectionList 原理与优化属性
前端·react native·react.js
cindershade1 小时前
鼠标 3D 倾斜卡片
前端
wordbaby1 小时前
从双端内卷到一库多端:基于 pnpm + Taro 4 + RNOH 鸿蒙的 Monorepo 跨端架构实战
前端·微信小程序·app