为什么我们还需要一门 .NET 语言?

📅 2026/7/19 2:51:29
为什么我们还需要一门 .NET 语言?
NET 开发者都记得自己写下的第一行 C# 代码。但你还记得那种微妙的困惑吗——明明只是想打印一句 “Hello, world”为什么要先造一座宫殿// C#一个 Hello World 需要 11 行namespace HelloWorld{class Program{static void Main(string[] args){Console.WriteLine(“Hello, world!”);}}}namespace、花括号、class、static、void、string[] args——这些概念在你理解 Console.WriteLine 之前就已经扑面而来。对于一个刚入门的新手这种仪式感更像是一道门槛。而对于从 Go、Kotlin 或 Swift 转过来的开发者这种体验近乎荒诞他们熟悉的世界里main 函数可以直接躺在文件的怀抱中不需要 class 的襁褓不需要 namespace 的围墙。C# 并非一直如此复杂。2002 年的 C# 1.0 还是一门精瘦的语言——没有泛型、没有 LINQ、没有 async/await。但二十余年过去从 C# 2.0 的泛型到 3.0 的 Lambda 和 LINQ从 4.0 的 dynamic 到 8.0 的可空引用类型从 9.0 的 records 到 12 的集合表达式和主构造函数C# 的语法表像一棵不断开枝散叶的大树[1]。每一项新增特性都解决了真实的问题——async/await 让异步代码可读records 减少了样板代码模式匹配让分支逻辑优雅。但叠加在一起就构成了一个庞大的认知版图。新手需要多久才能真正精通这门语言三个月半年这不是 C# 的错。作为 .NET 生态的旗舰语言它承载着向后兼容的契约和工业级应用的期望。每一行新语法都经过 ECMA 委员会的推敲每一个特性都要顾及千万行存量代码的呼吸。但正是这种幸福的烦恼在 .NET 生态中留下了一个微妙的缝隙如果有一群人他们不想背诵 static void Main(string[] args) 的咒语他们只想写代码——能不能给他们另一个选择2026 年 5 月一位工程师决定回答这个问题。1.3 G# 的登场David Obando 的名字在 .NET 社区还不算家喻户晓但他的履历足够让人侧目。哥斯达黎加理工学院软件工程专业出身后在德国获得数据分析硕士学位曾在微软工作如今任职于 AWS[2]。他的 GitHub 主页上活跃着多个 .NET 项目——其中最引人注目的是一个名叫 G# 的实验。这个实验始于 2026 年 5 月。彼时David 在 GitHub 上提交了第一批设计文档ADR勾勒出一门新语言的轮廓。短短两个月后v0.1 的设计规范落地又一个月v0.2 更新完成[3]。这不是象牙塔里的学术练习——每一行代码、每一个语法决策都通过 ADR架构决策记录留下了深思熟虑的痕迹。截至 2026 年 7 月项目已迭代至 v0.3.8合并了超过 242 个 pull request新增了 12 万行代码[4]。G# 的野心并不大——至少不是取代 C#那种宏大叙事。它的核心命题可以用一句话概括“相同的 .NET不同的语法。”[5]// G#Hello World 只要 6 行package HelloWorldimport Systemfunc Main() {Console.WriteLine(“Hello, world!”)}没有 namespace 的嵌套括号没有 class 的包裹没有 static 的修饰。package 声明文件归属func 声明一个函数——仅此而已。G# 编译器 gsc 将这段代码直接翻译成 .NET IL输出的 PE 程序集与 C# 编译的产物别无二致。Console.WriteLine 来自同一个 BCL运行的仍是同一个 CLR[6]。但 G# 的愿景不止于精简。它的目标受众非常清晰那些熟悉 Go 的 package 和 goroutine、Kotlin 的空安全和 data class、Swift 的 let/var 绑定却因为 .NET 的生态吸引力而犹豫的开发者。G# 想告诉他们你们不必先学会 C#也能享用 NuGet 上的数百万个包、MSBuild 的构建流水线、以及 .NET 运行时的性能优化[7]。“G# is for people who want a small, predictable language with direct access to the .NET ecosystem.”[8]这种定位的背后是 G# 对人体工学Ergonomics的执着追求——这个词在官方文档中反复出现[9]。它意味着更小的语法表面、类型后置的可读性、原生的空安全、内置的结构化并发。G# 借鉴了 Go 的 package 和 func、Kotlin 的可空类型系统、Swift 的 let/var 绑定将它们熔铸在一套统一的语法中。它不是任何一门语言的克隆而是一次审慎的采撷。David 的选择很务实。G# 不构建自己的运行时不另起炉灶造生态而是寄生在 .NET 这棵大树上享受其成熟的根系和养分。它是 .NET 的第二语言——不是竞争者而是补充者。那么G# 究竟如何用一套更小的语法表面撑起与 C# 同等的表达能力让我们从最基本的 package 和 func 开始一层层揭开它的设计。包与函数Go 风格的简洁骨架学习一门新语言最先映入眼帘的永远是代码长什么样。G# 在这一点上给出了极为明确的第一印象它不像 C#更像 Go。本章从两个最基础的构造单元入手——package 与 func——带你感受 G# 的语法骨架究竟精简到了什么程度。2.1 package 替代 namespace2.1.1 Go 风格的包声明去掉大括号包裹的层级C# 的 namespace 是一条层层嵌套的俄罗斯套娃。你的代码必须蜷缩在大括号里像住在一个精装但是拥挤的公寓楼中——每一层都有门禁每层都贴着此处属于某某命名空间的标签。// C#: namespace 包裹一切namespace MyCompany.MyApp.Services{using System;using System.Collections.Generic;class Program // 类型必须属于某个 namespace { static void Main() { // 所有代码都缩进在 namespace class 的双重包裹内 } }}G# 的做法干脆得多。package 声明只占一行之后所有的类型和函数都直接属于这个包不再有额外的层级嵌套。这是一种更接近文件系统的直觉一个文件就是一个包一个包就是一个命名空间。package MyCompany.MyApp.Servicesimport Systemimport System.Collections.Generic// 代码直接在 package 下无大括号包裹func Main() {Console.WriteLine(“Hello from G#!”)}这里有两个细节值得注意。第一package 是点分隔的标识符MyCompany.MyApp.Services不是 C# 那样的嵌套语法。第二G# 的 import 采用 Go 风格的无括号声明每行一个导入视觉上比 C# 的 using 块更松散也更适合快速扫读。2.1.2 import 语法更贴近文件系统的组织直觉G# 的 import 继承了 Go 的简洁同时兼容了 CLR 世界的习惯。别名导入和静态导入同样支持但语法略有不同// C#: using 别名 静态导入using sys System;using static System.Math;class Demo{static double Hypotenuse(double a, double b){return Sqrt(a * a b * b); // 静态导入后直接使用}}package Demoimport sys Systemimport System.Math // 非别名导入类型时行为类似 using staticfunc hypotenuse(a float64, b float64) float64 {return Sqrt(a * a b * b) // 静态导入后直接使用}编译器默认隐式导入 System这意味着你在多数情况下连 import System 这一行都省掉了。这种先帮你把最常用的东西摆上桌的设计和 Go 要求显式导入一切的做法略有不同——它更像是 C# 的 using System 被默默执行了一遍只是你不需要看到它。2.2 func 替代 static void2.2.1 类型后置func add(a int32, b int32) int32这是 G# 最具辨识度的语法特征之一。C# 延续了 C 家族的传统类型在前名称在后。// C#: 返回类型在前参数类型在前public static int Add(int a, int b){return a b;}// C#: 表达式体简写C# 6public static int Add(int a, int b) a b;G# 把它整个翻了过来// G#: 参数名在前、类型在后返回值类型在参数列表之后func add(a int32, b int32) int32 {return a b}// G#: 表达式体简写func add(a int32, b int32) int32 a b为什么类型后置更好你可以把它想象成中文的日常表达。我们说小明今年三十岁了——先告诉你这个人是谁再说他的属性。C# 的 int Add(int a, int b) 更像是在说整型函数 加法整型甲整型乙阅读时需要做一次脑筋急转弯。G# 的 func add(a int32, b int32) int32 则顺着你的目光自然流淌函数名叫 add第一个参数叫 a类型是 int32第二个参数叫 b类型也是 int32最后返回 int32。参数名先行对代码可读性的提升在大型项目中尤为明显。当你扫过一行函数签名最先看到的是标识符——也就是人类语言中最具语义密度的部分。2.2.2 顶级函数告别 class 容器C# 中每一个函数都必须是某个类型的成员。哪怕你只想写一个孤零零的工具函数也得先给它造一个 class 容器——像是给一个只想站街的人硬塞一张办公桌。// C#: 函数必须住在 class 里public static class MathUtils{public static int Square(int x) x * x;public static int Cube(int x) x * x * x;}G# 接受函数独立存在。你声明的 func 直接挂在 package 下不需要 class 这个中间层。package MathUtils// 函数直接在 package 下没有 class 容器func square(x int32) int32 x * xfunc cube(x int32) int32 x * x * x这不仅减少了样板代码更重要的是改变了心智模型。函数不再是一个类的行为而是一个包的能力——这个区别很微妙但当你习惯了之后回头看 C# 的 static class 工具类会觉得格外累赘。2.2.3 变长参数与扩展函数的 Go 风味G# 的变长参数语法直接照搬了 Go 的三点号// C#: params 关键字 数组类型static int Sum(params int[] nums){int total 0;foreach (var v in nums) total v;return total;}// 调用Sum(1, 2, 3, 4, 5);// G#: …T 变长参数语法func sum(nums …int32) int32 {var total 0for v in nums {total total v}return total}// 调用sum(1, 2, 3, 4, 5)// 也可以传入切片let arr []int32{10, 20, 30}sum(arr) // 60G# 的 …int32 在编译后会附加 [ParamArray] 属性底层仍然是 CLR 的数组语义但表面语法和 Go 保持了一致。更有趣的是调用时你既可以展开多个参数也可以直接传入一个切片——这比 C# 的 params 多了一分灵活。扩展函数方面G# 抛弃了 C# 的 this 关键字改用 Go 的接收者语法// C#: 扩展方法用 this 关键字static int Abs(this int value) value 0 ? -value : value;// 调用int x -5;Console.WriteLine(x.Abs());// G#: 接收者语法func (value int32) Abs() int32 {if value 0 {return -value}return value}// 调用var x -5Console.WriteLine(x.Abs())