Go 语言中的反射机制

欢迎大家到我的博客浏览,更好的阅读体验请点击 反射 | YinKai's Blog

反射在大多数的应用和服务中并不常见,但是很多框架都依赖 Go 语言的反射机制简化代码。<!--more-->因为 Go 语言的语法元素很少、设计简单,所以它没有特别强的表达能力,却可以通过 reflect 包能够弥补它在语法上reflect.Type的一些劣势。

reflect 实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两队非常重要的函数和类型,两个函数分别是:

  • reflect.TypeOf 能获取类型信息;

  • reflect.ValueOf 能获取数据的运行时表示;

两个类型是 reflect.Type 和 reflect.Value ,它们与函数是一一对应的关系:

类型 reflect.Type 是反射包定义的一个接口,我们可以使用 reflect.TypeOf 函数获取任意变量的类型,reflect.Type 接口中定义了一些有趣的方法,MethodByName 可以获取当前类型对应方法的引用、Implements 可以判断当前类型是否实现了某个接口:

type Type interface {Align() int             // Align 返回类型的对齐要求。FieldAlign() int        // FieldAlign 返回字段的对齐要求。Method(int) Method      // Method 根据方法索引返回对应的 Method 值。MethodByName(string) (Method, bool) // MethodByName 根据方法名称返回对应的 Method 值,如果找不到则返回 false。NumMethod() int         // NumMethod 返回接口实现的方法数量。// ... 其他方法和属性Implements(u Type) bool // Implements 返回类型是否实现了给定的接口 u。// ... 其他方法和属性
}

反射包中 reflect.Value 的类型与 reflect.Type 不同,它被声明成了结构体。这个结构体没有对外暴露的字段,但是提供了获取或者写入数据的方法:

type Value struct {// 包含过滤的或者未导出的字段
}
// Addr 返回一个 Value 结构的指针,用于获取该值的地址。
func (v Value) Addr() Value
// Bool 将 Value 转换为布尔值并返回结果。
func (v Value) Bool() bool
// Bytes 返回 Value 的字节切片表示形式。
func (v Value) Bytes() []byte
// ... 其他方法

反射包中的所有方法都是围绕着 reflect.Typereflect.Value 两个类型设计的。我们通过 reflect.TypeOfreflect.ValueOf 可以将一个普通的变量转换成反射包中提供的 reflect.Typereflect.Value,然后就可以使用反射包中的方法对它们进行复杂的操作了。

1、三大法则

运行时反射是程序在运行期间检查其自身结构的一种方式。反射带来的灵活性是一把双刃剑,反射作为一种元编程方式可以减少重复代码,但是过量使用反射会使我们的程序变得难以理解并且运行缓慢。下面我们将介绍 Go 语言反射的三大法则:

  1. interface{} 变量可以反射出反射对象;

  2. 从反射对象可以获取interface{} 变量;

  3. 要修改反射对象,其值必须可设置;

第一法则

反射的第一法则是我们能将 Go 语言的 interface{} 变量转换成反射对象。当我们执行 reflect.ValueOf(1) 时,虽然看起来是获取了基本类型 int 对应的反射类型,但是由于 reflect.TypeOfreflect.ValueOf 两个方法的入参都是 interface{} 类型,所以在方法执行的过程中发生了类型转换。

因为 Go 语言的函数调用都是值传递,所以变量会在函数调用时进行类型转换。基本类型 int 会转换成 interface{} 类型,这就是为什么第一条法则是从接口到反射类型。

上面提到的 reflect.TypeOfreflect.ValueOf 函数就能完成这里的转换。如果把 Go 语言和类型和反射类型比作两个世界,那么这两个函数就是连接这两个世界的桥梁。

通过下面的例子简单介绍一下它们的作用,refelct.TypeOf 获取了变量author 的类型,reflect.ValueOf 获取了变量的值 draven。我们知道了一个变量的类型和值,就意味着我们知道了这个变量的所有信息。

package main
​
import ("fmt""reflect"
)
​
func main() {author := "draven"fmt.Println("TypeOf author:", reflect.TypeOf(author))fmt.Println("ValueOf author:", reflect.ValueOf(author))
}
​
$ go run main.go
TypeOf author: string
ValueOf author: draven

有了变量的类型之后,我们可以通过 Method 方法获得类型实现的方法,通过 Field 获取类型包含的全部字段。对于不同的类型,我们也可以调用不同的方法获取相关信息:

  • 结构体:获取字段的数量并通过下标和字段名后去字段 StructField

  • 哈希表:获取哈希表的 Key 类型

  • 函数或方法:获取入参和返回值类型

  • ……

综上所述,使用 reflect.TypeOfreflect.ValueOf 能够获取 Go 语言中变量对应的反射对象,一旦获取了反射对象,我们就能得到跟当前类型相关数据和操作,并可以使用这些运行时获取的结构执行方法。

第二法则

反射的第二法则是我们可以从反射对象获取 interface{} 变量。既然能将接口类型的变量转变成反射对象,那么一定需要其他方法将反射对象还原成接口类型的变量,reflect 中的 reflect.Value.Interface 就能完成这项工作:

不过调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,如果想要将其还原成最原始的状态还需要经过如下所示的显示类型转换:

v := reflect.ValueOf(1)
v.Interface().(int)

从反射对象到接口值的过程是从接口值到反射对象的镜面过程,两个过程都需要经历两次转换:

  • 从接口值到反射对象:

    • 从基本类型到接口类型的类型转换;

    • 从接口类型到反射对象的转换;

  • 从反射对象到接口值:

    • 反射对象转换成接口类型;

    • 通过显式类型转换变成原始类型;

当然不是所有的变量都需要类型转换这一过程。如果变量本身就是 interface{} 类型的,那么它不需要类型转换,因为类型转换这一过程一般都是隐式的,所以我不太需要关心它,只有在我们需要将反射对象转换回基本类型时才需要显式的转换操作。

第三法则

Go 语言反射的最后一条法则是与值是否可以被更改有关,如果我们想要更新一个 reflect.Value,那么它持有的值一定是可以被更新的,假设我们有以下代码:

func main() {i := 1v := reflect.ValueOf(i)v.SetInt(10)fmt.Println(i)
}
​
$ go run reflect.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
​
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82, 0x1014c0)/usr/local/go/src/reflect/value.go:247 +0x180
reflect.flag.mustBeAssignable(...)/usr/local/go/src/reflect/value.go:234
reflect.Value.SetInt(0x100dc0, 0x414020, 0x82, 0x1840, 0xa, 0x0)/usr/local/go/src/reflect/value.go:1606 +0x40
main.main()/tmp/sandbox590309925/prog.go:11 +0xe0

运行上述代码会导致程序崩溃并报出 “reflect: reflect.flag.mustBeAssignable using unaddressable value” 错误,仔细思考一下就能够发现出错的原因:由于 Go 语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量,程序为了防止错误就会崩溃。

想要修改原变量只能使用如下的方法:

func main() {i := 1v := reflect.ValueOf(&i)v.Elem().SetInt(10)fmt.Println(i)
}
$ go run reflect.go
10
  1. 调用 reflect.ValueOf获取变量指针;

  2. 调用 reflect.Value.Elem获取指针指向的变量;

  3. 调用 refelct.Value.SetInt 更新变量的值

由于 Go 语言的函数调用都是值传递,所以我们只能用迂回的方式改变原变量:先获取指针对应的reflect.Value,再通过reflect.Value.Elem方法得到可以被设置的变量,我们可以通过下面的代码理解这个过程:

func main() {i := 1v := &i*v = 10
}

如果不能直接操作 i 变量修改其持有的值,我们就只能获取 i 变量所在地址并使用 *v 修改所在地址中存储的整数。

2、类型和值

Go 语言的 interface{} 类型在语言内部是通过 reflect.emptyInterface 结体表示的,其中的 rtype 字段用于表示变量的类型,另一个 word 字段指向内部封装的数据:

type emptyInterface struct {typ  *rtypeword unsafe.Pointer
}

用于获取变量类型的 reflect.TypeOf 函数将传入的变量隐式转换成 reflect.emptyInterface 类型并获取其中存储的类型信息 reflect.rtype:

func TypeOf(i interface{}) Type {eface := *(*emptyInterface)(unsafe.Pointer(&i))return toType(eface.typ)
}func toType(t *rtype) Type {if t == nil {return nil}return t
}

reflect.rtype 是一个实现了 reflect.Type 接口的结构体,该结构体实现的 reflect.rtype.String 方法可以帮助我们获取当前类型的名称:

func (t *rtype) String() string {s := t.nameOff(t.str).name()if t.tflag&tflagExtraStar != 0 {return s[1:]}return s
}

reflect.TypeOf 的实现原理其实并不复杂,它只是将一个 interface{} 变量转换成了内部的 reflect.emptyInterface 表示,然后从中获取相应的类型信息。

用于获取接口值 reflect.Value 的函数 reflect.ValueOf 实现也非常简单,在该函数中我们先调用了 reflect.escapes 保证当前值逃逸到堆上,然后通过 reflect.unpackEface 从接口中获取 reflect.Value 结构体:

func ValueOf(i interface{}) Value {if i == nil {return Value{}}escapes(i)return unpackEface(i)
}func unpackEface(i interface{}) Value {e := (*emptyInterface)(unsafe.Pointer(&i))t := e.typif t == nil {return Value{}}f := flag(t.Kind())if ifaceIndir(t) {f |= flagIndir}return Value{t, e.word, f}
}

reflect.unpackEface 会将传入的接口转换成 reflect.emptyInterface,然后将具体类型和指针包装成 reflect.Value 结构体后返回。

reflect.TypeOf 和 reflect.ValueOf 的实现都很简单。我们已经分析了这两个函数的实现,现在需要了解编译器在调用函数之前做了哪些工作:

package mainimport ("reflect"
)func main() {i := 20_ = reflect.TypeOf(i)
}$ go build -gcflags="-S -N" main.go
...
MOVQ	$20, ""..autotmp_20+56(SP) // autotmp = 20
LEAQ	type.int(SB), AX           // AX = type.int(SB)
MOVQ	AX, ""..autotmp_19+280(SP) // autotmp_19+280(SP) = type.int(SB)
LEAQ	""..autotmp_20+56(SP), CX  // CX = 20
MOVQ	CX, ""..autotmp_19+288(SP) // autotmp_19+288(SP) = 20
...

从上面这段截取的汇编语言,我们可以发现在函数调用之前已经发生了类型转换,上述指令将 int 类型的变量转换成了占用 16 字节 autotmp_19+280(SP) ~ autotmp_19+288(SP) 的接口,两个 LEAQ 指令分别获取了类型的指针 type.int(SB) 以及变量 i 所在的地址。

当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换,将变量的类型和值转换成了 interface{} 并等待运行期间使用 reflect 包获取接口中存储的信息。

3、更新变量

reflect.TypeOf 和 reflect.ValueOf 的实现都很简单。我们已经分析了这两个函数的实现,现在需要了解编译器在调用函数之前做了哪些工作:

package mainimport ("reflect"
)func main() {i := 20_ = reflect.TypeOf(i)
}$ go build -gcflags="-S -N" main.go
...
MOVQ	$20, ""..autotmp_20+56(SP) // autotmp = 20
LEAQ	type.int(SB), AX           // AX = type.int(SB)
MOVQ	AX, ""..autotmp_19+280(SP) // autotmp_19+280(SP) = type.int(SB)
LEAQ	""..autotmp_20+56(SP), CX  // CX = 20
MOVQ	CX, ""..autotmp_19+288(SP) // autotmp_19+288(SP) = 20
...

从上面这段截取的汇编语言,我们可以发现在函数调用之前已经发生了类型转换,上述指令将 int 类型的变量转换成了占用 16 字节 autotmp_19+280(SP) ~ autotmp_19+288(SP) 的接口,两个 LEAQ 指令分别获取了类型的指针 type.int(SB) 以及变量 i 所在的地址。

当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换,将变量的类型和值转换成了 interface{} 并等待运行期间使用 reflect 包获取接口中存储的信息。

4、实现协议

reflect 包还为我们提供了 reflect.rtype.Implements 方法可以用于判断某些类型是否遵循特定的接口。在 Go 语言中获取结构体的反射类型 reflect.Type 还是比较容易的,但是想要获得接口类型需要通过以下方式:

reflect.TypeOf((*<interface>)(nil)).Elem()

我们通过一个例子在介绍如何判断一个类型是否实现了某个接口。假设我们需要判断如下代码中的 CustomError 是否实现了 Go 语言标准库中的 error 接口:

type CustomError struct{}func (*CustomError) Error() string {return ""
}func main() {typeOfError := reflect.TypeOf((*error)(nil)).Elem()customErrorPtr := reflect.TypeOf(&CustomError{})customError := reflect.TypeOf(CustomError{})fmt.Println(customErrorPtr.Implements(typeOfError)) // #=> truefmt.Println(customError.Implements(typeOfError)) // #=> false
}

上述代码的运行结果正如我们在接口一节中介绍的:

  • CustomError 类型并没有实现 error 接口;

  • *CustomError 指针类型实现了 error 接口;

抛开上述的执行结果不谈,我们来分析一下 reflect.rtype.Implements 方法的工作原理:

func (t *rtype) Implements(u Type) bool {if u == nil {panic("reflect: nil type passed to Type.Implements")}if u.Kind() != Interface {panic("reflect: non-interface type passed to Type.Implements")}return implements(u.(*rtype), t)
}

reflect.rtype.Implements 会检查传入的类型是不是接口,如果不是接口或者是空值就会直接崩溃并中止当前程序。在参数没有问题的情况下,上述方法会调用私有函数 reflect.implements 判断类型之间是否有实现关系:

func implements(T, V *rtype) bool {t := (*interfaceType)(unsafe.Pointer(T))if len(t.methods) == 0 {return true}...v := V.uncommon()i := 0vmethods := v.methods()for j := 0; j < int(v.mcount); j++ {tm := &t.methods[i]tmName := t.nameOff(tm.name)vm := vmethods[j]vmName := V.nameOff(vm.name)if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) {if i++; i >= len(t.methods) {return true}}}return false
}

如果接口中不包含任何方法,就意味着这是一个空的接口,任意类型都自动实现该接口,这时会直接返回 true

在其他情况下,由于方法都是按照字母序存储的,reflect.implements 会维护两个用于遍历接口和类型方法的索引 ij 判断类型是否实现了接口,因为最多只会进行 n 次比较(类型的方法数量),所以整个过程的时间复杂度是 O(n)。

5、方法调用

作为一门静态语言,如果我们想要通过 reflect 包利用反射在运行期间执行方法不是一件容易的事情,下面的十几行代码就使用反射来执行 Add(0, 1) 函数:

func Add(a, b int) int { return a + b }func main() {v := reflect.ValueOf(Add)if v.Kind() != reflect.Func {return}t := v.Type()argv := make([]reflect.Value, t.NumIn())for i := range argv {if t.In(i).Kind() != reflect.Int {return}argv[i] = reflect.ValueOf(i)}result := v.Call(argv)if len(result) != 1 || result[0].Kind() != reflect.Int {return}fmt.Println(result[0].Int()) // #=> 1
}
  1. 通过调用 reflect.ValueOf 获取函数 Add 对应的反射对象;

  2. 通过调用 reflect.rtype.NumIn 获取函数的入参个数;

  3. 多次调用 reflect.ValueOf 函数逐一设置argv数组中的各个参数;

  4. 调用反射对象 Addrelfect.Value.Call 方法并传入参数列表;

  5. 获取返回值数组、验证数组的长度以及类型并打印其中的数据;

使用反射来调用方法非常复杂,原本只需要一行代码就能完成的工作,现在需要十几行代码才能完成,但这也是在静态语言中使用动态特性需要付出的成本。

func (v Value) Call(in []Value) []Value {v.mustBe(Func)v.mustBeExported()return v.call("Call", in)
}

reflect.Value.Call 是运行时调用方法的入口,它通过两个 MustBe 开头的方法确定了当前反射对象的类型是函数以及可见性,随后调用 reflect.Value.call 完成方法调用,这个私有方法的执行过程会分成以下的几个部分:

  1. 检查输入参数以及类型的合法性;

  2. 将传入的 reflect.Value 参数数组设置到栈上;

  3. 通过函数指针和输入参数调用函数;

  4. 从栈上获取函数的返回值;

我们将按照上面的顺序分析使用 reflect 进行函数调用的几个过程。

参数检查

参数检查是通过反射调用方法的第一步,在参数检查期间我们会从反射对象中取出当前的函数指针 unsafe.Pointer,如果该函数指针是方法,那么我们会通过 reflect.methodReceiver 获取方法的接收者和函数指针。

func (v Value) call(op string, in []Value) []Value {t := (*funcType)(unsafe.Pointer(v.typ))...if v.flag&flagMethod != 0 {rcvr = vrcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)} else {...}n := t.NumIn()if len(in) < n {panic("reflect: Call with too few input arguments")}if len(in) > n {panic("reflect: Call with too many input arguments")}for i := 0; i < n; i++ {if xt, targ := in[i].Type(), t.In(i); !xt.AssignableTo(targ) {panic("reflect: " + op + " using " + xt.String() + " as type " + targ.String())}}

上述方法还会检查传入参数的个数以及参数的类型与函数签名中的类型是否可以匹配,任何参数的不匹配都会导致整个程序的崩溃中止。

准备参数

当我们已经对当前方法的参数完成验证后,就会进入函数调用的下一个阶段,为函数调用准备参数,在前面函数调用一节中,我们已经介绍过 Go 语言的函数调用惯例,函数或者方法在调用时,所有的参数都会被依次放到栈上。

nout := t.NumOut()frametype, _, retOffset, _, framePool := funcLayout(t, rcvrtype)var args unsafe.Pointerif nout == 0 {args = framePool.Get().(unsafe.Pointer)} else {args = unsafe_New(frametype)}off := uintptr(0)if rcvrtype != nil {storeRcvr(rcvr, args)off = ptrSize}for i, v := range in {targ := t.In(i).(*rtype)a := uintptr(targ.align)off = (off + a - 1) &^ (a - 1)n := targ.size...addr := add(args, off, "n > 0")v = v.assignTo("reflect.Value.Call", targ, addr)*(*unsafe.Pointer)(addr) = v.ptroff += n}
  1. 通过 reflect.funcLayout 计算当前函数需要的参数和返回值的栈布局,也就是每一个参数和返回值所占的空间大小;

  2. 如果当前函数有返回值,需要为当前函数的参数和返回值分配一片内存空间 args

  3. 如果当前函数是方法,需要向将方法的接收接收者者拷贝到 args 内存中;

  4. 将所有函数的参数按照顺序依次拷贝到对应 args 内存中

    1. 使用 reflect.funcLayout 返回的参数计算参数在内存中的位置;

    2. 将参数拷贝到内存空间中;

准备参数是计算各个参数和返回值占用的内存空间并将所有的参数都拷贝内存空间对应位置的过程,该过程会考虑函数和方法、返回值数量以及参数类型带来的差异。

调用函数

准备好调用函数需要的全部参数后,就会通过下面的代码执行函数指针了。我们会向该函数传入栈类型、函数指针、参数和返回值的内存空间、栈的大小以及返回值的偏移量:

	call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))

上述函数实际上并不存在,它会在编译期间链接到 reflect.reflectcall 这个用汇编实现的函数上,我们在这里不会分析该函数的具体实现,感兴趣的读者可以自行了解其实现原理。

处理返回值

当函数调用结束之后,就会开始处理函数的返回值:

  • 如果函数没有任何返回值,会直接清空 args 中的全部内容来释放内存空间;

  • 如果当前函数有返回值;

    1. args 中与输入参数有关的内存空间清空;

    2. 创建一个 nout 长度的切片用于保存由反射对象构成的返回值数组;

    3. 从函数对象中获取返回值的类型和内存大小,将 args 内存中的数据转换成 reflect.Value 类型并存储到切片中;

	var ret []Valueif nout == 0 {typedmemclr(frametype, args)framePool.Put(args)} else {typedmemclrpartial(frametype, args, 0, retOffset)ret = make([]Value, nout)off = retOffsetfor i := 0; i < nout; i++ {tv := t.Out(i)a := uintptr(tv.Align())off = (off + a - 1) &^ (a - 1)if tv.Size() != 0 {fl := flagIndir | flag(tv.Kind())ret[i] = Value{tv.common(), add(args, off, "tv.Size() != 0"), fl}} else {ret[i] = Zero(tv)}off += tv.Size()}}return ret
}

由 reflect.Value 构成的 ret 数组会被返回到调用方,到这里为止使用反射实现函数调用的过程就结束了。

5、小结

Go 语言的 reflect 包为我们提供了多种能力,包括如何使用反射来动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能,通过分析反射包中方法的原理能帮助我们理解之前看起来比较怪异、令人困惑的现象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/201514.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql的所有数据类型和它们的区别

一、数值类型 1. 普通整数数值类型 以下数据类型只能用以保存整数 整数数值类型类型存储大小&#xff08;字节&#xff09;有符号的取值范围&#xff08;允许存在负数&#xff09;无符号的取值范围TINYINT1-128 ~ 1270 ~ 255SMALLINT2- 327678 ~ 327670 ~ 65535MEDIUMINT3- 8…

华清远见嵌入式学习——C++——作业6

作业要求&#xff1a; 代码&#xff1a; #include <iostream>using namespace std;class Animal { public:virtual void perform() 0;};class Lion:public Animal { private:string foods;string feature; public:Lion(){}Lion(string foods,string feature):foods(foo…

【Spring Boot】如何在IntelliJ IDEA中由同一份spring boot源码运行多个不同端口的实例

我们需要使用一个服务有多个实例的测试场景&#xff0c;那么我们就需要在IntelliJ IDEA中通过不同的端口运行不同的实例&#xff0c;并且运行时的源代码是一样的&#xff0c;那么我们可以在IntelliJ IDEA这样操作&#xff0c;接下来以UserApplication服务为例&#xff1a; 复制…

使用Java API操作HDFS

文章目录 一、了解HDFS Java API&#xff08;一&#xff09;HDFS Java API概述1、配置&#xff08;Configuration&#xff09;2、文件系统&#xff08;FileSystem&#xff09;3、路径&#xff08;Path&#xff09;4、输入输出流&#xff08;FSDataInputStream 和 FSDataOutputS…

codeforces 题目 Powers Of Two

目录 题目&#xff1a; 题目描述&#xff1a; 思路&#xff1a; AC代码&#xff1a; 题目&#xff1a; 题目描述&#xff1a; 给你两个整数 n 和 k 问是否能找到 k 个2的幂&#xff0c;使其总和为 n 若能&#xff0c;则输出这 k 个 2的幂&#xff1b;若不能&#xff0c;…

预览控制;预见控制;预测控制;预观控制(preview control)

预演控制&#xff08;preview control&#xff09;作为一种新兴的控制方法&#xff0c;首次在轮式车辆中被提出。 参考文献&#xff1a; https://www.sciencedirect.com/science/article/pii/S0016003219300390https://www.sciencedirect.com/science/article/pii/S0016003219…

Ardupilot开源飞控之VTOL之旅:配件试装

Ardupilot开源飞控之VTOL之旅&#xff1a;配件试装 1. 源由2. 分析2.1 【修改使用】FC & PDB & GPS打印件2.2 【直接使用】VTX & CRSF打印件 3. 试装3.1 【结构】问题1&#xff1a;GPS座子尺寸非常紧凑&#xff0c;需要用力压入卡座内。3.2 【结构】问题2&#xff…

实验报告-实验四(时序系统实验)

软件模拟电路图 说明 SW&#xff1a;开关&#xff0c;共六个Q1~Q3&#xff1a;输出Y0~Y3&#xff1a;输出 74LS194 首先&#xff0c;要给S1和S0高电位&#xff0c;将A~D的数据存入寄存器中&#xff08;如果开始没有存入数据&#xff0c;那么就是0000在里面移位&#xff0c;不…

智慧小区园区如何布局网络对讲系统

智慧小区园区如何布局网络对讲系统 随着小区住宅的不断更新发展&#xff0c;小区的管理人员也对小区内部的通讯也有了新的要求&#xff0c;要求在工作区域无盲区、语音通讯清晰&#xff0c;小区的安保后勤都能够随时在小区的地下室和室外工作区域、任何时间进行通信。提高小区…

Python 云服务器应用,Https,定时重启

Python 云服务器应用,Https,定时重启 环境搭建Python模块模块导入生成Flask实例GET处理启动服务器打开网页验证 GET接入证书 支持https申请证书下载证书保留 xxx.crt 和 xxx.key文件就可以了 copy到python项目目录ssl_context 配置 宝塔面板操作在www目录下新建python工作目录在…

CRM立项正当时|走过复杂多变的2023年,明年如何锚定确定性增长?

正值年末&#xff0c;又到复盘今年、规划明年的重要节点。 2023年&#xff0c;黑天鹅和新风口轮番登场&#xff0c;当不确定成常态&#xff0c;环境愈发错综复杂&#xff0c;企业家们如何深谋远虑&#xff0c;带领企业实现可持续、高质量发展&#xff1f;这里提供三个思考视角…

LoadRunner12.55的简介与安装

提示&#xff1a;https://mp.weixin.qq.com/s/iK-fh0VP7v8mNSDNxjkBow 文章目录 LoadRunner的简介与安装loadrunner概述loadrunner的下载与安装 LoadRunner的使用启用VuGen LoadRunner的简介与安装 LoadRunner官网&#xff1a;https://www.microfocus.com/zh-cn/products/load…

智能优化算法应用:基于水基湍流算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于水基湍流算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于水基湍流算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.水基湍流算法4.实验参数设定5.算法结果6.参考…

递增子序列(回溯)

题目描述 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。 数组中可能含有重复元素&#xff0c;如出现两个整数相等&#xff0c;也可以视作递增序列的一种特殊情况。 样例…

Unity使用打成图集的Sprite作为模型贴图使用的问题

大家好&#xff0c;我是阿赵。   有时候用Unity引擎做项目的时候&#xff0c;会遇到这样的需求&#xff0c;美术做了一些模型或者特效&#xff0c;然后策划想在游戏运行的时候&#xff0c;读取一些游戏图标放在特效或者模型上面当做贴图使用。   这个需求实现起来很简单&am…

销售人员如何自我提升?

销售人员如何自我提升&#xff1f; 在美国有这么一句流行语&#xff1a;不当总统就干销售员。其实在国内很多老板&#xff0c;高收入人群等大部分是来自销售岗位。因为销售是离钱最近的职业&#xff0c;在销售职业生涯中能收获到很多&#xff0c;比如人际关系能力&#xff0c;…

【刘二大人】pytorch深度学习实践(三):如何实现线性模型的反向传播+代码实现详解(Tensor、backward函数)

目录 参考资料一、反向传播流程1.1 问题1.2 方法1.3 步骤1.4 例题 二、Pytorch中前向传播和反馈的计算2.1 tensor数据类型2.2 定义线性模型并且计算损失2.2.1 torch.tensor.item()2.2.2 代码 2.3 反向传播2.3.1 torch.tensor.backward()2.3.2 tensor.zero_( )2.3.3 代码实现 三…

SystemWeaver—电子电气系统协同研发平台

背景概述 当前电子电气系统在汽车领域应用广泛&#xff0c;其设计整合了多门工程学科&#xff0c;也因系统的复杂性、关联性日益提升&#xff0c;需要其提供面向软件、硬件、网络、电气等多领域交织而导致的复杂系统解决方案。并且随着功能安全、AUTOSAR、SOA、以太网通讯等新要…

Linux基础命令(测试相关)

软件测试相关linux基础命令笔记 操作系统 常见Linux&#xff1a; Redhat系列&#xff1a;RHSL、Centos、FedoraDebian系列&#xff1a;Debian、Ubuntu以上操作系统都是在原生Linux系统上&#xff0c;增加了一些软件或功能。linux的文件及路径特点 Linux没有盘符的概念&#xf…

群星璀璨!亚信科技、TM Forum联合举办数字领导力中国峰会,助百行千业打造转型升级双引擎

11月30日&#xff0c;亚信科技携手著名国际组织TM Forum&#xff08;TeleManagement Forum 电信管理论坛&#xff09;联合举办的2023数字领导力中国峰会在京隆重召开&#xff0c;国内外数百位行业领袖、专家学者、企业高管和生态伙伴齐聚一堂。大会由“数字领导力峰会”“IT数字…