solidity-有符号整数-学习文档

· ☕ 14 分钟阅读
solidity-有符号整数-学习文档

Solidity 有符号整数(Signed Integer)深度学习文档

基于 RareSkills 文章整理,面向有 Solidity 基础的中级开发者 参考来源:https://rareskills.io/post/signed-int-solidity


一、什么是有符号整数?

有符号整数(Signed Integer) 是可以表示正数、零和负数的整数类型。

Solidity 中的有符号整数类型:

  • int8, int16, int24, …, int256
  • intint256 的别名

对比无符号整数:

类型范围位数
uint80 到 2558 bits
int8-128 到 1278 bits
uint2560 到 2²⁵⁶-1256 bits
int256-2²⁵⁵ 到 2²⁵⁵-1256 bits

关键观察: 有符号整数的最大正数比无符号整数少一半,因为要用一半的范围表示负数。


二、二进制补码(Two’s Complement)

2.1 什么是二进制补码?

Solidity(以及几乎所有现代计算机系统)使用 二进制补码(Two’s Complement) 来表示有符号整数。

规则:

  • 最高位(Most Significant Bit, MSB)是符号位
    • 0 = 正数或零
    • 1 = 负数
  • 正数的表示与无符号整数相同
  • 负数通过特定规则编码

2.2 int8 的完整编码表(部分)

十进制二进制十六进制
0000000000x00
1000000010x01
2000000100x02
127011111110x7F
-1111111110xFF
-2111111100xFE
-127100000010x81
-128100000000x80

2.3 如何计算负数的补码表示

将正数 N 转换为 -N 的方法:

Step 1: 写出 N 的二进制
Step 2: 按位取反(0→1, 1→0)
Step 3: 加 1

示例:求 int8(-5) 的二进制

Step 1: 5 的二进制 = 00000101
Step 2: 按位取反    = 11111010
Step 3: 加 1        = 11111011
结果: int8(-5)      = 11111011 = 0xFB

验证: 5 + (-5) 应该等于 0

  00000101  (5)
+ 11111011  (-5)
----------
 100000000  → 溢出的第 9 位被截断 → 00000000 = 0 ✓

2.4 为什么使用二进制补码?

优势说明
零的唯一表示只有 00000000 表示零,不像反码有 +0 和 -0
加法统一正数和负数的加法使用相同的硬件电路
无需额外标志不需要单独的”符号位+绝对值”处理逻辑

三、Solidity 中的有符号整数操作

3.1 声明和赋值

int8 a = -1;      // 合法
int8 b = 127;     // 合法(int8 最大值)
int8 c = -128;    // 合法(int8 最小值)
// int8 d = 128;  // ❌ 编译错误:超出范围
// int8 e = -129; // ❌ 编译错误:超出范围

int256 x = -1;    // 存储为 0xFFFF...FFFF(256位全1)

3.2 获取类型边界值

int256 minVal = type(int256).min;  // -2^255
int256 maxVal = type(int256).max;  // 2^255 - 1

int8 minInt8 = type(int8).min;    // -128
int8 maxInt8 = type(int8).max;    // 127

3.3 算术运算

int256 a = 10;
int256 b = -3;

int256 sum  = a + b;   // 7
int256 diff = a - b;   // 13
int256 prod = a * b;   // -30
int256 quot = a / b;   // -3(向零截断!)
int256 mod  = a % b;   // 1(余数符号跟随被除数)

四、有符号整数除法与取模

4.1 除法向零截断(Truncation Toward Zero)

这是 Solidity 中最容易犯错的地方之一

int256 result1 = -7 / 2;    // = -3(不是 -4!向零截断)
int256 result2 = 7 / -2;    // = -3
int256 result3 = -7 / -2;   // = 3
int256 result4 = 7 / 2;     // = 3

向零截断 vs 向下取整:

表达式向零截断(Solidity)向下取整(数学)
-7 / 2-3-4
-1 / 20-1
7 / 233

4.2 取模运算(%)—— 余数符号跟随被除数

int256 mod1 = -7 % 2;    // = -1(被除数 -7 是负数,余数为负)
int256 mod2 = 7 % -2;    // = 1(被除数 7 是正数,余数为正)
int256 mod3 = -7 % -2;   // = -1(被除数 -7 是负数,余数为负)

恒等式: a == (a / b) * b + (a % b) 始终成立

验证:-7 == (-7/2) * 2 + (-7%2)-7 == (-3)*2 + (-1)-7 == -6 + (-1)-7 == -7

4.3 完整对照表

表达式除法结果取模结果验证
-7 / 2-3-1(-3)*2+(-1) = -7 ✓
7 / -2-31(-3)*(-2)+1 = 7 ✓
-7 / -23-13*(-2)+(-1) = -7 ✓
7 / 2313*2+1 = 7 ✓

五、类型转换(Type Casting)

5.1 int → uint 转换(位重新解释)

关键理解: 类型转换不改变底层位,只改变解释方式

int8 a = -1;            // 二进制: 11111111
uint8 b = uint8(a);     // b = 255(同样的位 11111111,解释为无符号)

int256 x = -1;          // 256位全1
uint256 y = uint256(x); // y = 2^256 - 1(type(uint256).max)

5.2 uint → int 转换

uint8 a = 200;          // 二进制: 11001000
int8 b = int8(a);       // b = -56(同样的位,解释为有符号)
// 因为 MSB 是 1,所以被解释为负数

uint8 c = 100;          // 二进制: 01100100
int8 d = int8(c);       // d = 100(MSB 是 0,正数不变)

5.3 ⚠️ 转换不会触发溢出检查!

// 即使在 Solidity 0.8+ 中,类型转换也不受 overflow 保护
int256 neg = -1;
uint256 result = uint256(neg);
// result = 115792089237316195423570985008687907853269984665640564039457584007913129639935
// 这不会 revert!转换只是重新解释位

// 如果你想要安全转换,使用 SafeCast

5.4 使用 OpenZeppelin SafeCast 安全转换

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

int256 signedValue = -5;
// uint256 result = SafeCast.toUint256(signedValue); // ❌ reverts! 负数不能安全转换

int256 positiveValue = 42;
uint256 result = SafeCast.toUint256(positiveValue); // ✅ result = 42

uint256 bigValue = type(uint256).max;
// int256 signed = SafeCast.toInt256(bigValue); // ❌ reverts! 超出 int256 范围

uint256 safeValue = 1000;
int256 signed = SafeCast.toInt256(safeValue); // ✅ signed = 1000

六、溢出与下溢(Overflow & Underflow)

6.1 Solidity 0.8+ 的默认行为

从 Solidity 0.8.0 开始,算术运算默认带溢出检查

int8 a = 127;
// int8 b = a + 1;  // ❌ 运行时 revert!127 + 1 = 128 超出 int8 范围

int8 c = -128;
// int8 d = c - 1;  // ❌ 运行时 revert!-128 - 1 = -129 超出 int8 范围

6.2 unchecked 块中的溢出行为

int8 a = 127;
int8 b;
unchecked {
    b = a + 1;  // b = -128(溢出回绕!01111111 + 1 = 10000000)
}

int8 c = -128;
int8 d;
unchecked {
    d = c - 1;  // d = 127(下溢回绕!10000000 - 1 = 01111111)
}

6.3 int256 的溢出边界

int256 maxInt = type(int256).max;   // 2^255 - 1
int256 minInt = type(int256).min;   // -2^255

// 在 unchecked 中:
unchecked {
    int256 overflow  = maxInt + 1;   // = type(int256).min = -2^255
    int256 underflow = minInt - 1;   // = type(int256).max = 2^255 - 1
}

七、取反的陷阱(Negation Pitfall)

7.1 核心陷阱:-type(int256).min 溢出!

int256 x = type(int256).min;  // = -2^255

// -x 应该等于 2^255,但 int256 最大只能表示 2^255 - 1
// 所以 -x 会溢出!

// 在 checked 模式下:
// int256 y = -x;  // ❌ revert!

// 在 unchecked 模式下:
unchecked {
    int256 y = -x;  // y = type(int256).min(回绕到自身!)
}

为什么? 二进制补码的不对称性:

  • int8 能表示 -128,但不能表示 +128
  • int256 能表示 -2²⁵⁵,但不能表示 +2²⁵⁵

7.2 安全的 abs() 函数

// ❌ 不安全版本
function unsafeAbs(int256 x) pure returns (int256) {
    return x >= 0 ? x : -x;
    // 当 x == type(int256).min 时,-x 溢出!
}

// ✅ 安全版本 1:revert on min
function safeAbs(int256 x) pure returns (int256) {
    require(x != type(int256).min, "abs overflow");
    return x >= 0 ? x : -x;
}

// ✅ 安全版本 2:返回 uint256(永远不会溢出)
function abs(int256 x) pure returns (uint256) {
    if (x >= 0) {
        return uint256(x);
    } else {
        // 对于 type(int256).min,这种写法是安全的
        unchecked {
            return uint256(-x);  // unchecked 允许回绕,但转换为 uint256 后值正确
        }
    }
}

为什么版本 2 安全?

  • type(int256).min 的位表示是 1000...0000(256位)
  • -type(int256).min 在 unchecked 中仍然是 1000...0000
  • 1000...0000 解释为 uint256 = 2²⁵⁵,这正是我们想要的绝对值

八、位运算(Bitwise Operations)

8.1 右移的关键区别:算术移位 vs 逻辑移位

// 有符号整数:算术右移(Arithmetic Right Shift)
// 高位用符号位填充
int8 a = -8;           // 二进制: 11111000
int8 b = a >> 1;       // 二进制: 11111100 = -4(符号位保持1)
int8 c = a >> 2;       // 二进制: 11111110 = -2

// 无符号整数:逻辑右移(Logical Right Shift)
// 高位用 0 填充
uint8 x = 248;         // 二进制: 11111000(与 int8(-8) 相同的位)
uint8 y = x >> 1;      // 二进制: 01111100 = 124(高位填 0)
uint8 z = x >> 2;      // 二进制: 00111110 = 62

8.2 符号扩展(Sign Extension)

当有符号整数从小类型转为大类型时,高位用符号位填充:

int8 small = -1;       // 8位:  11111111
int256 big = small;    // 256位: 11111111...11111111(全1,仍然是 -1)

int8 pos = 5;          // 8位:  00000101
int256 bigPos = pos;   // 256位: 00000000...00000101(全0填充,仍然是 5)

8.3 位运算操作符

int8 a = -1;   // 11111111
int8 b = 15;   // 00001111

int8 and = a & b;   // 00001111 = 15
int8 or  = a | b;   // 11111111 = -1
int8 xor = a ^ b;   // 11110000 = -16
int8 not = ~a;      // 00000000 = 0(~(-1) = 0)

8.4 左移(Left Shift)

左移对有符号和无符号整数行为相同(低位补 0):

int8 a = 3;         // 00000011
int8 b = a << 2;    // 00001100 = 12

int8 c = -2;        // 11111110
int8 d = c << 1;    // 11111100 = -4

九、有符号整数与无符号整数的混合使用

9.1 Solidity 禁止直接混合比较

int256 a = -1;
uint256 b = 100;

// if (a < b) { ... }  // ❌ 编译错误!不允许直接比较 int 和 uint

9.2 安全的混合比较模式

function safeCompare(int256 a, uint256 b) pure returns (bool aIsSmaller) {
    // 如果 a 是负数,它一定小于任何 uint
    if (a < 0) {
        return true;
    }
    // a >= 0,可以安全转为 uint256
    return uint256(a) < b;
}

9.3 混合运算

int256 a = -5;
uint256 b = 10;

// int256 c = a + int256(b);  // 需要显式转换
// 或者
// uint256 d = uint256(a) + b;  // ⚠️ 危险!a 是负数,转 uint 会变成巨大正数

// 正确做法:
int256 c = a + int256(b);  // c = 5(前提是 b 不超过 int256.max)

十、实际使用场景

10.1 价格变化 / Delta 值

contract PriceTracker {
    int256 public lastPriceChange;

    function updatePrice(uint256 oldPrice, uint256 newPrice) external {
        // 价格变化可能是正数(涨)或负数(跌)
        lastPriceChange = int256(newPrice) - int256(oldPrice);
    }

    function isPriceUp() external view returns (bool) {
        return lastPriceChange > 0;
    }
}

10.2 坐标系统

struct Position {
    int256 x;
    int256 y;
}

function distance(Position memory a, Position memory b) pure returns (uint256) {
    int256 dx = a.x - b.x;
    int256 dy = a.y - b.y;
    // 需要取绝对值
    uint256 absDx = dx >= 0 ? uint256(dx) : uint256(-dx);
    uint256 absDy = dy >= 0 ? uint256(dy) : uint256(-dy);
    return absDx + absDy; // 曼哈顿距离
}

10.3 盈亏计算

contract PnLTracker {
    mapping(address => int256) public unrealizedPnL;

    function updatePnL(address trader, int256 change) external {
        unrealizedPnL[trader] += change;
    }

    function isInProfit(address trader) external view returns (bool) {
        return unrealizedPnL[trader] > 0;
    }
}

Chainlink 的价格 Feed 返回 int256 类型:

interface AggregatorV3Interface {
    function latestRoundData() external view returns (
        uint80 roundId,
        int256 answer,    // ← 有符号!价格可能为负(如石油期货)
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    );
}

function getPrice() external view returns (uint256) {
    (, int256 price,,,) = priceFeed.latestRoundData();
    require(price > 0, "Invalid price");
    return uint256(price);
}

十一、常见陷阱汇总

陷阱 1:取反 min 值溢出

int256 x = type(int256).min;
int256 y = -x;  // ❌ REVERT(或 unchecked 下回绕到自身)

陷阱 2:类型转换不检查溢出

int256 neg = -1;
uint256 huge = uint256(neg);  // = 2^256 - 1,不会 revert!

陷阱 3:除法向零截断的精度丢失

int256 a = -1;
int256 b = 2;
int256 c = a / b;  // = 0(不是 -1!)
// 如果用于计算利率等场景,会导致精度问题

陷阱 4:右移不是除以 2(对于负数)

int256 a = -7;
int256 shifted = a >> 1;   // = -4(算术右移,向负无穷方向)
int256 divided = a / 2;    // = -3(除法向零截断)
// shifted ≠ divided !

陷阱 5:混合比较编译错误

int256 a = -1;
uint256 b = 0;
// bool result = a < b;  // ❌ 编译错误

陷阱 6:不对称范围

int8 x = -128;
int8 y = -x;    // ❌ 128 不在 int8 范围内!
// |min| > max 对于所有有符号整数都成立

十二、最佳实践

12.1 何时使用 int vs uint

场景推荐类型原因
Token 余额uint256余额永远 ≥ 0
价格变化量int256可能涨或跌
时间戳uint256永远 ≥ 0
坐标int256可能为负
盈亏int256亏损为负
数组索引uint256永远 ≥ 0
预言机价格int256Chainlink 返回 int

12.2 转换时的安全原则

  1. 永远使用 SafeCast 进行 int/uint 互转
  2. 先检查符号再转换require(x >= 0) 然后 uint256(x)
  3. type(int).min 特殊处理:取反前先检查
  4. 不要假设除法向下取整:Solidity 的 int 除法向零截断

12.3 编码规范

// ✅ 好:显式处理边界
function negate(int256 x) pure returns (int256) {
    require(x > type(int256).min, "Cannot negate min value");
    return -x;
}

// ✅ 好:使用 SafeCast
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
using SafeCast for int256;
using SafeCast for uint256;

uint256 unsigned = signedValue.toUint256();
int256 signed = unsignedValue.toInt256();

// ❌ 坏:裸转换
uint256 result = uint256(negativeValue);  // 静默产生巨大正数

十三、速查表

int 类型范围

类型最小值最大值位数
int8-1281278
int16-32,76832,76716
int32-2,147,483,6482,147,483,64732
int64-9.2 × 10¹⁸9.2 × 10¹⁸64
int128-1.7 × 10³⁸1.7 × 10³⁸128
int256-5.8 × 10⁷⁶5.8 × 10⁷⁶256

关键运算行为

运算行为示例
除法 /向零截断-7/2 = -3
取模 %余数符号跟随被除数-7%2 = -1
右移 >>算术移位(符号位填充)-8>>1 = -4
左移 <<低位补 0-2<<1 = -4
取反 -x对 min 值溢出-(int8.min) 溢出
类型转换位不变,解释方式变int8(-1)→uint8 = 255

十四、关键概念回顾

  1. 二进制补码 — 负数表示方法:取反加一,让加法电路统一处理正负数
  2. 不对称范围 — |min| > max,导致取反 min 值时溢出
  3. 向零截断 — int 除法不是向下取整,负数除法需特别注意
  4. 算术右移 — 有符号右移保留符号位,与无符号的逻辑右移不同
  5. 类型转换是位重解释 — 不检查溢出,不改变底层位
  6. SafeCast — 安全转换的必备工具
  7. 禁止混合比较 — int 和 uint 不能直接用 < > 比较

文档整理日期:2026-05-31 参考来源:RareSkills - Signed Integers in Solidity

💬 评论