Skip to content

第 1 章:声明 — 在你写第一行代码之前

从一个让你不安的实验开始

打开你的电脑,用 8 种语言各写一遍下面这件事:

声明一个变量 orderId,赋值为 "A1001"。然后试图把 orderId 改成 "B2002"。再声明一个变量 maxRetry,赋值为 3

如果一切顺利,这段代码会在 5 分钟内写完。

但有趣的事在后面。问问自己:你能确定 orderId 的类型是什么吗?谁能改它?改的时候会报错吗?如果你把 3 传给一个只接受 Double 的函数呢?

这 8 门语言的答案全都不一样。而它们的差异,在你写下第一行代码的那一刻就已经注定了。

声明不是一个语法问题。它是一个制度问题:这门语言要求你在写代码之前,先说清多少事。


1.1 语法基础:8 种声明,8 种姿态

以下是 8 种语言对 "声明一个订单号" 这件事的不同写法。注意的不只是语法,而是每一行背后,语言在问你什么

Python

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 的态度:类型标注是你的选择,不是你的义务。你可以先写逻辑,后补类型。但这意味着类型错误不会在编译期被拦住——它们会在运行时以 AttributeErrorTypeError 的形式出现。

JavaScript

javascript
let orderId = "A1001";            // 可变变量
orderId = "B2002";                // 没问题

const maxRetry = 3;               // 常量引用:不能重新赋值
// maxRetry = 5;                  // TypeError: Assignment to constant variable

var oldWay = "legacy";            // 历史遗留,不要在新代码中用 var

JavaScript 的态度letconst 是 ES6 之后的主力。没有类型标注——JavaScript 根本不关心 orderId 是字符串还是数字,直到你调用一个只有字符串才有的方法。const 只保护引用,不保护值内部——如果 const obj = { a: 1 },你依然可以 obj.a = 2

Java

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++

cpp
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 的态度分裂:有人觉得它简洁,有人觉得它让代码可读性下降。constconstexpr 的区分(一个是运行时不变,一个是编译期求值)也反映了 C++ 对 "控制" 的执念——它不只区分 "变与不变",还区分 "什么时候可以确定不变"。

Rust

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

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

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

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 种声明

概念PythonJavaScriptJavaC++RustGoSwiftKotlin
可变声明x = 1let x = 1var x = 1int x = 1int x = 1auto x = 1let mut x = 1x := 1var x = 1var x = 1var x = 1
不可变声明无语法级(约定全大写)const x = 1final int x = 1const int x = 1let x = 1const x = 1let x = 1val x = 1
类型可省略是(不写类型就无类型)局部变量可(var可(auto通常可推断可(:=var x =
默认可变?是(let否 — 默认不可变倡导 val

1.2 深度对照一:let 这个词在不同语言里是三个人

"let" 可能是现代编程语言里最被滥用的关键词。它在 8 门语言中出现次数极其频繁,但每一次,它表达的是不同的承诺。

JavaScript 的 let:块级作用域的 "我以后可能变"

javascript
let x = 10;
x = 20;     // 正常
// let x = 30;  // 同一作用域里报错:已经声明过

if (true) {
    let x = 99;  // 块级作用域,不影响外层 x
}
console.log(x);  // 20

let 解决的是 var 的遗留问题:var 只有函数作用域,let 有块级作用域。但和 "不可变" 无关——JavaScript let 是可变的。

Swift 的 let:深度的 "你不能改了"

swift
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

rust
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:可以标注,但标注不是契约

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
// 纯 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:传统上必须说,现在可以不说(有时)

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:可以推断的让你省,不能推断的让你写

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:简单到不需要你费心

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 门语言里有三层不同的回答。

第一层:重新赋值不可变

只保证变量名不能被重新绑定到一个新值。不保证值的内容不变。

javascript
const order = { id: "A1001" };
// order = { id: "B2002" };    // TypeError: 重新赋值不行
order.id = "B2002";             // 但改内容完全没问题

JavaScript 的 const、Java 的 final(对引用类型)、Kotlin 的 val(对可变对象)都在这一层。

第二层:结构不可变

值的内容也不能改。对值类型(struct)天然成立,对引用类型需要额外机制。

swift
struct Order { var id: String }
let order = Order(id: "A1001")
// order.id = "B2002"  // 编译错误!即使 id 是 var

Swift 的 let + struct、Kotlin 的 data class(全 val 属性)、Rust 的 let(所有权语义)都在这一层或更深处。

第三层:编译期常量

不只是不可变,而且在编译期就已经确定值。

cpp
constexpr int MAX = 100;        // C++: 编译期求值
rust
const MAX: u32 = 100;           // Rust: 编译期常量
kotlin
const val MAX = 100             // Kotlin: 必须顶层,编译期确定

第三层常量的关键区别是:它们可以用于数组大小、模板参数等编译期决策。Go 的 const 只支持基本类型,C++ 的 constexpr 可以执行函数计算。

为什么不直接全用第三层?

因为很多 "不可变" 是运行时决定的:

rust
let max = config.get_max_retry();  // 从配置文件读的,编译期不知道

你不可能把所有不可变都变成编译期常量。三层区分不是疏忽,是对"什么时候能确定"的诚实


1.5 边界探索:最容易带错的跨语言习惯

陷阱一:从 Python 到 Rust——把 let 当成了 =

Python 程序员第一次写 Rust,最常写出的错误代码:

rust
let x = 10;
x = 20;  // 编译错误!why?在 Python 里这完全可以

根因:你在用 Python 的心智模型(声明 = 创建一个可变槽位)操作 Rust(声明 = 创建一个不可变绑定)。需要改成 let mut x = 10

陷阱二:从 JavaScript 到 Swift——把 let 当成了 const

JavaScript 程序员写 Swift:

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——觉得 valfinal 一样

kotlin
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, JavaScriptSwift, Kotlin
  • 左下角(Go, Python, JavaScript):"快写,出问题再说。"
  • 左上角(Java, C++):"先说清,但说完你可以随便改。"
  • 右上角(Rust):"先说清,说清了也别随便改。"
  • 右下角(Swift, Kotlin):"不用说太清,但也别随便改。"

每一种组合,都对应一种团队协作假设:

  • 小团队 + 快速迭代:左下角最舒服
  • 大团队 + 长期维护:左上角渐感吃力,开始向右迁移
  • 系统软件 + 正确性优先:右上角
  • 移动端 + 平衡体验:右下角

你在声明一个变量时做的选择,不只是语法偏好。你在替你未来的同事(和三个月后的自己)做决定:当他们读到这一行时,他们能确定多少事。


本章小结

  • 声明不只是创建变量,而是建立约束。 每多一个 constvalletfinal,你就多告诉读代码的人一件事:"这个值你可以放心,它不会变。"

  • 类型标注的差异不在语法,在 "什么时候被发现"。 动态语言把发现推迟到运行时(开发快,出事晚),静态语言强制提前到编译期(开发慢,出事早)。不存在 "更好",只存在 "代价你更愿意付在什么时候"。

  • 不可变有三个层次。 知道你的语言落在哪一层(只锁引用 / 锁内容 / 编译期常量),就不会在跨语言时犯低级错误。

  • let 这个词在不同语言里是不同的人。 不要因为拼写相同就默认语义相同。这是跨语言迁移中最危险的舒适区。


如果你只用一门语言,你不会学到这个:

一门语言要求你在声明时说多少,本质上是它对 "未来自己会不会后悔现在的偷懒" 这件事的预判。