# 字符串转端口号:解析之后还要校验
学习目标:分清转换、解析、断言与业务校验,并显式消费成功值和失败结果。
前置知识:函数、字符串、分支;下文同时示范结果类型和异常的基本调用方式。范围:解析已经是字符串的配置值,不处理文件读取、网络连接或任意对象的类型校验。
# 问题与边界
从配置中读取服务连接端口。配置值已经是字符串,本例要求:
- 只接受 1~5 个 ASCII 数字,可有前导零。
- 数值范围为 1~65535;本例不允许用 0 请求系统分配监听端口。
- 拒绝空白、正负号、小数、指数形式、尾随字符和非 ASCII 数字。
- 无效输入必须报告失败,不能静默使用默认值。
这是本配置格式的约定,并非所有端口配置都必须采用同一语法。先统一输入契约,才能比较同一件事。
| 输入 | 预期 |
|---|---|
"8080" |
8080 |
"00080" |
80 |
"1"、"65535" |
分别得到 1、65535 |
""、"0"、"65536" |
失败 |
" 80"、"+80"、"-80"、"80x"、"80\n" |
失败;\n 表示实际换行字符 |
"1e3"、"80.0"、"80"、"000080" |
失败 |
# 关键差异
先区分四种操作,再比较实现:
| 操作 | 问题 | 本例关系 |
|---|---|---|
| 数值转换 | 一个已知数值能否用另一数值类型表示? | 与字符串解析不同 |
| 文本解析 | 这段字符能否按数字格式解释? | 得到数值 |
| 类型断言 | 开发者向类型检查器声明了什么? | 不负责验证配置 |
| 业务校验 | 这个数值是否符合当前业务规则? | 排除 0 和超范围值 |
| 语言 | 标准解析或转换的关键行为 | 本函数如何报告失败 |
|---|---|---|
| Rust | parse::<u16>() 返回 Result,同时检查 u16 范围 |
Err("invalid port") |
| Go | Atoi 返回数值和 error,不替代业务范围检查 |
非 nil 的 error |
| Java | parseInt 解析失败或溢出时抛异常 |
IllegalArgumentException |
| JavaScript / TypeScript | Number 接受空白等额外形式,无法转换时可得 NaN;parseInt 可只解析前缀 |
本函数主动抛 RangeError |
| Kotlin | toInt 失败时抛异常;这里的前置条件已保证可转换 |
require 抛 IllegalArgumentException |
| Python | int 接受的语法比本协议宽,非法文本抛 ValueError |
ValueError |
| C++ | from_chars 返回错误码和停止位置,不用异常表示解析失败 |
空的 optional |
# 各语言实现
以下按“实现块 → 驱动块”的顺序组装,每种语言放在独立目录。驱动分别消费 "8080" 的成功值和 "80x" 的失败;预期两行输出为 8080、invalid port。这些驱动用于展示调用方分支,不把失败静默替换成 0 或默认端口。
统一业务契约,但保留各语言的失败表达方式。有消息的失败统一使用 invalid port;C++ 的 nullopt 不携带消息,由驱动自行输出该提示。生产代码可区分格式错误和范围错误。输入必须是字符串;Java 的 null、JavaScript 的任意外部对象等不属于此契约。
# Rust
函数片段,保存为 main.rs;将下面的驱动追加到同一文件后再编译。
Rustfn parse_port(text: &str) -> Result<u16, &'static str> {
if text.is_empty() || text.len() > 5
|| !text.bytes().all(|b| b.is_ascii_digit())
{
return Err("invalid port");
}
let port = text.parse::<u16>().map_err(|_| "invalid port")?;
if port == 0 {
return Err("invalid port");
}
Ok(port)
}
第一块是函数片段;第二块是驱动片段,依次拼接为 main.rs。执行 rustc --edition=2021 main.rs,再在 Windows PowerShell 执行 .\main.exe,或在 POSIX shell 执行 ./main。
Rustfn main() {
for text in ["8080", "80x"] {
match parse_port(text) {
Ok(port) => println!("{port}"),
Err(message) => println!("{message}"),
}
}
}
u16 的解析同时检查可表示范围;? 将失败返回给调用者,调用者用 match 分别消费结果。Rust parse 文档。
# Go
函数片段(含包声明和导入),保存为 main.go;还需追加下面的驱动。
Gopackage main
import (
"errors"
"fmt"
"strconv"
)
func parsePort(text string) (int, error) {
if len(text) == 0 || len(text) > 5 {
return 0, errors.New("invalid port")
}
for _, c := range text {
if c < '0' || c > '9' {
return 0, errors.New("invalid port")
}
}
port, err := strconv.Atoi(text)
if err != nil || port < 1 || port > 65535 {
return 0, errors.New("invalid port")
}
return port, nil
}
第一块包含包声明、导入和函数;将第二块驱动追加到其后,保存为 main.go,执行 go run main.go。
Gofunc main() {
for _, text := range []string{"8080", "80x"} {
port, err := parsePort(text)
if err != nil {
fmt.Println(err)
continue
}
fmt.Println(port)
}
}
调用者先检查 err,只有 err == nil 时才使用端口值;错误时返回的 0 不是可用结果。strconv.Atoi 文档。
# Java
完整的工具类,保存为 Ports.java;它没有程序入口,配合下面的 Main.java 驱动运行。
Javapublic class Ports {
public static int parsePort(String text) {
if (!text.matches("[0-9]{1,5}")) {
throw new IllegalArgumentException("invalid port");
}
int port = Integer.parseInt(text);
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("invalid port");
}
return port;
}
}
第一块是完整的 Ports 类,保存为 Ports.java;第二块驱动单独保存为 Main.java。执行 javac Ports.java Main.java,再执行 java Main。
Javapublic class Main {
public static void main(String[] args) {
for (String text : new String[]{"8080", "80x"}) {
try {
int port = Ports.parsePort(text);
System.out.println(port);
} catch (IllegalArgumentException error) {
System.out.println(error.getMessage());
}
}
}
}
前置格式限制使数字不会超过 int 范围;输入契约不包含 null。调用失败时控制流转入 catch,不会执行成功输出。Integer.parseInt 文档。
# JavaScript
函数片段,保存为 main.js;还需追加下面的驱动。
JavaScriptfunction parsePort(text) {
if (text.length < 1 || text.length > 5 || /[^0-9]/.test(text)) {
throw new RangeError("invalid port");
}
const port = Number(text);
if (port < 1 || port > 65535) {
throw new RangeError("invalid port");
}
return port;
}
第一块是函数片段;将下列驱动追加其后保存为 main.js,执行 node main.js。该函数只接收字符串;任意外部值进入前还需检查 typeof。
JavaScriptfor (const text of ["8080", "80x"]) {
try {
const port = parsePort(text);
console.log(port);
} catch (error) {
if (!(error instanceof RangeError)) {
throw error;
}
console.log(error.message);
}
}
驱动只消费本函数约定的 RangeError,其他异常继续抛出,避免掩盖不在契约内的错误。这个驱动也能直接用于下面的 TypeScript 版本。
不能用 parseInt(text, 10) 单独完成校验:它允许从文本前缀得到整数,如 parseInt("80x", 10) 得到 80。参见 ECMAScript parseInt。
# TypeScript
TypeScriptfunction parsePort(text: string): number {
if (text.length < 1 || text.length > 5 || /[^0-9]/.test(text)) {
throw new RangeError("invalid port");
}
const port = Number(text);
if (port < 1 || port > 65535) {
throw new RangeError("invalid port");
}
return port;
}
for (const text of ["8080", "80x"]) {
try {
const port = parsePort(text);
console.log(port);
} catch (error) {
if (!(error instanceof RangeError)) {
throw error;
}
console.log(error.message);
}
}
驱动中的 instanceof 同时将严格模式下的 unknown 异常收窄为 RangeError。
number 没有表达“1~65535 的整数”这一业务约束;检查仍在函数体内完成。类型断言也不能替代检查,见 TypeScript 类型断言。
# Kotlin
函数片段,保存为 Main.kt;还需追加下面的驱动。
Kotlinfun parsePort(text: String): Int {
require(text.length in 1..5 && text.all { it in '0'..'9' }) {
"invalid port"
}
val port = text.toInt()
require(port in 1..65535) { "invalid port" }
return port
}
第一块是函数片段;将下列驱动追加其后保存为 Main.kt。执行 kotlinc Main.kt -include-runtime -d main.jar,再执行 java -jar main.jar。
Kotlinfun main() {
for (text in listOf("8080", "80x")) {
try {
val port = parsePort(text)
println(port)
} catch (error: IllegalArgumentException) {
println(error.message)
}
}
}
1~5 位 ASCII 数字的最大值是 99999,必在 Int 范围内,因此通过前置检查后可以直接 toInt(),无须再用 toIntOrNull() 分流。字符检查也避免每次调用构造正则对象;范围检查仍用于排除 0 和大于 65535 的值。参见 Kotlin toInt。
# Python
函数片段,保存为 main.py;还需追加下面的驱动。
Pythondef parse_port(text: str) -> int:
if not (1 <= len(text) <= 5) or not all(
"0" <= c <= "9" for c in text
):
raise ValueError("invalid port")
port = int(text)
if not 1 <= port <= 65535:
raise ValueError("invalid port")
return port
第一块是函数片段;将下列驱动追加其后保存为 main.py,执行 python main.py。
Pythonfor text in ("8080", "80x"):
try:
port = parse_port(text)
print(port)
except ValueError as error:
print(error)
类型标注不会自动验证传入对象。int 本身接受的文本形式比本配置协议更宽,不能省略协议校验;驱动只捕获约定的 ValueError。Python int 文档。
# C++
函数片段(含头文件),保存为 main.cpp;还需追加下面的驱动。
C++#include <charconv>
#include <optional>
#include <string_view>
#include <system_error>
std::optional<int> parse_port(std::string_view text) {
if (text.empty() || text.size() > 5
|| text.find_first_not_of("0123456789") != std::string_view::npos) {
return std::nullopt;
}
int port = 0;
const char* end = text.data() + text.size();
auto [ptr, ec] = std::from_chars(text.data(), end, port);
if (ec != std::errc{} || ptr != end || port < 1 || port > 65535) {
return std::nullopt;
}
return port;
}
第一块包含头文件和函数;将下面包含驱动所需头文件的第二块追加其后,保存为 main.cpp。执行 g++ -std=c++17 main.cpp -o main,再在 Windows PowerShell 执行 .\main.exe,或在 POSIX shell 执行 ./main。
C++#include <array>
#include <iostream>
int main() {
for (std::string_view text :
std::array<std::string_view, 2>{"8080", "80x"}) {
const auto port = parse_port(text);
if (port) {
std::cout << *port << '\n';
} else {
std::cout << "invalid port\n";
}
}
}
调用者先检查 optional 是否有值,再解引用;nullopt 不是异常,也不包含错误字符串。这里仅需成功或失败,因此使用 optional;需要详细失败原因时应设计更丰富的结果类型。
from_chars 提供错误码和停止位置。一般解析场景下,两者都需要检查;本例提前限制字符后仍保留完整检查。C++ 工作草案:from_chars。该链接是流动工作草案,不是 C++17 版本兼容性证明。
# 迁移误区与适用限制
- 代码更长不一定是语言解析能力弱,可能只是业务契约比标准库更严格。不能只比较“转整数”的那一行,就宣称实现等价。
Result、error、异常和optional是不同失败通道;调用者必须按接口消费,不能忽略错误、直接取空值或静默设置默认端口。- 本文驱动为展示批量输入而打印失败并继续下一项;实际启动服务时,应向上报告并停止该配置的启动流程,而不是在失败后继续使用未验证的值。
- ASCII 数字限制是协议选择。不要未经讨论改成 Unicode 数字判断、去除空白或允许前缀解析,这会改变输入契约。
# 运行与验证
语言与版本基线见主题首页。按各小节顺序组装;除 Java 使用两个文件外,每种语言生成一个源文件。
先运行短驱动,观察成功值与失败分支;再将驱动的两个输入替换或扩展为问题与边界表中的全部输入。C++ 使用 std::array 时,同时将模板参数 2 改为输入数量。各语言字符串字面量中的 "80\n" 都应含转义 \n,运行时才会形成待拒绝的实际换行。
成功输入应输出表中的明确数值(尤其是 1、65535、前导零的 80);所有失败输入应进入显式失败分支并输出 invalid port,而不是得到端口值。这里描述预期行为;TypeScript 的短驱动与 16 项边界实际执行结果见本次修复验证,其余语言状态见逐篇索引,不能从一种语言通过推断其他语言已验证。