C# 语法要点(写给 Java/TS 背景)

要点

这篇是给有 Java 或 TypeScript 经验的人的 C# 落地指南,挑六个”不一样才值得讲”的点:属性、可空引用类型、LINQ、record 与模式匹配、async/await、内建依赖注入。每个点配一段可运行代码和一张对照表——相似的部分直接跳过,篇幅全部花在差异和坑上。

一、属性 property:字段的门卫

Java 里字段私有、暴露 getter/setter 是铁律;C# 把这对方法升格为语言特性——属性(property):

public class Order
{
    public string Id { get; init; }        // init:只能在对象初始化时赋值,之后只读
    public decimal Amount { get; set; }    // 自动属性,编译器生成隐藏字段
    public decimal Tax => Amount * 0.13m;  // 表达式属性:只有 get,每次访问现算

    private int _stock;                    // 需要写校验逻辑时才手写字段
    public int Stock
    {
        get => _stock;
        set => _stock = value >= 0 ? value : throw new ArgumentException("库存不能为负");
    }
}
var o = new Order { Id = "A1" };  // 对象初始化器配 init 属性,没有构造函数爆炸
能力 C# 属性 Java
只读暴露 get; init; 或 get; private set; 手写 getter,不给 setter
去样板 自动属性一行 Lombok @Getter/@Setter 或 IDE 生成
校验逻辑 set 访问器里写,调用方无感 setter 方法里写

从外部看属性长得像字段(o.Amount),从实现看它是方法——将来加校验、加日志不破坏调用方。C# 14 的 field 关键字让”带校验的半自动属性”不再需要手写字段,属于锦上添花。

二、可空引用类型:编译期防空

.NET 10 模板默认开启可空引用类型(nullable reference types,NRT):string 表示”我不为 null”,string? 表示”我可能为 null”。这是编译器警告级别的静态分析,不是运行时检查:

void Greet(string name) { ... }      // 契约:name 不为 null
Greet(null!);                        // 编译警告 + ! 是"我强行压掉警告",出事自负

string? nick = GetUserNick();
Console.WriteLine(nick?.Trim());     // ?. null 时不调用,整体为 null
var len = nick?.Length ?? 0;         // ?? 给默认值
概念 C# 对照
可空标记 T?(引用与值类型统一语法) TS 的 string | null / strictNullChecks
值类型可空 int? 即 Nullable,装箱结构 Java 的 Integer(陷阱更多)
“可能没有”的表达 T? 直用,或 TryGet 模式 out 参数 Java Optional 包装
压制警告 null! 或禁用整文件 #nullable disable TS 的 as / @ts-ignore

关键差异:Java 用 Optional 在运行时表达缺席,C# 选择在编译期用类型系统标注缺席,运行时 string? 与 string 是同一个类型。所以 NRT 不是护栏而是安全带——跨边界数据(HTTP、JSON)仍要运行时校验。

三、LINQ:集合操作的统一语言

LINQ 的方法语法(method syntax)是一套集合标准库,与 Java Stream、JS 数组方法三者神似,命名对照着记最快:

var adults = people
    .Where(p => p.Age >= 18)                 // filter
    .OrderByDescending(p => p.Age)           // sort
    .Select(p => new { p.Name, p.Age })      // map,匿名类型
    .Take(10)                                // slice/limit
    .ToList();                               // 终结:立即执行(否则只是"查询定义")
操作 C# Java Stream JS 数组
过滤 Where filter filter
映射 Select map map
分组 GroupBy(返回懒序列) Collectors.groupingBy Object.groupBy(较新)
终结 ToList/ToArray/Sum/First collect/toList 数组方法本就立即执行

最大的坑是延迟执行(deferred execution):Where/Select 只是在组装查询,遍历或 ToList 才真正跑。枚举两次就查两次;每遍历一次才求值一次。不确定时立即 ToList 固化结果。

四、record 与模式匹配:数据建模的正确姿势

record 是不可变数据类型,一句话定义,自动获得值相等、解构、with 拷贝。与模式匹配(pattern matching)配合是 C# 处理”数据 + 分支”的杀手锏:

public record Link(string Code, string Url, int Clicks);

var bumped = link with { Clicks = link.Clicks + 1 };   // 非破坏性更新

string Describe(object obj) => obj switch              // switch 表达式
{
    Link { Clicks: > 1000 } l => $"热门短链 {l.Code}", // 属性 + 关系模式
    Link l => $"短链 {l.Code}",
    string s => $"普通字符串 {s}",
    null => "空值",                                     // null 模式显式处理
    _ => "其他"                                        // 丢弃模式 = default
};
能力 C# Java
不可变数据载体 record(C# 9 起),值相等内建 record(Java 16 起)
非破坏更新 with 表达式,任意字段 无对应,手写复制构造
模式匹配 switch 表达式 + 属性/关系/列表模式(C# 8-11 渐进) Java 21 起模式匹配 for switch

站内《Java 类与 Record》讲过 Java 侧的取舍,两边结论一致:DTO、API 契约、值对象优先 record;有生命周期和行为的实体才用 class。

五、async/await 与 Task:像 JS 的语法,不像 JS 的语义

async Task<string[]> FetchAllAsync(string[] urls)
{
    var tasks = urls.Select(FetchAsync);       // 发起,不等待
    return await Task.WhenAll(tasks);          // 并发聚合,等价 Promise.all
}
方面 C# JS / TS
await 的本质 真暂停方法,释放线程;延续可能回到原上下文 单线程事件循环的回调注册
任务类型 Task / Task / ValueTask Promise / thenable
并发聚合 Task.WhenAll / WhenAny Promise.all / race
同步等结果 .Result 会死锁(有同步上下文时) 不存在此问题(本就单线程)
忘记 await 编译警告 CS4014,任务静默丢弃 浮悬 Promise 无警告

两个必踩坑:反模式一:async void——异常没人接、无法等待,只允许事件处理器用;要”发后不理”就返回 Task 并由调用方决定。反模式二:在异步代码里调 .Result / .Wait()——桌面程序里典型死锁,服务器里白白占线程;从上到下全 async 才是正解。

六、依赖注入为何内建

Spring 用户到 C# 最惊讶的一件事:ASP.NET Core 的依赖注入(dependency injection,DI)容器在运行时里,不在框架里:

// 注册:寿命三档,语义直白
builder.Services.AddSingleton<IShortCode, ShortCodeGenerator>();  // 全进程一份
builder.Services.AddScoped<LinkDbContext>();                     // 每个 HTTP 请求一份
builder.Services.AddTransient<IReportBuilder, ReportBuilder>();  // 每次解析新建

// 消费:构造器注入,无注解、无反射扫描
app.MapGet("/{code}", async (string code, LinkDbContext db) =>
    await db.Links.FirstOrDefaultAsync(l => l.Code == code));
方面 .NET(Microsoft.Extensions.DependencyInjection) Spring
容器来源 运行时自带库,控制台程序 3 行也能用 框架核心,依托容器
注册方式 显式逐条注册(编译期可见) 注解 + 类路径扫描
生命周期 Singleton/Scoped/Transient 三档显式声明 默认单例,作用域靠注解配置
漏注册的暴露时机 第一次解析时抛异常 容器启动早期即失败

为什么内建:.NET 团队把”配置、日志、DI、选项”做成共享底座 Microsoft.Extensions.*,Web 框架、云库、后台服务全建在其上——学一次,全生态通用。代价是显式注册到几百行时需要组织(扩展方法按模块分文件),Spring 的自动装配在工程效率上仍有优势。

小结与下一篇

六个点的共同线索:C# 大量把 Java 靠惯例、靠库、靠注解处理器的做法收进语言与运行时——属性收编 getter/setter,NRT 收编 Optional,LINQ 收编 Stream,DI 收编容器。代价是语言更大,收益是惯例统一。逐条速查、含常见报错对照的内容,收在下一篇《C# 基础知识参考字典》;Java 侧的现代特性背景可回看《Java 现代语法特性》。

版本核实(以官方文档为准):本文基准为 .NET 10(当前 LTS,2028-11 前受支持)与 C# 14;record 与顶级语句为 C# 9 起,模式匹配演进至 C# 11(列表模式),C# 14 新增 field 关键字与扩展成员(本文未展开);可空引用类型自 .NET 6 起模板默认开启;Java 侧对照事实:record 为 Java 16 起、switch 模式匹配为 Java 21 起、紧凑源文件为 Java 25 起。具体以官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇