Golang反射reflect

本文详细介绍了Golang反射的基本概念,包括Kind、Type和Value的用法,以及如何通过tag解析配置文件并动态赋值给结构体。通过实际案例展示了如何利用反射进行结构体字段的配置加载,以及注意事项和常见应用场景。
摘要由CSDN通过智能技术生成

反射的基本介绍:

  • 反射可以在运行时动态获取变量的各种信息,比如变量的类型、类别。
  • 如果是结构体变量,还可以获取到结构体本身的信息(包含结构体的字段、tag、方法等)。
  • 通过反射可以修改变量的值,可以调用关联的方法。
  • 反射在很多框架中都有使用,使用反射可以写出一些比较通用性的方法,比如json数据的编解码。

在这里插入图片描述

1、Golang反射中的一些定义

1.1 Kind

Kind代表Type类型值表示的具体分类。零值表示非法分类。

Type是类型,Kind是类别。Type和Kind可能相同,也可能不相同:

比如: var num int = 10 num的Type为int Kind也为Int

var stu Student stu的Type是包名.Student ,Kind是Struct

所以类别是更加广泛的,比如定义了很多不同类型结构体,它们的类别都是相同的,都是结构体 。

在reflet包中定义了如下类型的Kind,也就是变量的分类:

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

 

1.2 Type

Type类型用来表示一个go类型。不是所有go类型的Type值都能使用所有方法。在调用有分类限定的方法时,应先使用Kind方法获知类型的分类。调用该分类不支持的方法会导致运行时的panic。

type中包含了一个变量的类型信息,比如对于一个结构体,type中包含了结构体的字段数量、每个字段、绑定的方法数量、每个方法以及每个方法的参数等。这些都是一个变量的类型所拥有的,而不是变量所拥有的,比如变量中存储的值,变量不同,值可能也就不同。但是对于每一个同类型的变量来说,它拥有的字段、方法都是相同的。

type Type interface {
    // Kind返回该接口的具体分类
    Kind() Kind
    // Name返回该类型在自身包内的类型名,如果是未命名类型会返回""
    Name() string
    // PkgPath返回类型的包路径,即明确指定包的import路径,如"encoding/base64"
    // 如果类型为内建类型(string, error)或未命名类型(*T, struct{}, []int),会返回""
    PkgPath() string
    // 返回类型的字符串表示。该字符串可能会使用短包名(如用base64代替"encoding/base64")
    // 也不保证每个类型的字符串表示不同。如果要比较两个类型是否相等,请直接用Type类型比较。
    String() string
    // 返回要保存一个该类型的值需要多少字节;类似unsafe.Sizeof
    Size() uintptr
    // 返回当从内存中申请一个该类型值时,会对齐的字节数
    Align() int
    // 返回当该类型作为结构体的字段时,会对齐的字节数
    FieldAlign() int
    // 如果该类型实现了u代表的接口,会返回真
    Implements(u Type) bool
    // 如果该类型的值可以直接赋值给u代表的类型,返回真
    AssignableTo(u Type) bool
    // 如该类型的值可以转换为u代表的类型,返回真
    ConvertibleTo(u Type) bool
    // 返回该类型的字位数。如果该类型的Kind不是Int、Uint、Float或Complex,会panic
    Bits() int
    // 返回array类型的长度,如非数组类型将panic
    Len() int
    // 返回该类型的元素类型,如果该类型的Kind不是Array、Chan、Map、Ptr或Slice,会panic
    Elem() Type
    // 返回map类型的键的类型。如非映射类型将panic
    Key() Type
    // 返回一个channel类型的方向,如非通道类型将会panic
    ChanDir() ChanDir
    // 返回struct类型的字段数(匿名字段算作一个字段),如非结构体类型将panic
    NumField() int
    // 返回struct类型的第i个字段的类型,如非结构体或者i不在[0, NumField())内将会panic
    Field(i int) StructField
    // 返回索引序列指定的嵌套字段的类型,
    // 等价于用索引中每个值链式调用本方法,如非结构体将会panic
    FieldByIndex(index []int) StructField
    // 返回该类型名为name的字段(会查找匿名字段及其子字段),
    // 布尔值说明是否找到,如非结构体将panic
    FieldByName(name string) (StructField, bool)
    // 返回该类型第一个字段名满足函数match的字段,布尔值说明是否找到,如非结构体将会panic
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 如果函数类型的最后一个输入参数是"..."形式的参数,IsVariadic返回真
    // 如果这样,t.In(t.NumIn() - 1)返回参数的隐式的实际类型(声明类型的切片)
    // 如非函数类型将panic
    IsVariadic() bool
    // 返回func类型的参数个数,如果不是函数,将会panic
    NumIn() int
    // 返回func类型的第i个参数的类型,如非函数或者i不在[0, NumIn())内将会panic
    In(i int) Type
    // 返回func类型的返回值个数,如果不是函数,将会panic
    NumOut() int
    // 返回func类型的第i个返回值的类型,如非函数或者i不在[0, NumOut())内将会panic
    Out(i int) Type
    // 返回该类型的方法集中方法的数目
    // 匿名字段的方法会被计算;主体类型的方法会屏蔽匿名字段的同名方法;
    // 匿名字段导致的歧义方法会滤除
    NumMethod() int
    // 返回该类型方法集中的第i个方法,i不在[0, NumMethod())范围内时,将导致panic
    // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
    // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
    Method(int) Method
    // 根据方法名返回该类型方法集中的方法,使用一个布尔值说明是否发现该方法
    // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
    // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
    MethodByName(string) (Method, bool)
    // 内含隐藏或非导出方法
}

reflect.TypeOf函数可以用来获取一个变量的Type。

 

1.3 Value

Value为go值提供了反射接口。不是所有go类型值的Value表示都能使用所有方法。在调用有分类限定的方法时,应先使用Kind方法获知该值的分类。

Value是一个结构体,它里面的字段我们不用关心。

Value绑定了一些方法,是用来获取一个变量的值属性的,比如变量的值,一个切片的长度、容量等。

type Value struct {
    // typ 保存由 Value 表示的值的类型。
	typ *rtype

	// 指针值数据,或者,如果设置了 flagIndir,则指向数据的指针.
	// 当设置 flagIndir 或 typ.pointers() 为真时有效。
	ptr unsafe.Pointer
	
	flag
}

reflect.ValueOf可以获取一个变量的value信息。

Value绑定的方法介绍:

// IsNil报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
func (v Value) IsNil() bool

// 获取v的Kind
func (v Value) Kind() Kind
// 也可以通过Value来获取Type
func (v Value) Type() Type

// 将v转换为类型t的值,并返回该值的Value封装。如果go转换规则不支持这种转换,会panic
func (v Value) Convert(t Type) Value

// Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;
func (v Value) Elem() Value

获取当前Value持有的对象的值:

// 获取值的方法:
// 获取变量的值,如果调用的不是该变量类型所对应的方法就会panic,
// 比如一个变量为int类型但是调用了Float,那么就会panic
func (v Value) Bool() bool
func (v Value) Int() int64
func (v Value) Uint() uint64
func (v Value) Float() float64
func (v Value) Complex() complex128
func (v Value) Bytes() []byte
func (v Value) String() string

// 可以将任何类型转为interface	
func (v Value) Interface() (i interface{})

/*
将v持有的值作为一个指针返回。本方法返回值不是unsafe.Pointer类型,
以避免程序员不显式导入unsafe包却得到	   
unsafe.Pointer类型表示的指针。如果v的Kind不是Chan、Func、Map、Ptr、Slice或UnsafePointer会panic。
如果v的Kind是Func,返回值是底层代码的指针,但并不足以用于区分不同的函数;
只能保证当且仅当v持有函数类型零值nil时,返回值为0。
如果v的Kind是Slice,返回值是指向切片第一个元素的指针。如果持有的切片为nil,返回值为0;
如果持有的切片没有元素但不是nil,返回值不会是0。
*/
func (v Value) Pointer() uintptr

// 返回v[i:j](v持有的切片的子切片的Value封装);如果v的Kind不是Array、Slice或String会panic。
// 如果v是一个不可寻址的数组,或者索引出界,也会panic
func (v Value) Slice(i, j int) Value

// 是Slice的3参数版本,返回v[i:j:k] ;如果v的Kind不是Array、Slice或String会panic。
// 如果v是一个不可寻址的数组,或者索引出界,也会panic。
func (v Value) Slice3(i, j, k int) Value

// 返回v持有值的容量,如果v的Kind不是Array、Chan、Slice会panic
func (v Value) Cap() int

// 返回v持有值的长度,如果v的Kind不是Array、Chan、Slice、Map、String会panic
func (v Value) Len() int

// 返回v持有值的第i个元素。如果v的Kind不是Array、Chan、Slice、String,或者i出界,会panic
func (v Value) Index(i int) Value

// 返回v持有值里key持有值为键对应的值的Value封装。如果v的Kind不是Map会panic。
// 如果未找到对应值或者v持有值是nil映射,会返回Value零值。key的持有值必须可以直接赋值给v持有值类型的键类型。
func (v Value) MapIndex(key Value) Value

// 返回一个包含v持有值中所有键的Value封装的切片,该切片未排序。如果v的Kind不是Map会panic。
// 如果v持有值是nil,返回空切片(非nil)。
func (v Value) MapKeys() []Value

给当前Value持有的对象赋值:

// 设置值的方法
// 同样,如果类型对不上就会panic
func (v Value) SetBool(x bool)
func (v Value) SetInt(x int64)
func (v Value) SetUint(x uint64)
func (v Value) SetFloat(x float64)
func (v Value) SetComplex(x complex128)
func (v Value) SetBytes(x []byte)
func (v Value) SetString(x string)
func (v Value) SetPointer(x unsafe.Pointer)
func (v Value) SetCap(n int)
func (v Value) SetLen(n int)
func (v Value) SetMapIndex(key, val Value)
// 将v的持有值修改为x的持有值。
func (v Value) Set(x Value)

结构体相关:

// 返回结构体的字段数,如果不是结构体 panic
func (v Value) NumField() int

// 返回结构体的第i个字段(的Value封装)。如果v的Kind不是Struct或i出界会panic
func (v Value) Field(i int) Value

// 返回索引序列指定的嵌套字段的Value表示,等价于用索引中的值链式调用本方法,如v的Kind非Struct将会panic
func (v Value) FieldByIndex(index []int) Value

// 返回该类型名为name的字段(的Value封装)(会查找匿名字段及其子字段),如果v的Kind不是Struct会panic;
// 如果未找到会返回Value零值。
func (v Value) FieldByName(name string) Value

// 返回该类型第一个字段名满足match的字段(的Value封装)(会查找匿名字段及其子字段),
// 如果v的Kind不是Struct会panic;如果未找到会返回Value零值
func (v Value) FieldByNameFunc(match func(string) bool) Value

// 返回v持有值的方法集的方法数目。
func (v Value) NumMethod() int

// 返回v持有值类型的第i个方法的已绑定(到v的持有值的)状态的函数形式的Value封装。
func (v Value) Method(i int) Value

// 回v的名为name的方法的已绑定(到v的持有值的)状态的函数形式的Value封装。
func (v Value) MethodByName(name string) Value

//Call方法使用输入的参数in调用v持有的函数。例如,如果len(in) == 3,
// v.Call(in)代表调用v(in[0], in[1], in[2])(其中Value值表示其持有值)。
// 如果v的Kind不是Func会panic。它返回函数所有输出结果的Value封装的切片
func (v Value) Call(in []Value) []Value

 

1.4 reflect中的其它定义

StructField是用来保存一个结构体中的一个字段的相关信息的,例如字段名、字段类型、字段标签等。通过Type的Field()方法可以获取到一个StrucetField结构体。

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    Name    string
    PkgPath string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

type StructTag string

// Get方法返回标签字符串中键key对应的值。如果标签中没有该键,会返回""。
func (tag StructTag) Get(key string) string

Method是用来保存一个方法的相关信息的,通过Type的Method()方法可以获取该结构体。

type Method struct {
    // Name是方法名。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 结合PkgPath和Name可以从方法集中指定一个方法。
    Name    string
    PkgPath string
    Type  Type  // 方法类型
    Func  Value // 方法的值
    Index int   // 用于Type.Method的索引
}

创建变量用的函数:

// New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针,返回值的Type为PtrTo(typ)。
func New(typ Type) Value

// MakeSlice创建一个新申请的元素类型为typ,长度len容量cap的切片类型的Value值。
func MakeSlice(typ Type, len, cap int) Value

// MakeMap创建一个特定映射类型的Value值。
func MakeMap(typ Type) Value

// MakeChan创建一个元素类型为typ、有buffer个缓存的通道类型的Value值。
func MakeChan(typ Type, buffer int) Value

func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value

赋值函数:

// 向切片类型的Value值s中添加一系列值,x等Value值持有的值必须能直接赋值给s持有的切片的元素类型。
func Append(s Value, x ...Value) Value
// 类似Append函数,但接受一个切片类型的Value值。将切片t的每一个值添加到s。
func AppendSlice(s, t Value) Value

 

1.5 Value和Type的关系

Value与Type是相互关联的,通过reflect.TypeOf可以获取变量的类型信息,通过Value.Type也可以获取变量的类型信息。

在这里插入图片描述

在反射中变量、interface{}和reflect.Value是可以相互转换的:

在这里插入图片描述

变量、interface{}和reflect.Value的相互转换:

在这里插入图片描述

 

1.6 判断变量的类别

不管是通过Value还是Type我们都可以调用Kind()函数来获取一个变量的类别,我们可以使用switch来判断变量的类别,对不同的类型执行不同的操作:

switch reflect.ValueOf(meta).Kind() {
	case reflect.String:
		...
	case reflect.Int:
		...
	case reflect.Struct:
		...
	case reflect.Map:
    	...
	}

 

1.7 反射中的注意事项

通过反射来修改变量,注意当使用SetXX方法来设置值需要通过对应的指针类型来完成,这样才能改变传入的变量的值,而且在传入参数时,也要传入指针,需要用到reflect.ValueOf().Elem()来获取。

// Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;
func (v Value) Elem() Value

比如下面的代码:

package main

import (
	"fmt"
	"reflect"
)

// 第一种
func main() {
	var a int = 0
	SetValue(a)       // 不使用指针
	fmt.Println(a)
}

func SetValue(v interface{}) {
    elem := reflect.ValueOf(v)    // 不使用Elem()
	elem.SetInt(666);
}

// 报错
panic: reflect: reflect.Value.SetInt using unaddressable value

// 第二种
func main() {
	var a int = 0
	SetValue(&a)         // 使用指针
	fmt.Println(a)
}

func SetValue(v interface{}) {
	elem := reflect.ValueOf(v)   // 但是没使用Elem
	elem.SetInt(666);
}

// 报错
panic: reflect: reflect.Value.SetInt using unaddressable value

// 第三种
func main() {
	var a int = 0
	SetValue(a)      // 没使用指针
	fmt.Println(a)
}

func SetValue(v interface{}) {
    elem := reflect.ValueOf(v).Elem() // 使用了Elem()
	elem.SetInt(666);
}

// 报错
panic: reflect: call of reflect.Value.Elem on int Value

// 因此,如果要修改变量,需要使用指针,同时使用Elem()返回的Value来修改
func main() {
	var a int = 0
	SetValue(&a)          // 既使用指针
	fmt.Println(a)
}

func SetValue(v interface{}) {
    elem := reflect.ValueOf(v).Elem()   // 又使用Elem()
	elem.SetInt(666);
}

// 运行成功
666

Elem()跟指针的解引用是比较相似的,比如:

// 定义了一个*int类型,是一个指针类型
var a *int = new(int)

// 想要给a赋值,就要解引用再赋值
*a = 10

// val为一个Value类型,里面保存了a的指针
val := reflect.ValueOf(&a)

// 想要给val中的a赋值,需要使用Elem(),相当于解引用了
val.Elem().SetInt(100)

 

2、案例,通过结构体的tag给结构体赋值

比如当我们要写一个通用的读取配置文件的库,将读取到的配置文件中的配置赋值到一个结构体中时,就可以使用反射。在json包中就使用到了反射,以及一个操作数据库的一些框架中也用到了反射,传入一个结构体,框架就能将从数据库中读出的数据赋值给对应的字段。

注意:结构体的字段需要要首字母大写,因为如果不在同一个包中,就访问不到结构体中的字段。

比如有下面一个配置文件,需要解析配置文件并将配置赋值给对应结构体的字段:

[server]
host = "0.0.0.0"
port = 6387
name = "myserver"
version = "sv 1.0"
max_conn = 1024
max_package_size = 1024
worker_pool_size = 8
max_worker_task_len = 1024
time_out = 120

[mysql]
data_source_name = "root:1234569@(192.168.226.128:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
max_open_conns = 20
max_idle_conns = 10

[redis]
addr = "192.168.226.128:6379"
pool_size = 10
min_idle_conns = 5
password = ""
db = 0

对于每一个作用域,比如server算一个作用域、mysql算一个、redis算一个。对于每一个作用域我们都定义一个结构体:

为了方便赋值,需要给结构体的每个字段都加一个tag。

type ServerConf struct {
	Host string                  `conf:"host"`
	Port int                     `conf:"port"`
	Name string                  `conf:"name"`
	Version string               `conf:"version"`
	MaxConn int                  `conf:"max_conn"`
	MaxPackageSize uint32        `conf:"max_package_size"`
	WorkerPoolSize uint32        `conf:"worker_pool_size"`
	MaxWorkerTaskLen uint32      `conf:"max_worker_task_len"`
	TimeOut uint32               `conf:"time_out"`
}

type MysqlConf struct{
	DataSourceName string  `conf:"data_source_name"`
	MaxOpenConns uint32    `conf:"max_open_conns"`
	MaxIdleConns uint32    `conf:"max_idle_conns"`
}

type redisConf struct{
	Addr string         `conf:"addr"`
	Password string     `conf:"password"`
	Db int              `conf:"db"`
	PoolSize int        `conf:"pool_size"`
	MinIdleConns int    `conf:"min_idle_conns"`
}

为了解析上面的配置文件,可以定义一个函数Decode(filepath, field string, value interface{}) error来解析配置文件并将配置赋值给结构体。filepath为文件的路径,field为指定的域,value为传入的结构体。

  1. 首先需要读取配置文件,找到指定的域;
  2. 然后,依次读取一行,直到读取到下一个域,或者读取到文件结尾,然后将键值对存入一个map中。
  3. 最后,通过反射给结构体赋值。

代码如下:

package confparser

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"strconv"
	"strings"
)

/*
* @Desc: This package is used to parse configuration files.
*/


func Decode(filepath, field string, value interface{}) error {
    // 判断文件是否存在
	if !fileExist(filepath) {
		return fmt.Errorf("File %s not exist", filepath)
	}

    // 域为空就报错
	if field == "" {
		return errors.New("File format error")
	}

    // 解析配置文件
	return decodeField(filepath, field, value)
}


func decodeField(filepath string, field string, value interface{}) error {
	file, err := os.Open(filepath)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := bufio.NewReader(file)
    // 首先找到指定的域
	for {
		line, err := reader.ReadString('\n')
		if err != nil || io.EOF == err {
			break
		}
		s := trimnr(line)
		l := strings.TrimSpace(s)
		if len(l) < 1 {
			continue
		}
		if l[0] != '[' {
			continue
		}

		index := strings.Index(l, "]")
		if index == -1 {
			panic("File format error, line 53")
		}
		if l[1:index] != field {
			continue
		} else {
			break
		}
	}

	m := make(map[string]string)
    // 依次读取域中的每一行并解析
	for {
		line, err := reader.ReadString('\n')
		if err != nil || io.EOF == err {
			break
		}
		l := strings.TrimSpace(line)
		// 忽略空行
		if len(l) < 1 {
			continue
		}

		// 忽略注释
		if l[0] == '#' {
			continue
		}

		if l[0] == '[' {
			break
		}

		// 可能在配置行的后面也有注释
		index := strings.LastIndex(l, "#")
		if index != -1 {   // 后面有注释, 忽略注释
			l = l[:index]
		}

		index = strings.Index(l, "=")
		if index == -1 {
			continue
		}
		k := strings.TrimSpace(l[:index])
		if index + 1 >= len(l) {
			continue
		}
		v := strings.TrimSpace(l[index + 1:])

		m[k] = v
	}

	return reflectParseValue(m, value)
}

// 通过反射给结构体赋值
func reflectParseValue(m map[string]string, value interface{}) error {
	if value == nil {
		return errors.New("Value can't be nil.")
	}
	val := reflect.ValueOf(value).Elem()
	for i := 0; i< val.NumField(); i++ {
		fieldInfo := val.Type().Field(i)
		tag := fieldInfo.Tag
		stag := tag.Get("conf")
		if stag == "" {
			continue
		}

		if confVal, ok := m[stag]; ok {
			f := val.Field(i)
			switch f.Kind() {
			case reflect.Uint64:
				n, err := strconv.ParseUint(confVal, 10, 64)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(n))
			case reflect.Uint32:
				n, err := strconv.Atoi(confVal)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(uint32(n)))
			case reflect.Uint16:
				n, err := strconv.Atoi(confVal)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(uint16(n)))
			case reflect.Uint8:
				n, err := strconv.Atoi(confVal)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(uint8(n)))
			case reflect.Int:
				n, err := strconv.Atoi(confVal)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(n))
			case reflect.Bool:
				b, err := strconv.ParseBool(confVal)
				if err != nil {
					break
				}
				f.Set(reflect.ValueOf(b))
			case reflect.String:
				l := len(confVal) - 1
				if confVal[0] == '"' && confVal[l] == '"' {
					confVal = confVal[1:l]
				}
				f.Set(reflect.ValueOf(confVal))
			}

		}


	}

	return nil
}

// 去除每行后的\r\n
func trimnr(str string) string {
	l := len(str)
	if l < 1 {
		return str
	}
	l--
	for {
		if str[l] == '\n' || str[l] == '\r' {
			l--
			if l < 0 {
				return ""
			}
		} else {
			break
		}
	}

	return str[:l + 1]
}

// 判断文件是否存在
func fileExist(path string) bool {
	_, err := os.Stat(path)
	if err != nil{
		if os.IsExist(err){
			return true
		}
		if os.IsNotExist(err){
			return false
		}
		fmt.Println(err)
		return false
	}
	return true
}

上面这个解析配置文件的方法就比较通用一些,只要符合配置文件的格式,我们可以定义一个结构体,在结构体中加入标签,然后调用Decode方法传入配置文件路径、作用域以及结构体变量指针就可以成功将配置文件解析到结构体中。

使用反射也可以调用结构体绑定的方法,通过Value.Method(i int)可以获取指定索引的方法,然后通过Call来调用。注意:方法的索引与方法定义的位置是无关的,它是根据方法名的Ascil码来排序的

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值