C# 与 .NET:运行在 CLR 之上

要理解 C#,先要把它和 .NET 分清:C# 是一门语言,.NET 是承载它的运行时与类库平台。你写的 C# 代码先被编译成中间语言(IL),运行时由公共语言运行时 CLR 把 IL 即时编译(JIT)成机器码再执行。多出来的这一层,换来的是自动内存管理、跨语言互操作和统一的类型系统。

自动内存管理是 CLR 最大的礼包:堆上的对象由垃圾回收器(GC)负责释放,开发者不必手动 delete。代价是“不确定何时回收”——GC 只在内存压力或特定时机触发,所以持有大量非托管资源(文件句柄、数据库连接)时仍需显式释放,这就要用到 IDisposable 与 using。

托管不等于“不用管内存”。托管只解决了“对象作为垃圾时谁来回收”,不解决“宝贵的非托管句柄何时必须交还”。前者交给 GC,后者交给 using。
本文核心知识点
  • CLR把 IL 编译为机器码并负责 GC、类型检查与异常处理。
  • 值类型与引用类型前者存数据、后者存引用,决定复制与比较语义。
  • LINQ用统一的查询语法操作集合、数据库与 XML。
  • async/await以同步写法编写异步逻辑,避免线程阻塞。
C# 与 .NET 运行时示意:机械臂装配悬浮多面体模块,中央发光齿轮引擎被管道状编译与运行链路环绕
对象模块被运行时装配成应用:CLR 负责编译、内存与类型安全,开发者专注业务逻辑。

语法基础:类型、变量与字符串

C# 是静态强类型语言,变量在声明时就有确定的类型。它把类型分成两大类,理解这个二分法是后面所有行为的钥匙:值类型直接保存数据,赋给另一个变量时会整体复制;引用类型保存的是对象的引用,复制的是“指向”,两者随后仍指向同一个对象。

维度值类型引用类型
常见例子int、double、bool、char、struct、enumclass、string、数组、委托、接口
内存位置栈或宿主对象内部堆(变量持有引用)
赋值语义复制整份数据复制引用,指向同一对象
默认值0 / false 等零值null
using System;

class Program {
    static void Main() {
        int a = 10;
        int b = a;
        b = 20;
        Console.WriteLine($"a={a} b={b}");        // a=10 b=20,值类型互不影响

        string s = "hello";
        string u = s.ToUpper();                    // 字符串不可变,返回新串
        Console.WriteLine($"{s} / {u}");           // hello / HELLO
        Console.WriteLine($"{s.Length}, {s[0]}");  // 5, h
    }
}

注意 string 是不可变(immutable)的,任何“修改”都产生新字符串,所以循环里频繁拼接字符串应改用 StringBuilder。字符串插值 $"..." 是推荐写法,既清晰又高效。

面向对象:类、接口与继承

C# 的面向对象支持得很完整:类封装状态与行为,接口定义契约,继承复用实现。工程上一条重要原则是“面向接口编程”——调用方依赖抽象,具体实现可以替换,测试时可注入假对象。

using System;

public interface IShape {
    double Area();
}

public class Circle : IShape {
    public double Radius { get; }
    public Circle(double r) => Radius = r;
    public double Area() => Math.PI * Radius * Radius;
}

public class Rect : IShape {
    public double W { get; set; }
    public double H { get; set; }
    public double Area() => W * H;
}

class Program {
    static void Main() {
        IShape[] shapes = { new Circle(1), new Rect { W = 3, H = 4 } };
        double total = 0;
        foreach (var s in shapes) total += s.Area();
        Console.WriteLine($"总面积 = {total:F2}");   // 15.14
    }
}

=> 是表达式成员语法,{ get; } 是只读自动属性。多态让 foreach 里的每个对象各按自己的 Area 计算,调用方完全无需区分具体类型——这正是抽象的收益。

集合与 LINQ 查询

LINQ(Language Integrated Query)把查询能力直接做进语言:无论数据来自内存集合、数据库还是 XML,都能用同一套 Where / Select / OrderBy / GroupBy 表达。它背后是扩展方法与延迟执行——查询链在真正枚举时才运行。

using System;
using System.Linq;

class Program {
    static void Main() {
        var nums = new[] { 5, 12, 8, 20, 3, 17, 9 };

        var result = nums
            .Where(n => n % 2 == 0)      // 只保留偶数
            .OrderByDescending(n => n)   // 降序
            .Select(n => n * n)          // 平方
            .ToList();

        Console.WriteLine(string.Join(", ", result));   // 400, 144, 64
    }
}

“延迟执行”既省事也埋坑:如果查询里引用了会被后续修改的变量,结果可能在枚举时才定。需要固定结果时用 ToList() / ToArray() 立即物化。对于数据库查询,LINQ 会被表达式树翻译成 SQL,这时要避免把无法翻译的本地方法写进查询,否则会退化成全表拉取。

异步编程:async 与 await

IO 操作(网络请求、读写文件)真正慢的是“等待”,而不是 CPU 计算。异步的价值在于:等待期间把线程还回线程池,而不是让线程干等,从而用少量线程支撑大量并发请求。async/await 让你用几乎同步的写法表达异步逻辑。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static readonly HttpClient http = new HttpClient();

    static async Task<int> GetLengthAsync(string url) {
        string body = await http.GetStringAsync(url);   // 等待时不阻塞线程
        return body.Length;
    }

    static async Task Main() {
        int len = await GetLengthAsync("https://example.com/");
        Console.WriteLine($"页面长度:{len}");
    }
}

两条纪律:异步要“一路异步到底”,不要在异步方法里用 .Result 或 .Wait() 同步阻塞,否则容易死锁并失去并发优势;命名上异步方法以 Async 结尾,让调用方一眼看出需要 await。CPU 密集型的并行计算则应交给 Task.Run 或 Parallel,而不是靠 async。

异常处理与资源的自动释放

C# 用 try / catch / finally 处理异常,但比异常更常见的是“资源必须被释放”的问题——打开的文件、数据库连接、锁。C# 的答案同样是 RAII 的思路:让类型实现 IDisposable,用 using 语句保证退出作用域时自动调用 Dispose,无论正常结束还是抛异常。

using System;
using System.IO;

class Program {
    static void Main() {
        string path = "notes.txt";

        // using 声明:作用域结束即自动释放
        using var writer = new StreamWriter(path);
        writer.WriteLine("第一行");
        writer.WriteLine("第二行");

        using (var reader = new StreamReader(path)) {
            string line;
            int no = 0;
            while ((line = reader.ReadLine()) != null) {
                Console.WriteLine($"{++no}: {line}");
            }
        }
    }
}

异常处理的通用原则:只捕获你能处理的异常,其余让它向上冒泡;捕获后要么恢复、要么包装上更有意义的上下文再抛出;不要把异常吞掉(空的 catch)——那会让问题在更远的地方以更难看的方式暴露。