Go中的反射定律

哈Ha! 我向您介绍语言创建者的文章“反射定律”的译文。

反思是程序探索自身结构的能力,尤其是通过类型。 这是元编程的一种形式,也是造成混乱的主要原因。
在Go中,反射被广泛使用,例如在test和fmt软件包中。 在本文中,我们将通过解释反射在Go中的工作原理来摆脱“魔力”。

类型和接口


由于反射基于类型系统,因此让我们重新了解Go中的类型知识。
Go是静态类型的。 每个变量在编译时只有一个固定的静态类型: int, float32, *MyType, []byte ...如果声明:

 type MyInt int var i int var j MyInt 

那么i的类型是intj的类型是MyInt 。 变量ij具有不同的静态类型,尽管它们具有相同的基本类型,但是如果不进行转换就无法将它们彼此分配。

接口是重要的类型类别之一,它们是固定的方法集。 接口可以存储任何特定(非接口)值,只要该值实现接口的方法即可。 一对著名的示例是io.Reader和io.Writer ,它们是io包中的Reader和Writer类型:

 // Reader -  ,    Read(). type Reader interface { Read(p []byte) (n int, err error) } // Writer -  ,    Write(). type Writer interface { Write(p []byte) (n int, err error) } 

据说,任何使用此签名实现Read()Write()方法的io.Writer分别实现io.Readerio.Writer 。 这意味着io.Reader类型的变量可以包含Read()类型的任何值:

 var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) 

重要的是要了解可以为r分配实现io.Reader任何值。 Go是静态类型的,而静态类型rio.Reader

接口类型的一个非常重要的例子是空接口:

 interface{} 

它是∅方法的空集,并且可以通过任何值实现。
有人说Go接口是动态类型化的变量,但这是一个谬论。 它们是静态类型的:具有接口类型的变量始终具有相同的静态类型,并且尽管在运行时存储在接口变量中的值可以更改类型,但此值将始终满足接口。 (没有undefinedNaN或其他破坏程序逻辑的东西。)

必须理解这一点-反射和接口密切相关。

接口的内部表示


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示意地包含一对(, ) --> (tty, *os.File) 。 注意*os.File类型实现了Read()之外的方法; 即使接口值仅提供对Read()方法的访问,该值也包含有关该值类型的所有信息。 这就是为什么我们可以做这样的事情:

 var w io.Writer w = r.(io.Writer) 

此分配中的表达式是一个类型语句; 它声称r内的元素也实现了io.Writer ,因此我们可以将其分配给w 。 分配后, w将包含一对(tty, *os.File) 。 这与r对相同。 接口的静态类型确定可以在接口变量上调用哪些方法,尽管更广泛的方法集可以在内部具有特定值。

继续,我们可以执行以下操作:

 var empty interface{} empty = w 

并且empty字段的empty值将再次包含同一对(tty, *os.File) 。 这很方便:空接口可以包含任何值以及我们从中需要的所有信息。

这里我们不需要类型断言,因为已知w满足空接口。 在将值从Reader传输到Writer的示例中,我们需要显式使用类型断言,因为Writer方法不是Reader的子集。 尝试转换与接口不匹配的值会引起恐慌。

一个重要的细节是,接口内的一对始终具有形式(值,特定类型),而不能具有形式(值,接口)。 接口不支持将接口作为值。

现在我们准备研究反思。

反射的第一定律反映


  • 反射从界面延伸到对象的反射。

从根本上讲,反射只是一种检查存储在接口变量内的类型和值对的机制。 首先,我们需要了解两种类型: reflect.Typereflect.Value 。 这两种类型提供对接口变量内容的访问,并分别由简单函数reflect.TypeOf()和reflect.ValueOf()返回。 它们从接口的含义中提取部分。 (此外, reflect.Value容易获得reflect.Type ,但是现在我们不要混合使用ValueType的概念。)

让我们从TypeOf()开始:

 package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) } 

程序将输出
type: float64

该程序类似于将简单变量float64 x传递给reflect.TypeOf() 。 您看到界面了吗? 它是-根据函数声明, reflect.TypeOf()接受一个空接口:

 // TypeOf()  reflect.Type    . 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()方法,因为默认情况下,fmt包会解压缩以reflect.Value值并输出特定值。)
reflect.Typereflect.Value都有很多方法,使您可以探索和修改它们。 一个重要的示例是reflect.Value具有一个Type()方法,该方法返回值的类型。 reflect.Typereflect.Value有一个Kind()方法,该方法返回一个常量,该常量指示要存储的原始元素: Uint, Float64, Slice ...这些常量在反射包中的枚举中声明。 名称为Int()Float() Value方法使我们能够提取包含在其中的值(例如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() ,但是要使用它们,我们需要了解可沉降性,这是反射第三定律的主题。

反射库具有几个需要突出显示的属性。 首先,为了使API保持简单,“ getter”和“ setter” Value方法作用于可以包含值的最大类型:所有有int64整数的int64 。 也就是说, Value值的Int()方法返回int64 ,而SetInt()值采用int64 ; 可能需要转换为实际类型:

 var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) x = uint8(v.Uint()) // v.Uint  uint64. 

将是

 type: uint8 kind is uint8: true 

这里v.Uint()将返回uint64 ,需要一个显式的类型声明。

第二个属性是对象的Kind()反映的是基本类型,而不是静态类型。 如果反射对象包含用户定义的整数类型的值,例如

 type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) // v   Value. 

v.Kind() == reflect.Int ,尽管x的静态类型是MyInt ,而不是int 。 换句话说, MyInt Type() MyIntKind()无法将intMyInt区分开。 Kind只能接受内置类型的值。

反射第二定律反映


  • 反射从反射对象延伸到界面。

就像物理反射一样,Go中的反射也会产生相反的效果。

有了reflect.Value ,我们可以使用Interface()方法恢复接口的值; 该方法将类型和值信息打包回接口,并返回结果:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
例如:

 y := v.Interface().(float64) // y   float64. fmt.Println(y) 

打印由反射对象v表示的float64的值。
但是,我们可以做得更好。 如前面的示例一样, fmt.Println()fmt.Printf()中的参数作为空接口传递,然后由fmt包在内部对其进行解压缩。 因此,正确打印reflect.Value内容所需reflect.Value就是将Interface()方法的结果传递给格式化的输出函数:

 fmt.Println(v.Interface()) 

(为什么不使用fmt.Println(v) ?因为v的类型为reflect.Value ;我们希望获取包含在其中的值。)由于我们的值是float64 ,因此,如果需要,我们甚至可以使用浮点格式:

 fmt.Printf("value is %7.1e\n", v.Interface()) 

将在特定情况下输出
3.4e+00

同样,无需v.Interface()结果类型v.Interface()float64 ; 空接口值包含有关特定值的信息,而fmt.Printf()还原该值。
简而言之, Interface()方法与ValueOf()函数相反,但其结果始终是静态类型interface{}

重复:反射从接口值扩展到反射对象,反之亦然。

反射第三定律反射


  • 要更改反射对象,该值必须可设置。

第三定律是最微妙和令人困惑的。 我们从第一个原则开始。
此代码无效,但值得关注。

 var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) //  

如果运行此代码,它将因紧急消息而崩溃,并显示一条关键消息:
panic: reflect.Value.SetFloat
问题不在于字面意义7.1没有得到解决。 这是v无法安装的内容。 可设置性是reflect.Value的属性,并非每个reflect.Value都有它。
reflect.Value.CanSet()方法reflect.Value.CanSet()正在设置reflect.Value.CanSet() ; 在我们的情况下:

 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) 

将打印:
settability of v: false

在非托管值上调用Set()方法时发生错误。 但是什么是可安装性?

可持续性有点像寻址能力,但是更严格。 这是反射对象可以更改用于创建反射对象的存储值的属性。 可持续性由反射对象包含源元素还是仅包含源元素来确定。 当我们写:

 var x float64 = 3.4 v := reflect.ValueOf(x) 

我们将x的副本传递给reflect.ValueOf() ,因此将接口创建为reflect.ValueOf()的参数-这是x的副本,而不是x本身。 因此,如果声明:

 v.SetFloat(7.1) 

如果执行,它将不会更新x ,尽管v看起来好像是从x创建的。 相反,他将更新存储在v值内的x的副本,并且x本身不会受到影响。 禁止这样做以免引起问题,可安装性是用于防止问题的属性。

这似乎并不奇怪。 这是穿不寻常衣服的普遍情况。 考虑将x传递给函数:
f(x)

我们不希望f()能够更改x ,因为我们传递了x值的副本,而不是x本身。 如果我们希望f()直接更改x ,则必须将指向x的指针传递给我们的函数:
f(&x)

这是直接且熟悉的,并且反射的工作方式与此类似。 如果要使用反射来更改x ,则必须为反射库提供指向要更改的值的指针。

来吧 首先,我们照常初始化x ,然后创建一个指向它的reflect.Value p

 var x float64 = 3.4 p := reflect.ValueOf(&x) //   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()方法,该方法通过指针间接获取值,并将结果存储在reflect.Value v

 v := p.Elem() fmt.Println("settability of v:", v.CanSet()) 

现在v是一个可安装的对象;
settability of v: true
并且由于它表示x ,我们最终可以使用v.SetFloat()来更改x的值:

 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) 

结论如预期
7.1
7.1

Reflect可能很难理解,但是它确实可以完成语言的工作,尽管可以借助reflect.Typereflect.Type来隐藏正在发生的事情。 请记住, reflection.Value需要变量的地址才能对其进行更改。

结构体


在我们之前的示例中, v不是指针,而是从其派生的。 创建这种情况的一种常见方法是使用反射来更改结构字段。 只要有了结构的地址,就可以更改其字段。

这是一个简单的示例,它分析结构t的值。 我们使用结构的地址创建一个反射对象,以便以后对其进行修改。 然后将typeOfT设置为其类型,并使用简单的方法调用遍历字段( 有关详细说明,请参见软件包 )。 请注意,我们正在从结构类型中提取字段名称,但是字段本身是常规的reflect.Value

 type T struct { A int B 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}
如果更改程序以便从t而不是&t创建&t ,则对SetInt()SetString()的调用SetInt()紧急方式结束,因为无法设置字段t

结论


回顾反射定律:

  • 反射从界面延伸到对象的反射。
  • 反射从对象的反射延伸到界面。
  • 要更改反射对象,必须设置该值。

罗伯·派克Rob Pike)发表。

Source: https://habr.com/ru/post/zh-CN415171/


All Articles