CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
C# 与 .NET 基础 语法、集合、算法与数据库概念。
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() / awaitawait 是语言级,无回调地狱 interface + default 方法同 几乎一致 finalreadonly(字段)/ 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),先建对照表再写代码,迁移成本只剩惯用语熟练度。
延伸