Giter VIP home page Giter VIP logo

Comments (20)

flycash avatar flycash commented on September 23, 2024 1

所以有限度的检测一下类型是否匹配就可以。将 Converter 设计为泛型之后,就很难犯错了。真要是犯错,我们也只能说,我也想不到你会错。

from ekit.

Stone-afk avatar Stone-afk commented on September 23, 2024

mark

from ekit.

wangbintao1992 avatar wangbintao1992 commented on September 23, 2024

给每种类型通过注释的tag 绑定固定处理器,绑定别名
预置基础类型如int -> string ,string -> time.Time
自定义复杂类型可以实现Converter,通过tag的handler 指定
copy之前预先注册Converter。转换完清理cache
简单写个demo

var converter map[string]*Converter

type A struct {
	Age int `target:AgeStr,handler:IntToString`
}

type B struct {
	AgeStr string ``
}

type Converter interface {
	Convert(src any) (any, error)
}

type IntToString struct {
}

func (i IntToString) Convert(src any) (any, error) {
	return strconv.Itoa(src.(int)), nil
}

from ekit.

flycash avatar flycash commented on September 23, 2024

不能使用 tag,因为场景不一样。
比如说,我今天可能是 A 复制到 B,明天就可能是 A 复制到 C。也就是在不同的接口里面,目标类型不同。

另外,Tag 虽然是声明式的,但是缺乏编译器的保护,也不是一个特别好的东西

from ekit.

hookokoko avatar hookokoko commented on September 23, 2024

关于使用方式的疑问想确认下,
如果通过这种方式 cp.Copy(src, dst, ConvertField(field, converter)) 进行调用,相当于树建好了才进行Convert,但是Time2String这个场景树是建不起来的。因为在创建树的时候,会进行src和dst的类型判断,例如Time2String转换,src是time.Time{},dst是string,

func createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error {
    ...
    // 如果是Time2String的转换,src是time.Time{},dst是string,这里就过不去;并且这个判断看着是必须的
    if srcFieldTypStruct.Type.Kind() != dstFieldTypStruct.Type.Kind() {
        return newErrKindNotMatchError(srcFieldTypStruct.Type.Kind(), dstFieldTypStruct.Type.Kind(), dstFieldTypStruct.Name)
    }

    ...
    // 为什么上面判断看着是必须的,因为如果src是struct会递归建树了。
    } else if fieldSrcTyp.Kind() == reflect.Struct {
        if err := createFieldNodes(&child, fieldSrcTyp, fieldDstTyp); err != nil {
	    return err
	}
    ....

上面的判断得加上额外的条件,比如判断src的field是不是存在Converter,所以感觉是不是只能通过这种方式做了?

cp := NewReflectCopier(FieldsConverter(map[string]Converter))

from ekit.

hookokoko avatar hookokoko commented on September 23, 2024

准备在options中建一个map,通过这种方式做,发现了上面的问题,先确认下~

type options struct {
	// ignoreFields 执行复制操作时,需要忽略的字段
	ignoreFields *set.MapSet[string]

	fieldsConvert map[string]Converter
}
func ConvertField(field string, c Converter) option.Option[options] {
	return func(opt *options) {
		if field == "" || c == nil {
			return
		}

		if opt.fieldsConvert == nil {
			opt.fieldsConvert = make(map[string]Converter, 16)
		}

		opt.fieldsConvert[field] = c
	}
}

from ekit.

flycash avatar flycash commented on September 23, 2024

去掉那个判断。

    if srcFieldTypStruct.Type.Kind() != dstFieldTypStruct.Type.Kind() {
        return newErrKindNotMatchError(srcFieldTypStruct.Type.Kind(), dstFieldTypStruct.Type.Kind(), dstFieldTypStruct.Name)
    }

这个直接去掉。

然后在递归的时候,time.Time 不需要递归。这个你可以直接做成一个写死的条件。

from ekit.

flycash avatar flycash commented on September 23, 2024

然后你在执行 copy 的时候再判断目标类型是不是相同的,如果不相同,就必须存在一个 converter。

因为我们这个 option 是可以在 Copy 调用的时候才传入进来的,所以只能推迟到那个时间点。

from ekit.

hookokoko avatar hookokoko commented on September 23, 2024
type ConverterFunc func(src any) (any, error)

再确认一哈,函数式编程的实现的这种使用前提,是不是就要要求srcTyp和dstTyp都是同一个了

from ekit.

flycash avatar flycash commented on September 23, 2024

你都是 any,怎么会一样?

from ekit.

archer-wyz avatar archer-wyz commented on September 23, 2024

cp.Copy(src, dst, ConvertField(field, converter))中,我们内部需要对比Convert(src any) (any, error)的输入与输出的类型是否与要拷贝的字段吻合,所以直接使用Convert(src any) (any, error)是不方便的,所以可以引入一个构建函数
type ConverterWithCheckFunc func(srcVal reflect.Value, dstPtrVal reflect.Value) error
提供给cp.Copy(src, dst, ConvertField(field, converter))使用,
它由函数func NewConvertFieldOption(converter Converter) (ConvertFieldOption, error) 创建

// Converter 从 src类型到dst类型的接口
type Converter interface {
	Convert(src any) (any, error)
}

// ConverterFunc 函数式的写法,它也实现了 Converter 接口
type ConverterFunc func(src any) (any, error)

// ConverterWithCheckFunc 在使用newConverterWithCheckFunc后,我们在copier中实际用到的函数
// 1. srcVal的类型对应 ConverterFunc 中src的类型的值
// 2. dstVal对应 ConverterFunc 中的返回any的指针类型,从而在内部进行拷贝
type ConverterWithCheckFunc func(srcVal reflect.Value, dstPtrVal reflect.Value) error

func newConverterWithCheckFunc(converter Converter) (ConverterWithCheckFunc, error) {
	val := reflect.ValueOf(converter)
	convertMethod := val.MethodByName("Convert")
	// 检查方法是否存在
	if !convertMethod.IsValid() {
		return nil, errors.New("convert method not found")
	}
	// 获取方法的类型
	methodType := convertMethod.Type()
	// 获取参数和返回值的类型
	srcType := methodType.In(0)  // 获取第一个参数的类型
	dstType := methodType.Out(0) // 获取第一个返回值的类型

	return func(srcVal reflect.Value, dstPtrVal reflect.Value) error {
		if reflect.TypeOf(srcVal) != srcType {
			return errors.New("invalid src")
		}

		dstTypePtr := reflect.TypeOf(dstPtrVal)
		if dstTypePtr.Kind() != reflect.Ptr {
			return errors.New("dst must be pointer")
		}
		if dstTypePtr.Elem() != dstType {
			return errors.New("invalid dst")
		}

		dstVal, err := converter.Convert(srcVal.Interface())
		if err != nil {
			return err
		}

		// 获取指针内的元素,并设置值
		dstElem := dstPtrVal.Elem()
		if !dstElem.CanSet() {
			return fmt.Errorf("dst cannot be set")
		}
		dstElem.Set(reflect.ValueOf(dstVal))
		return nil
	}, nil
}

在options中存ConverterWithCheckFunc而不是ConverterFunc

// options 执行复制操作时的可选配置
type options struct {
        covertFields map[string]ConverterWithCheckFunc
	// ignoreFields 执行复制操作时,需要忽略的字段
	ignoreFields *set.MapSet[string]
}

在函数copyTreeNode中优先判定converter

from ekit.

flycash avatar flycash commented on September 23, 2024

使用泛型呢?

type Converter interface {
	Convert(src any) (any, error)
}

type ConvertFunc[Src any, Dst any] func(src Src) (Dst, error)

func (c ConvertFunc[Src, Dst]) Convert(src any) (any, error) {
	// 要对 src 的 nil 进行特殊处理
	dst, err := c(src.(Src))
	return dst, err
}

当然,这种机制只能保护住使用 ConverFunc 的人,但是如果他们自己实现了接口,就不行。

又或者我们直接在 Converter 上就定义成泛型接口,这样必然不会出错。就是用户在使用的时候可能代码会比较长一点。

from ekit.

archer-wyz avatar archer-wyz commented on September 23, 2024

总的来说我比较疑惑的是,field匹配之后,用户传入的函数和它描述的不符合,我们是不是不用为他检查了,毕竟设置值失败会 panic。

1. 引入泛型

把原来的变成私有,放到copier包中,但这样有点耦合的意思(倒也没那么多)

type converter interface {
	Convert(src any) (any, error)
}

用户实现变成这样,对熟悉函数式编程的会觉得优雅,不熟悉的会觉得难受。

type Converter[Src any, Dst any] func(src Src) (Dst, error)

func (c Converter[Src, Dst]) Convert(src any) (any, error) {
	// 要对 src 的 nil 进行特殊处理
	srcVal, ok := src.(Src)
	if !ok {
		return nil, errors.New("xxxxx")
	}
	dst, err := c(srcVal)
	// 没发判断 dst 是否符合······
	return dst, err
}

func NewTime2String(pattern string) Converter[time.Time, string] {
	return func(src time.Time) (string, error) {
		return src.Format(pattern), nil
	}
}

2. 直接改接口泛型

“Converter 上就定义成泛型接口” ,用户的实现倒确实很直观

type Converter[Src any, Dst any] interface {
	Convert(src Src) (Dst, error)
}

type Time2String struct {
	Pattern string // 就是格式化的目标格式,那个 2006 啥的
}

func (t Time2String) Convert(src time.Time) (string, error) {
	return src.Format(t.Pattern), nil
}

传入的函数就得用 any 了

func ConvertField(field string, c any) option.Option[options] {
	return func(opt *options) {
		if field == "" || c == nil {
			return
		}
		if opt.fieldsConvert == nil {
			opt.fieldsConvert = make(map[string]Converter, 16)
		}
		opt.fieldsConvert[field] = c
	}
}

具体实现的时候我们得用反射拿到convertFunc := reflect.ValueOf(converter).MethodByName("Convert"),但同样躲不过需要我们用反射作类型检查,又或者这点就直接让用户自己保障,如果匹配失败了之后panic自己负责?

from ekit.

flycash avatar flycash commented on September 23, 2024

应该说,尽量帮用户检测,但是代价太高就不会。正常来说,如果用户需要实现这么一个接口,那么它应该知道会发生什么。

可以考虑尽量用泛型,这样会非常直观,而且也不会出现问题。

这个思路呢?

// options 执行复制操作时的可选配置
type options struct {
	// ignoreFields 执行复制操作时,需要忽略的字段
	ignoreFields *set.MapSet[string]
	converters   map[string]converterWrapper
}

type converterWrapper func(src any) (any, error)

func ConvertField[Src any, Dst any](field string, c Converter[Src, Dst]) option.Option[options] {
	return func(t *options) {
		t.converters[field] = func(src any) (any, error) {
			if src == nil {
				dst, err := c.Convert(nil)
				return dst, err
			}
			srcVal, ok := src.(Src)
			if !ok {
				var dst Dst
				return dst, errors.New("报错")
			}
			dst, err := c.Convert(srcVal)
			return dst, err
		}
	}
}

wrapper 主要是要解决泛型的问题,比如说 Java 里面 List 你可以用 List 这种原生类型,但是 GO 不可以。如果你用 map[string]converter[any, any],我记得是会报错的,所以依旧需要转换。那么这里我就直接定义一个 wrapper 出来,感觉更加清晰。

泛型的接口设计,保证了用户可以明确意识到自己源类型是什么,目标类型是什么,绝对不会搞错。

因此在这里, Src 的检测是无可避免的,但是也直接用泛型参数解决,(我记得是可以用类型参数来断言的),也避免了反射的问题。

from ekit.

flycash avatar flycash commented on September 23, 2024

src 为 nil 这个场景还是要考虑兼容一下,比如说我希望为 nil 的时候,转为目标类型的零值之类的。但是这里也会有歧义。比如说我的 converter 如果是 string 转别的,会发生什么事情我也不确定,所以单元测试要考虑覆盖这个场景。

from ekit.

hookokoko avatar hookokoko commented on September 23, 2024

研究了挺久两位大佬的讨论,我觉得类型不一致最终导致的问题就是对转换后的值执行copy时的set时类型不匹配导致的panic,那我set前判断一下是不是就好?

func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue reflect.Value, dstType reflect.Type, dstValue reflect.Value, root *fieldNode) error {
        .....
	if root.isLeaf {
		convert, ok := r.options.convertFields[root.name]
		if !ok && srcTyp.Kind() != dstType.Kind() {
			return nil
		}
		if !ok && srcTyp.Kind() == dstType.Kind() && dstValue.CanSet() {
			dstValue.Set(srcValue)
			return nil
		}

		convSrcVal, err := convert.Convert(srcValue.Interface())
		if err != nil {
			return err
		}

                // set前提前判断下类型是否匹配
		if reflect.TypeOf(convSrcVal) != dstType {
			return fmt.Errorf("待设置的value和转换获取的value类型不匹配")
		}
		
		// 类型不匹配最终的问题是这里set的时候panic,所以我在上一步检查一下就好了?
		if dstValue.CanSet() {
			dstValue.Set(reflect.ValueOf(convSrcVal))
		}

		return nil
	}

from ekit.

hookokoko avatar hookokoko commented on September 23, 2024

应该说,尽量帮用户检测,但是代价太高就不会。正常来说,如果用户需要实现这么一个接口,那么它应该知道会发生什么。

可以考虑尽量用泛型,这样会非常直观,而且也不会出现问题。

这个思路呢?

// options 执行复制操作时的可选配置
type options struct {
	// ignoreFields 执行复制操作时,需要忽略的字段
	ignoreFields *set.MapSet[string]
	converters   map[string]converterWrapper
}

type converterWrapper func(src any) (any, error)

func ConvertField[Src any, Dst any](field string, c Converter[Src, Dst]) option.Option[options] {
	return func(t *options) {
		t.converters[field] = func(src any) (any, error) {
			if src == nil {
				dst, err := c.Convert(nil)
				return dst, err
			}
			srcVal, ok := src.(Src)
			if !ok {
				var dst Dst
				return dst, errors.New("报错")
			}
			dst, err := c.Convert(srcVal)
			return dst, err
		}
	}
}

wrapper 主要是要解决泛型的问题,比如说 Java 里面 List 你可以用 List 这种原生类型,但是 GO 不可以。如果你用 map[string]converter[any, any],我记得是会报错的,所以依旧需要转换。那么这里我就直接定义一个 wrapper 出来,感觉更加清晰。

泛型的接口设计,保证了用户可以明确意识到自己源类型是什么,目标类型是什么,绝对不会搞错。

因此在这里, Src 的检测是无可避免的,但是也直接用泛型参数解决,(我记得是可以用类型参数来断言的),也避免了反射的问题。

这种方式实现的话,对于dst的类型判断还是无能为力,包括后面说的src为nil时对dst的零值处理,因为不知道dst是哪种类型,除非利用反射。

dst, err := c.Convert(srcVal)
return dst, err

from ekit.

archer-wyz avatar archer-wyz commented on September 23, 2024
// options 执行复制操作时的可选配置
type options struct {
	// ignoreFields 执行复制操作时,需要忽略的字段
	ignoreFields *set.MapSet[string]
	converters   map[string]converterWrapper
}

type converterWrapper func(src any) (any, error)

func ConvertField[Src any, Dst any](field string, c Converter[Src, Dst]) option.Option[options] {
	return func(t *options) {
		t.converters[field] = func(src any) (any, error) {
			if src == nil {
				dst, err := c.Convert(nil)
				return dst, err
			}
			srcVal, ok := src.(Src)
			if !ok {
				var dst Dst
				return dst, errors.New("报错")
			}
			dst, err := c.Convert(srcVal)
			return dst, err
		}
	}
}

那就这个方案,开写

from ekit.

archer-wyz avatar archer-wyz commented on September 23, 2024
	copier, err := NewReflectCopier[ArraySrc, ArrayDst]()
	if err != nil {
		return nil, err
	}
	return copier.Copy(&ArraySrc{
		A: []SimpleSrc{
			{
				Name:    "大明",
				Age:     ekit.ToPtr[int](18),
				Friends: []string{"Tom", "Jerry"},
			},
			{
				Name:    "小明",
				Age:     ekit.ToPtr[int](8),
				Friends: []string{"Tom"},
			},
		},
	}, ConvertField[[]string, []string]("Friends", func(src []string) ([]string, error) {
		for i, friend := range src {
			src[i] = friend + "太丑了"
		}
		return src, nil
	}))

这种切片里是结构体需要进一步支持?现在构建树的时候把切片认为是叶子了,所以不会进一步进入到结构体里面进行判断,而是直接SetValue了,那这样就进入不了内部判断了,要支持这个的话要在fieldNode里对 slice 作一些类似isSlice的特殊处理了

PS:
我觉得外部传入的话还是传函数好点,不然对于一些简单的场景要写个结构体会觉得很麻烦,如果设计较为复杂的方案,让结构体搞一个生成这个函数的函数即可。

type ConvertHandler[Src any, Dst any] func(src Src) (Dst, error)

func ConvertField[Src any, Dst any](field string, c converter.ConvertHandler[Src, Dst]) option.Option[options]

from ekit.

WeiJiadong avatar WeiJiadong commented on September 23, 2024

Converter接口有两个方法:CanConvert用于检查是否可以进行转换,Convert用于实际进行转换。ConverterFunc是一个函数类型,它实现了Converter接口,可以方便地创建新的转换器。Time2String是一个具体的转换器,它可以将time.Time转换为字符串。

ReflectCopier结构体中,FieldsConverter是一个map,键是字段名,值是转换器。DefaultConverter是默认的转换器,如果没有为某个字段指定转换器,就使用这个默认的转换器。

NewReflectCopier函数接收一系列选项,可以用来配置ReflectCopier。WithFieldsConverter和WithDefaultConverter是两个选项,分别用于设置字段转换器和默认转换器。

type Converter interface {
	CanConvert(srcType, dstType reflect.Type) bool
	Convert(src interface{}) (interface{}, error)
}

type ConverterFunc func(src interface{}) (interface{}, error)

func (f ConverterFunc) CanConvert(srcType, dstType reflect.Type) bool {
	// 这里是校验逻辑...
	return true
}

func (f ConverterFunc) Convert(src interface{}) (interface{}, error) {
	return f(src)
}

type Time2String struct {
	Pattern string
}

func (t Time2String) CanConvert(srcType, dstType reflect.Type) bool {
	return srcType == reflect.TypeOf(time.Time{}) && dstType == reflect.TypeOf("")
}

func (t Time2String) Convert(src interface{}) (interface{}, error) {
	if timeVal, ok := src.(time.Time); ok {
		return timeVal.Format(t.Pattern), nil
	}
	return nil, fmt.Errorf("expected time.Time, got %T", src)
}

type ReflectCopier struct {
	FieldsConverter  map[string]Converter
	DefaultConverter Converter
}

func NewReflectCopier(options ...func(*ReflectCopier)) *ReflectCopier {
	copier := &ReflectCopier{
		FieldsConverter: make(map[string]Converter),
		DefaultConverter: ConverterFunc(func(src interface{}) (interface{}, error) {
			return src, nil
		}),
	}
	for _, option := range options {
		option(copier)
	}
	return copier
}

func WithFieldsConverter(fieldsConverter map[string]Converter) func(*ReflectCopier) {
	return func(copier *ReflectCopier) {
		copier.FieldsConverter = fieldsConverter
	}
}

func WithDefaultConverter(defaultConverter Converter) func(*ReflectCopier) {
	return func(copier *ReflectCopier) {
		copier.DefaultConverter = defaultConverter
	}
}

from ekit.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.