四语言 Hello World 对照表
让我们用一张表收束本章对比四种 .NET 生态相关语言的 Hello World 全貌维度 C# G# Go Kotlin完整代码 namespace Hello { class Program { static void Main() { Console.WriteLine(“Hello!”); } } } package Hello\nimport System\nfunc Main() { Console.WriteLine(“Hello!”) } package main\nimport “fmt”\nfunc main() { fmt.Println(“Hello!”) } fun main() { println(“Hello!”) }代码行数 11 行 6 行 7 行 3 行包声明 namespace 大括号 package 单行 package 单行 无文件级默认入口函数 static void Main() func Main() func main() fun main()需要 class 容器 是 否 否 否导入语句 隐式 using System 隐式 import System 显式 import “fmt” 隐式 kotlin.io.*// C#: 完整的 Hello Worldnamespace HelloWorld{class Program // 需要 class{static void Main(string[] args) // 需要 static{Console.WriteLine(“Hello, world!”);}}}// G#: 简洁的 Hello Worldpackage HelloWorldimport Systemfunc Main() { // 无需 class无需 staticConsole.WriteLine(“Hello, world!”)}// Go: 参考对比package mainimport “fmt”func main() {fmt.Println(“Hello, world!”)}// Kotlin: 参考对比fun main() {println(“Hello, world!”)}这张表清晰地展示了 G# 的定位比 C# 简洁大约 45%与 Go 的样板代码量相当但在 .NET 生态中保留了完整的 CLR 互操作能力。它用 package 替代了 namespace 的大括号嵌套用 func 替代了 static void 的类成员约束用类型后置的参数声明换取了更顺滑的阅读体验。下一章我们将深入 G# 的类型系统——那里有更多值得细品的语法设计。类型系统显式、安全、数据优先类型系统是编程语言的骨架。C# 用了二十年时间在骨架上不断嫁接新器官——可空引用类型是后装的record 是后加的主构造函数更是 C# 12 才姗姗来迟。G# 选择了一条不同的路从设计第一天起就让类型系统既精确又舒适。它不是修补而是重建。3.1 宽度承载的数值类型3.1.1 int32 替代 int跨平台确定性的工程理性C# 的 int 到底多少位绝大多数 .NET 开发者会脱口而出32位——这在今天是对的因为 CLR 的 System.Int32 固定为 32 位。但这个答案里藏着一层微妙的历史妥协int 这个名称本身并不携带宽度信息它只是一个 C 语言家族遗留下来的别名。在 C 语言中int 的宽度随平台变化在 C# 中它恰好映射到 Int32但这个映射关系需要开发者记住。G# 的做法直截了当规范名称就是 int32、int64、float32、float64。这种 width-bearing宽度承载的命名消除了所有歧义。你不需要记住 float 是 32 位还是 double 是 64 位——类型名本身就告诉你答案。// G#: 类型名即规格书var population int32 2_147_483_647var atoms int64 9_223_372_036_854_775_807var pi float64 3.141592653589793// G# 也接受友好别名var i int 42 // 解析为 int32var l long 42 // 解析为 int64var f float 3.14 // 解析为 float32// C#: 别名没有宽度信息int i 42; // System.Int32long l 42L; // System.Int64float f 3.14f; // System.Single两种写法编译后的 IL 完全相同。区别只在源代码层面——而正是这个层面决定了代码的可读性和新开发者的学习曲线。当你看到 int32不需要查文档就知道它在内存中占 4 个字节当你看到 int需要额外的知识才能确认这一点。3.1.2 精确数字类型与底层 IL 的映射G# 的数值类型系统与 CLR 类型之间是一一对应的透明映射G# 类型 CLR 类型 位宽 C# 对应int32 System.Int32 32 位有符号 intint64 System.Int64 64 位有符号 longuint32 System.UInt32 32 位无符号 uintuint64 System.UInt64 64 位无符号 ulongfloat32 System.Single 32 位 IEEE-754 floatfloat64 System.Double 64 位 IEEE-754 doubledecimal System.Decimal 128 位定点 decimal这个映射是双向透明的G# 代码可以无缝调用 C# 库类型完全兼容从 C# 代码中使用 G# 编译的类型看到的也是熟悉的 CLR 类型。G# 只是在源代码层面增加了一层自文档化的命名约定。值得一提的是G# 不会静默提升任意混合的数值操作数。两个不同类型的数值运算需要显式转换——这种严格性在初期可能显得繁琐但它消灭了 C# 中那些难以察觉的隐式转换 bug。3.2 data class 与 data struct3.2.1 Kotlin 风格的数据类在绝大多数业务代码中我们定义的 class 本质上都是数据的容器。一个 Person 类有 Name 和 Age一个 Order 类有 Id 和 Amount——这些类不需要复杂的继承层次它们只需要安全地存储数据、方便地比较相等、优雅地创建副本。G# 的 data class 直接借鉴了 Kotlin 的设计哲学用一行代码替代了传统 OOP 的十几行样板// G#: data class —— 一行声明全套功能data class Person(name string, age int32)func main() {let p1 Person(“Alice”, 30)let p2 Person(“Alice”, 30)// 自动值相等性比较的是内容不是引用 Console.WriteLine(p1 p2) // true}// C#: 传统 class 需要手写大量样板public class Person{public string Name { get; set; }public int Age { get; set; }public Person(string name, int age) { Name name; Age age; } // 手写 Equals... public override bool Equals(object obj) obj is Person p Name p.Name Age p.Age; // 手写 GetHashCode... public override int GetHashCode() HashCode.Combine(Name, Age);}注意 G# 的语法data class Person(name string, age int32)。括号里的参数列表不仅是构造函数参数它们自动成为类的属性。这种 primary constructor主构造函数的风格让数据类的声明极其紧凑——你写下的每一个字符都在传递有效信息。3.2.2 自动相等性、解构、copy/with 模式data 关键字不仅仅是语法糖它触发编译器生成一整套数据操作能力data class Person(name string, age int32)func main() {let p1 Person(“Alice”, 30)// 1. 自动值相等性 let p2 Person(Alice, 30) Console.WriteLine(p1 p2) // true逐字段比较 Console.WriteLine(p1 ! p2) // true不同引用 // 2. copy-with 表达式非破坏性修改 let p3 p1 with { age 31 } // Kotlin 风格的 copy Console.WriteLine(p3.name) // Alice Console.WriteLine(p3.age) // 31 Console.WriteLine(p1.age) // 30原对象不变 // 3. 解构声明 let (name, age) p1 Console.WriteLine($name is $age years old) // 4. 自动 ToString Console.WriteLine(p1) // Person(nameAlice, age30)}这四项能力——值相等性、with 表达式、解构、自动字符串表示——覆盖了数据类 90% 的日常使用场景。在 C# 中即便有了 record你也需要分别了解 with 表达式C# 9、解构C# 10、主构造函数C# 12这些特性分散在四个不同版本的发布中。G# 将它们打包成一个整体一次性交付。3.2.3 C# record vs G# data class 的对比C# 9 引入的 record 与 G# 的 data class 看起来相似但设计理念和工程体验上有显著差异。// C#: record —— 语法依赖版本语义有选择成本public record Person(string Name, int Age); // C# 9public record class Person(string Name, int Age); // C# 10 显式public record struct Point(int X, int Y); // C# 10 value type// with 表达式var p2 p1 with { Age 31 };// 解构需要手动声明位置属性或者使用扩展解构// G#: data class / data struct —— 从一开始就统一设计data class Person(name string, age int32) // 引用类型data struct Point(x int32, y int32) // 值类型 自动相等性// 统一的 with、解构、copy 语义let p2 p1 with { age 31 }let (name, age) p1关键差异有三点。其一类型选择意图清晰。 G# 用 data class 和 data struct 明确区分引用语义和值语义C# 的 record 默认为 record classrecord struct 是 C# 10 才补充的开发者容易混淆默认行为。其二封闭性设计。 G# 的 data class 默认是不可继承的类似于 C# 的 sealed这符合数据类的使用场景——你很少真的需要继承一个纯粹的数据载体。C# 的 record 默认可继承带来了额外的复杂性。其三data struct 填补了 C# 的一个空白。 C# 的 struct 不提供自动相等性实现开发者仍需手写 Equals 和 GetHashCode。G# 的 data struct 让值类型也能享受完整的数据类待遇——结构相等性、with 表达式、解构一样不少。3.3 可空类型从 C# 的渐进式到 G# 的原生式3.3.1 C# nullable reference types 的半吊子困境C# 8.0 引入的可空引用类型Nullable Reference Types本意是好的——让编译器帮你捕获潜在的 NullReferenceException。但它在工程实践中遇到了一个根本性的矛盾它是 opt-in 的。你需要在 csproj 文件中显式启用enable 这个设计决策的后果是深远的。旧代码库默认不启用空引用异常依然是生产环境的常客。混合代码可空感知与非可空感知互操作产生大量难以处理的警告。库作者被迫维护两套 API 签名的心智模型。正如 endjin.com 的技术分析所指出的可空类型 fortunately 是渐进式采纳的——但如果它们不是默认关闭的大多数开发者可能会选择退出 C# 8.0。这句话本身就是一个设计困境的自白[5]。更深层的矛盾在于语义不一致。值类型用 Nullable语法糖为 T?引用类型用 T? 标注可空——同样是问号背后的机制完全不同。null 既是关键字、也是值、也是类型修饰符三重身份让它在代码中无处不在却又难以驯服。3.3.2 G# 的可空系统T?、nil、if let、guard letG# 走了另一条路空安全不是附加功能是类型系统的基石。// G#: 引用类型默认不可空可空需要显式标注func findUser(id int32) User? { // 返回可空 Userif id 0 { return nil } // G# 用 nil不是 nullreturn User(“Alice”, 30)}// 编译器会阻止你直接访问可空值func bad(user User?) {Console.WriteLine(user.name) // 编译错误user 是 User? 不是 User}G# 的可空设计有几个核心原则。默认不可空。 引用类型默认不可为空你需要显式写 T? 来表达这个值可能缺失。这不是一个可以被关闭的编译器开关——它是语言的一部分。nil 替代 null。 G# 用 nil 作为空值字面量如果你不小心输入 null编译器会抛出诊断错误 GS0273 并提示你改正。这个命名上的区分是一个精心设计的心理暗示nil 是 Swift 和 Go 的选择它带有一种值缺失的含义而不像 null 那样带有指针空洞的历史包袱。T? 编译为 CLR 的 Nullable 或 [Nullable] 注解。 G# 在 CLR 互操作层面统一处理可空性——值类型的 T? 编译为 Nullable引用类型的 T? 通过 [Nullable] 属性注解。从 C# 代码中调用 G# 库时可空签名被正确保留反之亦然。// if let: 条件解包——Swift 风格的优雅func greet(name string?) {if let n name {Console.WriteLine(“hi $n”) // n 是 string非可空} else {Console.WriteLine(“hi stranger”)}}// guard let: 提前返回模式func length(s string?) int32 {guard let v s else {return -1 // else 分支必须无条件退出}return v.Length // v 是 string非可空}if let 和 guard let 的可贵之处在于它们将空检查与值绑定合二为一。在 if let n name 的 then 分支中n 的类型已经被收窄为不可空的 string——不是编译器假装知道而是类型系统的正式收窄。guard let 则服务于防御性编程它要求 else 分支必须无条件退出return、throw、break 或 continue绑定后的值在剩余代码块中自动变为非可空类型。这套机制的完整图景还包括 ??null 合并、?.空条件访问、!!非空断言和 ??复合赋值。G# 从 Swift 和 Kotlin 那里继承了一整套经过验证的可空处理工具箱然后将其无缝嵌入 .NET 的类型生态中。而这一切都发生在编译期。不是运行时的反射检查不是 IDE 的分析器警告——是类型系统本身的硬约束。这意味着当你的 G# 代码通过编译空引用异常这类 bug 已经被消灭在萌芽状态。在下一章中我们将深入探索 G# 的可空类型系统在实际代码中的运用模式——从 if let 的嵌套技巧到与 C# 遗留代码互操作时的边界处理你会看到原生空安全如何在工程实践中转化为真正的开发效率。空值安全Swift/Kotlin 式的 if let 革命“十亿美元的错误”——Tony Hoare 这样形容他在 1965 年引入的 null 引用。六十年后现代语言终于学会了与空值和平共处。如果说 G# 的类型系统是一栋精心设计的建筑那么空值安全就是它的消防系统平时看不见关键时刻能救命。第三章我们已经见识了 T? 和 nil 的基本面这一章要深入的是 G# 空值处理的王牌特性——if let 和 guard let。这两把从 Swift 借来的利器让可空值的解包变得前所未有的优雅。4.1 if let优雅地处理可空值4.1.1 一次绑定双重使命想象这样一个日常场景你从配置字典中读取一个用户名然后决定如何打招呼。C# 的写法是什么样的// C#传统 null 检查需要显式比较和重新赋值void Greet(string? maybeName){if (maybeName ! null){// maybeName 在这里仍然是 string?编译器聪明时会收窄Console.WriteLine($“Hello, {maybeName}!”);}else{Console.WriteLine(“Hello, stranger!”);}}看起来还行但这只是最简单的情况。一旦涉及多次解包、链式访问C# 的嵌套就会迅速膨胀。G# 给出了另一种思路// G#if let 同时完成非空检查和值绑定func greet(maybeName string?) {if let name maybeName {// name 在此作用域内类型为 string非可空Console.WriteLine(“Hello, $name!”)} else {Console.WriteLine(“Hello, stranger!”)}}if let name maybeName 这一行做了两件事首先检查 maybeName 是否为 nil如果是非空值则将其绑定到新变量 name 上。name 的类型不再是 string?而是干净的 string。一次语法动作完成了逻辑判断和类型收窄没有冗余的 null 比较没有额外的变量声明。4.1.2 类型收窄编译器替你把关这就是 if let 的精妙之处——类型收窄type narrowing。进入 if let 的 then-branch 后编译器自动将 T? 收窄为 T。这不是运行时转换而是编译期的静态保证。你不可能在 if let 块内意外访问到一个 nil 值因为编译器根本不允许。G# 还支持多绑定 if let一次性解包多个可空值// G#多绑定——所有值都非空时才进入 then 分支func pair(left string?, right string?) {if let a left, let b right {Console.WriteLine(“$a $b”) // a 和 b 都是 string} else {Console.WriteLine(“Missing at least one value”)}}多绑定的语义是短路的如果 left 为 nil编译器不会再去检查 right直接走 else 分支。这种写法在处理多个依赖条件时特别实用——代码扁平、意图清晰没有箭头代码的噩梦。4.1.3 C# 模式匹配力拔山兮气盖世C# 也不是没有解包可空值的手段。C# 8 之后的模式匹配提供了一种替代方案// C#模式匹配 is { } 语法——能工作但不够自然void Greet(string? maybeName){if (maybeName is { } name){Console.WriteLine($“Hello, {name}!”);}else{Console.WriteLine(“Hello, stranger!”);}}maybeName is { } name 这行代码的工作原理是先用属性模式 { } 检查值非空同时将结果绑定到 name 变量。它确实能实现类似的效果但让我们诚实一点——is { } 是什么一个空属性模式这更像是模式匹配系统的副作用被借来做空值检查而不是为此场景专门设计的语法。对比 if let name maybeName 和 if (maybeName is { } name)前者像是一句自然的英语后者像是一道谜语。好的语法应该让意图一目了然而不是让读者停下来解码模式匹配的规则。

相关新闻