第3章:C# 核心语法速通
这一章,我们聊聊C#里最绕不开的几个核心点。说实话,很多新手甚至工作两三年的朋友,在这几个地方都容易栽跟头。我当年带团队做代码评审时,光是值类型和引用类型的误用,就够我写半本避坑指南了。
3.1 类型系统:值类型 vs 引用类型
先问个问题:int是类吗?string是值类型吗?
很多人会答错。int是结构体,是值类型。string是引用类型,但它用起来像值类型——这就是陷阱所在。
3.1.1 核心区别
| 特性 | 值类型 | 引用类型 |
|---|---|---|
| 存储位置 | 栈(通常) | 堆 |
| 赋值行为 | 复制值 | 复制引用 |
| 默认值 | 0 / false / null?(可空) | null |
| 典型例子 | int, bool, struct, enum | class, string, array, delegate |
我个人习惯用一句话记:值类型是「你的就是你的」,引用类型是「你的可能是别人的」。
3.1.2 避坑:struct 滥用
我曾经接手过一个项目,里面定义了几十个struct,每个都超过64字节。结果每次方法传参都在栈上复制一大块数据,性能惨不忍睹。记住:struct 超过16字节,就该考虑用class了。
3.1.3 string 的特殊性
string是引用类型,但它是不可变的。每次拼接都会创建新对象。我见过有人用 + 号在循环里拼字符串,结果内存暴涨。正确做法是用 StringBuilder。
// 错误示范
string s = "";
for (int i = 0; i < 10000; i++)
{
s += i.ToString(); // 每次创建新对象
}
// 正确做法
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
}
string result = sb.ToString();
3.2 异步编程:async/await 实战
异步编程,说白了就是「不等了,先干别的」。但很多人用成了「同步代码加个async关键字」——这完全没用。
3.2.1 核心原则
- async 方法要返回 Task 或 Task<T>,不要用 void(事件处理除外)
- await 会释放线程,不是阻塞等待
- 不要用 .Result 或 .Wait(),会死锁
我记得有一次线上事故,就是因为有人用 .Result 在UI线程上等异步结果,直接死锁了。排查了整整一下午。
3.2.2 最佳实践
public async Task<User> GetUserAsync(int id)
{
// 异步获取数据
var user = await _dbContext.Users.FindAsync(id);
// 异步处理业务
var orders = await _orderService.GetOrdersAsync(user.Id);
user.OrderCount = orders.Count;
return user;
}
3.2.3 避坑:ConfigureAwait
在类库代码中,我建议加上 ConfigureAwait(false)。为什么?因为默认情况下,await 会尝试回到原来的上下文(比如UI线程),这在类库中完全没必要,还会增加死锁风险。
// 类库中的推荐写法
await _httpClient.GetStringAsync(url).ConfigureAwait(false);
3.3 LINQ 基础:查询的艺术
LINQ 让数据操作变得优雅。但优雅不等于高效。我见过太多人用 LINQ 写出 O(n²) 的代码而不自知。
3.3.1 两种写法
| 方式 | 示例 | 适用场景 |
|---|---|---|
| 查询语法 | from u in users where u.Age > 18 select u | 简单查询,类似SQL |
| 方法语法 | users.Where(u => u.Age > 18) | 复杂操作,链式调用 |
我个人偏爱方法语法,因为可以配合 lambda 做更灵活的操作。但查询语法在 group by 场景下更直观。
3.3.2 常见陷阱
- 延迟执行:LINQ 查询不会立即执行,直到你调用 ToList()、First() 等。这意味着如果你多次遍历同一个查询,它会重复执行。
- N+1 问题:在循环里用 LINQ 查数据库,每次查询都发一条 SQL。我曾经在代码评审里看到过这种写法,直接让接口响应时间从 200ms 飙到 5s。
3.3.3 实用示例
// 分组统计
var grouped = orders
.GroupBy(o => o.Category)
.Select(g => new
{
Category = g.Key,
Total = g.Sum(o => o.Amount),
Count = g.Count()
});
// 多条件筛选
var result = products
.Where(p => p.Price > 100 && p.Stock > 0)
.OrderByDescending(p => p.Sales)
.Take(10);
3.4 异常处理最佳实践
异常处理,不是简单的 try-catch。我见过最离谱的代码,是整个方法包在一个 try-catch 里,然后 catch 里只写一句 return null。这叫「吞异常」,是生产环境的大忌。
3.4.1 什么时候用 try-catch
- 外部资源访问:文件、网络、数据库
- 用户输入解析:类型转换、格式验证
- 第三方库调用:你不知道它会抛什么
业务逻辑里,能用 if 判断的,就别用 try-catch。异常的开销很大,不仅仅是性能,还有代码可读性。
3.4.2 正确姿势
public async Task<Order> GetOrderAsync(int orderId)
{
try
{
var order = await _dbContext.Orders.FindAsync(orderId);
if (order == null)
{
throw new NotFoundException($"订单 {orderId} 不存在");
}
return order;
}
catch (NotFoundException)
{
// 自定义异常,直接往上抛
throw;
}
catch (DbException ex)
{
// 数据库异常,记录日志后重新包装
_logger.LogError(ex, "数据库查询失败");
throw new ApplicationException("系统繁忙,请稍后重试", ex);
}
catch (Exception ex)
{
// 未知异常,记录日志
_logger.LogCritical(ex, "未处理的异常");
throw;
}
}
3.4.3 自定义异常
我建议为每个业务模块定义自己的异常类。比如 NotFoundException、ValidationException、UnauthorizedException。这样在全局异常处理中间件里,可以针对不同异常返回不同的 HTTP 状态码。
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
public NotFoundException(string message, Exception inner) : base(message, inner) { }
}
3.4.4 避坑:using 与 finally
对于实现了 IDisposable 的资源,用 using 语句。它等价于 try-finally,但更简洁。我曾经见过有人手动调用 Dispose(),结果在异常发生时忘记释放资源,导致连接池耗尽。
// 推荐
using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
// 不推荐
var conn = new SqlConnection(connectionString);
try
{
await conn.OpenAsync();
}
finally
{
conn.Dispose(); // 容易忘
}
好了,这一章的内容就这些。类型系统、异步编程、LINQ、异常处理——这四个点吃透了,C# 后端开发的基本功就算扎实了。下一章我们开始实战,用 ASP.NET Core 搭一个真正的项目。