CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/C# 与 .NET 基础
RawBlame
课时目录12 节

ASP.NET Core 实战入门

  1. 01ASP.NET Core 基础
  2. 02C# 与 .NET 基础
  3. 03Entity Framework Core
  4. 04依赖注入
  5. 05REST / GraphQL / gRPC
  6. 06数据存储与缓存
  7. 07SignalR 实时通信
  8. 08测试与 E2E
  9. 09消息队列
  10. 10微服务与网关
  11. 11日志、任务与工具链
  12. 12综合练习

C# 与 .NET 基础

语法、集合、算法与数据库概念。

35 分钟

C# 12 与 .NET 8 语言基础

这一课你将能做到

  1. 读懂 .NET 8 后端项目里 90% 的常见语法:record、属性、LINQ、async/await、模式匹配
  2. 用 C# 写出判题平台业务里高频的数据转换、聚合统计与异步并发代码
  3. 对每个语法点说出它在 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 起就有。

讲清楚要点

  1. IEnumerable<T> 与 IQueryable<T> 必须分清:前者是内存集合的迭代契约,LINQ 方法在本地执行;后者携带表达式树,进数据库执行。在 EF Core 里 Where 写在 IQueryable 上是 SQL 过滤,先 ToList() 再 Where 就是把整表拉回内存过滤——性能天壤之别。
  2. 属性是 C# 的一等公民:public int TimeMs { get; init; } 中 init 表示只能在初始化时赋值(record 自动如此),等价 Java 的 final 字段 + getter,但没有样板。
  3. 扩展方法:public static int WordCount(this string s) 让你能给 string 加方法。LINQ 的所有操作符本质就是 IEnumerable<T> 的扩展方法,理解这一点就理解了 LINQ 无魔法。
  4. 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() 比较 recordrecord 已重写 ToString,可用直接输出 record 实例,格式就是构造参数列表
catch (Exception) 一把梭后不抛不记吞异常,问题无迹可循至少 logger.LogError(ex, "..."),或重新抛出

卡点急救

  1. 满屏 CS8602 可空警告:确认项目文件有 <Nullable>enable</Nullable>(.NET 8 模板默认有),然后逐个用 ?. / ?? / 显式判空处理,禁止 null! 消音。
  2. LINQ 查询报「无法翻译」运行时错误(EF 场景):说明表达式里用了数据库不认识的方法(如自定义 C# 函数),把它移到 ToList() 之后,或改写成可翻译的等价形式。
  3. 异步方法卡死不返回:检查是否在 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 从运行期问题变成编译期问题。

自测三问

  1. record 的 with 表达式做了什么?Java record 如何实现等价效果?
  2. Task.WhenAll 和逐个 await 的区别是什么?
  3. LINQ 的延迟执行发生在哪一行触发?

小结一句话

C# 12 = Java 的肌肉记忆 + 三件新武器(可空类型系统、LINQ 全域化、语言级 async),先建对照表再写代码,迁移成本只剩惯用语熟练度。

延伸

  • 官方文档:C# 12 新特性
  • 下一课:EF Core 8 数据访问——DbContext、迁移、N+1 治理,对照 JPA 逐概念映射。
加载本节练习…
← 上一课ASP.NET Core 基础下一课 →Entity Framework Core