第 1 章:声明 — 在你写第一行代码之前
从一个让你不安的实验开始
打开你的电脑,用 8 种语言各写一遍下面这件事:
声明一个变量
orderId,赋值为"A1001"。然后试图把orderId改成"B2002"。再声明一个变量maxRetry,赋值为3。
如果一切顺利,这段代码会在 5 分钟内写完。
但有趣的事在后面。问问自己:你能确定 orderId 的类型是什么吗?谁能改它?改的时候会报错吗?如果你把 3 传给一个只接受 Double 的函数呢?
这 8 门语言的答案全都不一样。而它们的差异,在你写下第一行代码的那一刻就已经注定了。
声明不是一个语法问题。它是一个制度问题:这门语言要求你在写代码之前,先说清多少事。
1.1 语法基础:8 种声明,8 种姿态
以下是 8 种语言对 "声明一个订单号" 这件事的不同写法。注意的不只是语法,而是每一行背后,语言在问你什么。
Python
order_id: str = "A1001" # 你可以写类型,也可以不写
order_id = "A1001" # Python 不会抱怨你省略类型
MAX_RETRY: Final[int] = 3 # 约定:全大写表示不要改(但语言不强制)
# MAX_RETRY = 5 # 运行不会报错,除非用类型检查器
# Python 3.10+ 你也可以显式标记 Final
from typing import Final
MAX_RETRY: Final = 3 # mypy 会帮你检查,但解释器不管Python 的态度:类型标注是你的选择,不是你的义务。你可以先写逻辑,后补类型。但这意味着类型错误不会在编译期被拦住——它们会在运行时以 AttributeError 或 TypeError 的形式出现。
JavaScript
let orderId = "A1001"; // 可变变量
orderId = "B2002"; // 没问题
const maxRetry = 3; // 常量引用:不能重新赋值
// maxRetry = 5; // TypeError: Assignment to constant variable
var oldWay = "legacy"; // 历史遗留,不要在新代码中用 varJavaScript 的态度:let 和 const 是 ES6 之后的主力。没有类型标注——JavaScript 根本不关心 orderId 是字符串还是数字,直到你调用一个只有字符串才有的方法。const 只保护引用,不保护值内部——如果 const obj = { a: 1 },你依然可以 obj.a = 2。
Java
String orderId = "A1001"; // 必须声明类型
// var orderId = "A1001"; // Java 10+ 也可以用 var 推断
orderId = "B2002"; // 可变
final int MAX_RETRY = 3; // 不可重新赋值
// MAX_RETRY = 5; // 编译错误
// record 的字段隐式 final
record Order(String id, int amount) {}
// o.id = "new"; // 编译错误Java 的态度:传统上你必须把类型说清。Java 10 引入 var 让局部变量可以省略类型声明,但字段、参数、返回值仍然需要显式类型。final 很常见但不是文化强制。一个有意思的细节:Java final 变量在 lambda 里可以被捕获,非 final 则不能——这让 final 不只是 "不变",还变成了 "可以在闭包中安全使用" 的通行证。
C++
std::string order_id = "A1001"; // 必须声明类型(或使用 auto)
auto order_id2 = "A1001"s; // C++14+ auto 推断,注意 s 后缀得到 string
order_id = "B2002"; // 可变
const int MAX_RETRY = 3; // 编译期常量
// MAX_RETRY = 5; // 编译错误
constexpr int MAX_TIMEOUT = 5000; // 更严格:编译期求值的常量C++ 的态度:auto 几乎可以做任何类型推断,但 C++ 社区对 auto 的态度分裂:有人觉得它简洁,有人觉得它让代码可读性下降。const 和 constexpr 的区分(一个是运行时不变,一个是编译期求值)也反映了 C++ 对 "控制" 的执念——它不只区分 "变与不变",还区分 "什么时候可以确定不变"。
Rust
let order_id = String::from("A1001"); // let 默认不可变!
// order_id = String::from("B2002"); // 编译错误:cannot assign twice to immutable variable
let mut order_id = String::from("A1001"); // 显式 mut 才可变
order_id = String::from("B2002"); // 现在可以了
const MAX_RETRY: u32 = 3; // 编译期常量,类型必须显式注明
let max_retry = 3u32; // 运行时不可变绑定,类型可推断Rust 的态度:let 默认不可变是 Rust 最标志性的设计选择之一。在大多数语言里 "可变" 是默认的,你需要额外关键词来限制;在 Rust 里反过来——你必须主动说 mut 来要求可变权限。这反映 Rust 的核心世界观:默认收紧权限,需要时才主动开放。 这和借取检查器、所有权系统是一脉相承的。
Go
var orderId string = "A1001" // 完整声明
var orderId = "A1001" // 类型推断
orderId := "A1001" // 短声明,最常用
orderId = "B2002" // 可变
const MaxRetry = 3 // 常量(Go 的 const 只能是基本类型的编译期常量)
// MaxRetry = 5 // 编译错误Go 的态度::= 短声明是 Go 最标志性的语法之一——它把 "声明 + 推断 + 赋值" 压缩成一个符号,非常务实。但 Go 的 const 很受限:只能是数字、字符串、布尔值,不能是函数返回值或复杂结构。Go 的态度是:常量就是常量,别把它搞成编译期计算引擎。
Swift
var orderId = "A1001" // 类型推断 + 可变
orderId = "B2002" // 没问题
let maxRetry = 3 // let = 不可变
// maxRetry = 5 // 编译错误
// Swift 的 let 是深度不可变:
struct Order { let id: String }
var order = Order(id: "A1001")
// order.id = "B2002" // 编译错误:即使 order 是 var,id 是 let 就不能改Swift 的态度:let 在 Swift 里不只是 "不能重新赋值",而是 "这个绑定的值不可变",深入到结构体和值类型的内部。配合值语义(struct),let 保护的是整个值树。这和 JavaScript 的 const(只锁引用,不锁内容)有本质区别。
Kotlin
val orderId = "A1001" // val = 不可变(推荐默认用 val)
// orderId = "B2002" // 编译错误
var orderId2 = "A1001" // var = 可变
orderId2 = "B2002" // 可以
const val MAX_RETRY = 3 // 编译期常量(必须顶层或 object 内)
// 普通 val 是运行期不可变,const val 才是编译期常量Kotlin 的态度:Kotlin 强烈鼓励用 val(不可变)作为默认选择。IDE 会在你写 var 时给出 "可以改为 val" 的提示。这种 "默认不可变" 的文化渗透在 Kotlin 的几乎所有惯用写法里——从 data class 的 val 属性,到集合操作默认返回新集合而非修改原集合。
语法速查:一眼记住 8 种声明
| 概念 | Python | JavaScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| 可变声明 | x = 1 | let x = 1 | var x = 1 或 int x = 1 | int x = 1 或 auto x = 1 | let mut x = 1 | x := 1 或 var x = 1 | var x = 1 | var x = 1 |
| 不可变声明 | 无语法级(约定全大写) | const x = 1 | final int x = 1 | const int x = 1 | let x = 1 | const x = 1 | let x = 1 | val x = 1 |
| 类型可省略 | 是 | 是(不写类型就无类型) | 局部变量可(var) | 可(auto) | 通常可推断 | 可(:= 或 var x =) | 可 | 可 |
| 默认可变? | 是 | 是(let) | 是 | 是 | 否 — 默认不可变 | 是 | 是 | 倡导 val |
1.2 深度对照一:let 这个词在不同语言里是三个人
"let" 可能是现代编程语言里最被滥用的关键词。它在 8 门语言中出现次数极其频繁,但每一次,它表达的是不同的承诺。
JavaScript 的 let:块级作用域的 "我以后可能变"
let x = 10;
x = 20; // 正常
// let x = 30; // 同一作用域里报错:已经声明过
if (true) {
let x = 99; // 块级作用域,不影响外层 x
}
console.log(x); // 20let 解决的是 var 的遗留问题:var 只有函数作用域,let 有块级作用域。但和 "不可变" 无关——JavaScript let 是可变的。
Swift 的 let:深度的 "你不能改了"
let x = 10
// x = 20 // 编译错误
struct Container { var value: Int }
let c = Container(value: 42)
// c.value = 99 // 编译错误!即使 value 被声明为 var
// 因为 c 本身是 let,整个结构体都被锁定了Swift 的 let 远比 JavaScript const 更强:如果你把一个 struct 声明为 let,它的所有属性——即使原本是 var——都不能改了。对于 class(引用类型),let 锁引用但不锁内容,这点和 JavaScript const 类似。
Rust 的 let:默认不可变,需要时申请 mut
let x = 10;
// x = 20; // 编译错误
let mut y = 10; // 你必须主动说:这个变量我需要改
y = 20; // 现在可以了Rust 反过来了:在其他语言里你需要 const / final / let 来声明不可变;在 Rust 里你需要 mut 来声明可变。这背后是 Rust 的核心理念:应该默认收紧权限,需要宽松时你主动申请。这种设计和借用检查器、无数据竞争保证是同一个思路的延伸。
为什么这点重要? 当你从 JavaScript 切到 Rust,你会觉得
let mut是多余的仪式。但当你从 Rust 切回 JavaScript,你会发现:哦,我之前在 JavaScript 里写的let变量,其实有一半根本不需要可变——我只是懒得思考而已。Rust 不是在折磨你,它是在帮你做一件你迟早要自己做、但自己做会偷懒的事。
1.3 深度对照二:类型,谁说、什么时候说
"这个变量是什么类型?"
8 门语言的回答落在一条光谱上。从左到右,从 "几乎不用说" 到 "几乎必须说"。
光谱定位
几乎不说 ←————————————————————————→ 几乎必须说
JS Python Swift Kotlin Go Java C++ Rust
(动态) (可选标注) (推断为主) (:=推断) (传统必须) (let推断
但字段必须)但这条光谱藏着一个更深的维度:类型标注的 "强制程度" 和 "执行时刻" 是两回事。
Python:可以标注,但标注不是契约
def process(order_id: str) -> int:
return len(order_id)
process(42) # 运行时不报错!Python 解释器不检查类型标注
# 但你会在某个地方炸掉——当 len() 试图对 int 求长度时Python 的类型标注是 渐进式的、运行时不执行的。它们本质上是给类型检查器(mypy、pyright)和 IDE 看的文档。这在 "灵活度" 和 "安全性" 之间做了一个有趣的折中:你得到了可选的类型提示,但你不能依赖它们在运行时保护你。
这就是为什么 Python 的大型项目会依赖 mypy 做 CI 检查。 语言本身把类型标注当成建议,但团队可以把它们变成强制——通过在 CI 里跑类型检查。
JavaScript/TypeScript:动态的底,静态的顶
// 纯 JavaScript
let orderId = "A1001";
orderId = 42; // 完全不报错
orderId.toFixed(2); // "84.00" —— 等等,它什么时候变成数字了?
// TypeScript
let orderId: string = "A1001";
// orderId = 42; // TypeScript 编译报错JavaScript 的类型系统属于 "运行时发现"——你写错类型的代码,它安静地通过,直到运行到那一行才以某种匪夷所思的方式炸掉。TypeScript 是 JavaScript 世界对类型缺失的制度回应:在语言之上加一层编译期类型检查。这个模式如此成功,以至于 Python、Ruby 社群都在追随——渐进类型化。
Java:传统上必须说,现在可以不说(有时)
// 传统写法:类型无处不在
Map<String, List<Order>> ordersByUser = new HashMap<String, List<Order>>();
// 现代写法:能用 var 和 diamond 的地方省略
var ordersByUser = new HashMap<String, List<Order>>(); // Java 10+
// 但字段声明必须显式:
private final Map<String, List<Order>> ordersByUser = new HashMap<>();Java 的演变很有意思:类型标注的比重在下降(var、diamond operator),但这些简化的前提始终是——类型在编译期仍然完全确定。你可以不写出来,但编译器知道。这叫 "类型推断"(type inference),不是 "动态类型"(dynamic typing)。
Rust:可以推断的让你省,不能推断的让你写
let order_id = String::from("A1001"); // 推断
let mut orders = Vec::new(); // 推断失败!Vec<???> 不知道存什么类型
// orders.push("A1001"); // 在后面 push 之后编译器能推断出来
// 函数签名必须显式:
fn process(order_id: &str) -> usize { // 参数和返回类型必须写
order_id.len()
}Rust 的类型推断非常强大——局部变量几乎都能推断。但函数签名必须是显式的。这不是技术限制(编译器理论上也能推断函数类型),而是设计选择:函数边界就是契约边界,契约应该说清楚。
Go:简单到不需要你费心
orderId := "A1001" // 推断
var orderId string // 零值初始化
var orderId string = "A1001" // 完整写法Go 的类型系统刻意做得简单——没有泛型参数推断的复杂度(Go 1.18 引入泛型后仍然保持了推断了简洁性),也没有继承链的类型层级。:= 是一个工程上很聪明的设计:它让 Go 代码看起来清爽,同时保持了完全的静态类型安全。
核心分水岭:标注是编译器在问什么
| 语言 | "这是什么类型"是在问谁 | 什么时候问 |
|---|---|---|
| JavaScript | 问程序员(运行时才知道) | 运行到那一行 |
| Python | 问程序员 + 问类型检查器 | CI / IDE 里问 |
| TypeScript | 问编译器 | 编译期 |
| Go / Java / Kotlin / Swift | 问编译器 | 编译期 |
| Rust | 问编译器 + 问程序员(函数边界) | 编译期 |
把这张表放在 8 门语言的生产事故数据旁边看,规律很明显:
类型越晚被发现错,修复成本越高,但早期开发速度越快。
这不是哪个更好。这是在付不同的代价。
1.4 深度对照三:不可变——三个层次,三种承诺
"这个值能改吗?" 在 8 门语言里有三层不同的回答。
第一层:重新赋值不可变
只保证变量名不能被重新绑定到一个新值。不保证值的内容不变。
const order = { id: "A1001" };
// order = { id: "B2002" }; // TypeError: 重新赋值不行
order.id = "B2002"; // 但改内容完全没问题JavaScript 的 const、Java 的 final(对引用类型)、Kotlin 的 val(对可变对象)都在这一层。
第二层:结构不可变
值的内容也不能改。对值类型(struct)天然成立,对引用类型需要额外机制。
struct Order { var id: String }
let order = Order(id: "A1001")
// order.id = "B2002" // 编译错误!即使 id 是 varSwift 的 let + struct、Kotlin 的 data class(全 val 属性)、Rust 的 let(所有权语义)都在这一层或更深处。
第三层:编译期常量
不只是不可变,而且在编译期就已经确定值。
constexpr int MAX = 100; // C++: 编译期求值const MAX: u32 = 100; // Rust: 编译期常量const val MAX = 100 // Kotlin: 必须顶层,编译期确定第三层常量的关键区别是:它们可以用于数组大小、模板参数等编译期决策。Go 的 const 只支持基本类型,C++ 的 constexpr 可以执行函数计算。
为什么不直接全用第三层?
因为很多 "不可变" 是运行时决定的:
let max = config.get_max_retry(); // 从配置文件读的,编译期不知道你不可能把所有不可变都变成编译期常量。三层区分不是疏忽,是对"什么时候能确定"的诚实。
1.5 边界探索:最容易带错的跨语言习惯
陷阱一:从 Python 到 Rust——把 let 当成了 =
Python 程序员第一次写 Rust,最常写出的错误代码:
let x = 10;
x = 20; // 编译错误!why?在 Python 里这完全可以根因:你在用 Python 的心智模型(声明 = 创建一个可变槽位)操作 Rust(声明 = 创建一个不可变绑定)。需要改成 let mut x = 10。
陷阱二:从 JavaScript 到 Swift——把 let 当成了 const
JavaScript 程序员写 Swift:
struct Container { var value: Int }
let c = Container(value: 10)
c.value = 20 // 编译错误!在 JavaScript 里 const { value } 的 value 是可以改的根因:JavaScript const 只锁引用(第一层),Swift let + struct 锁了内容(第二层)。
陷阱三:从 Java 到 Kotlin——觉得 val 和 final 一样
val list = mutableListOf(1, 2, 3)
list.add(4) // 可以!val 只锁引用Kotlin val 和 Java final 在这一层是一致的(都是第一层不可变)。但 Kotlin 文化强烈鼓励你配合不可变集合使用:listOf() 而不是 mutableListOf()。如果你从 Java 带过来 "声明不可变就够了" 的习惯,Kotlin 社区会说 "还不够"。
陷阱四:从 Go 到 Rust——觉得 := 和 let 一样
Go 的 := 创建可变变量,Rust 的 let 创建不可变绑定。看起来都是 "声明 + 推断 + 赋值" 的快捷方式,但默认的可变性完全相反。
通用规律:跨语言迁移时,看起来最像的语法,往往最危险。因为它们让你放松警惕,让你以为 "这和我熟悉的那个一样"。
1.6 洞察:声明不是语法糖——它是语言对人的第一层假设
如果你只用一门语言,你不会发现:
可变性与类型标注的默认值,组合起来构成了语言对人的第一层信任假设。
画一个 2×2 矩阵:
| 默认可变 | 默认不可变 | |
|---|---|---|
| 类型必须说 | Java, C++(传统) | Rust |
| 类型可以省 | Go, Python, JavaScript | Swift, Kotlin |
- 左下角(Go, Python, JavaScript):"快写,出问题再说。"
- 左上角(Java, C++):"先说清,但说完你可以随便改。"
- 右上角(Rust):"先说清,说清了也别随便改。"
- 右下角(Swift, Kotlin):"不用说太清,但也别随便改。"
每一种组合,都对应一种团队协作假设:
- 小团队 + 快速迭代:左下角最舒服
- 大团队 + 长期维护:左上角渐感吃力,开始向右迁移
- 系统软件 + 正确性优先:右上角
- 移动端 + 平衡体验:右下角
你在声明一个变量时做的选择,不只是语法偏好。你在替你未来的同事(和三个月后的自己)做决定:当他们读到这一行时,他们能确定多少事。
本章小结
声明不只是创建变量,而是建立约束。 每多一个
const、val、let、final,你就多告诉读代码的人一件事:"这个值你可以放心,它不会变。"类型标注的差异不在语法,在 "什么时候被发现"。 动态语言把发现推迟到运行时(开发快,出事晚),静态语言强制提前到编译期(开发慢,出事早)。不存在 "更好",只存在 "代价你更愿意付在什么时候"。
不可变有三个层次。 知道你的语言落在哪一层(只锁引用 / 锁内容 / 编译期常量),就不会在跨语言时犯低级错误。
let这个词在不同语言里是不同的人。 不要因为拼写相同就默认语义相同。这是跨语言迁移中最危险的舒适区。
如果你只用一门语言,你不会学到这个:
一门语言要求你在声明时说多少,本质上是它对 "未来自己会不会后悔现在的偷懒" 这件事的预判。