大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
C# 12 与 .NET 8 语言基础
这一课你将能做到
- 读懂 .NET 8 后端项目里 90% 的常见语法:record、属性、LINQ、async/await、模式匹配
- 用 C# 写出判题平台业务里高频的数据转换、聚合统计与异步并发代码
- 对每个语法点说出它在 Java 里对应的写法,以及容易踩坑的差异处
为什么要学
后端语言之间迁移,卡住人的从来不是流程控制,而是「惯用语」。Java 开发者写 C#,真正的门槛是三件事:可空引用类型(nullable reference types)、LINQ 替代 Stream、async/await 替代 CompletableFuture。这一课只讲这些迁移点,不讲 if/else 这类零差异内容。
人话:C# 和 Java 是同一棵树上的两个分支,语法糖各有千秋。你的目标不是「重新学一门语言」,而是建立一张精确的对照表,并处理掉 Java 没有的新概念。
先建立图像
| Java 写法 | C# 12 写法 | 备注 |
|---|---|---|
Optional<String> | string? + #nullable enable | 编译器级非空检查,不是包装对象 |
record Point(int x, int y) | 同名同构,另有 with 表达式 | C# record 可写逻辑,更灵活 |
Stream.of(...).filter().collect() | LINQ .Where().Select().ToList() | LINQ 同时支持集合与数据库(EF Core) |
CompletableFuture.supplyAsync() | Task.Run() / await | await 是语言级,无回调地狱 |
interface + default 方法 | 同 | 几乎一致 |
final | readonly(字段)/ sealed(类) | 词不同义近 |
跟着做
步骤 1:可空引用类型(.NET 项目默认开启)
#nullable enable
string name = null; // ❌ 编译警告 CS8600,非空类型不能赋 null
string? nickname = null; // ✅ ? 明确声明可为 null
// 不检查就使用,编译器直接警告
Console.WriteLine(nickname.Length); // ⚠️ CS8602 解引用可能为 null
Console.WriteLine(nickname?.Length ?? 0); // ✅ 空条件运算符 + 空合并,等价 Java 的 Optional.map().orElse()
Java 里 NPE 靠运行期炸出来,C# 8 起编译期就把「可能为 null 还直接用」标成警告,严格项目把警告当错误(TreatWarningsAsErrors)。?? 与 ?. 两个符号用熟,可空处理基本告别 if 判断。
步骤 2:record 与主构造函数(判题结果建模)
// 判题结果:不可变数据载体,一行搞定
public record JudgeResult(
long SubmissionId,
string Status, // AC / WA / TLE / RE
int TimeMs,
int MemoryKb);
var r1 = new JudgeResult(1001, "WA", 32, 8192);
var r2 = new JudgeResult(1001, "WA", 32, 8192);
Console.WriteLine(r1 == r2); // True —— record 自带值相等语义(Java record 同款)
// with 表达式:复制并修改,Java record 没有
var fixed = r2 with { Status = "AC" };
主构造函数同样适用于普通类(C# 12 新能力),构造参数在整个类体内可用:
public class JudgeWorker(int maxConcurrency, ILogger<JudgeWorker> logger)
{
public int MaxConcurrency => maxConcurrency; // 直接用主构造参数实现属性
}
步骤 3:集合表达式与集合类型(C# 12 新语法)
List<string> langs = ["cpp", "java", "python"]; // [] 替代 new List<string> {...}
int[] scores = [90, 85, ..passedScores]; // .. 展开运算符
List<string> copy = [..langs, "rust"]; // 复制并追加,简洁
Dictionary<string, int> limits = new()
{
["cpp"] = 2000, ["java"] = 4000, ["python"] = 6000 // 各语言时限 ms
};
集合类型选择和 Java 一致:List<T>(ArrayList)、Dictionary<K,V>(HashMap)、HashSet<T>、Queue<T>/Stack<T> 均有对应物,接口统一走 IEnumerable<T>(Iterable/Stream 的合体)。
步骤 4:LINQ——比 Stream 更进一步
对提交记录做统计(业务示例):
record Submission(long Id, string UserName, string Status, int TimeMs, DateTime CreatedAt);
var submissions = new List<Submission>
{
new(1, "alice", "AC", 120, DateTime.UtcNow.AddDays(-1)),
new(2, "alice", "WA", 90, DateTime.UtcNow),
new(3, "bob", "AC", 200, DateTime.UtcNow),
};
// 过滤 + 投影 + 排序:对应 Stream 的 filter/map/sorted/collect
var recent = submissions
.Where(s => s.CreatedAt > DateTime.UtcNow.AddDays(-7))
.OrderByDescending(s => s.CreatedAt)
.Select(s => new { s.UserName, s.Status })
.ToList(); // ToList() 才真正执行,等价 collect(toList())
// 按用户分组,取每人最好成绩:Collectors.groupingBy 的直译
var best = submissions
.GroupBy(s => s.UserName)
.ToDictionary(g => g.Key, g => g.Min(s => s.TimeMs));
// 查询语法(可选,等价上面方法链)
var acUsers = from s in submissions where s.Status == "AC" select s.UserName;
关键差异:LINQ 表达式同时能作用于内存集合和数据库查询(IQueryable<T>),后者会被 EF Core 翻译成 SQL——这是 Java Stream 没有的能力,第 3 课会用到。
步骤 5:async/await——判题任务异步化
public async Task<JudgeResult> JudgeAsync(Submission s, CancellationToken ct)
{
await Task.Delay(100, ct); // 模拟沙箱执行(IO/CPU 混合)
var result = await CompileAsync(s.Code, ct); // await 处让出线程,不阻塞
return result;
}
// 并发执行多个判题:等价 CompletableFuture.allOf
Task<JudgeResult>[] tasks = submissions.Select(s => JudgeAsync(s, default)).ToArray();
JudgeResult[] results = await Task.WhenAll(tasks);
心智模型:await 不是「等待」,是「登记一个回调后把线程还回线程池」。方法被编译器改写成状态机,到 await 处挂起、完成后从断点续跑。语义上等价 Java 的 thenCompose 链,但写法是同步风格。
步骤 6:switch 表达式与模式匹配(结果分类)
string Describe(JudgeResult r) => r.Status switch
{
"AC" => "通过",
"WA" => $"答案错误,耗时 {r.TimeMs}ms",
"TLE" when r.TimeMs > 10_000 => "严重超时",
"TLE" => "超时",
"RE" or "CE" => "运行/编译错误",
_ => "未知状态" // 默认分支
};
when 守卫子句 + or 模式 + 弃元 _,等价 Java 21 的 switch 模式匹配,C# 7 起就有。
讲清楚要点
IEnumerable<T>与IQueryable<T>必须分清:前者是内存集合的迭代契约,LINQ 方法在本地执行;后者携带表达式树,进数据库执行。在 EF Core 里Where写在IQueryable上是 SQL 过滤,先ToList()再Where就是把整表拉回内存过滤——性能天壤之别。- 属性是 C# 的一等公民:
public int TimeMs { get; init; }中init表示只能在初始化时赋值(record 自动如此),等价 Java 的 final 字段 + getter,但没有样板。 - 扩展方法:
public static int WordCount(this string s)让你能给 string 加方法。LINQ 的所有操作符本质就是IEnumerable<T>的扩展方法,理解这一点就理解了 LINQ 无魔法。 var是编译期类型推断(不是动态类型),等价 Java 的var(Java 10+),编译后与显式类型完全一致。
你可能会懵的地方
-
string和String哪个对? 完全等价,string是System.String的关键字别名。惯例:类型引用用string,静态成员用String.Format这类写法(现在多用插值$"{x}"替代)。 -
ValueTask和Task什么时候区分? 高频同步完成路径(如缓存命中直接返回)用ValueTask减少分配;一般业务用Task即可。判题这类必然有 IO 的场景直接Task。 -
C# 有没有 Java 的 Checked Exception? 没有,C# 设计者明确放弃了。异常统一走运行期异常 +
try/catch,方法签名不声明throws。配合async方法抛出的异常会封装在返回的Task里,await时重新抛出。 -
record和class性能差异? record 编译后仍是普通类,无运行时开销;只是编译器帮你生成相等性、ToString、with 的代码。数据载体用 record 是零成本的好习惯。 -
async void为什么是禁区? 调用方无法等待它,异常会直接炸到线程池导致进程崩溃。除了事件处理器(UI 回调)外,异步方法一律async Task。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
用 == 比较 List 内容 | 比较的是引用 | 用 Enumerable.SequenceEqual 或逐元素比较(record 才有值相等) |
await 套在 Task.WhenAll 的每个元素上逐个 await | 变成串行执行 | 先收集任务数组,再一次 await Task.WhenAll(tasks) |
对 IQueryable 先 ToList() 再过滤 | 全表加载进内存 | 过滤条件写在 IQueryable 阶段,最后才物化 |
用 Java 习惯写 ToString() 比较 record | record 已重写 ToString,可用 | 直接输出 record 实例,格式就是构造参数列表 |
catch (Exception) 一把梭后不抛不记 | 吞异常,问题无迹可循 | 至少 logger.LogError(ex, "..."),或重新抛出 |
卡点急救
- 满屏 CS8602 可空警告:确认项目文件有
<Nullable>enable</Nullable>(.NET 8 模板默认有),然后逐个用?./??/ 显式判空处理,禁止null!消音。 - LINQ 查询报「无法翻译」运行时错误(EF 场景):说明表达式里用了数据库不认识的方法(如自定义 C# 函数),把它移到
ToList()之后,或改写成可翻译的等价形式。 - 异步方法卡死不返回:检查是否在
async方法里用了.Result/.Wait()(同步阻塞 + 上下文死锁),统一改成一路await到底。
面试常问
-
async/await 在 .NET 里是如何实现的?和 Java 的 CompletableFuture 本质区别? async 方法被 Roslyn 编译成状态机(实现
IAsyncStateMachine),await 时把剩余逻辑注册为续体并把线程归还线程池,完成后由 Task 调度器恢复执行。CompletableFuture 是库层面的回调组合子,语义等价但代码形态是链式回调;C# 把它提升到语言级,同步风格写异步,栈的连续性和可读性更好。 -
IEnumerable 和 IQueryable 的区别,为什么 EF Core 关心这个? IEnumerable 面向内存迭代,LINQ-to-Objects 本地执行;IQueryable 持有表达式树,提供程序可把它翻译成目标语言(如 SQL)。EF Core 依赖这一机制把
Where(s => s.Status == "AC")翻成WHERE status = 'AC',因此在数据库上下文里保持 IQueryable、延迟到最终物化,是性能的关键。 -
C# 的可空引用类型解决什么 Java 没解决的问题? Java 的 Optional 只是显式标记返回值,且无法约束参数和字段;C# 把 nullability 纳入类型系统与编译器流分析,赋值、解引用、参数传递全程静态检查,把 NPE 从运行期问题变成编译期问题。
自测三问
record的with表达式做了什么?Java record 如何实现等价效果?Task.WhenAll和逐个await的区别是什么?- LINQ 的延迟执行发生在哪一行触发?
小结一句话
C# 12 = Java 的肌肉记忆 + 三件新武器(可空类型系统、LINQ 全域化、语言级 async),先建对照表再写代码,迁移成本只剩惯用语熟练度。
延伸
- 官方文档:C# 12 新特性
- 下一课:EF Core 8 数据访问——DbContext、迁移、N+1 治理,对照 JPA 逐概念映射。