# 变量与可变性:计数器和固定配置
学习目标:区分声明、赋值与遮蔽,用固定上限和可变计数器观察类型推断及作用域。
前置知识与范围:整数比较、简单循环和函数;本篇只讨论局部标量,不展开共享状态或并发同步。
# 问题与边界
一次任务最多执行 3 步:固定配置 limit = 3,计数器从 0 开始,每次循环增加 1,达到上限停止。预览范围使用自己的上限 1,不能修改任务上限;最终计数必须为 3。每个实现都检查初始值、预览值、外层配置与最终值。
这里没有外部输入和业务失败分支:上限是代码内固定的正整数。若断言失败,代表程序违反自身不变量,直接让程序失败,不伪装成正常输出。本篇不接受用户配置、不设计可热更新的配置容器,也不将“绑定不能重赋值”等同于“对象深度不可变”。
# 关键差异
| 语言 | 固定配置 | 可变计数器及推断 | 同名局部遮蔽 |
|---|---|---|---|
| Rust | 默认不可变 let |
let mut |
新 let 可遮蔽,甚至改变类型 |
| Go | const 常量 |
:= 推断,= 赋值 |
内层块可以;同块 := 须有新变量 |
| Java | final |
var 推断局部类型 |
不可在重叠局部作用域重复声明 |
| JS / TS | const |
let;TS 静态推断 |
内层块可以 |
| Kotlin | val |
var |
内层作用域可以 |
| Python | 大写命名约定 | 名称动态绑定 | if/for 不建局部作用域;函数可以 |
| C++ | const |
auto 推断 |
内层块可以 |
# 各语言实现
下列程序各自完整,使用标准库,分别保存到独立目录。不要把不同语言或不同文章的同名文件混放。
# Rust
Rustfn main() {
let limit = 3;
let mut count = 0;
assert_eq!(count, 0);
while count < limit {
count += 1;
}
{
let limit = 1;
assert_eq!(limit, 1);
}
assert_eq!(limit, 3);
assert_eq!(count, 3);
println!("{count}");
}
mut 允许对同一绑定重新赋值,但不会允许改变它的类型。遮蔽创建新绑定,与修改旧值不同;内层离开后旧绑定仍在。let 不可重赋值也不等同于编译期 const。参见 Rust 变量与可变性。
# Go
Gopackage main
import "fmt"
func main() {
const limit = 3
count := 0
if count != 0 { panic("wrong initial count") }
for count < limit {
count++
}
{
const limit = 1
if limit != 1 { panic("wrong preview limit") }
}
if limit != 3 || count != 3 { panic("outer state changed") }
fmt.Println(count)
}
Go 没有通用的不可变局部变量关键字;这里的数值可以用真正的常量。若配置来自运行时函数,不能把结果放进 const。内层 := 容易误建同名变量,本例用小块有意展示这个边界。参见 Go 常量与短变量声明。
# Java
Javapublic class Main {
static final int limit = 3;
static void preview(int limit) {
if (limit != 1 || Main.limit != 3) throw new AssertionError();
}
public static void main(String[] args) {
final var taskLimit = limit;
var count = 0;
if (count != 0) throw new AssertionError();
while (count < taskLimit) {
count++;
}
preview(1);
if (taskLimit != 3 || limit != 3 || count != 3) throw new AssertionError();
System.out.println(count);
}
}
var 是编译期局部类型推断,不是动态类型。Java 不允许在仍可见的局部 taskLimit 内再声明同名局部;因此本例用方法参数 limit 遮蔽类字段,显式用 Main.limit 访问字段,而不是写出无法编译的“八语言对齐”内层局部。参见 Java 局部变量与遮蔽规则。
# JavaScript
JavaScriptfunction run() {
const limit = 3;
let count = 0;
if (count !== 0) throw new Error("wrong initial count");
while (count < limit) {
count += 1;
}
{
const limit = 1;
if (limit !== 1) throw new Error("wrong preview limit");
}
if (limit !== 3 || count !== 3) throw new Error("outer state changed");
return count;
}
console.log(run());
const 禁止重新绑定;如果值是对象,仍可能修改其属性。let 和 const 有块作用域,不应把旧式 var 的函数作用域规则直接套上来。参见 ECMAScript let 与 const。
# TypeScript
TypeScriptfunction run(): number {
const limit = 3;
let count = 0;
if (count !== 0) throw new Error("wrong initial count");
while (count < limit) {
count += 1;
}
{
const limit = 1;
if (limit !== 1) throw new Error("wrong preview limit");
}
if (limit !== 3 || count !== 3) throw new Error("outer state changed");
return count;
}
console.log(run());
TS 推断 count 为 number,后续不能赋字符串;这不意味着运行时的 JavaScript 变量获得了额外保护。const 的静态字面量类型与对象的深层只读也是不同问题。参见 TypeScript 变量声明。
# Kotlin
Kotlinfun main() {
val limit = 3
var count = 0
check(count == 0)
while (count < limit) {
count += 1
}
run {
val limit = 1
check(limit == 1)
}
check(limit == 3 && count == 3)
println(count)
}
val 是只读引用,var 可重新赋值。run { ... } 在此只用来建立预览的局部作用域并立即执行,不涉及异步任务;局部同名声明可能触发遮蔽警告,教学示例有意保留。参见 Kotlin 变量与作用域函数 run。
# Python
不要使用会关闭断言的 -O。
Pythondef run() -> int:
LIMIT = 3
count = 0
assert count == 0
while count < LIMIT:
count += 1
def preview() -> None:
LIMIT = 1
assert LIMIT == 1
preview()
assert LIMIT == 3 and count == 3
return count
print(run())
LIMIT 大写只是约定,解释器不会阻止重新赋值。函数中的赋值默认创建该函数局部名称,除非声明 global 或 nonlocal;这里刻意通过嵌套函数获得独立作用域。参见 Python 名称绑定与 PEP 8 常量。
# C++
C++#include <cassert>
#include <iostream>
int main() {
const auto limit = 3;
auto count = 0;
assert(count == 0);
while (count < limit) {
++count;
}
{
const auto limit = 1;
assert(limit == 1);
}
assert(limit == 3 && count == 3);
std::cout << count << '\n';
}
auto 在初始化时推断一次类型,不允许之后变成字符串。const auto 的这个整数不可修改;指针的顶层 const 与所指对象是否 const 则是另一层问题。参见 C++ 占位类型与块作用域。引用是滚动工作草案,不作为新增标准特性的基线证明。
# 迁移误区与适用限制
- 在计数循环体里重新声明
count,可能使外层计数器永远不增加;count = count + 1是更新,新的let/:=/ 声明可能是遮蔽。 - 把 Python 的
if当作块作用域,在其中写LIMIT = 1会改掉同一函数内的名称;应显式使用新名称或函数边界。 - 把
final、val、const一概解释成“整个对象冻结”,会让集合配置仍被悄悄修改。本例用整数刻意排除对象别名;换成可变容器时需要重新定义配置所有权。 - 类型推断降低声明重复,不取消类型约束。Java、C++、Go、Rust、Kotlin 和 TS 的推断结果不能像 Python 名称那样任意改绑成不同类型。
# 运行与验证
语言版本统一见示例基线,实际执行状态见验证记录。本篇给出的是预期行为,不宣称已经运行通过;各语言按上方命令独立执行。Java 使用显式检查而非默认关闭的 assert;C++ 不要定义 NDEBUG,Python 不使用优化模式,以保留示例断言。
内置检查覆盖初始零值、三次更新、内层配置 1、外层配置保持 3,以及最终计数 3。本篇没有把应当编译失败的代码混入可运行程序;不可重赋值和遮蔽规则由相邻官方引用说明。
预期标准输出:按顺序输出 3,每项独占一行,末尾有换行。 断言失败或未预期异常应使运行失败,而不是继续输出成功提示。
# 相关阅读
- 遍历与提前退出:查找首个匹配文件:用循环查找而不是单纯计数。
- 函数与参数:将固定配置变成有契约的参数。