Solidity 有符号整数(Signed Integer)深度学习文档
基于 RareSkills 文章整理,面向有 Solidity 基础的中级开发者 参考来源:https://rareskills.io/post/signed-int-solidity
一、什么是有符号整数?
有符号整数(Signed Integer) 是可以表示正数、零和负数的整数类型。
Solidity 中的有符号整数类型:
int8,int16,int24, …,int256int是int256的别名
对比无符号整数:
| 类型 | 范围 | 位数 |
|---|---|---|
uint8 | 0 到 255 | 8 bits |
int8 | -128 到 127 | 8 bits |
uint256 | 0 到 2²⁵⁶-1 | 256 bits |
int256 | -2²⁵⁵ 到 2²⁵⁵-1 | 256 bits |
关键观察: 有符号整数的最大正数比无符号整数少一半,因为要用一半的范围表示负数。
二、二进制补码(Two’s Complement)
2.1 什么是二进制补码?
Solidity(以及几乎所有现代计算机系统)使用 二进制补码(Two’s Complement) 来表示有符号整数。
规则:
- 最高位(Most Significant Bit, MSB)是符号位
0= 正数或零1= 负数
- 正数的表示与无符号整数相同
- 负数通过特定规则编码
2.2 int8 的完整编码表(部分)
| 十进制 | 二进制 | 十六进制 |
|---|---|---|
| 0 | 00000000 | 0x00 |
| 1 | 00000001 | 0x01 |
| 2 | 00000010 | 0x02 |
| 127 | 01111111 | 0x7F |
| -1 | 11111111 | 0xFF |
| -2 | 11111110 | 0xFE |
| -127 | 10000001 | 0x81 |
| -128 | 10000000 | 0x80 |
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 / 2 | 0 | -1 |
| 7 / 2 | 3 | 3 |
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 | -3 | 1 | (-3)*(-2)+1 = 7 ✓ |
-7 / -2 | 3 | -1 | 3*(-2)+(-1) = -7 ✓ |
7 / 2 | 3 | 1 | 3*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,但不能表示 +128int256能表示 -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;
}
}
10.4 Chainlink 价格预言机
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 |
| 预言机价格 | int256 | Chainlink 返回 int |
12.2 转换时的安全原则
- 永远使用 SafeCast 进行 int/uint 互转
- 先检查符号再转换:
require(x >= 0)然后uint256(x) - 对
type(int).min特殊处理:取反前先检查 - 不要假设除法向下取整: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 | -128 | 127 | 8 |
| int16 | -32,768 | 32,767 | 16 |
| int32 | -2,147,483,648 | 2,147,483,647 | 32 |
| 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 |
十四、关键概念回顾
- 二进制补码 — 负数表示方法:取反加一,让加法电路统一处理正负数
- 不对称范围 — |min| > max,导致取反 min 值时溢出
- 向零截断 — int 除法不是向下取整,负数除法需特别注意
- 算术右移 — 有符号右移保留符号位,与无符号的逻辑右移不同
- 类型转换是位重解释 — 不检查溢出,不改变底层位
- SafeCast — 安全转换的必备工具
- 禁止混合比较 — int 和 uint 不能直接用
<>比较
文档整理日期:2026-05-31 参考来源:RareSkills - Signed Integers in Solidity