# 空值与默认值:配置缺失、零与假值陷阱
学习目标:区分“键或值缺失”与“有效但为零/空字符串”的本质区别,掌握各语言安全合并默认值的机制。
前置知识与范围:基本类型、哈希映射/字典与条件分支。处理内存配置对象的默认值回退。
# 问题与边界
从配置源中解析服务器地址 host 和超时秒数 timeout_sec:
host契约:若未提供该配置(缺失),回退到默认值"localhost";若显式配置了空字符串"",必须保留为"",不能视作缺失。timeout_sec契约:若未提供该配置(缺失),回退到默认值30;若显式配置了数值0(如表示非阻塞或特定策略),必须保留为0,严禁因布尔假值(falsy)误将其覆盖为默认值30。
测试用例:
- 完全缺失:
host = None, timeout = None-> 得到"localhost", 30。 - 显式非空:
host = "10.0.0.1", timeout = 5-> 得到"10.0.0.1", 5。 - 假值边界:
host = "", timeout = 0-> 必须得到"", 0!
# 关键差异
| 语言 | 空值与存在性表示 | 默认值合并操作 | 假值(0 与 “”)危险检查 |
|---|---|---|---|
| Rust | Option<T> |
opt.unwrap_or(default) |
静态类型安全,Some(0) 绝不会触发 unwrap_or |
| Go | 指针 *string, *int 或 map 查询 val, ok |
if !ok 或 if ptr == nil 显式赋值 |
零值(Zero value)与未设置必须区分指针或存在性 |
| Java | Optional<T> 或 null 检查 |
optional.orElse(default) |
null 比较;空串和 0 是有效对象/基本值 |
| JavaScript | null / undefined |
空值合并操作符 ?? |
切勿使用逻辑或 ` |
| TypeScript | 沿用 JS 空值合并 ?? |
val ?? default |
静态联合类型约束 number | undefined |
| Kotlin | 可空类型 T? |
Elvis 操作符 val ?: default |
?: 仅检查 null,不将 0 视为空 |
| Python | Optional[T](None) |
val if val is not None else default |
切勿使用 val or 30,因为 bool(0) is False |
| C++ | std::optional<T> |
opt.value_or(default) |
只有 std::nullopt 触发 value_or |
# 各语言实现
# Rust
Ruststruct Config {
host: String,
timeout_sec: i32,
}
fn resolve_config(raw_host: Option<&str>, raw_timeout: Option<i32>) -> Config {
Config {
host: raw_host.unwrap_or("localhost").to_string(),
timeout_sec: raw_timeout.unwrap_or(30),
}
}
fn main() {
let c1 = resolve_config(None, None);
assert_eq!(c1.host, "localhost");
assert_eq!(c1.timeout_sec, 30);
let c2 = resolve_config(Some("10.0.0.1"), Some(5));
assert_eq!(c2.host, "10.0.0.1");
assert_eq!(c2.timeout_sec, 5);
let c3 = resolve_config(Some(""), Some(0));
assert_eq!(c3.host, "");
assert_eq!(c3.timeout_sec, 0);
println!("host: '{}', timeout: {}", c3.host, c3.timeout_sec);
}
Rust 中 Some("") 和 Some(0) 均为有效的枚举变体,unwrap_or 仅在 None 时启用备选值。
# Go
Gopackage main
import "fmt"
type Config struct {
Host string
TimeoutSec int
}
func ResolveConfig(rawHost *string, rawTimeout *int) Config {
host := "localhost"
if rawHost != nil {
host = *rawHost
}
timeout := 30
if rawTimeout != nil {
timeout = *rawTimeout
}
return Config{Host: host, TimeoutSec: timeout}
}
func main() {
c1 := ResolveConfig(nil, nil)
if c1.Host != "localhost" || c1.TimeoutSec != 30 {
panic("c1 failed")
}
strVal := ""
intVal := 0
c2 := ResolveConfig(&strVal, &intVal)
if c2.Host != "" || c2.TimeoutSec != 0 {
panic("c2 boundary failed: 0 or empty string overwritten")
}
fmt.Printf("host: '%s', timeout: %d\n", c2.Host, c2.TimeoutSec)
}
Go 采用指针区分“未提供”与“零值”,仅当指针等于 nil 时采用默认值。
# Java
Javaimport java.util.Optional;
public class Main {
public record Config(String host, int timeoutSec) {}
public static Config resolveConfig(Optional<String> rawHost, Optional<Integer> rawTimeout) {
return new Config(
rawHost.orElse("localhost"),
rawTimeout.orElse(30)
);
}
public static void main(String[] args) {
Config c1 = resolveConfig(Optional.empty(), Optional.empty());
assert "localhost".equals(c1.host()) && c1.timeoutSec() == 30;
Config c2 = resolveConfig(Optional.of(""), Optional.of(0));
assert "".equals(c2.host()) && c2.timeoutSec() == 0;
System.out.println("host: '" + c2.host() + "', timeout: " + c2.timeoutSec());
}
}
Java 标准库 Optional.orElse 仅在容器为空时解包备选值。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
function resolveConfig(rawHost, rawTimeout) {
return {
host: rawHost ?? "localhost",
timeoutSec: rawTimeout ?? 30,
};
}
const c1 = resolveConfig(undefined, undefined);
assert.equal(c1.host, "localhost");
assert.equal(c1.timeoutSec, 30);
const c2 = resolveConfig("", 0);
assert.equal(c2.host, "");
assert.equal(c2.timeoutSec, 0);
console.log(`host: '${c2.host}', timeout: ${c2.timeoutSec}`);
ECMAScript 2020 引入空值合并操作符 ??,仅当左侧操作数为 null 或 undefined 时返回右侧值,避免了传统 || 覆盖 0 和 "" 的陷阱。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
interface Config {
host: string;
timeoutSec: number;
}
function resolveConfig(rawHost?: string, rawTimeout?: number): Config {
return {
host: rawHost ?? "localhost",
timeoutSec: rawTimeout ?? 30,
};
}
const c1 = resolveConfig();
assert.equal(c1.host, "localhost");
assert.equal(c1.timeoutSec, 30);
const c2 = resolveConfig("", 0);
assert.equal(c2.host, "");
assert.equal(c2.timeoutSec, 0);
console.log(`host: '${c2.host}', timeout: ${c2.timeoutSec}`);
TypeScript 的可选形参语法 rawHost?: string 本质为 string | undefined,与 ?? 严格适配。
# Kotlin
Kotlindata class Config(val host: String, val timeoutSec: Int)
fun resolveConfig(rawHost: String?, rawTimeout: Int?): Config {
return Config(
host = rawHost ?: "localhost",
timeoutSec = rawTimeout ?: 30
)
}
fun main() {
val c1 = resolveConfig(null, null)
check(c1.host == "localhost" && c1.timeoutSec == 30)
val c2 = resolveConfig("", 0)
check(c2.host == "" && c2.timeoutSec == 0)
println("host: '${c2.host}', timeout: ${c2.timeoutSec}")
}
Kotlin 的 Elvis 操作符 ?: 仅当左侧为 null 时触发计算,数值 0 与空串并非空引用。
# Python
Pythonfrom typing import Optional, NamedTuple
class Config(NamedTuple):
host: str
timeout_sec: int
def resolve_config(raw_host: Optional[str] = None, raw_timeout: Optional[int] = None) -> Config:
host = raw_host if raw_host is not None else "localhost"
timeout_sec = raw_timeout if raw_timeout is not None else 30
return Config(host=host, timeout_sec=timeout_sec)
if __name__ == "__main__":
c1 = resolve_config()
assert c1.host == "localhost" and c1.timeout_sec == 30
c2 = resolve_config(raw_host="", raw_timeout=0)
assert c2.host == "" and c2.timeout_sec == 0
print(f"host: '{c2.host}', timeout: {c2.timeout_sec}")
Python 没有内置的空合并运算符,切忌写 raw_timeout or 30;必须显式使用 is not None 判断。
# C++
C++#include <iostream>
#include <string>
#include <optional>
#include <cassert>
struct Config {
std::string host;
int timeout_sec;
};
Config resolve_config(std::optional<std::string> raw_host, std::optional<int> raw_timeout) {
return Config{
raw_host.value_or("localhost"),
raw_timeout.value_or(30)
};
}
int main() {
auto c1 = resolve_config(std::nullopt, std::nullopt);
assert(c1.host == "localhost" && c1.timeout_sec == 30);
auto c2 = resolve_config(std::string(""), 0);
assert(c2.host == "" && c2.timeout_sec == 0);
std::cout << "host: '" << c2.host << "', timeout: " << c2.timeout_sec << "\n";
}
C++17 的 std::optional::value_or 在包装有值时返回对应值,不考虑内容的布尔真假性。
# 迁移误区与适用限制
- 经典假值覆盖(Truthy/Falsy):在 JS 写
timeout = rawTimeout || 30,在 Python 写timeout = raw_timeout or 30,当传入0时均会错误回退为30,直接破坏业务语义。 - 函数默认参数的生效时机:多数语言的默认参数仅在传入参数为
undefined(如 JS)或未传参时生效,若调用方显式传递了null,默认参数不会生效,可能导致意料之外的null穿透。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本host: '', timeout: 0
零值与空字符串边界断言均已验证通过。
# 相关阅读
- 字符串转端口号:统一输入校验契约
- 相等性:值相等与身份相等