AI Library · 知识库

# 函数与参数:计算分页偏移

学习目标:通过计算分页查询偏移量,理解各语言的默认参数、命名参数、参数重载与入参契约校验。

前置知识与范围:基本算数运算、函数声明与调用。偏移公式为 (page - 1) * page_size。本篇聚焦入参校验与参数形式,不连接实际数据库。

# 问题与边界

计算数据查询时的基准跳过条数 offset,八种实现采用同一契约:

  1. 令 MAX = 9007199254740991(2^53 - 1)。page 与 page_size 都必须是 1..MAX 内的整数;页码从 1 开始,省略容量时使用 20。
  2. 成功结果为精确的 (page - 1) * page_size,且必须在 0..MAX 内。这是为跨语言精确比较而选定的共同值域,不是“支持任意整数”的承诺。
  3. 非整数、零、负数、超上限输入以及超上限结果都必须明确拒绝(返回错误或抛异常),不得截断、回绕或静默替换成默认值。即使 page=1 会使结果为零,也必须先检查容量上限。
  4. 先校验输入,再用整数商检查 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 兼容性验证记录)。

# 迁移误区与适用限制

# 运行与验证

语言版本基线见示例基线。每种语言分别使用一个新建的临时工作目录(例如临时根目录下的 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 修复验证。

# 相关阅读