# 数字与运算:平均值不是整数商
学习目标:计算平均值和完成比例,识别整数除法、浮点误差和数值范围。前置知识:变量、函数、条件;只需知道可空返回值表示“没有平均值”。
# 问题与边界
统计任务接收已经校验的非负整数 total 和 count,二者均不超过 1,000,000。返回 total / count 的浮点平均值;count == 0 时报告 empty,不能返回 0 假装有样本。输入类型、负数和超出上述业务范围的校验属于入口,不在本函数重复实现。
mean(7, 2) 为 3.5;mean(0, 1) 为 0;mean(1, 4) 为 0.25,可用于已完成数除以总数的比例;mean(0, 0) 无结果。驱动还覆盖范围上界。统一输出两行 3.5、empty。
本篇不承诺任意实数的精确小数表示,不实现财务舍入或任意精度平均值。限制输入范围,保证整数转双精度前没有丢失这些输入的精度;除法结果仍可能近似。
# 关键差异
| 语言 | 整数操作数的除法 | 默认整数范围注意点 |
|---|---|---|
| Rust / Go / Java / Kotlin / C++ | 向零截断,如 -7 / 2 = -3 | 固定宽度或平台相关;先溢出再转浮点无法补救 |
| JavaScript / TypeScript | number 的 / 是浮点除法 |
连续安全整数范围为 ±(2^53−1);bigint 是另一类型 |
| Python | / 是真除法;// 向负无穷取整 |
int 可增长,但浮点仍有限 |
先转成浮点再除,而非将整数商再转换。若平均值来自多个大整数相加,必须另行解决求和溢出;本例直接接收已存在的总量。固定宽度整数溢出的语言语义也不同:Java/Kotlin 的普通整数运算会回绕,C++ 有符号溢出是未定义行为,Rust 是否触发溢出检查与构建选项有关,不能靠“最后都会变负”推理。
# 各语言实现
各程序独立,均是完整程序;断言失败即中止,empty 是由调用者消费的业务失败。
# Rust
Rustfn mean(total: u32, count: u32) -> Option<f64> {
if count == 0 { None } else { Some(f64::from(total) / f64::from(count)) }
}
fn main() {
assert_eq!(-7 / 2, -3);
assert_eq!(mean(0, 1), Some(0.0));
assert_eq!(mean(1, 4), Some(0.25));
assert_eq!(mean(1_000_000, 1_000_000), Some(1.0));
println!("{}", mean(7, 2).unwrap());
match mean(0, 0) { Some(x) => println!("{x}"), None => println!("empty") }
}
这里 unwrap 仅用于已知非零分母的示范;可变输入用下面的 match。整数除法与溢出条件见 Rust 运算符规则。
# Go
Gopackage main
import "fmt"
func mean(total, count int) (float64, bool) {
if count == 0 { return 0, false }
return float64(total) / float64(count), true
}
func main() {
for _, c := range [][2]int{{0, 1}, {1, 4}, {1000000, 1000000}} {
x, ok := mean(c[0], c[1])
if !ok || x != float64(c[0])/float64(c[1]) { panic("mean") }
}
if -7/2 != -3 { panic("division") }
x, ok := mean(7, 2)
if !ok || x != 3.5 { panic("mean") }
fmt.Println(x)
if _, ok := mean(0, 0); !ok { fmt.Println("empty") } else { panic("zero") }
}
bool 明确区分“没有结果”和合法的 0;int 的位宽不应当被默认为 64。参见 Go 整数运算。
# Java
Javaimport java.util.OptionalDouble;
public class Main {
static OptionalDouble mean(int total, int count) {
return count == 0 ? OptionalDouble.empty()
: OptionalDouble.of((double) total / count);
}
public static void main(String[] args) {
assert -7 / 2 == -3;
assert mean(0, 1).orElseThrow() == 0;
assert mean(1, 4).orElseThrow() == 0.25;
assert mean(1000000, 1000000).orElseThrow() == 1;
System.out.println(mean(7, 2).orElseThrow());
var result = mean(0, 0);
if (result.isPresent()) throw new AssertionError("zero");
System.out.println("empty");
}
}
只转换一个操作数就会触发浮点运算;把 (double) 放到整个整数除法外面已经太晚。参见 JLS 除法。
# JavaScript
JavaScriptfunction mean(total, count) {
return count === 0 ? null : total / count;
}
function check(ok) { if (!ok) throw new Error("check"); }
check(mean(0, 1) === 0 && mean(1, 4) === 0.25);
check(mean(1000000, 1000000) === 1);
check(-7 / 2 === -3.5 && -7n / 2n === -3n);
check(Number.isSafeInteger(2 ** 53 - 1));
check(!Number.isSafeInteger(2 ** 53));
check(2 ** 53 + 1 === 2 ** 53);
console.log(mean(7, 2));
const result = mean(0, 0);
if (result === null) console.log("empty"); else throw new Error("zero");
bigint 整数除法向零截断,不能与 number 直接混算。上面的安全整数断言说明“值可表示”不等于“相邻整数都能区分”。Number 规则见 ECMAScript Number 类型,bigint 除法见 BigInt::divide。
# TypeScript
TypeScriptfunction mean(total: number, count: number): number | null {
return count === 0 ? null : total / count;
}
function check(ok: boolean): void { if (!ok) throw new Error("check"); }
check(mean(0, 1) === 0 && mean(1, 4) === 0.25);
check(mean(1000000, 1000000) === 1);
check(-7 / 2 === -3.5 && -7n / 2n === -3n);
check(Number.isSafeInteger(2 ** 53 - 1));
check(!Number.isSafeInteger(2 ** 53));
check(2 ** 53 + 1 === 2 ** 53);
console.log(mean(7, 2));
const result = mean(0, 0);
if (result === null) console.log("empty"); else throw new Error("zero");
number 不编码“安全整数”,运行时仍是 JS 的精度规则,见 TypeScript 基本类型。
# Kotlin
Kotlinfun mean(total: Int, count: Int): Double? =
if (count == 0) null else total.toDouble() / count
fun main() {
check(-7 / 2 == -3)
check(mean(0, 1) == 0.0 && mean(1, 4) == 0.25)
check(mean(1000000, 1000000) == 1.0)
println(mean(7, 2))
val result = mean(0, 0)
if (result == null) println("empty") else error("zero")
}
可空 Double? 不用 0 当哨兵;转换位置与 Java 相同。参见 Kotlin 数字及除法。
# Python
不要使用会关闭断言的 -O。
Pythondef mean(total: int, count: int) -> float | None:
return None if count == 0 else total / count
assert -7 // 2 == -4 and int(-7 / 2) == -3
assert mean(0, 1) == 0 and mean(1, 4) == 0.25
assert mean(1_000_000, 1_000_000) == 1
print(mean(7, 2))
result = mean(0, 0)
if result is None:
print("empty")
else:
raise AssertionError("zero")
// 不是其他语言整数除法的直接替换,负数反例尤其重要。无限精度 int 也不会让 / 返回无限精度有理数。参见 Python 数值运算。
# C++
C++#include <cassert>
#include <iostream>
#include <optional>
std::optional<double> mean(int total, int count) {
if (count == 0) return std::nullopt;
return static_cast<double>(total) / count;
}
int main() {
assert(-7 / 2 == -3);
assert(mean(0, 1) == 0.0 && mean(1, 4) == 0.25);
assert(mean(1000000, 1000000) == 1.0);
std::cout << *mean(7, 2) << '\n';
if (!mean(0, 0)) std::cout << "empty\n";
else return 1;
}
optional 的解引用要求有值,调用者不能直接解引用失败结果。整数除法见 C++ 工作草案乘除规则;该在线草案会更新,本例只使用 C++17 已有机制。代码假设 int 能容纳 1,000,000,常见桌面工具链满足这一条件;面向更窄平台应改用足够宽的整数类型。
# 迁移误区与适用限制
float(total / count)式迁移可能先丢掉小数;转换必须发生在除法之前。- 用
0.1 + 0.2 == 0.3检查业务正确性会混入二进制近似。误差容限必须由业务尺度决定,不能随手选一个常量解决所有数值比较。 - 整数除零与浮点除零不能一概描述为“抛异常”。本函数先处理空样本,让跨语言调用者获得统一语义;这不是掩盖计算错误,而是明确契约。
- JS 接收超大 JSON 数字后再转
BigInt,已经损失的低位不会恢复。应从文本按大整数协议解析;不要把数据库大整数 ID 当作任意number。
# 运行与验证
版本沿用主题基线,实际运行情况见验证记录。本文预期输出均为 3.5\nempty\n,并包含零值、空样本、比例、输入范围上界和负数除法断言。预期不等于已执行结果。Rust/C++ 生成程序在 PowerShell 用 .\main.exe,POSIX 用 ./main;各语言使用独立目录,避免同名输出覆盖。