# 方法与封装:受控修改计数器
学习目标:理解面向对象与模块级封装机制,通过保护内部计数器不变式掌握接收者方法、属性与访问可见性。
前置知识与范围:结构体与函数声明。演示将状态隐藏在类型边界之后,仅暴露合法操作方法。
# 问题与边界
设计一个计数器对象 Counter,保证其非递减的不变量(Invariant):
- 内部计数状态由类型私有管理,外部不可直接赋值篡改。
- 初始值默认为
0。 - 操作方法:
increment():使计数值自增 1。add(delta):增加指定增量,契约要求delta >= 0;若传入负数(如-5),必须拒绝(返回错误或抛出异常),且内部计数维持原值不变。value():只读读取当前计数值。
# 关键差异
| 语言 | 字段可见性控制 | 方法定义形态 | 负增量拒绝方式 |
|---|---|---|---|
| Rust | 字段不加 pub 默认为模块私有 |
impl Counter 关联函数,&mut self 接收者 |
返回 Result<(), &'static str> |
| Go | 字段小写首字母即包内私有 | 指针接收者方法 func (c *Counter) Add(...) |
返回 error |
| Java | private 关键字 |
类成员方法 | 抛出 IllegalArgumentException |
| JavaScript | #value 私有字段语法(ES2022) |
实例方法与 getter | 抛出 RangeError |
| TypeScript | private value: number 或 #value |
实例方法与 getter | 抛出 RangeError |
| Kotlin | var value: Int private set |
属性与类方法 | 抛出 IllegalArgumentException |
| Python | _value 惯用法或 @property |
self 参数方法 |
抛出 ValueError |
| C++ | private: 访问标号 |
成员函数 | 抛出 std::invalid_argument |
# 各语言实现
# Rust
Rustpub struct Counter {
value: i32, // 模块外私有
}
impl Counter {
pub fn new() -> Self {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn add(&mut self, delta: i32) -> Result<(), &'static str> {
if delta < 0 {
return Err("delta must be >= 0");
}
self.value += delta;
Ok(())
}
pub fn value(&self) -> i32 {
self.value
}
}
fn main() {
let mut c = Counter::new();
c.increment();
assert_eq!(c.value(), 1);
assert!(c.add(5).is_ok());
assert_eq!(c.value(), 6);
assert!(c.add(-3).is_err());
assert_eq!(c.value(), 6); // 保持原值不变
println!("counter: {}", c.value());
}
Rust 的可见性以模块(mod)为边界,通过 &mut self 独占可变引用确保线程与数据修改的安全性。
# Go
Gopackage main
import (
"errors"
"fmt"
)
type Counter struct {
value int // 首字母小写,包外无法直接访问
}
func NewCounter() *Counter {
return &Counter{value: 0}
}
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Add(delta int) error {
if delta < 0 {
return errors.New("delta must be >= 0")
}
c.value += delta
return nil
}
func (c *Counter) Value() int {
return c.value
}
func main() {
c := NewCounter()
c.Increment()
if c.Value() != 1 {
panic("mismatch")
}
if err := c.Add(5); err != nil || c.Value() != 6 {
panic("add failed")
}
if err := c.Add(-3); err == nil || c.Value() != 6 {
panic("negative delta should fail and preserve value")
}
fmt.Printf("counter: %d\n", c.Value())
}
Go 的方法接收者必须是指针 *Counter,才能就地修改实例内部字段。
# Java
Javapublic class Main {
public static class Counter {
private int value = 0;
public void increment() {
this.value++;
}
public void add(int delta) {
if (delta < 0) {
throw new IllegalArgumentException("delta must be >= 0");
}
this.value += delta;
}
public int getValue() {
return this.value;
}
}
public static void main(String[] args) {
Counter c = new Counter();
c.increment();
assert c.getValue() == 1;
c.add(5);
assert c.getValue() == 6;
try {
c.add(-3);
assert false : "should reject negative delta";
} catch (IllegalArgumentException expected) {
assert c.getValue() == 6; // 维持原值不变
}
System.out.println("counter: " + c.getValue());
}
}
Java 经典的 private 字段配合公开 getter/mutator 构成标准的 JavaBean 封装范式。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
class Counter {
#value = 0; // ES2022 私有属性
increment() {
this.#value++;
}
add(delta) {
if (delta < 0) {
throw new RangeError("delta must be >= 0");
}
this.#value += delta;
}
get value() {
return this.#value;
}
}
const c = new Counter();
c.increment();
assert.equal(c.value, 1);
c.add(5);
assert.equal(c.value, 6);
assert.throws(() => c.add(-3), RangeError);
assert.equal(c.value, 6);
console.log(`counter: ${c.value}`);
ECMAScript 2022 原生私有字段 #value 具有真正的语言级不可见性,外部无法通过字符串反射直接读取。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
class Counter {
#value: number = 0;
increment(): void {
this.#value++;
}
add(delta: number): void {
if (delta < 0) {
throw new RangeError("delta must be >= 0");
}
this.#value += delta;
}
get value(): number {
return this.#value;
}
}
const c = new Counter();
c.increment();
assert.equal(c.value, 1);
c.add(5);
assert.equal(c.value, 6);
assert.throws(() => c.add(-3), RangeError);
assert.equal(c.value, 6);
console.log(`counter: ${c.value}`);
推荐在 TS 中优先使用原生的 #value 私有字段,因为它在编译后依然提供运行时强制隔离,而 private 关键字仅在编译期拦截。
# Kotlin
Kotlinclass Counter {
var value: Int = 0
private set // 外部只读,内部可写
fun increment() {
value++
}
fun add(delta: Int) {
require(delta >= 0) { "delta must be >= 0" }
value += delta
}
}
fun main() {
val c = Counter()
c.increment()
check(c.value == 1)
c.add(5)
check(c.value == 6)
var failed = false
try {
c.add(-3)
} catch (e: IllegalArgumentException) {
failed = true
}
check(failed)
check(c.value == 6)
println("counter: ${c.value}")
}
Kotlin 支持属性的 setter 独立可见性(private set),无需手写冗余的 getter 方法。
# Python
Pythonclass Counter:
def __init__(self) -> None:
self._value = 0 # 惯用单下划线保护
def increment(self) -> None:
self._value += 1
def add(self, delta: int) -> None:
if delta < 0:
raise ValueError("delta must be >= 0")
self._value += delta
@property
def value(self) -> int:
return self._value
if __name__ == "__main__":
c = Counter()
c.increment()
assert c.value == 1
c.add(5)
assert c.value == 6
try:
c.add(-3)
assert False, "should fail on negative delta"
except ValueError:
pass
assert c.value == 6
print(f"counter: {c.value}")
Python 依靠命名约定与 @property 装饰器实现受控暴露;虽然可通过 _value 访问,但契约层面明确约定为私有。
# C++
C++#include <iostream>
#include <stdexcept>
#include <cassert>
class Counter {
private:
int value_{0};
public:
void increment() {
value_++;
}
void add(int delta) {
if (delta < 0) {
throw std::invalid_argument("delta must be >= 0");
}
value_ += delta;
}
int value() const {
return value_;
}
};
int main() {
Counter c;
c.increment();
assert(c.value() == 1);
c.add(5);
assert(c.value() == 6);
try {
c.add(-3);
assert(false);
} catch (const std::invalid_argument&) {
assert(c.value() == 6);
}
std::cout << "counter: " << c.value() << "\n";
}
C++ 通过 const 成员函数标明只读方法,编译器严格拦截在 const 成员方法中修改字段的行为。
# 迁移误区与适用限制
- Go 值接收者无法修改状态:若将
func (c Counter) Increment()误写为值接收者,方法操作的只是原实例的本地副本,调用后原实例的value绝不会发生变化。 - Python 与 JS 的“私有属性”幻觉:Python 的私有变量依赖开发者自觉;而 TS 的
private关键字仅是编译期类型检查,编译为 JS 后在运行时完全是普通公开属性,唯有#原生私有字段才具备真运行时隔离。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本counter: 6
自增、合法累加与非法负增量拒绝(且维持原值)测试全部通过。