# 函数与参数:计算分页偏移
学习目标:通过计算分页查询偏移量,理解各语言的默认参数、命名参数、参数重载与入参契约校验。
前置知识与范围:基本算数运算、函数声明与调用。偏移公式为 (page - 1) * page_size。本篇聚焦入参校验与参数形式,不连接实际数据库。
# 问题与边界
计算数据查询时的基准跳过条数 offset,八种实现采用同一契约:
- 令
MAX = 9007199254740991(2^53 - 1)。page与page_size都必须是1..MAX内的整数;页码从 1 开始,省略容量时使用 20。 - 成功结果为精确的
(page - 1) * page_size,且必须在0..MAX内。这是为跨语言精确比较而选定的共同值域,不是“支持任意整数”的承诺。 - 非整数、零、负数、超上限输入以及超上限结果都必须明确拒绝(返回错误或抛异常),不得截断、回绕或静默替换成默认值。即使
page=1会使结果为零,也必须先检查容量上限。 - 先校验输入,再用整数商检查
page - 1 > MAX / page_size,最后才能乘法。Python 使用//;JS/TS 使用BigInt整数商比较,避免浮点除法舍入影响边界,然后返回精确的number结果。
共同回归用例(每个语言的驱动均包含):
输入 (page, page_size) |
预期 |
|---|---|
(1,20)、(3,10)、(2,默认值) |
分别为 0、20、20 |
(2,MAX) |
MAX,恰好达到结果上限 |
(3,4503599627370495) |
MAX-1 |
(3,4503599627370496) |
拒绝:结果恰好是 MAX+1 |
(MAX,1)、(1,MAX) |
分别为 MAX-1、0 |
(9007199254740990,3) |
拒绝:保留原精度丢失反例,不返回舍入后的数字 |
(MAX,MAX) |
拒绝:不能先乘法再检查机器整数溢出 |
(0,20)、(-1,20)、(1,0)、(1,-1) |
拒绝:非正数 |
(MAX+1,1)、(1,MAX+1) |
拒绝:输入超上限,包括结果本可为零的情况 |
JS/TS 另检查小数、NaN、无穷值;JS 还检查字符串和布尔值,Python 只接受精确的内置 int 类型(排除 bool、浮点数与字符串)。静态整数签名的示例要求调用方直接传入相应整数类型,不负责恢复调用前显式或隐式类型转换已丢失的信息。
# 关键差异
| 语言 | 默认参数支持 | 命名参数支持 | 契约违例处理 |
|---|---|---|---|
| Rust | 原生不支持;本例用 Option<i64> |
不支持原生命名传参 | 返回 Result<i64, &'static str> |
| Go | 原生不支持;本例提供默认值辅助函数 | 不支持命名实参 | 返回 (int64, error) |
| Java | 不支持默认参数;通过方法重载提供 | 不支持命名实参 | 抛出 IllegalArgumentException |
| JavaScript | 原生支持 (page, pageSize = 20) |
可通过对象解构模拟 | 抛出 RangeError |
| TypeScript | 原生默认参数;静态类型加运行时校验 | 可通过对象解构模拟 | 抛出 RangeError |
| Kotlin | 原生默认参数 pageSize: Long = 20 |
原生命名实参 | 抛出 IllegalArgumentException |
| Python | 原生默认参数 page_size: int = 20 |
原生关键字实参 | 抛出 ValueError |
| C++ | 声明或定义处支持尾部默认参数 | 不支持命名函数实参;指定初始化器是对象初始化 | 抛出 std::invalid_argument |
# 各语言实现
# Rust
Rustconst MAX: i64 = 9_007_199_254_740_991;
fn page_offset(page: i64, page_size: Option<i64>) -> Result<i64, &'static str> {
let size = page_size.unwrap_or(20);
if !(1..=MAX).contains(&page) || !(1..=MAX).contains(&size) {
return Err("page and page_size must be in 1..=MAX");
}
if page - 1 > MAX / size {
return Err("offset exceeds MAX");
}
Ok((page - 1) * size)
}
fn main() {
for (page, size, expected) in [
(1, 20, 0), (3, 10, 20), (2, MAX, MAX),
(3, 4_503_599_627_370_495, MAX - 1),
(MAX, 1, MAX - 1), (1, MAX, 0),
] {
assert_eq!(page_offset(page, Some(size)), Ok(expected));
}
assert_eq!(page_offset(2, None), Ok(20)); // 默认值
for (page, size) in [
(0, 20), (-1, 20), (1, 0), (1, -1),
(MAX + 1, 1), (1, MAX + 1),
(3, 4_503_599_627_370_496),
(9_007_199_254_740_990, 3), (MAX, MAX),
] {
assert!(page_offset(page, Some(size)).is_err());
}
match page_offset(3, Some(10)) {
Ok(offset) => println!("offset: {offset}"),
Err(err) => panic!("unexpected error: {err}"),
}
}
Rust 不提供默认参数语法,本例用 Option<i64> 表达是否提供容量,返回值采用 Result。选用固定 64 位的 i64,而不是随目标平台变化的 usize;完整输入域可被容纳,但乘法仍需在执行前检查共同结果上限。
# Go
Gopackage main
import (
"errors"
"fmt"
)
const maxOffset int64 = 9_007_199_254_740_991
func PageOffset(page int64, pageSize int64) (int64, error) {
if page < 1 || page > maxOffset || pageSize < 1 || pageSize > maxOffset {
return 0, errors.New("page and pageSize must be in 1..MAX")
}
if page-1 > maxOffset/pageSize {
return 0, errors.New("offset exceeds MAX")
}
return (page - 1) * pageSize, nil
}
func PageOffsetDefault(page int64) (int64, error) {
return PageOffset(page, 20)
}
func main() {
for _, tc := range [][3]int64{
{1, 20, 0}, {3, 10, 20}, {2, maxOffset, maxOffset},
{3, 4_503_599_627_370_495, maxOffset - 1},
{maxOffset, 1, maxOffset - 1}, {1, maxOffset, 0},
} {
offset, err := PageOffset(tc[0], tc[1])
if err != nil || offset != tc[2] {
panic(fmt.Sprintf("unexpected result for %v: %d, %v", tc, offset, err))
}
}
offset, err := PageOffsetDefault(2)
if err != nil || offset != 20 {
panic("expected default offset 20")
}
for _, tc := range [][2]int64{
{0, 20}, {-1, 20}, {1, 0}, {1, -1},
{maxOffset + 1, 1}, {1, maxOffset + 1},
{3, 4_503_599_627_370_496},
{9_007_199_254_740_990, 3}, {maxOffset, maxOffset},
} {
if _, err := PageOffset(tc[0], tc[1]); err == nil {
panic(fmt.Sprintf("expected rejection for %v", tc))
}
}
fmt.Printf("offset: %d\n", offset)
}
Go 不支持函数重载与默认形参,本例用 PageOffsetDefault 明确提供默认容量。返回错误时调用方不能使用数值返回项。使用 int64 而不是可能只有 32 位的 int,保证共同输入域不随平台缩小。
# Java
运行时使用 -ea 启用断言。
Javapublic class Main {
static final long MAX = 9_007_199_254_740_991L;
public static long pageOffset(long page, long pageSize) {
if (page < 1 || page > MAX || pageSize < 1 || pageSize > MAX) {
throw new IllegalArgumentException("page and pageSize must be in 1..MAX");
}
if (page - 1 > MAX / pageSize) {
throw new IllegalArgumentException("offset exceeds MAX");
}
return (page - 1) * pageSize;
}
public static long pageOffset(long page) {
return pageOffset(page, 20);
}
public static void main(String[] args) {
long[][] valid = {
{1, 20, 0}, {3, 10, 20}, {2, MAX, MAX},
{3, 4_503_599_627_370_495L, MAX - 1},
{MAX, 1, MAX - 1}, {1, MAX, 0},
};
for (long[] tc : valid) {
assert pageOffset(tc[0], tc[1]) == tc[2];
}
assert pageOffset(2) == 20;
long[][] invalid = {
{0, 20}, {-1, 20}, {1, 0}, {1, -1},
{MAX + 1, 1}, {1, MAX + 1},
{3, 4_503_599_627_370_496L},
{9_007_199_254_740_990L, 3}, {MAX, MAX},
};
for (long[] tc : invalid) {
try {
pageOffset(tc[0], tc[1]);
throw new AssertionError("expected rejection");
} catch (IllegalArgumentException expected) {
// 仅预期的契约异常算作拒绝
}
}
System.out.println("offset: " + pageOffset(3, 10));
}
}
Java 通过方法重载提供默认容量;改用 64 位 long 容纳共同输入域。入参或结果越界时抛出未检查的 IllegalArgumentException,重载也委托同一个受检查的实现。
# JavaScript
.cjs 明确使用 CommonJS,不依赖父目录的 package.json;断言来自 Node 标准库。
JavaScriptconst assert = require("node:assert/strict");
const MAX = Number.MAX_SAFE_INTEGER;
function pageOffset(page, pageSize = 20) {
if (!Number.isSafeInteger(page) || !Number.isSafeInteger(pageSize)
|| page < 1 || pageSize < 1) {
throw new RangeError("page and pageSize must be integers in 1..MAX");
}
if (BigInt(page - 1) > BigInt(MAX) / BigInt(pageSize)) {
throw new RangeError("offset exceeds MAX");
}
return (page - 1) * pageSize;
}
for (const [page, size, expected] of [
[1, 20, 0], [3, 10, 20], [2, MAX, MAX],
[3, 4503599627370495, MAX - 1],
[MAX, 1, MAX - 1], [1, MAX, 0],
]) {
assert.equal(pageOffset(page, size), expected);
}
assert.equal(pageOffset(2), 20);
assert.equal(pageOffset(2, undefined), 20);
for (const [page, size] of [
[0, 20], [-1, 20], [1, 0], [1, -1],
[MAX + 1, 1], [1, MAX + 1],
[3, 4503599627370496], [9007199254740990, 3], [MAX, MAX],
[1.5, 20], [1, 1.5], [NaN, 20], [1, NaN],
[Infinity, 20], [1, Infinity],
["2", 20], [1, "20"], [true, 20], [1, true],
]) {
assert.throws(() => pageOffset(page, size), RangeError);
}
console.log(`offset: ${pageOffset(3, 10)}`);
默认值只在实参为 undefined 或未传递时生效,不把 0 等非法值改成 20。Number.isSafeInteger 同时限制整数性与安全范围,不做字符串转换。这里的 BigInt 只用于乘法前的精确整数商比较,避免把浮点 / 当作整数除法;通过检查后乘积在安全整数范围内,返回类型仍为 number。
# TypeScript
.cts 生成 CommonJS 的 .cjs,无需 package.json 或 Node 类型包;驱动使用简洁显式检查。
TypeScriptconst MAX = Number.MAX_SAFE_INTEGER;
function pageOffset(page: number, pageSize: number = 20): number {
if (!Number.isSafeInteger(page) || !Number.isSafeInteger(pageSize)
|| page < 1 || pageSize < 1) {
throw new RangeError("page and pageSize must be integers in 1..MAX");
}
if (BigInt(page - 1) > BigInt(MAX) / BigInt(pageSize)) {
throw new RangeError("offset exceeds MAX");
}
return (page - 1) * pageSize;
}
const valid: [number, number, number][] = [
[1, 20, 0], [3, 10, 20], [2, MAX, MAX],
[3, 4503599627370495, MAX - 1],
[MAX, 1, MAX - 1], [1, MAX, 0],
];
for (const [page, size, expected] of valid) {
if (pageOffset(page, size) !== expected) {
throw new Error(`unexpected result for ${page}, ${size}`);
}
}
if (pageOffset(2) !== 20 || pageOffset(2, undefined) !== 20) {
throw new Error("expected default offset 20");
}
const invalid: [number, number][] = [
[0, 20], [-1, 20], [1, 0], [1, -1],
[MAX + 1, 1], [1, MAX + 1],
[3, 4503599627370496], [9007199254740990, 3], [MAX, MAX],
[1.5, 20], [1, 1.5], [NaN, 20], [1, NaN],
[Infinity, 20], [1, Infinity],
];
for (const [page, size] of invalid) {
try {
pageOffset(page, size);
throw new Error("expected rejection");
} catch (error: unknown) {
if (!(error instanceof RangeError)) throw error;
}
}
console.log(`offset: ${pageOffset(3, 10)}`);
运行时算法与 JavaScript 一致。number 类型不能静态表达“正的安全整数”,小数、NaN 和无穷值仍需校验;字符串和布尔值不能直接通过此静态签名。类型标注不会替代运行时守卫。
# Kotlin
Kotlinconst val MAX: Long = 9_007_199_254_740_991L
fun pageOffset(page: Long, pageSize: Long = 20): Long {
require(page in 1L..MAX && pageSize in 1L..MAX) {
"page and pageSize must be in 1..MAX"
}
require(page - 1 <= MAX / pageSize) {
"offset exceeds MAX"
}
return (page - 1) * pageSize
}
fun main() {
val valid = arrayOf(
longArrayOf(1, 20, 0), longArrayOf(3, 10, 20), longArrayOf(2, MAX, MAX),
longArrayOf(3, 4_503_599_627_370_495L, MAX - 1),
longArrayOf(MAX, 1, MAX - 1), longArrayOf(1, MAX, 0),
)
for ((page, size, expected) in valid) {
check(pageOffset(page, size) == expected)
}
check(pageOffset(2) == 20L) // 默认参数
check(pageOffset(pageSize = 10, page = 3) == 20L) // 命名参数
val invalid = arrayOf(
longArrayOf(0, 20), longArrayOf(-1, 20),
longArrayOf(1, 0), longArrayOf(1, -1),
longArrayOf(MAX + 1, 1), longArrayOf(1, MAX + 1),
longArrayOf(3, 4_503_599_627_370_496L),
longArrayOf(9_007_199_254_740_990L, 3), longArrayOf(MAX, MAX),
)
for ((page, size) in invalid) {
try {
pageOffset(page, size)
error("expected rejection")
} catch (expected: IllegalArgumentException) {
// error() 抛出的 IllegalStateException 不会被吞掉
}
}
println("offset: ${pageOffset(3, 10)}")
}
Kotlin 原生支持默认形参与命名实参,本例使用 64 位 Long 而不是 Int。标准库 require 在条件为假时抛出 IllegalArgumentException,驱动的 check 不依赖 JVM 的断言开关。
# Python
不要使用会关闭断言的 -O。
PythonMAX = 9_007_199_254_740_991
def page_offset(page: int, page_size: int = 20) -> int:
if type(page) is not int or type(page_size) is not int:
raise ValueError("page and page_size must be built-in int values")
if not 1 <= page <= MAX or not 1 <= page_size <= MAX:
raise ValueError("page and page_size must be in 1..MAX")
if page - 1 > MAX // page_size:
raise ValueError("offset exceeds MAX")
return (page - 1) * page_size
if __name__ == "__main__":
for page, size, expected in [
(1, 20, 0), (3, 10, 20), (2, MAX, MAX),
(3, 4_503_599_627_370_495, MAX - 1),
(MAX, 1, MAX - 1), (1, MAX, 0),
]:
assert page_offset(page, size) == expected
assert page_offset(2) == 20
assert page_offset(page_size=10, page=3) == 20 # 关键字实参
for page, size in [
(0, 20), (-1, 20), (1, 0), (1, -1),
(MAX + 1, 1), (1, MAX + 1),
(3, 4_503_599_627_370_496),
(9_007_199_254_740_990, 3), (MAX, MAX),
(1.5, 20), (1, 1.5), (2.0, 20), (1, 20.0),
(float("nan"), 20), (1, float("nan")),
(float("inf"), 20), (1, float("inf")),
(True, 20), (1, True), ("2", 20), (1, "20"),
]:
try:
page_offset(page, size)
raise AssertionError("expected rejection")
except ValueError:
pass
print(f"offset: {page_offset(3, 10)}")
Python 原生支持默认值与关键字实参。int 具有任意精度,而 bool 是其子类型,所以本例用 type(value) is int 排除布尔值和其他类型,并主动施加共同上限;类型注解本身不执行检查。
# C++
不要定义会关闭断言的 NDEBUG;实现须提供 <cstdint> 的 std::int64_t。
C++#include <cassert>
#include <cstdint>
#include <iostream>
#include <stdexcept>
constexpr std::int64_t MAX = 9'007'199'254'740'991;
std::int64_t page_offset(std::int64_t page, std::int64_t page_size = 20) {
if (page < 1 || page > MAX || page_size < 1 || page_size > MAX) {
throw std::invalid_argument("page and page_size must be in 1..MAX");
}
if (page - 1 > MAX / page_size) {
throw std::invalid_argument("offset exceeds MAX");
}
return (page - 1) * page_size;
}
int main() {
const std::int64_t valid[][3] = {
{1, 20, 0}, {3, 10, 20}, {2, MAX, MAX},
{3, 4'503'599'627'370'495, MAX - 1},
{MAX, 1, MAX - 1}, {1, MAX, 0},
};
for (const auto& tc : valid) {
assert(page_offset(tc[0], tc[1]) == tc[2]);
}
assert(page_offset(2) == 20);
const std::int64_t invalid[][2] = {
{0, 20}, {-1, 20}, {1, 0}, {1, -1},
{MAX + 1, 1}, {1, MAX + 1},
{3, 4'503'599'627'370'496},
{9'007'199'254'740'990, 3}, {MAX, MAX},
};
for (const auto& tc : invalid) {
bool caught = false;
try {
page_offset(tc[0], tc[1]);
} catch (const std::invalid_argument&) {
caught = true;
}
assert(caught);
}
std::cout << "offset: " << page_offset(3, 10) << "\n";
}
C++ 支持尾部默认实参,但不支持命名函数实参。C++20 的指定初始化器用于聚合对象成员初始化,不等于按名称传递函数参数;本例保持 C++17 的位置实参。参见工作草案的默认实参与聚合初始化(在线草案持续更新,不是 C++17 兼容性验证记录)。
# 迁移误区与适用限制
- 默认参数求值时机:在 Python 中,若默认形参是可变对象(如列表、字典),其在模块加载时只求值一次并被多次调用共享;虽然整数/标量参数无此问题,但该特性跨语言迁移时极易出错。
- 契约不可静默篡改:遇到
page < 1时,切忌隐式在内部执行if (page < 1) page = 1;,除非业务接口明确约定该“宽容”语义,否则会掩盖上游调用方的逻辑错误。 - 默认参数位置规则并非各语言完全相同;本例都把可省略的容量放在最后,调用形状保持简单。
- 更宽的机器整数并不能自动保证本题契约:输入可容纳不代表乘积可容纳,结果限制也比有符号 64 位上限更小。输入检查与乘法前检查必须留在函数内,不能只放在演示驱动里。
# 运行与验证
语言版本基线见示例基线。每种语言分别使用一个新建的临时工作目录(例如临时根目录下的 page-offset-rust、page-offset-go、page-offset-java、page-offset-js、page-offset-ts、page-offset-kotlin、page-offset-python、page-offset-cpp),将对应完整代码块以 UTF-8 保存后,在该目录执行小节中的命令。不要在仓库内覆盖同名文件;尤其不要让 TS 产物覆盖手写 JS。目录只放本语言示例和编译产物,用完后可删除自行创建的这些目录。
驱动先检查正常值、默认参数、最大输入与结果、紧邻越界、零/负数及原反例;JS/TS/Python 还检查动态数值或类型边界。所有检查通过时,各程序的预期控制台输出均为:
纯文本offset: 20
以上是驱动覆盖范围与预期结果,不是已执行声明。各语言实际命令、环境、结果与未覆盖边界统一见 2026-10-17 P1 修复验证。