《C# 开发中 20 个真实项目最容易踩的坑,新手到中级都建议看看》
前言
在 C# 开发中,很多问题并不是语法不会,而是一些“看起来没问题”的代码,在真实项目里非常容易出 Bug。
尤其是在 WinForms、ASP.NET、数据库操作、多线程、集合遍历、字符串处理等场景中,一些细节如果处理不好,轻则程序异常,重则数据错乱、界面卡死、数据库被注入攻击。
本文总结几个 C# 开发中非常常见的坑,并附上错误写法、正确写法和原因分析。
一、NullReferenceException:空引用异常
这是 C# 中最经典、最常见的异常之一。
错误写法
User user = GetUser();
Console.WriteLine(user.Name);
如果 GetUser() 返回的是 null,那么访问 user.Name 时就会报错:
System.NullReferenceException: 未将对象引用设置到对象的实例
正确写法
User user = GetUser();
if (user != null)
{
Console.WriteLine(user.Name);
}
else
{
Console.WriteLine("用户不存在");
}
或者使用空条件运算符:
Console.WriteLine(user?.Name);
原因分析
C# 中的引用类型变量存储的是对象地址。如果变量为 null,说明它没有指向任何对象。
常见容易为空的对象有:
DataTable dt;
DataRow row;
List<T> list;
string str;
对象属性;
方法返回值;
所以在访问对象成员之前,要先判断对象是否为 null。
二、字符串比较不要直接使用 == 忽略业务场景
错误写法
string input = "Admin";
if (input == "admin")
{
Console.WriteLine("登录成功");
}
这段代码不会进入判断,因为 C# 默认区分大小写。
正确写法
string input = "Admin";
if (string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("登录成功");
}
原因分析
== 可以比较字符串内容,但它默认是区分大小写的。
在账号、状态码、配置项判断中,如果业务允许忽略大小写,建议使用:
StringComparison.OrdinalIgnoreCase
它比转换大小写更规范。
不推荐这样写:
if (input.ToLower() == "admin")
{
}
因为如果 input 是 null,会直接报空引用异常。
三、List 遍历时删除元素会报错
错误写法
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
foreach (int num in nums)
{
if (num % 2 == 0)
{
nums.Remove(num);
}
}
这段代码会报错:
Collection was modified; enumeration operation may not execute.
正确写法一:倒序 for 循环
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
for (int i = nums.Count - 1; i >= 0; i--)
{
if (nums[i] % 2 == 0)
{
nums.RemoveAt(i);
}
}
正确写法二:RemoveAll
nums.RemoveAll(x => x % 2 == 0);
原因分析
foreach 遍历集合时,集合结构不能被修改。删除元素会改变集合版本号,导致枚举器失效。
如果要删除元素,推荐:
RemoveAll()
或者使用倒序 for 循环。
四、decimal 和 double 不要混用,金额必须用 decimal
错误写法
double price = 0.1;
double total = price * 3;
Console.WriteLine(total);
你可能以为结果是:
0.3
但实际可能是:
0.30000000000000004
正确写法
decimal price = 0.1m;
decimal total = price * 3;
Console.WriteLine(total);
原因分析
double 是二进制浮点数,适合科学计算、图形计算,但不适合金额。
金额、余额、单价、总价、税率等业务数据,应该使用:
decimal
注意 decimal 字面量后面要加 m:
decimal money = 99.99m;
五、DateTime.Now 滥用导致时间逻辑混乱
错误写法
DateTime startTime = DateTime.Now;
// 业务处理
Thread.Sleep(1000);
DateTime endTime = DateTime.Now;
Console.WriteLine((endTime - startTime).TotalSeconds);
这段代码在普通情况下没问题,但如果涉及服务器时间、跨时区、日志、接口对接,就可能出现问题。
推荐写法
DateTime startTime = DateTime.UtcNow;
// 业务处理
Thread.Sleep(1000);
DateTime endTime = DateTime.UtcNow;
Console.WriteLine((endTime - startTime).TotalSeconds);
原因分析
DateTime.Now 获取的是本地时间,会受系统时区影响。
如果是:
日志时间;
接口时间;
定时任务;
跨服务器时间比较;
数据库时间记录;
更推荐使用:
DateTime.UtcNow
如果只是 WinForms 本地显示当前时间,使用 DateTime.Now 没问题。
六、WinForms 中子线程不能直接操作 UI 控件
错误写法
Task.Run(() =>
{
label1.Text = "处理完成";
});
这段代码可能报错:
线程间操作无效: 从不是创建控件的线程访问它
正确写法
Task.Run(() =>
{
string result = "处理完成";
this.Invoke(new Action(() =>
{
label1.Text = result;
}));
});
更严谨写法
private void SetLabelText(string text)
{
if (label1.InvokeRequired)
{
label1.Invoke(new Action(() =>
{
label1.Text = text;
}));
}
else
{
label1.Text = text;
}
}
调用:
Task.Run(() =>
{
SetLabelText("处理完成");
});
原因分析
WinForms 控件只能在创建它的 UI 线程中访问。
后台线程负责耗时操作,UI 更新必须切回主线程。
常见场景:
Task.Run;
Thread;
BackgroundWorker;
Socket 接收数据;
串口回调;
定时任务回调;
这些地方都不能直接操作界面控件。
七、SQL 拼接字符串容易造成 SQL 注入
错误写法
string userName = txtUserName.Text;
string password = txtPassword.Text;
string sql = "select * from Users where UserName = '"
+ userName + "' and Password = '"
+ password + "'";
如果用户输入:
' or '1'='1
就可能绕过登录验证。
正确写法
string sql = "select * from Users where UserName = @UserName and Password = @Password";
using (SqlConnection conn = new SqlConnection(connStr))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@UserName", txtUserName.Text);
cmd.Parameters.AddWithValue("@Password", txtPassword.Text);
conn.Open();
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.Read())
{
Console.WriteLine("登录成功");
}
else
{
Console.WriteLine("账号或密码错误");
}
}
}
原因分析
SQL 拼接有两个问题:
第一,容易 SQL 注入。
第二,字符串中如果包含单引号,SQL 语句会报错。
例如:
O'Connor
如果直接拼接进 SQL,就会破坏 SQL 语法。
所以数据库操作必须优先使用参数化查询。
八、SqlConnection、SqlDataReader 不释放会导致连接池耗尽
错误写法
SqlConnection conn = new SqlConnection(connStr);
SqlCommand cmd = new SqlCommand("select * from Users", conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader["UserName"]);
}
conn.Close();
如果中途异常,conn.Close() 可能不会执行,导致数据库连接没有及时释放。
正确写法
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["UserName"]);
}
}
}
原因分析
SqlConnection、SqlCommand、SqlDataReader 都实现了 IDisposable 接口。
使用 using 可以确保对象在使用完后自动释放,即使中途发生异常,也能执行释放逻辑。
数据库连接是稀缺资源,不能长时间占用。
九、DataTable 取值时没有判断 DBNull
错误写法
DataRow row = dt.Rows[0];
string name = row["Name"].ToString();
int age = Convert.ToInt32(row["Age"]);
如果数据库里的 Age 是 NULL,这段代码可能报错。
正确写法
DataRow row = dt.Rows[0];
string name = row["Name"] == DBNull.Value ? "" : row["Name"].ToString();
int age = row["Age"] == DBNull.Value ? 0 : Convert.ToInt32(row["Age"]);
或者封装一个通用方法:
public static string GetString(DataRow row, string columnName)
{
if (row == null || row[columnName] == DBNull.Value)
{
return string.Empty;
}
return row[columnName].ToString();
}
public static int GetInt(DataRow row, string columnName)
{
if (row == null || row[columnName] == DBNull.Value)
{
return 0;
}
return Convert.ToInt32(row[columnName]);
}
原因分析
数据库中的 NULL 到 C# 中不是 null,而是:
DBNull.Value
所以不能只判断:
row["Name"] == null
这通常是无效的。
十、int.Parse 容易报错,用户输入建议用 TryParse
错误写法
int age = int.Parse(txtAge.Text);
如果用户输入空字符串、中文、特殊符号,就会报异常。
正确写法
int age;
if (int.TryParse(txtAge.Text, out age))
{
Console.WriteLine("年龄:" + age);
}
else
{
MessageBox.Show("请输入正确的年龄");
}
原因分析
Parse 适合确定格式一定正确的场景。
用户输入、接口返回、配置文件读取,建议使用:
TryParse
常见类型都有类似方法:
int.TryParse();
decimal.TryParse();
DateTime.TryParse();
double.TryParse();
十一、异常不要直接吞掉
错误写法
try
{
SaveData();
}
catch
{
}
这种写法非常危险。
程序明明出错了,但你完全不知道错误原因。
正确写法
try
{
SaveData();
}
catch (Exception ex)
{
LogError(ex);
MessageBox.Show("保存失败,请联系管理员");
}
日志方法示例:
public static void LogError(Exception ex)
{
string log = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
+ Environment.NewLine
+ ex.ToString()
+ Environment.NewLine
+ "-----------------------------------"
+ Environment.NewLine;
File.AppendAllText("error.log", log);
}
原因分析
异常处理的核心不是“让程序不报错”,而是:
记录错误
保护程序
提示用户
方便排查
直接空 catch 会让问题隐藏起来,后期排查非常困难。
十二、不要在循环中频繁拼接字符串
错误写法
string result = "";
for (int i = 0; i < 10000; i++)
{
result += i.ToString() + ",";
}
正确写法
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
sb.Append(",");
}
string result = sb.ToString();
原因分析
string 是不可变对象。
每次执行:
result += "内容";
都会创建新的字符串对象。
如果循环次数很多,会产生大量临时对象,影响性能。
大量字符串拼接推荐使用:
StringBuilder
十三、List.Contains 判断对象时可能不符合预期
错误写法
List<User> users = new List<User>();
users.Add(new User { Id = 1, Name = "张三" });
bool exists = users.Contains(new User { Id = 1, Name = "张三" });
Console.WriteLine(exists);
你可能以为结果是:
true
但实际是:
false
原因分析
对于自定义对象,Contains 默认比较的是对象引用地址,而不是属性内容。
两个对象即使属性值一样,只要不是同一个实例,默认就是不相等。
正确写法一:使用 Any
bool exists = users.Any(x => x.Id == 1);
正确写法二:重写 Equals 和 GetHashCode
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public override bool Equals(object obj)
{
User other = obj as User;
if (other == null)
{
return false;
}
return this.Id == other.Id;
}
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
十四、异步方法 async void 慎用
错误写法
public async void LoadData()
{
await Task.Delay(1000);
throw new Exception("加载失败");
}
async void 中的异常很难被外部捕获。
正确写法
public async Task LoadDataAsync()
{
await Task.Delay(1000);
}
调用:
try
{
await LoadDataAsync();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
原因分析
除了事件处理方法之外,不建议使用 async void。
例如 WinForms 按钮事件可以这样写:
private async void btnLoad_Click(object sender, EventArgs e)
{
try
{
await LoadDataAsync();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
普通异步方法应该返回:
Task
Task<T>
这样调用方才能等待它完成,也能捕获异常。
十五、WinForms 界面卡死:耗时任务放在 UI 线程
错误写法
private void btnStart_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
MessageBox.Show("执行完成");
}
点击按钮后,界面会卡住 5 秒,用户无法拖动窗口,也无法点击其他按钮。
正确写法
private async void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
await Task.Run(() =>
{
Thread.Sleep(5000);
});
btnStart.Enabled = true;
MessageBox.Show("执行完成");
}
原因分析
WinForms 是单 UI 线程模型。
如果在 UI 线程执行耗时任务,消息循环会被阻塞,界面就会假死。
耗时操作应该放到后台线程,例如:
Task.Run();
BackgroundWorker;
Thread;
但是更新 UI 时,仍然要回到主线程。
十六、配置文件读取没有判断 Key 是否存在
错误写法
string value = ConfigurationManager.AppSettings["ApiUrl"].ToString();
如果配置文件中没有 ApiUrl,这里会报空引用异常。
正确写法
string value = ConfigurationManager.AppSettings["ApiUrl"];
if (string.IsNullOrEmpty(value))
{
throw new Exception("配置项 ApiUrl 不存在");
}
原因分析
配置文件属于外部输入,不应该默认它一定存在。
尤其是部署到客户现场时,经常会出现:
配置文件缺失
配置项拼写错误
配置值为空
测试环境和正式环境配置不一致
所以读取配置时要做校验。
十七、密码不要明文存储
错误写法
string sql = "insert into Users(UserName, Password) values(@UserName, @Password)";
cmd.Parameters.AddWithValue("@Password", txtPassword.Text);
如果数据库泄露,所有用户密码都会直接暴露。
正确思路
密码不能明文存储,应该存储哈希值。
简单示例:
public static string Sha256(string input)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(input);
byte[] hash = sha256.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
foreach (byte b in hash)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
}
保存密码:
string passwordHash = Sha256(txtPassword.Text);
原因分析
密码安全的基本原则是:
不存明文密码
不直接可逆加密密码
使用哈希算法
最好加盐
实际项目中更推荐使用成熟方案,比如带盐哈希、PBKDF2、BCrypt 等。
十八、AddWithValue 可能导致 SQL Server 类型推断问题
常见写法
cmd.Parameters.AddWithValue("@UserName", userName);
这虽然方便,但在 SQL Server 中可能导致参数类型和数据库字段类型不一致,从而影响索引使用。
更推荐写法
cmd.Parameters.Add("@UserName", SqlDbType.NVarChar, 50).Value = userName;
cmd.Parameters.Add("@Age", SqlDbType.Int).Value = age;
cmd.Parameters.Add("@CreateTime", SqlDbType.DateTime).Value = DateTime.Now;
原因分析
AddWithValue 会根据传入的 C# 值自动推断 SQL 类型。
例如数据库字段是:
varchar(50)
但 C# 字符串可能被推断为:
nvarchar
在某些场景下会影响索引命中,导致查询性能下降。
小项目里可以用 AddWithValue,但对性能要求高的系统,推荐明确指定参数类型和长度。
十九、finally 中不要覆盖原异常
错误写法
try
{
DoWork();
}
catch (Exception ex)
{
throw ex;
}
正确写法
try
{
DoWork();
}
catch
{
throw;
}
原因分析
throw ex; 会重置异常堆栈信息,导致你看不到真正出错的位置。
throw; 会保留原始异常堆栈,更利于排查问题。
二十、判断字符串为空不要只用 == ""
错误写法
if (name == "")
{
Console.WriteLine("姓名为空");
}
如果 name 是 null,这个判断不够完整。
正确写法
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("姓名为空");
}
如果还要判断空格:
if (string.IsNullOrWhiteSpace(name))
{
Console.WriteLine("姓名为空");
}
原因分析
三种情况都可能代表用户没有有效输入:
null
""
" "
推荐根据业务场景选择:
string.IsNullOrEmpty()
string.IsNullOrWhiteSpace()
总结
C# 开发中很多坑并不是语法层面的,而是工程实践层面的。
本文总结的常见问题包括:
1. 空引用异常
2. 字符串比较
3. 集合遍历删除
4. decimal 和 double 的区别
5. DateTime.Now 滥用
6. WinForms 跨线程操作 UI
7. SQL 字符串拼接
8. 数据库连接未释放
9. DBNull 判断
10. Parse 和 TryParse
11. 异常被吞掉
12. 字符串循环拼接
13. 对象 Contains 判断
14. async void 慎用
15. UI 线程卡死
16. 配置文件读取
17. 密码明文存储
18. AddWithValue 类型推断
19. throw ex 丢失堆栈
20. 字符串空值判断
写 C# 代码时,不仅要考虑“能不能运行”,还要考虑:
是否稳定
是否安全
是否易维护
是否方便排查问题
是否适合真实业务场景
真正成熟的开发,不是写出复杂代码,而是把简单代码写得稳定、清晰、可维护。
更多推荐



所有评论(0)