# 相等性:值相等与身份相等
学习目标:厘清“内容相同(Value Equality)”与“同一内存实例(Identity/Reference Equality)”的本质区别,掌握各语言操作符的对应关系。
前置知识与范围:基本数据结构与引用类型。本篇以二维坐标点 Point(x, y) 为例演示结构比对与引用比对。
# 问题与边界
定义结构 Point { x, y },分别实例化:
p1 = Point(1, 2)p2 = Point(1, 2)(独立分配的新实例,属性值相同)p3 = p1(指向同一实例的引用/别名)
验证断言:
p1与p2:内容相等(Value 等于 true),但引用不相等(Identity 等于 false)。p1与p3:内容相等且引用相等。
# 关键差异
| 语言 | 值相等判断(Value) | 身份/引用相等判断(Identity) | 默认 == 的语义 |
|---|---|---|---|
| Rust | p1 == p2(实现 PartialEq) |
std::ptr::eq(&p1, &p2) |
值相等 |
| Go | p1 == p2(可比结构体逐字段) |
&p1 == &p2(指针比较) |
结构体为值相等,指针为地址比较 |
| Java | p1.equals(p2)(或 record) |
p1 == p2 |
引用相等(极易踩坑) |
| JavaScript | 属性逐个比对或自定义函数 | p1 === p2 或 Object.is |
引用相等(对象无内置值比对) |
| TypeScript | 沿用 JS 语义 | p1 === p2 |
引用相等 |
| Kotlin | p1 == p2(自动调用 equals) |
p1 === p2 |
值相等(与 Java 相反) |
| Python | p1 == p2(调用 __eq__) |
p1 is p2 |
值相等 |
| C++ | p1 == p2(重载 operator==) |
&p1 == &p2(取地址比较) |
未重载时不可编译,重载后为值相等 |
# 各语言实现
# Rust
Rust#[derive(Debug, PartialEq, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
let p3 = &p1;
// 值相等
assert_eq!(p1, p2);
// 引用/指针相等
assert!(!std::ptr::eq(&p1, &p2));
assert!(std::ptr::eq(&p1, p3));
println!("val_eq: {}, id_eq: {}", p1 == p2, std::ptr::eq(&p1, &p2));
}
Rust 的 == 映射到 PartialEq,比较指针身份使用 std::ptr::eq。
# Go
Gopackage main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
p3 := &p1
// 结构体直接用 == 比对值
if p1 != p2 {
panic("expected value equal")
}
// 比较指针地址
if &p1 == &p2 {
panic("expected distinct addresses")
}
if &p1 != p3 {
panic("expected same address")
}
fmt.Printf("val_eq: %t, id_eq: %t\n", p1 == p2, &p1 == &p2)
}
Go 的规范规定,包含可比较字段的结构体类型直接支持 == 值比对。
# Java
Javapublic class Main {
public record Point(int x, int y) {}
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = p1;
// 值相等通过 equals 比对
assert p1.equals(p2);
// == 比较引用地址
assert p1 != p2;
assert p1 == p3;
System.out.println("val_eq: " + p1.equals(p2) + ", id_eq: " + (p1 == p2));
}
}
在 Java 中,对对象使用 == 永远比较的是堆引用地址,这是最常见的跨语言迁移误区之一。
# 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;
}
}
};
function Point(x, y) {
return { x, y };
}
function pointEqual(a, b) {
return a.x === b.x && a.y === b.y;
}
const p1 = Point(1, 2);
const p2 = Point(1, 2);
const p3 = p1;
assert.equal(pointEqual(p1, p2), true);
assert.equal(p1 === p2, false);
assert.equal(p1 === p3, true);
console.log(`val_eq: ${pointEqual(p1, p2)}, id_eq: ${p1 === p2}`);
JavaScript 原生没有重载操作符的能力,对象间的 === 纯粹比对引用一致性。
# 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;
}
}
};
interface Point {
readonly x: number;
readonly y: number;
}
function pointEqual(a: Point, b: Point): boolean {
return a.x === b.x && a.y === b.y;
}
const p1: Point = { x: 1, y: 2 };
const p2: Point = { x: 1, y: 2 };
const p3: Point = p1;
assert.equal(pointEqual(p1, p2), true);
assert.equal(p1 === p2, false);
assert.equal(p1 === p3, true);
console.log(`val_eq: ${pointEqual(p1, p2)}, id_eq: ${p1 === p2}`);
TypeScript 的类型系统在编译期抹除,运行时相等性规则与 JavaScript 严格相同。
# Kotlin
Kotlindata class Point(val x: Int, val y: Int)
fun main() {
val p1 = Point(1, 2)
val p2 = Point(1, 2)
val p3 = p1
// == 在 Kotlin 中翻译为 .equals()
check(p1 == p2)
// === 在 Kotlin 中用于比较引用
check(p1 !== p2)
check(p1 === p3)
println("val_eq: ${p1 == p2}, id_eq: ${p1 === p2}")
}
Kotlin 特别反转了 Java 的操作符设计:== 代表结构相等,=== 专门用于引用相等。
# Python
Pythonfrom dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
if __name__ == "__main__":
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1
# == 比较值
assert p1 == p2
# is 比较身份
assert p1 is not p2
assert p1 is p3
print(f"val_eq: {p1 == p2}, id_eq: {p1 is p2}")
Python 的 == 触发 __eq__ 方法,身份比对必须使用原生关键字 is。
# C++
C++#include <iostream>
#include <cassert>
struct Point {
int x;
int y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
int main() {
Point p1{1, 2};
Point p2{1, 2};
Point* p3 = &p1;
// 值比对通过重载的 operator==
assert(p1 == p2);
// 地址比较
assert(&p1 != &p2);
assert(&p1 == p3);
std::cout << std::boolalpha << "val_eq: " << (p1 == p2)
<< ", id_eq: " << (&p1 == &p2) << "\n";
}
C++17 结构体需显式提供 operator==(C++20 提供 = default 比较),指针比对直接使用 &p1 == &p2。
# 迁移误区与适用限制
- Java 字符串比对误用
==:当字符串来自输入流而非字面量常量池时,"foo" == input会判定为false,必须使用.equals()。 - JavaScript 对象属性比较误区:直接将两个内容相同的普通对象放入
Set或作为Map键,由于基于引用比较,它们会被当成两个完全不同的键。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本val_eq: true, id_eq: false
内容相同但地址独立的测试实例已全面断言通过。