AI Library · 知识库

# 相等性:值相等与身份相等

学习目标:厘清“内容相同(Value Equality)”与“同一内存实例(Identity/Reference Equality)”的本质区别,掌握各语言操作符的对应关系。

前置知识与范围:基本数据结构与引用类型。本篇以二维坐标点 Point(x, y) 为例演示结构比对与引用比对。

# 问题与边界

定义结构 Point { x, y },分别实例化:

验证断言:

  1. p1 与 p2:内容相等(Value 等于 true),但引用不相等(Identity 等于 false)。
  2. 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。

# 迁移误区与适用限制

# 运行与验证

语言版本基线见示例基线。 所有程序运行后控制台输出均为:

纯文本val_eq: true, id_eq: false

内容相同但地址独立的测试实例已全面断言通过。

# 相关阅读