第10章 变量
一、导读
1.1 本章学习目标
第10章深入讲解了变量的正确使用方式。McConnell 指出,变量是程序中最基本的数据载体,其使用方式直接影响代码的可读性和可维护性。本章的核心学习目标包括:
- 掌握变量命名的艺术:有意义的名称,避免缩写。
- 理解变量作用域的原则:尽可能缩小作用域。
- 学会变量初始化的最佳实践:在声明时初始化。
- 理解变量生命周期的概念:变量的存活时间和影响范围。
- 掌握避免全局变量的策略:减少耦合。
- 学会常量的使用:替代魔术数字。
1.2 为什么本章如此重要
变量看似简单,但其使用方式对代码质量有着深远影响。McConnell 指出:好的变量命名让代码自解释,好的作用域控制让代码更安全,好的初始化习惯让 bug 更少。
本章的重要性在于它提供了变量使用的系统化指南。你会理解如何通过良好的变量使用习惯来提升代码质量。
1.3 核心问题引导
变量命名应该遵循什么规则?如何做到"名副其实"?
变量作用域应该多大?为什么要"尽可能缩小"?
变量应该在什么时候初始化?为什么?
全局变量有什么危害?如何避免?
常量和魔术数字有什么区别?如何使用常量?
二、核心概念详解
2.1 变量命名
2.1.1 命名原则
好的变量名应该:
- 名副其实:反映变量的本质
- 避免误导:不使用含义模糊的名称
- 可搜索:避免单字母变量(循环变量除外)
- 使用英文:避免拼音或中文
// 差:含义模糊
int d; // 表示什么?
String s; // 什么字符串?
List<int[]> list; // 什么的列表?
// 好:名副其实
int elapsedTimeInDays;
String customerName;
List<int[]> cellGrid;
// 差:误导
int accountCount; // 实际是 Account 对象的计数,不是 int 的计数
// 应该用:numberOfAccounts
// 差:不可搜索
String e; // 在代码中搜索 "e" 会找到大量无关结果
// 应该用:errorMessage2.1.2 命名约定
// 布尔变量:使用 is/has/can/should 前缀
boolean isActive;
boolean hasPermission;
boolean canEdit;
boolean shouldRetry;
// 集合变量:使用复数名词
List<User> users;
Map<String, Order> ordersById;
Set<String> tags;
// 临时变量:使用描述性名称
// 差
String tmp = user.getName();
// 好
String userName = user.getName();
// 循环变量:可以使用单字母(短循环)
for (int i = 0; i < items.size(); i++) {
// ...
}
// 但长循环应该用描述性名称
for (int rowIndex = 0; rowIndex < matrix.length; rowIndex++) {
for (int colIndex = 0; colIndex < matrix[rowIndex].length; colIndex++) {
// ...
}
}2.1.3 避免缩写
// 差:过度缩写
int qty; // quantity
String custNm; // customerName
Date crtDt; // createDate
// 好:完整名称
int quantity;
String customerName;
Date createDate;
// 例外:广泛认可的缩写
int id; // identifier
String url; // uniform resource locator
String http; // hypertext transfer protocol2.2 变量作用域
2.2.1 作用域原则
变量作用域应该尽可能缩小。原因:
- 减少耦合:变量影响范围越小,与其他代码的耦合越小。
- 提高可读性:变量在附近使用,容易理解。
- 减少错误:变量不会在不该使用的地方被使用。
// 差:作用域过大
public class BadExample {
private int counter; // 类级别作用域
public void method1() {
counter = 0;
for (int i = 0; i < 10; i++) {
counter++;
}
System.out.println(counter);
}
public void method2() {
// counter 可以被意外访问和修改
counter = 100;
}
}
// 好:作用域最小化
public class GoodExample {
public void method1() {
int counter = 0; // 方法级别作用域
for (int i = 0; i < 10; i++) {
counter++;
}
System.out.println(counter);
}
public void method2() {
// 没有 counter 变量,不会意外访问
}
}
// 更好:作用域进一步缩小
public void processItems(List<Item> items) {
// 差:在循环外声明
// Item currentItem;
// for (int i = 0; i < items.size(); i++) {
// currentItem = items.get(i);
// process(currentItem);
// }
// 好:在循环内声明
for (Item item : items) {
process(item); // item 只在循环内可见
}
}2.2.2 作用域与生命周期
// 作用域:变量在代码中可见的范围
// 生命周期:变量在内存中存在的时间
// 理想情况:作用域和生命周期一致
public void processOrder(Order order) {
Money total = calculateTotal(order); // total 的作用域从这里开始
saveTotal(total); // total 在这里使用
// total 的作用域到这里结束
}
// 不理想:作用域大于生命周期
public void processOrder(Order order) {
Money total; // total 在这里声明
// ... 20行代码,total 未被使用 ...
total = calculateTotal(order); // total 在这里初始化
saveTotal(total);
// ... 20行代码,total 未被使用 ...
// total 的作用域到这里结束
}2.3 变量初始化
2.3.1 初始化原则
变量应该在声明时初始化,或者在使用前立即初始化。
// 差:延迟初始化
public void processOrder(Order order) {
Money total;
Money discount;
Money tax;
// ... 很多代码 ...
total = calculateTotal(order);
discount = calculateDiscount(order);
tax = calculateTax(total, discount);
// ... 使用变量 ...
}
// 好:声明时初始化
public void processOrder(Order order) {
Money total = calculateTotal(order);
Money discount = calculateDiscount(order);
Money tax = calculateTax(total, discount);
// ... 使用变量 ...
}
// 更好:在使用前立即初始化
public void processOrder(Order order) {
Money total = calculateTotal(order);
Money discount = calculateDiscount(order);
Money netAmount = total.subtract(discount);
Money tax = calculateTax(netAmount);
Money finalAmount = netAmount.add(tax);
saveOrder(order, finalAmount);
}2.3.2 避免未初始化变量
// 差:可能使用未初始化的变量
public String getUserName(User user) {
String name;
if (user != null) {
name = user.getName();
}
return name; // 如果 user 为 null,name 未初始化!
}
// 好:确保初始化
public String getUserName(User user) {
String name = ""; // 默认值
if (user != null) {
name = user.getName();
}
return name;
}
// 更好:使用 Optional
public Optional<String> getUserName(User user) {
if (user == null) {
return Optional.empty();
}
return Optional.of(user.getName());
}2.4 避免全局变量
2.4.1 全局变量的危害
全局变量的问题:
- 耦合:任何代码都可以访问和修改,导致高度耦合。
- 不可预测:不知道何时何地会被修改。
- 难以测试:测试时需要处理全局状态。
- 并发问题:多线程环境下容易出现竞态条件。
// 差:全局变量
public class GlobalState {
public static User currentUser;
public static List<Order> pendingOrders;
}
// 任何地方都可以修改
public void processOrder(Order order) {
GlobalState.currentUser = order.getCustomer(); // 隐式依赖
GlobalState.pendingOrders.add(order); // 隐式修改
}
// 好:通过参数传递
public void processOrder(Order order, User currentUser) {
// 明确的依赖关系
validateOrder(order, currentUser);
orderRepository.save(order);
}2.4.2 替代全局变量的方案
// 方案1:依赖注入
public class OrderService {
private final UserRepository userRepository;
private final OrderRepository orderRepository;
public OrderService(UserRepository userRepository,
OrderRepository orderRepository) {
this.userRepository = userRepository;
this.orderRepository = orderRepository;
}
public void processOrder(OrderId orderId, UserId userId) {
User user = userRepository.findById(userId);
Order order = orderRepository.findById(orderId);
// ...
}
}
// 方案2:单例模式(谨慎使用)
public class Configuration {
private static final Configuration INSTANCE = new Configuration();
private final Map<String, String> settings;
private Configuration() {
settings = loadSettings();
}
public static Configuration getInstance() {
return INSTANCE;
}
public String getSetting(String key) {
return settings.get(key);
}
}
// 方案3:线程局部变量
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setCurrentUser(User user) {
currentUser.set(user);
}
public static User getCurrentUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}2.5 常量的使用
2.5.1 替代魔术数字
魔术数字(Magic Number)是指代码中没有解释的数字常量。
// 差:魔术数字
public double calculateTax(double amount) {
return amount * 0.08; // 0.08 是什么?
}
public void setSessionTimeout(int timeout) {
if (timeout > 86400) { // 86400 是什么?
throw new IllegalArgumentException("Timeout too long");
}
}
// 好:使用常量
public class TaxConstants {
public static final double STANDARD_TAX_RATE = 0.08;
}
public double calculateTax(double amount) {
return amount * TaxConstants.STANDARD_TAX_RATE;
}
public class TimeConstants {
public static final int SECONDS_PER_DAY = 86400;
}
public void setSessionTimeout(int timeout) {
if (timeout > TimeConstants.SECONDS_PER_DAY) {
throw new IllegalArgumentException("Timeout too long");
}
}2.5.2 常量的命名
// 常量命名:全大写,下划线分隔
public class OrderConstants {
public static final int MAX_ITEMS_PER_ORDER = 100;
public static final double MIN_ORDER_AMOUNT = 10.0;
public static final String DEFAULT_CURRENCY = "USD";
// 枚举也是常量的好选择
public enum OrderStatus {
CREATED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
}
// 使用常量
public class Order {
private final List<OrderItem> items;
public void addItem(OrderItem item) {
if (items.size() >= OrderConstants.MAX_ITEMS_PER_ORDER) {
throw new IllegalStateException(
"Cannot add more than " + OrderConstants.MAX_ITEMS_PER_ORDER + " items");
}
items.add(item);
}
}三、实战案例
3.1 案例一:改善变量使用
// 改善前:糟糕的变量使用
public class BadVariableUsage {
private static List<Order> orders; // 全局变量
public void process(String s, int i, boolean b) { // 模糊的参数名
String t = "";
double d = 0;
if (b) {
t = s + "_processed";
d = i * 0.08;
}
orders.add(new Order(t, d)); // 隐式依赖全局变量
}
}
// 改善后:良好的变量使用
public class GoodVariableUsage {
public OrderResult processOrder(String orderName, int quantity,
boolean applyTax) {
String processedName = orderName + "_processed";
double totalAmount = quantity * OrderConstants.UNIT_PRICE;
if (applyTax) {
double tax = totalAmount * TaxConstants.STANDARD_TAX_RATE;
totalAmount += tax;
}
Order order = new Order(processedName, totalAmount);
return OrderResult.success(order);
}
}四、本章小结
第10章讲解了变量的正确使用方式,核心要点包括:
变量命名要名副其实:名称反映变量的本质,避免模糊和误导。
作用域尽可能缩小:减少耦合,提高可读性和安全性。
声明时初始化:避免使用未初始化的变量。
避免全局变量:使用依赖注入、参数传递等替代方案。
常量替代魔术数字:提高代码可读性和可维护性。
McConnell 的核心观点是:变量的使用方式反映了程序员的专业素养。好的变量习惯是写出高质量代码的基础。
五、思考与练习
思考题
你的变量命名是否清晰?有没有模糊的命名?
你的变量作用域是否合理?有没有过大的作用域?
你的代码中有魔术数字吗?如何替换为常量?
你的代码中有全局变量吗?如何消除?
编程练习
找到一段代码,改善其中所有的变量命名。
重构一个使用全局变量的类,使用依赖注入替代。
找到代码中所有的魔术数字,替换为命名常量。