主流网站开发技术,怎么下学做衣服网站,关闭 百度云加速 后网站打不开了,怎么注册网址反思的法则 罗伯-派克 2011 年 9 月 6 日
引言
计算机中的反射是指程序检查自身结构的能力#xff0c;尤其是通过类型检查自身结构的能力#xff1b;它是元编程的一种形式。它也是造成混乱的一个重要原因。
在本文中#xff0c;我们试图通过解释 Go 中的反射是如何工作的…反思的法则 罗伯-派克 2011 年 9 月 6 日
引言
计算机中的反射是指程序检查自身结构的能力尤其是通过类型检查自身结构的能力它是元编程的一种形式。它也是造成混乱的一个重要原因。
在本文中我们试图通过解释 Go 中的反射是如何工作的来澄清问题。每种语言的反射模型都不尽相同许多语言根本不支持反射但本文是关于 Go 的因此在本文的其余部分反射 一词应理解为 “Go 语言中的反射”。
2022 年 1 月添加的注释这篇博文写于 2011 年早于 Go 中的参数多态性又称泛型。尽管由于 Go 语言的发展文章中没有任何重要的内容变得不正确但为了避免混淆熟悉现代 Go 语言的人我们还是对一些地方进行了调整。
类型和接口
因为反射建立在类型系统之上所以我们先来复习一下 Go 中的类型。
Go 是静态类型的。每个变量都有一个静态类型即在编译时已知并固定的类型int、float32、*MyType、[]byte 等等。如果我们声明
type MyInt intvar i int
j MyInt那么 i 的类型是 intj 的类型是 MyInt。变量 i 和 j 具有不同的静态类型尽管它们具有相同的底层类型但如果不进行转换就不能相互赋值。
接口类型是类型的一个重要类别它代表固定的方法集。(在讨论反射时我们可以忽略在多态代码中使用接口定义作为约束。接口变量可以存储任何具体非接口值只要该值实现了接口的方法。io.Reader和io.Writer是一对著名的例子它们是io包中的Reader和Writer类型
// Reader is the interface that wraps the basic Read method.
type Reader interface {Read(p []byte) (n int, err error)
}// Writer is the interface that wraps the basic Write method.
type Writer interface {Write(p []byte) (n int, err error)
}任何实现了具有此签名的读或写方法的类型都被称为实现了 io.Reader或 io.Writer。在本讨论中这意味着io.Reader 类型的变量可以容纳任何具有读取方法的值
var r io.Reader
r os.Stdin
r bufio.NewReader(r)
r new(bytes.Buffer)
// and so on需要明确的是无论 r 的具体值是什么r 的类型始终是 io.Reader Go 是静态类型的r 的静态类型就是 io.Reader。
接口类型的一个极其重要的例子是空接口
interface{}或其对应的别名、
any它表示方法的空集任何值都可以满足它因为每个值都有零个或多个方法。
有人说 Go 的接口是动态类型的但这是一种误导。它们是静态类型的接口类型的变量总是具有相同的静态类型即使在运行时存储在接口变量中的值可能会改变类型该值也总是满足接口的要求。
我们需要准确地理解这一切因为反射和接口密切相关。
接口的表示
Russ Cox 写过一篇关于 Go 中接口值表示的详细博文。我们没有必要在此重复全部内容但有必要做一个简化的总结。
接口类型的变量存储一对值分配给变量的具体值和该值的类型描述符。更准确地说值是实现接口的底层具体数据项而类型描述的是该数据项的完整类型。例如在
var r io.Reader
tty, err : os.OpenFile(/dev/tty, os.O_RDWR, 0)
if err ! nil {return nil, err
}
r tty从示意代码上看r 包含 (value, type) 对 (tty、*os.File)。请注意*os.File 类型实现了 Read 以外的其他方法尽管接口值只提供了对 Read 方法的访问但其中的值包含了该值的所有类型信息。这就是我们可以这样做的原因
w io.Writer
w r.(io.Writer)这个赋值中的表达式是一个类型断言它断言 r 中的项目也实现了 io.Writer因此我们可以将其赋值给 w。接口的静态类型决定了接口变量可以调用哪些方法即使里面的具体值可能有更多的方法集。
我们可以继续这样做
var empty interface{}
empty w我们的空接口值 empty 将再次包含相同的一对tty、*os.File。这很方便空接口可以容纳任何值并包含我们所需的关于该值的所有信息。
(这里我们不需要类型断言因为我们静态地知道 w 满足empty interface{}。在将一个值从 Reader 移到 Writer 的例子中我们需要明确地使用类型断言因为 Writer 的方法不是 Reader 方法的子集。
一个重要的细节是接口变量内部的变量对总是以 (value, concrete type)的形式存在而不能以(value, interface type).的形式存在。接口不保存接口值。
现在我们可以进行反射了。
反映的第一定律
1. 反射从接口值到反射对象。
从根本上说反射只是一种机制用于检查存储在接口变量中的类型和值对。开始时我们需要了解包 reflect 中的两种类型 Type and Value。这两种类型可以访问接口变量的内容而两个简单的函数即 reflect.TypeOf 和 reflect.ValueOf可以从接口值中获取 reflect.Type 和 reflect.Value 片段。(此外从reflect.Value也很容易获取相应的reflect.Type但我们现在还是把值和类型的概念分开吧。
让我们从 TypeOf 开始
package mainimport (fmtreflect
)func main() {var x float64 3.4fmt.Println(type:, reflect.TypeOf(x))
}
该程序将打印
type: float64您可能想知道接口在哪里因为程序看起来像是将float64变量x传递给reflect.TypeOf而不是接口值。但它就在那里正如 godoc 报告的那样reflect.TypeOf 的签名包含一个空接口
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type当我们调用 reflect.TypeOf(x) 时x 首先被存储在一个空接口中然后将该接口作为参数传递reflect.TypeOf 会解压该空接口以恢复类型信息。
当然reflect.ValueOf 函数会恢复值从这里开始我们将省略模板只关注可执行代码
var x float64 3.4
fmt.Println(value:, reflect.ValueOf(x).String())打印
value: float64 Value我们明确调用了 String 方法 (我们明确调用 String 方法是因为默认情况下 fmt 包会挖掘 reflect.Value 以显示其中的具体值。而 String 方法不会。
reflect.Type 和 reflect.Value 都有很多方法供我们检查和操作。一个重要的例子是Value 有一个 Type 方法用于返回 reflect.Value 的 Type。另一个例子是Type 和 Value 都有一个 Kind 方法该方法返回一个常量表示存储的是什么类型的项目 如 Uint、Float64、Slice 等。此外Value 上名称为 Int 和 Float 的方法也能让我们抓取存储在其中的值如 int64 和 float64
var x float64 3.4
v : reflect.ValueOf(x)
fmt.Println(type:, v.Type())
fmt.Println(kind is float64:, v.Kind() reflect.Float64)
fmt.Println(value:, v.Float())打印
type: float64
kind is float64: true
value: 3.4还有 SetInt 和 SetFloat 等方法但要使用这些方法我们需要了解可设置性即下文讨论的反射第三定律的主题。
反射库有几个特性值得一提。首先为了保持应用程序接口的简洁Value 的 getter 和 setter 方法都是在能容纳值的最大类型上操作的例如int64 表示所有带符号的整数。也就是说Value 的 Int 方法返回的是 int64而 SetInt 值取值的是 int64可能有必要转换为相关的实际类型
var x uint8 x
v : reflect.ValueOf(x)
fmt.Println(type:, v.Type()) // uint8.
fmt.Println(kind is uint8: , v.Kind() reflect.Uint8) // true.
x uint8(v.Uint()) 第二个属性是反射对象的 Kind 描述的是底层类型而不是静态类型。如果一个反射对象包含一个用户定义的整数类型的值如
type MyInt int
var x MyInt 7
v : reflect.ValueOf(x)v 的 Kind 仍然是 reflect.Int尽管 x 的静态类型是 MyInt而不是 int。换句话说即使类型可以区分 int 和 MyIntKind 也不能。
反射第二定律
2. 反射从反射对象到接口值。
与物理反射一样Go 中的反射也会产生自己的逆反。
给定一个 reflect.Value我们可以使用 Interface 方法恢复一个接口值实际上该方法将 type and value信息打包回接口表示中并返回结果
// Interface returns vs value as an interface{}.
func (v Value) Interface() interface{}因此我们可以说
y : v.Interface().(float64) // y 的类型是 float64。
fmt.Println(y)来打印反射对象 v 所代表的 float64 值。
不过我们还可以做得更好。fmt.Println、fmt.Printf 等的参数都以空接口值的形式传递然后由 fmt 包在内部解包就像我们在前面的示例中所做的那样。因此要正确打印 reflect.Value 的内容只需将 Interface 方法的结果传递给格式化打印例程即可
fmt.Println(v.Interface())(自本文撰写以来对 fmt 软件包进行了修改使其能像这样自动解压缩 reflect.Value因此我们可以直接说
fmt.Println(v)就能得到同样的结果但为了清晰起见我们在这里保留 .Interface() 调用。
由于我们的值是 float64因此我们甚至可以使用浮点格式
fmt.Printf(value is %7.1e\n, v.Interface())并在本例中得到
3.4e00同样我们也不需要将 v.Interface() 的结果类型验证为 float64空接口值内部包含了具体值的类型信息Printf 将恢复它。
简而言之Interface 方法就是 ValueOf 函数的逆过程只不过它的结果总是静态的 interface{} 类型。
重申 反射从接口值到反射对象再返回接口值。
反射的第三定律
3. 要修改反射对象其值必须是可设置的。
第三定律是最微妙、最容易混淆的但如果我们从第一条原则出发还是很容易理解的。
下面是一些不起作用但值得研究的代码。
var x float64 3.4
v : reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.如果运行这段代码会出现以下提示信息
panic: reflect.Value.SetFloat using unaddressable value问题不在于值 7.1 不可寻址而在于 v 不可设置。可设置性是reflection Value的一个属性并非所有reflection Value都具有该属性。
在我们的例子中Value 的 CanSet 方法会报告 Value 的可设置性、
var x float64 3.4
v : reflect.ValueOf(x)
fmt.Println(settability of v:, v.CanSet())打印
settability of v: false在不可设置的值上调用设置方法是一个错误。但什么是可设置性呢
可设置性有点像可寻址性但更严格。它是反射对象可以修改用于创建反射对象的实际存储空间的属性。可设置性取决于反射对象是否持有原始项目。当我们说
var x float64 3.4
v : reflect.ValueOf(x)时我们向 reflect.ValueOf 传递了 x 的副本因此作为 reflect.ValueOf 参数创建的接口值是 x 的副本而不是 x 本身。因此如果语句
v.SetFloat(7.1)会更新存储在反射值中的 x 的副本而 x 本身不会受到影响。这样做既混乱又无用因此是非法的而可设置性正是用来避免这一问题的属性。
如果这看起来很奇怪其实不然。实际上这是一个我们熟悉的情况只是披上了不寻常的外衣。想想把 x 传递给函数
f(x)我们不会指望 f 能够修改 x因为我们传递的是 x 值的副本而不是 x 本身。如果我们想让 f 直接修改 x就必须向函数传递 x 的地址即指向 x 的指针
f(x)这既简单又熟悉反射也是如此。如果我们想通过反射修改 x就必须给反射库一个指向我们要修改的值的指针。
让我们开始吧。首先我们像往常一样初始化 x然后创建一个指向它的反射值称为 p。
var x float64 3.4
p : reflect.ValueOf(x) // Note: take the address of x.
fmt.Println(type of p:, p.Type())
fmt.Println(settability of p:, p.CanSet())目前的输出结果是
type of p: *float64
settability of p: false反射对象 p 不可设置但我们要设置的不是 p而是实际上*p。为了获取 p 指向的内容我们调用了 Value 的 Elem 方法该方法通过指针进行间接操作并将结果保存在名为 v 的reflection Value 中
v : p.Elem()
fmt.Println(settability of v:, v.CanSet())正如输出结果所示现在 v 是一个可设置的反射对象、
settability of v: true由于 v 代表 x我们终于可以使用 v.SetFloat 来修改 x 的值了
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)正如预期的那样输出结果是
7.1
7.1反射可能很难理解但它确实在做语言所做的事情尽管通过反射类型和值可以掩盖正在发生的事情。请记住反射值需要某些东西的地址以便修改它们所代表的内容。
Structs
在我们前面的示例中v 本身并不是一个指针它只是从指针派生出来的。出现这种情况的常见方法是使用反射来修改结构体的字段。只要我们有结构体的地址就可以修改它的字段。
下面是一个分析 struct value t 的简单示例。我们用结构体的地址创建反射对象因为我们稍后要修改它。然后我们将 typeOfT 设置为其类型并使用直接的方法调用遍历字段详见包 reflect。请注意我们从结构类型中提取了字段的名称但字段本身是普通的 reflect.Value 对象。
type T struct {A intB string
}
t : T{23, skidoo}
s : reflect.ValueOf(t).Elem()
typeOfT : s.Type()
for i : 0; i s.NumField(); i {f : s.Field(i)fmt.Printf(%d: %s %s %v\n, i,typeOfT.Field(i).Name, f.Type(), f.Interface())
}该程序的输出结果是
0: A int 23
1: B string skidoo这里还顺便介绍了一个关于可设置性的要点T 的字段名是大写的导出因为只有结构体的导出字段才是可设置的。
因为 s 包含一个可设置的反射对象所以我们可以修改结构体的字段。
s.Field(0).SetInt(77)
s.Field(1).SetString(Sunset Strip)
fmt.Println(t is now, t)结果如下
t is now {77 Sunset Strip}如果我们修改程序使 s 是根据 t 而不是 t 创建的那么对 SetInt 和 SetString 的调用就会失败因为 t 的字段将不可设置。
Conclusion
这里又是反射法则: Reflection goes from interface value to reflection object. Reflection goes from reflection object to interface value. To modify a reflection object, the value must be settable.
一旦你理解了这些定律Go中的反射就会变得更容易使用尽管它仍然很微妙。这是一个强大的工具除非必要否则应该小心使用。 还有很多我们没有涉及到的问题——在channel上发送和接收、分配内存、使用slices和map、调用方法和函数——但是这篇文章已经足够长了。我们将在后面的文章中讨论其中的一些主题。