AI Library · 知识库
按精选导读跳读

# 字符串转端口号:解析之后还要校验

学习目标:分清转换、解析、断言与业务校验,并显式消费成功值和失败结果。

前置知识:函数、字符串、分支;下文同时示范结果类型和异常的基本调用方式。范围:解析已经是字符串的配置值,不处理文件读取、网络连接或任意对象的类型校验。

# 问题与边界

从配置中读取服务连接端口。配置值已经是字符串,本例要求:

这是本配置格式的约定,并非所有端口配置都必须采用同一语法。先统一输入契约,才能比较同一件事。

输入 预期
"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 版本兼容性证明。

# 迁移误区与适用限制

# 运行与验证

语言与版本基线见主题首页。按各小节顺序组装;除 Java 使用两个文件外,每种语言生成一个源文件。

先运行短驱动,观察成功值与失败分支;再将驱动的两个输入替换或扩展为问题与边界表中的全部输入。C++ 使用 std::array 时,同时将模板参数 2 改为输入数量。各语言字符串字面量中的 "80\n" 都应含转义 \n,运行时才会形成待拒绝的实际换行。

成功输入应输出表中的明确数值(尤其是 1、65535、前导零的 80);所有失败输入应进入显式失败分支并输出 invalid port,而不是得到端口值。这里描述预期行为;TypeScript 的短驱动与 16 项边界实际执行结果见本次修复验证,其余语言状态见逐篇索引,不能从一种语言通过推断其他语言已验证。

按精选导读跳读