第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了。

⚠️ 注意: 不要为了「省内存」而滥用struct。栈空间有限,大struct反而容易导致StackOverflow。

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;
}
💡 小技巧: 如果多个异步操作没有依赖关系,用 Task.WhenAll 并行执行,能大幅提升性能。

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。
✅ 正确做法: 先 ToList() 把数据拉到内存,再用 LINQ 操作。

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;
    }
}
⚠️ 注意: 永远不要 catch(Exception) 然后什么都不做。你吞掉的异常,总有一天会以更诡异的方式回来找你。

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 搭一个真正的项目。