跳到主要内容
版本:0.16

字段可空性

本页说明 Fory 在跨语言(xlang)序列化模式下如何处理字段可空性。

默认行为

在 xlang 模式下,字段默认都是不可空的。这意味着:

  • 字段值必须始终存在,不能为 null
  • 不会为该字段额外写入 null 标记字节
  • 序列化结果更紧凑

以下类型默认是可空的:

  • Java 和 C++ 可空包装类型:Optional<T>
  • Java 装箱类型(IntegerLongDouble 等)
  • Go 指针类型(*int32*string 等)
  • Rust Option<T>
  • Python 类型提示:Optional[T]
字段类型默认可空是否写入 null 标记
基础类型(intboolfloat 等)
String
List<T>Map<K,V>Set<T>
自定义结构体
枚举
Java 装箱类型(IntegerLong 等)
Go 指针类型(*int32*string
Optional<T> / Option<T>

编码格式

字段是否可空决定了值前面是否需要写入 null 标记字节

不可空字段: [value data]
可空字段: [null_flag] [value data if not null]

其中 null_flag 的含义如下:

  • -1NULL_FLAG):值为 null
  • -2NOT_NULL_VALUE_FLAG):值存在

可空性与引用跟踪

这两个概念相关,但并不相同:

概念目的标记值
可空性允许字段值为 null-1(null)、-2(非 null)
引用跟踪对共享引用做去重-1(null)、-2(非 null)、≥0(引用 ID)

关键区别:

  • 仅可空:只会写入 -1-2,不会去重共享引用。
  • 引用跟踪:在可空语义之上增加引用 ID(≥0),用于表示已出现过的对象。
  • 二者占用的是同一个标记字节位置,引用跟踪可以理解为可空机制的超集。

refTracking=true 时,这个标记字节会同时承担引用标记的职责:

ref_flag = -1  -> null 值
ref_flag = -2 -> 新对象(第一次出现)
ref_flag >= 0 -> 指向索引为 ref_flag 的已序列化对象

更详细的引用跟踪行为可参考 Reference Tracking

各语言示例

Java

public class Person {
// xlang 模式下默认不可空
String name; // 不能为 null
int age; // 基础类型,始终不可空
List<String> tags; // 不能为 null

// 显式声明为可空
@ForyField(nullable = true)
String nickname; // 可以为 null

// Optional 包装类型默认可空
Optional<String> bio; // 可以为空
}

Fory fory = Fory.builder()
.withLanguage(Language.XLANG)
.build();
fory.register(Person.class, "example.Person");

Python

from dataclasses import dataclass
from typing import Optional, List
import pyfory

@dataclass
class Person:
# 默认不可空
name: str
age: pyfory.int32
tags: List[str]

# Optional 表示可空
nickname: Optional[str] = None
bio: Optional[str] = None

fory = pyfory.Fory(xlang=True)
fory.register_type(Person, typename="example.Person")

Rust

use fory::{Fory, ForyObject};

#[derive(ForyObject)]
struct Person {
// 默认不可空
name: String,
age: i32,
tags: Vec<String>,

// Option<T> 默认可空
nickname: Option<String>,
bio: Option<String>,
}

Go

type Person struct {
// 默认不可空
Name string
Age int32
Tags []string

// 指针类型可表示可空字段
Nickname *string
Bio *string
}

fory := forygo.NewFory(forygo.WithXlang(true))
fory.RegisterNamedStruct(Person{}, "example.Person")

C++

struct Person {
// 默认不可空
std::string name;
int32_t age;
std::vector<std::string> tags;

// 使用 std::optional 表示可空
std::optional<std::string> nickname;
std::optional<std::string> bio;
};
FORY_STRUCT(Person, name, age, tags, nickname, bio);

自定义可空性

Java:@ForyField 注解

public class Config {
@ForyField(nullable = true)
String optionalSetting; // 显式可空

@ForyField(nullable = false)
String requiredSetting; // 显式不可空(也是默认行为)
}

C++:fory::field 包装器

struct Config {
// 显式声明为可空
fory::field<std::string, 1, fory::nullable<true>> optional_setting;

// 显式声明为不可空
fory::field<std::string, 2, fory::nullable<false>> required_setting;
};
FORY_STRUCT(Config, optional_setting, required_setting);

null 值处理

当不可空字段收到 null 值时,各语言的表现通常如下:

语言行为
Java抛出 NullPointerException 或序列化错误
Python抛出 TypeError 或序列化错误
Rust编译期就不允许把 None 赋给非 Option 字段
Go使用零值(空字符串、0 等)
C++使用默认构造值,或出现未定义行为

Schema 兼容性

可空标记是结构体 Schema 指纹的一部分。修改字段的可空性属于破坏性变更,会导致 Schema 版本不匹配。

Schema A: { name: String (不可空) }
Schema B: { name: String (可空) }
// 两者的指纹不同,因此不兼容

最佳实践

  1. 默认优先使用不可空字段,只在 null 具有明确语义时再声明为可空。
  2. 优先使用 Optional<T> / Option<T> 这类包装类型,而不是原始类型加注解。
  3. 跨语言字段要保持一致的可空语义。
  4. 在 API 或文档中明确说明哪些字段允许为 null。

相关主题