一张表收尾 + 三道小练习 · znzryb
## 一张表收尾
| | Go(运行时反射) | C++26(静态反射) |
|---|---|---|
| 类型信息存在哪 | 程序里(编译器写进去的说明书) | 只在编译器手里,编译完就丢掉 |
| 什么时候查 | 程序运行时 | 编译时 |
| 打印字段名的代价 | 每次现场查表、分情况处理,比手写慢 | 跟手写一样快,但每个类型多生成一份代码、编译变慢 |
| 能处理「运行时才知道类型」的值吗 | 能(`any` 里装什么都能查) | 不能 |
| 出错什么时候发现 | 运行时(比如前面那个 `panic`) | 编译时 |
| 入口 | `reflect.TypeOf` / `reflect.ValueOf` | `^^T`、`std::meta::*`、`[: :]`、`template for` |
## 打印只是最小的那个用处
一旦程序能看见自己的类型,「把结构体打出来」只是最简单的一件事。同一套能力还能用来:
- **序列化**:把结构体转成 JSON / 二进制,或者反过来。Go 的 `encoding/json` 就是靠反射读字段名和 tag 的;
- **数据库映射**:按字段名生成 SQL 的列名;
- **命令行参数、配置文件**:按字段名自动解析 `--port 8080`;
- **枚举转字符串**:`Color::Red` → `"Red"`,C++ 程序员手写了几十年的东西。
## 三道小练习
做完这三道,反射这一小时就算学到手了。答案点开看,都实际跑过。
**练习 1(Go)**:写一个函数 `keys(v any) []string`,按结构体的 `json` tag 列出键名:tag 是 `"-"` 的跳过,没写 tag 的用字段名。提示:`reflect.StructField` 有一个 `Tag` 字段,`f.Tag.Get("json")` 能读出 tag。
:::练习 1 参考答案
```go
package main
import (
"fmt"
"reflect"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"-"`
Age int
}
// keys 按 json tag 列出键名:tag 是 "-" 的跳过,没写 tag 的用字段名
func keys(v any) []string {
t := reflect.TypeOf(v)
var out []string
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
tag := f.Tag.Get("json")
switch {
case tag == "-":
continue
case tag == "":
out = append(out, f.Name)
default:
out = append(out, strings.Split(tag, ",")[0])
}
}
return out
}
func main() {
fmt.Println(keys(User{}))
}
```
```text
[id name Age]
```
`encoding/json` 决定用什么键名,走的就是这条路。
:::
**练习 2(C++26)**:写 `to_string(E e)`,把任意枚举值转成它的名字,不认识的值返回 `"<unknown>"`。提示:`std::meta::enumerators_of(^^E)` 列出枚举的所有成员,`[:m:]` 把成员的反射值变回那个枚举值。
:::练习 2 参考答案
```cpp
#include <meta>
#include <print>
#include <string_view>
#include <type_traits>
template <class E>
requires std::is_enum_v<E>
constexpr std::string_view to_string(E e) {
template for (constexpr auto m : std::define_static_array(std::meta::enumerators_of(^^E))) {
if (e == [:m:]) return std::meta::identifier_of(m);
}
return "<unknown>";
}
enum class Color { Red, Green, Blue };
int main() {
std::println("{} {} {}", to_string(Color::Red), to_string(Color::Blue), to_string(Color{7}));
static_assert(to_string(Color::Green) == "Green"); // 编译期就算好了
}
```
```text
Red Blue <unknown>
```
最后那行 `static_assert` 说明:整个查找在编译期就能算完。
:::
**练习 3(想一想,不用写代码)**:第 2 节那个 Go 的 `show` 函数,遇到 `Kind()` 是 `reflect.Ptr` 的字段(比如一个指向自己类型的指针,链表节点那种)会发生什么?如果照着「遇到结构体就递归」的思路去给指针也加上递归,会遇到什么问题?
:::练习 3 参考答案
现在的 `show` 碰到指针走 `default`,打出一个 `?`。要是改成「遇到指针就解引用再递归」,一个首尾相连的环形链表会让它**无限递归下去,直到栈溢出**。真正的 `fmt` 怎么处理的:只有最外层的那个指针会被解开(所以 `%+v` 打 `&P{...}` 能看到 `&{A:1 ...}`),结构体里面的指针字段只打地址,不再往里钻。
```go
type Node struct {
V int
Next *Node
}
a := &Node{V: 1}
a.Next = a // 自己指向自己
fmt.Printf("%+v\n", a)
```
```text
&{V:1 Next:0xc000010030}
```
(地址每次运行都不一样。)
:::
## 一小时之后
想接着往下学,可以按这个顺序:
1. Go 的官方文章 [The Laws of Reflection](https://go.dev/blog/laws-of-reflection):讲 `reflect` 怎么**修改**值(`CanSet`、`Elem()`),这篇里只讲了「读」;
2. 读一遍 `encoding/json` 的 `Marshal` 怎么遍历结构体,会发现骨架跟第 2 节的 `show` 一样;
3. C++ 这边,把 [P2996](https://wg21.link/p2996) 里的例子挑几个在 Compiler Explorer 上跑一跑,尤其是「按字段生成代码」那几个。