下面是一版 C# 桌面端开发工程师面试题 + 参考答案,按你列的方向整理,适合明天面试前背诵。
一、C# 基础
1. 值类型和引用类型有什么区别?
答:
值类型直接保存数据,常见有 int、double、bool、struct、enum。
引用类型保存对象的引用,对象本身在托管堆上,常见有 class、string、数组、委托、接口。
区别:
-
值类型赋值会复制数据本身
-
引用类型赋值会复制引用地址
-
值类型默认不能为
null -
引用类型默认值是
null -
值类型通常适合小数据结构,引用类型适合复杂对象
示例:
int a = 10;
int b = a;
b = 20;
// a 还是 10
Person p1 = new Person();
Person p2 = p1;
// p1 和 p2 指向同一个对象
2. 什么是装箱和拆箱?
答:
装箱是值类型转换成引用类型。
int a = 10;
object obj = a;
拆箱是引用类型转换回值类型。
int b = (int)obj;
装箱和拆箱会产生性能开销,频繁操作时应避免,比如优先使用泛型集合 List<int>,不要使用 ArrayList。
3. class 和 struct 有什么区别?
答:
class 是引用类型,struct 是值类型。
区别:
-
class对象通常在堆上分配 -
struct通常用于小型数据结构 -
class支持继承,struct不支持继承其他结构或类 -
struct不能显式声明无参构造函数 -
class默认值是null,struct默认值是字段默认值
比如坐标点、颜色、日期等适合用 struct;业务对象如用户、订单适合用 class。
4. ref、out、in 有什么区别?
答:
它们都是按引用传递参数。
ref:传入前必须赋值,方法内可以修改。
void Add(ref int x)
{
x++;
}
out:传入前可以不赋值,但方法内部必须赋值。
void GetValue(out int x)
{
x = 100;
}
in:按引用传递,但方法内部不能修改,适合传递较大的结构体,减少复制成本。
void Print(in int x)
{
Console.WriteLine(x);
}
5. 委托是什么?
答:
委托是类型安全的函数指针,可以把方法当成参数传递。
delegate void MyDelegate(string msg);
void Print(string msg)
{
Console.WriteLine(msg);
}
MyDelegate d = Print;
d("Hello");
常见用途:
-
回调函数
-
事件机制
-
异步回调
-
LINQ
-
UI 事件处理
6. 事件和委托有什么关系?
答:
事件是基于委托实现的。
委托可以在外部直接调用,而事件只能在声明它的类内部触发。
public event EventHandler Click;
桌面开发中,按钮点击、窗口关闭、文本变化,本质上都是事件机制。
7. 泛型有什么作用?
答:
泛型可以让类型参数化,提高代码复用性、类型安全性和性能。
List<int> list = new List<int>();
list.Add(1);
优点:
-
避免强制类型转换
-
避免装箱拆箱
-
编译期检查类型
-
提高代码复用性
常见泛型类型有:
List<T>
Dictionary<TKey, TValue>
Nullable<T>
Task<T>
8. 异常处理机制是什么?
答:
C# 使用 try-catch-finally 处理异常。
try
{
// 可能出错的代码
}
catch (Exception ex)
{
// 记录日志或提示用户
}
finally
{
// 释放资源
}
执行逻辑:
-
没异常:执行
try,再执行finally -
有异常并捕获:执行
catch,再执行finally -
有异常未捕获:执行
finally,然后继续向上抛出
9. throw 和 throw ex 有什么区别?
答:
throw 会保留原始异常堆栈。
catch (Exception)
{
throw;
}
throw ex 会重置堆栈信息,不利于定位真实错误位置。
catch (Exception ex)
{
throw ex;
}
实际开发中推荐使用 throw。
二、桌面开发 WinForms / WPF
10. WinForms 和 WPF 有什么区别?
答:
WinForms 是传统桌面 UI 框架,基于控件和事件,开发简单,适合企业管理系统。
WPF 是基于 XAML 的 UI 框架,支持数据绑定、样式、模板、动画和 MVVM。
区别:
-
WinForms 上手快,适合简单界面
-
WPF 表现力更强,适合复杂 UI
-
WPF 支持数据绑定和 MVVM
-
WPF 更容易做界面和逻辑分离
11. 为什么不能在子线程直接更新 UI?
答:
WinForms 和 WPF 的 UI 控件只能由创建它们的 UI 线程访问。
如果子线程直接修改 UI,可能出现跨线程异常。
WinForms:
this.Invoke(new Action(() =>
{
label1.Text = "完成";
}));
WPF:
Dispatcher.Invoke(() =>
{
textBlock.Text = "完成";
});
12. 什么是 UI 线程?
答:
UI 线程是负责创建和刷新界面的线程。
桌面程序中,窗口、按钮、文本框等控件都运行在 UI 线程上。
如果在 UI 线程执行耗时操作,比如数据库查询、文件读写、网络请求,就会导致界面卡死。
解决方式是把耗时操作放到后台线程,完成后再切回 UI 线程更新界面。
13. WPF 数据绑定是什么?
答:
数据绑定是 UI 和数据对象之间的自动同步机制。
<TextBox Text="{Binding UserName}" />
ViewModel:
public string UserName { get; set; }
常见绑定模式:
-
OneWay:数据源到界面 -
TwoWay:界面和数据源双向同步 -
OneTime:只绑定一次 -
OneWayToSource:界面到数据源
14. 什么是 MVVM?
答:
MVVM 是 WPF 常用架构模式。
-
Model:数据模型 -
View:界面 -
ViewModel:界面逻辑和绑定数据
优点:
-
UI 和业务逻辑解耦
-
方便维护
-
方便单元测试
-
减少后台代码
-
适合复杂桌面项目
15. WPF 中 Command 的作用是什么?
答:
Command 用于把按钮点击等操作绑定到 ViewModel,替代传统事件。
<Button Content="保存" Command="{Binding SaveCommand}" />
好处:
-
符合 MVVM
-
减少 View 后台代码
-
可以控制按钮是否可用
-
逻辑更容易测试
16. 什么是依赖属性 DependencyProperty?
答:
依赖属性是 WPF 的属性系统,用于支持数据绑定、样式、动画、属性值继承等功能。
普通属性:
public string Name { get; set; }
依赖属性:
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MyControl),
new PropertyMetadata("")
);
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
常用于自定义控件,让属性支持绑定和样式设置。
17. INotifyPropertyChanged 接口有什么作用?
答:
INotifyPropertyChanged 用来通知 UI:某个属性发生变化了。
如果 ViewModel 的属性变化后希望界面自动刷新,就需要实现它。
public class MainViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
面试可以这样说:
WPF 数据绑定时,如果后台属性变化后要通知界面刷新,通常需要实现
INotifyPropertyChanged。
三、异步、多线程、Task
18. 进程和线程有什么区别?
答:
进程是程序运行的基本单位,有独立的内存空间。
线程是进程中的执行单元,一个进程可以有多个线程。
区别:
-
进程之间相互独立
-
同一进程内的线程共享内存
-
线程切换成本比进程低
-
多线程共享数据时要注意线程安全
19. Thread 和 Task 有什么区别?
答:
Thread 是底层线程,需要手动创建和管理。
Thread t = new Thread(() =>
{
Console.WriteLine("运行");
});
t.Start();
Task 是更高级的任务抽象,通常使用线程池,适合异步和并发开发。
await Task.Run(() =>
{
Console.WriteLine("运行");
});
实际项目中更推荐使用 Task、async、await。
20. async 和 await 是什么?
答:
async 表示一个方法是异步方法。
await 表示等待一个异步任务完成,但不会阻塞当前线程。
private async void Button_Click(object sender, EventArgs e)
{
string data = await GetDataAsync();
textBox.Text = data;
}
桌面程序中使用 async/await 可以避免 UI 卡死。
21. Task.Run 的作用是什么?
答:
Task.Run 用于把 CPU 密集型或耗时同步操作放到线程池中执行。
await Task.Run(() =>
{
// 耗时计算
});
常见场景:
-
大量数据计算
-
文件处理
-
图片处理
-
不支持异步 API 的耗时操作
但如果是数据库、网络请求,优先使用真正的异步方法,比如 ExecuteReaderAsync()、HttpClient.GetAsync()。
22. 多线程会带来什么问题?
答:
多线程常见问题有:
-
线程安全问题
-
死锁
-
资源竞争
-
UI 跨线程访问异常
-
调试困难
-
任务取消和异常处理复杂
解决方式:
-
使用
lock -
使用线程安全集合
-
使用
SemaphoreSlim -
使用
CancellationToken -
UI 更新切回主线程
-
尽量使用
Task和async/await
23. 什么是线程安全?
答:
多个线程同时访问同一份数据时,程序结果仍然正确,就叫线程安全。
非线程安全示例:
count++;
count++ 不是原子操作,多个线程同时执行可能导致结果错误。
可以使用 lock:
private readonly object _lock = new object();
lock (_lock)
{
count++;
}
也可以使用原子操作:
Interlocked.Increment(ref count);
24. 什么是死锁?
答:
死锁是多个线程互相等待对方释放资源,导致程序一直卡住。
常见原因:
-
多个锁顺序不一致
-
在 UI 线程同步等待异步任务
-
锁内执行耗时操作
典型错误:
var result = GetDataAsync().Result;
在 UI 程序中可能导致死锁。
推荐:
var result = await GetDataAsync();
25. 多线程之间如何通信?
答:
常见方式:
-
共享变量 +
lock -
Monitor.Wait/Monitor.Pulse -
AutoResetEvent -
ManualResetEvent -
SemaphoreSlim -
BlockingCollection -
ConcurrentQueue -
事件回调
-
SynchronizationContext -
UI 框架中的
Dispatcher或Invoke
桌面开发中最常见的是:
后台线程处理数据,完成后通过
Dispatcher.Invoke或Control.Invoke通知 UI 线程更新界面。
26. 什么是协程?
答:
C# 标准语境里不常直接说"协程",更多使用 async/await、迭代器或状态机。
如果是 Unity 开发,协程通常指 IEnumerator:
IEnumerator LoadData()
{
yield return new WaitForSeconds(1);
Debug.Log("完成");
}
在普通 C#/.NET 桌面开发中,async/await 可以理解为一种类似协程的异步编程模型,它可以在等待耗时操作时让出执行权,完成后再继续执行后面的代码。
四、进程间通信 IPC
27. 什么是进程间通信?
答:
进程间通信就是不同进程之间交换数据。
因为不同进程内存空间相互独立,不能像线程一样直接共享变量,所以需要 IPC 机制。
常见方式:
-
命名管道
NamedPipe -
Socket
-
HTTP API
-
gRPC
-
消息队列
-
共享内存
-
文件
-
Windows 消息
-
COM
28. 桌面程序常用哪些进程间通信方式?
答:
常见选择:
-
本机两个程序通信:命名管道
-
跨机器通信:Socket、HTTP、gRPC
-
简单数据交换:文件、数据库
-
Windows 桌面程序集成:Windows 消息、COM
面试可以说:
如果是同一台机器上的两个 C# 程序通信,我会优先考虑命名管道;如果需要跨机器通信,我会考虑 Socket、HTTP 或 gRPC。
五、数据库
29. ADO.NET 操作数据库的流程是什么?
答:
基本流程:
-
创建连接
SqlConnection -
创建命令
SqlCommand -
打开连接
-
执行 SQL
-
读取结果
-
关闭连接
示例:
using SqlConnection conn = new SqlConnection(connStr);
using SqlCommand cmd = new SqlCommand("SELECT * FROM Users", conn);
conn.Open();
using SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
推荐使用 using 自动释放连接资源。
30. 如何防止 SQL 注入?
答:
使用参数化查询,不要拼接 SQL。
错误写法:
string sql = "SELECT * FROM Users WHERE Name = '" + name + "'";
正确写法:
string sql = "SELECT * FROM Users WHERE Name = @Name";
cmd.Parameters.AddWithValue("@Name", name);
参数化查询可以避免用户输入被当成 SQL 语句执行。
31. 什么是事务?
答:
事务用于保证一组数据库操作要么全部成功,要么全部失败。
事务有 ACID 特性:
-
原子性:全部成功或全部失败
-
一致性:数据从一个正确状态变成另一个正确状态
-
隔离性:事务之间互不干扰
-
持久性:提交后数据永久保存
示例:
SqlTransaction tran = conn.BeginTransaction();
try
{
cmd.Transaction = tran;
cmd.ExecuteNonQuery();
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
常见场景:转账、订单创建、库存扣减。
32. ExecuteNonQuery、ExecuteScalar、ExecuteReader 区别?
答:
ExecuteNonQuery:执行增删改,返回受影响行数。
cmd.ExecuteNonQuery();
ExecuteScalar:返回单个值,比如数量、ID。
int count = (int)cmd.ExecuteScalar();
ExecuteReader:读取多行多列数据。
SqlDataReader reader = cmd.ExecuteReader();
六、数据结构
33. 数组有什么特点?
答:
数组是一块连续内存,长度固定。
优点:
-
按下标访问快,时间复杂度
O(1) -
结构简单
缺点:
-
长度固定
-
中间插入和删除成本高,需要移动元素
适合场景:
-
数据数量固定
-
高频按下标访问
34. 哈希表有什么特点?
答:
哈希表通过哈希函数把 Key 映射到存储位置。
C# 中常用:
Dictionary<TKey, TValue>
Hashtable
特点:
-
查询快,平均
O(1) -
Key 唯一
-
不保证顺序
-
哈希冲突时需要处理冲突
实际开发中常用 Dictionary<TKey, TValue> 做缓存、映射、快速查找。
35. 链表有什么特点?
答:
链表由一个个节点组成,每个节点保存数据和指向下一个节点的引用。
特点:
-
插入、删除节点方便
-
不需要连续内存
-
按下标访问慢,需要从头遍历
-
查询复杂度通常是
O(n)
C# 中有:
LinkedList<T>
数组适合查询多,链表适合插入删除多。
七、项目经验题
36. 桌面程序界面卡死怎么排查?
答:
界面卡死通常是 UI 线程执行了耗时操作。
常见原因:
-
数据库查询太慢
-
文件读写耗时
-
网络请求阻塞
-
一次性加载大量数据
-
循环计算放在 UI 线程
解决方式:
-
使用
async/await -
耗时任务放到后台线程
-
分页加载数据
-
表格使用虚拟化
-
减少 UI 频繁刷新
-
增加日志定位耗时点
37. 如何做异常日志?
答:
一般会捕获异常并记录:
-
异常时间
-
异常类型
-
异常消息
-
堆栈信息
-
当前用户
-
操作步骤
-
输入参数
可以使用:
-
log4net -
NLog -
Serilog -
自定义文件日志
示例:
try
{
SaveData();
}
catch (Exception ex)
{
logger.Error(ex, "保存数据失败");
MessageBox.Show("保存失败,请联系管理员");
}
用户看到友好提示,开发人员通过日志定位问题。
38. 如何优化桌面程序性能?
答:
可以从几个方向优化:
-
UI 线程不执行耗时操作
-
大数据使用分页加载
-
表格开启虚拟化
-
数据库 SQL 加索引
-
减少重复查询,适当使用缓存
-
避免频繁创建大对象
-
及时释放资源
-
减少不必要的 UI 刷新
-
使用异步加载
-
通过日志或性能分析工具定位瓶颈
39. 表格数据很多时怎么展示?
答:
不能一次性全部加载到界面。
常见优化方式:
-
分页查询
-
滚动加载
-
虚拟化显示
-
后台异步加载
-
只加载必要字段
-
数据库层面加索引
-
搜索条件限制数据范围
WPF 中可以开启虚拟化:
<DataGrid EnableRowVirtualization="True"
EnableColumnVirtualization="True" />
40. 如果上线后程序报错,你怎么处理?
答:
我会这样排查:
-
查看日志,确认异常信息和堆栈
-
了解用户操作步骤
-
检查输入数据、配置、数据库和网络环境
-
本地尝试复现
-
定位代码问题并修复
-
增加异常保护和日志,避免再次难以排查
面试可以补一句:
我不会只看用户描述,会优先看日志和堆栈,因为这样定位更准确。
八、综合高频题
41. 如何避免资源泄露?
答:
对实现了 IDisposable 的对象使用 using。
using FileStream fs = new FileStream(path, FileMode.Open);
常见需要释放的资源:
-
数据库连接
-
文件流
-
网络连接
-
图片对象
-
Socket
-
定时器
42. List<T> 和数组有什么区别?
答:
数组长度固定,访问速度快。
List<T> 底层也是数组,但可以动态扩容,使用更方便。
区别:
-
数组长度固定
-
List<T>长度可变 -
List<T>支持Add、Remove、Find -
高频固定长度数据可以用数组
-
业务集合通常用
List<T>
43. Dictionary 和 List 查询有什么区别?
答:
List 查找通常需要遍历,复杂度是 O(n)。
Dictionary 通过 Key 查找,平均复杂度是 O(1)。
如果需要通过编号、名称、ID 快速查找对象,适合用 Dictionary。
44. 面试中怎么介绍自己的 C# 桌面项目?
答:
可以这样说:
我做过一个 C# 桌面端管理系统,使用 WinForms/WPF 开发界面,数据库使用 SQL Server/MySQL。项目中我主要负责界面功能、数据增删改查、表格展示、用户输入校验、异常日志和部分性能优化。
项目里遇到过界面卡顿问题,后来通过
async/await、后台任务、分页查询和减少 UI 刷新来优化。对于异常问题,我会记录日志,包括异常信息、堆栈和用户操作,方便后续排查。
明天面试时你可以重点背这几个关键词:
值类型/引用类型、委托/事件、泛型、try-catch-finally、WinForms/WPF、UI线程、MVVM、数据绑定、INotifyPropertyChanged、依赖属性、Task、async/await、线程安全、lock、多线程通信、ADO.NET、参数化查询、事务、分页加载、异常日志。
下面是一套适合 C# 桌面端开发工程师 面试前突击复习的题目和参考答案,重点覆盖:C# 基础、.NET、WinForms/WPF、线程、异步、数据库、MVVM、性能与项目经验。
一、C# 基础
1. C# 中值类型和引用类型有什么区别?
答案:
值类型直接存储数据,通常分配在栈上,例如:
int a = 10;
struct Point { public int X; public int Y; }
引用类型存储的是对象的引用,对象本身通常分配在托管堆上,例如:
class Person { public string Name; }
主要区别:
-
值类型赋值时复制数据本身
-
引用类型赋值时复制引用地址
-
值类型默认不能为
null,除非使用Nullable<T> -
常见值类型:
int、double、bool、struct、enum -
常见引用类型:
class、string、array、delegate、interface
2. ref 和 out 有什么区别?
答案:
ref 和 out 都是按引用传递参数。
区别:
-
ref参数在传入前必须初始化 -
out参数在方法内部必须赋值 -
ref更适合修改已有变量 -
out更适合返回多个结果
示例:
void Add(ref int x)
{
x += 10;
}
void GetResult(out int result)
{
result = 100;
}
3. const 和 readonly 有什么区别?
答案:
const 是编译期常量,声明时必须赋值,之后不能修改。
public const double PI = 3.14;
readonly 是运行时常量,可以在声明时或构造函数中赋值。
public readonly string Name;
public Person(string name)
{
Name = name;
}
区别:
-
const是静态的,属于类型 -
readonly可以是实例字段 -
const只能用于基础常量类型 -
readonly可以用于对象引用
4. string 和 StringBuilder 有什么区别?
答案:
string 是不可变对象,每次拼接都会产生新的字符串对象。
string s = "";
s += "A";
s += "B";
大量拼接时性能较差。
StringBuilder 是可变字符串对象,适合频繁拼接。
StringBuilder sb = new StringBuilder();
sb.Append("A");
sb.Append("B");
如果只是少量字符串拼接,使用 string 即可;如果在循环中大量拼接,推荐使用 StringBuilder。
二、面向对象
5. C# 中接口和抽象类有什么区别?
答案:
接口定义能力,抽象类定义公共基础。
接口:
interface ILogger
{
void Log(string message);
}
抽象类:
abstract class Animal
{
public abstract void Speak();
public void Eat()
{
Console.WriteLine("Eat");
}
}
区别:
-
一个类可以实现多个接口,但只能继承一个类
-
接口更强调行为规范
-
抽象类可以包含字段、构造函数、普通方法
-
抽象类适合有公共代码复用的场景
-
接口适合解耦和扩展
6. 什么是多态?
答案:
多态是指同一个方法调用,在不同对象上表现出不同的行为。
常见实现方式:
-
方法重写
override -
接口实现
-
虚方法
virtual
示例:
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog");
}
}
调用:
Animal animal = new Dog();
animal.Speak(); // 输出 Dog
三、集合与泛型
7. ArrayList 和 List<T> 有什么区别?
答案:
ArrayList 是非泛型集合,存储的是 object,使用时可能发生装箱、拆箱,类型不安全。
ArrayList list = new ArrayList();
list.Add(1);
list.Add("abc");
List<T> 是泛型集合,类型安全,性能更好。
List<int> list = new List<int>();
list.Add(1);
实际项目中一般使用 List<T>。
8. Dictionary<TKey, TValue> 的底层原理是什么?
答案:
Dictionary 基于哈希表实现,通过 Key 的哈希值快速定位数据。
特点:
-
查询速度快,平均时间复杂度接近
O(1) -
Key 必须唯一
-
Key 类型应正确实现
GetHashCode()和Equals() -
不保证顺序
常用于缓存、映射关系、快速查找。
四、异常处理
9. try-catch-finally 的执行顺序是什么?
答案:
正常情况下:
try
{
// 执行业务代码
}
catch
{
// 捕获异常
}
finally
{
// 无论是否异常都会执行
}
执行顺序:
-
没有异常:
try→finally -
有异常且被捕获:
try→catch→finally -
有异常但未捕获:
try→finally→ 向上抛出异常
finally 常用于释放资源,例如关闭文件、数据库连接。
五、委托、事件、Lambda
10. 什么是委托?
答案:
委托可以理解为类型安全的函数指针,用于把方法作为参数传递。
delegate void MyDelegate(string message);
void Print(string msg)
{
Console.WriteLine(msg);
}
MyDelegate del = Print;
del("Hello");
常见用途:
-
回调函数
-
事件机制
-
LINQ
-
异步编程
11. 事件和委托有什么关系?
答案:
事件是基于委托实现的。
委托可以在外部直接调用,而事件只能在声明它的类内部触发。
public event EventHandler Click;
事件常用于桌面开发中的按钮点击、窗口关闭、数据变化通知等场景。
六、线程与异步
12. 线程和进程有什么区别?
答案:
进程是程序运行的基本单位,拥有独立的内存空间。
线程是进程中的执行单元,多个线程共享同一个进程的资源。
区别:
-
一个进程可以有多个线程
-
进程之间相互独立
-
线程之间共享内存
-
线程切换成本比进程低
-
多线程需要注意线程安全问题
13. Task 和 Thread 有什么区别?
答案:
Thread 是比较底层的线程创建方式。
Thread thread = new Thread(() =>
{
Console.WriteLine("Run");
});
thread.Start();
Task 是更高级的任务抽象,基于线程池,更适合异步和并发操作。
await Task.Run(() =>
{
Console.WriteLine("Run");
});
实际项目中更推荐使用 Task 和 async/await。
14. async 和 await 的作用是什么?
答案:
async/await 用于简化异步编程,让异步代码写起来像同步代码。
private async void Button_Click(object sender, EventArgs e)
{
string result = await GetDataAsync();
MessageBox.Show(result);
}
优点:
-
避免阻塞 UI 线程
-
提高程序响应性
-
代码可读性更好
桌面程序中,耗时操作如网络请求、数据库查询、文件读写,应该使用异步方式,避免界面卡死。
15. 为什么不能在子线程直接更新 UI?
答案:
WinForms 和 WPF 的 UI 控件都只能由创建它们的 UI 线程访问。
如果在子线程直接更新 UI,可能会抛出跨线程访问异常。
WinForms 中可以使用:
this.Invoke(new Action(() =>
{
label1.Text = "完成";
}));
WPF 中可以使用:
Dispatcher.Invoke(() =>
{
TextBlock.Text = "完成";
});
七、WinForms / WPF 桌面开发
16. WinForms 和 WPF 有什么区别?
答案:
WinForms 是较早的桌面 UI 框架,基于控件和事件驱动,开发简单,适合传统企业管理系统。
WPF 是基于 XAML 的 UI 框架,支持数据绑定、样式、模板、动画和 MVVM 架构。
区别:
-
WinForms 上手简单,适合快速开发
-
WPF UI 表现力更强
-
WPF 支持数据绑定和 MVVM
-
WPF 更适合复杂界面和可维护性要求高的项目
17. WPF 中什么是数据绑定?
答案:
数据绑定是 UI 和数据源之间的自动同步机制。
示例:
<TextBox Text="{Binding UserName}" />
ViewModel:
public string UserName { get; set; }
当数据源变化时,UI 可以自动更新;当用户修改 UI 时,也可以同步到数据源。
常见绑定模式:
-
OneWay -
TwoWay -
OneTime -
OneWayToSource
18. 什么是 MVVM?
答案:
MVVM 是 WPF 常用架构模式,分为:
-
Model:数据模型 -
View:界面 -
ViewModel:界面逻辑和数据绑定
优点:
-
UI 和业务逻辑解耦
-
方便单元测试
-
代码更清晰
-
更适合复杂桌面项目
View 通过 Binding 绑定 ViewModel,按钮事件通常通过 Command 处理。
19. WPF 中 INotifyPropertyChanged 有什么作用?
答案:
INotifyPropertyChanged 用于通知 UI 属性发生变化。
示例:
public class MainViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
如果没有实现它,后台属性变化后,界面可能不会自动刷新。
20. WPF 中 Command 有什么作用?
答案:
Command 用于替代传统事件处理,把按钮点击等操作绑定到 ViewModel。
<Button Content="保存" Command="{Binding SaveCommand}" />
优点:
-
减少后台代码
-
方便 MVVM 解耦
-
可以控制按钮是否可用
-
便于测试业务逻辑
八、数据库与文件操作
21. ADO.NET 操作数据库的一般流程是什么?
答案:
一般流程:
-
创建数据库连接
SqlConnection -
创建命令对象
SqlCommand -
打开连接
-
执行 SQL
-
读取结果
-
关闭连接
示例:
using SqlConnection conn = new SqlConnection(connStr);
using SqlCommand cmd = new SqlCommand("SELECT * FROM Users", conn);
conn.Open();
using SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
推荐使用 using 自动释放数据库连接。
22. 如何防止 SQL 注入?
答案:
不要直接拼接 SQL 字符串,应使用参数化查询。
错误写法:
string sql = "SELECT * FROM Users WHERE Name = '" + name + "'";
正确写法:
string sql = "SELECT * FROM Users WHERE Name = @Name";
cmd.Parameters.AddWithValue("@Name", name);
参数化查询可以避免用户输入被当成 SQL 语句执行。
九、常见项目问题
23. 桌面程序界面卡死一般是什么原因?
答案:
通常是因为在 UI 线程执行了耗时操作,例如:
-
大量数据处理
-
数据库查询
-
网络请求
-
文件读写
-
循环计算
解决方式:
-
使用
Task.Run -
使用
async/await -
分页加载数据
-
后台线程处理,UI 线程只负责界面更新
24. 你如何优化一个 C# 桌面程序?
答案:
可以从几个方面优化:
-
避免 UI 线程执行耗时任务
-
大量数据使用分页或虚拟化
-
减少不必要的控件刷新
-
使用异步加载数据
-
及时释放数据库连接、文件流等资源
-
避免频繁创建大对象
-
使用缓存减少重复查询
-
使用日志定位性能瓶颈
25. 如果程序上线后出现异常,你怎么排查?
答案:
我会按以下步骤排查:
-
查看日志,确认异常类型和堆栈信息
-
确认用户操作路径和输入数据
-
本地尝试复现问题
-
检查数据库、配置文件、网络环境
-
定位代码问题并修复
-
增加异常处理和日志,避免同类问题再次难以排查
十、面试高频简答
26. 什么是垃圾回收 GC?
答案:
GC 是 .NET 的自动内存管理机制,用于回收不再被引用的托管对象。
它可以减少手动释放内存的复杂度,但并不代表所有资源都不用释放。
例如数据库连接、文件流、Socket 等非托管资源,仍然应该使用 Dispose() 或 using 及时释放。
27. IDisposable 有什么作用?
答案:
IDisposable 用于释放资源,尤其是非托管资源。
常见对象:
-
FileStream -
SqlConnection -
SqlDataReader -
Bitmap -
Socket
推荐写法:
using FileStream fs = new FileStream("a.txt", FileMode.Open);
using 结束后会自动调用 Dispose()。
28. == 和 Equals() 有什么区别?
答案:
== 是运算符,可以被重载。
Equals() 是对象方法,用于判断两个对象是否相等。
对于引用类型,默认情况下通常比较引用地址。
对于 string,== 和 Equals() 通常比较字符串内容,因为 string 重载了 ==。
29. 什么是装箱和拆箱?
答案:
装箱是值类型转换为引用类型。
int a = 10;
object obj = a;
拆箱是引用类型转换回值类型。
int b = (int)obj;
装箱和拆箱会带来性能开销,频繁操作时应尽量避免。
30. 你做过的桌面项目可以怎么介绍?
答案模板:
我之前做过一个基于 C# 的桌面端管理系统,主要使用 WinForms/WPF 开发界面,数据库使用 SQL Server/MySQL。
项目中我主要负责:
-
界面功能开发
-
数据增删改查
-
表格数据展示和分页
-
用户输入校验
-
异常处理和日志记录
-
部分耗时操作的异步处理
项目中遇到过界面卡顿问题,后来通过 async/await、后台任务和分页加载进行了优化,提升了用户操作体验。
面试回答技巧
你可以重点准备这几个方向:
-
C# 基础:值类型、引用类型、委托、事件、泛型、异常
-
桌面开发:WinForms/WPF、UI 线程、数据绑定、MVVM
-
异步线程:
Task、Thread、async/await -
数据库:ADO.NET、参数化查询、事务
-
项目经验:界面卡顿、异常日志、性能优化、数据展示
如果明天面的是初中级 C# 桌面端岗位,能把上面这些问题讲清楚,基本可以覆盖大部分面试内容。