设置不同结构体用作映射值的通用函数

huangapple go评论82阅读模式
英文:

Generic function to set field of different structs used as map values

问题

具有共同字段的结构体...

type Definition struct {
        Id string
        ...
}
type Requirement struct {
        Id string
        ...
}
type Campaign struct {
        Id string
        ...
}

...我有多个类似这样的函数:

func fillDefinitionIds(values *map[string]Definition) {           
        for key, value:=range *values { // 重复的代码
                value.Id=key            // 重复的代码
                (*values)[key]=value    // 重复的代码
        }                               // 重复的代码
}
func fillRequirementIds(values *map[string]Requirement) {           
        for key, value:=range *values { // 重复的代码
                value.Id=key            // 重复的代码
                (*values)[key]=value    // 重复的代码
        }                               // 重复的代码
}
func fillCampaignIds(values *map[string]Campaign) {           
        for key, value:=range *values { // 重复的代码
                value.Id=key            // 重复的代码
                (*values)[key]=value    // 重复的代码
        }                               // 重复的代码
}

我想要一个单一的函数,通过泛型(或接口,无论哪种方式)来概括访问,类似于...

func fillIds[T Definition|Requirement|Campaign](values *map[string]T) {           
        for key, value:=range *values {
                value.Id=key
                (*values)[key]=value
        }                                
}

当然,这会导致 value.Id undefined (type T has no field or method Id)。我已经多次成功解决类似的问题,但这次我找不到解决方案。

如何将这组函数抽象为一个单一的函数?

英文:

Having structures with common fields...

type Definition struct {
        Id string
        ...
}
type Requirement struct {
        Id string
        ...
}
type Campaign struct {
        Id string
        ...
}

...I have multiple functions like this:

func fillDefinitionIds(values *map[string]Definition) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}
func fillRequirementIds(values *map[string]Requirement) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}
func fillCampaignIds(values *map[string]Campaign) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}

I would like to have a single function, generalizing the access with generics (or interfaces, whatever), kind of...

func fillIds[T Definition|Requirement|Campaign](values *map[string]T) {           
        for key, value:=range *values {
                value.Id=key
                (*values)[key]=value
        }                                
}

Of course, this gives value.Id undefined (type T has no field or method Id). I've been able many times to overcome similar issues, but this time I can't find a solution for this.

How can be this set of functions be abstracted as a single one?

答案1

得分: 8

使用一个包含公共字段的结构体来组合你的结构体,并在该公共类型上定义一个设置器方法:

type Base struct {
	Id string
}

func (b *Base) SetId(id string) {
	b.Id = id
}

type Definition struct {
	Base
}
type Requirement struct {
	Base
}
type Campaign struct {
	Base
}

然后将接口约束定义为指针类型的联合,并指定设置器方法。你必须这样做,因为在当前版本的Go中,泛型字段访问是不可用的。

type IDer interface {
	*Definition | *Requirement | *Campaign
	SetId(id string)
}

func fillIds[T IDer](values map[string]T) {
	for key, value := range values {
		value.SetId(key)
		values[key] = value
	}
}

示例:https://go.dev/play/p/fJhyhazyeyc

func main() {
	m1 := map[string]*Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	for _, v := range m1 {
		fmt.Println("m1", v.Id) 
		// foo
		// bar
	}

	m2 := map[string]*Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	for _, v := range m2 {
		fmt.Println("m2", v.Id)
		// abc
		// def
	}
}
英文:

Compose your structs with a struct that includes the common field(s), and define a setter method on that common type:

type Base struct {
	Id string
}

func (b *Base) SetId(id string) {
	b.Id = id
}

type Definition struct {
	Base
}
type Requirement struct {
	Base
}
type Campaign struct {
	Base
}

Then define the interface constraint as a union of pointer types, and specify the setter method. You must do this, because generics field access isn't available in the current version of Go.

type IDer interface {
	*Definition | *Requirement | *Campaign
	SetId(id string)
}

func fillIds[T IDer](values map[string]T) {
	for key, value := range values {
		value.SetId(key)
		values[key] = value
	}
}

Example: https://go.dev/play/p/fJhyhazyeyc

func main() {
	m1 := map[string]*Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	for _, v := range m1 {
		fmt.Println("m1", v.Id) 
		// foo
		// bar
	}

	m2 := map[string]*Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	for _, v := range m2 {
		fmt.Println("m2", v.Id)
		// abc
		// def
	}
}

答案2

得分: 3

泛型适用于同一段代码适用于任意数量的类型的情况,例如:

func Ptr[T any](v T) *T {
    return &v
}

如果你想要使用泛型来实际修改多种不同类型的特定字段,那么泛型并不是最合适的选择。这实际上并不是它们的预期用途,而且Go语言已经有了一些功能可以实现这一点:组合和接口。

你已经确定了共享字段,很好,所以创建一个类型并在需要的地方进行嵌入:

type Common struct {
    ID string
}

type Foo struct {
    Common
    FooSpecificField int64
}

type Bar struct {
    Common
    BarOnly string
}

现在在公共类型上添加一个设置器:

func (c *Common) SetID(id string) {
    c.ID = id
}

现在所有嵌入Common的类型都有一个ID字段和一个相应的设置器:

f := Foo{}
f.SetID("fooID")
fmt.Println(f.ID) // fooID

b := Bar{}
b.SetID("barID")
fmt.Println(b.ID) // barID

要接受一个允许设置ID的所有类型的映射,你只需要让fillIDs接受所需的接口:

type IDs interface {
    SetID(string)
}

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k, v := range vals {
        v.SetID(k)
        vals[k] = v
    }
    return vals
}

因为设置器应该是指针接收者,你可能可以更简洁地编写相同的函数:

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k := range vals {
        vals[k].SetID(k)
    }
    return vals
}

使用接口表示该函数希望通过已知/定义的接口与传递给它的对象进行交互。使用泛型表示你希望提供将作为整体使用的数据。设置字段并不是使用整体数据,因此我认为泛型不是完成此任务的正确工具。泛型可以非常强大,在某些情况下非常有用。不久前,我在代码审查上发表了一篇关于使用泛型创建并发安全映射的评论。那是泛型的一个很好的用例,以至于我最终根据评论实现了这样一个类型,并将其放在了GitHub上。

我想提一下,我并不反对泛型。它们可以非常有用。我反对的是过度使用这个特性,这往往会导致代码变得臭味相投,并且更难阅读/维护。

英文:

Generics are for cases where the same code works for any number of types, like:

func Ptr[T any](v T) *T {
    return &v
}

You're wanting to use generics to actually modify specific fields in a number of different types, then generics aren't really the way to go. That's essentially not what they're intended to be used for, and golang already has features that allow you to do just that: Composition and interfaces.

You have identified shared fields, great, so create a type and embed it where needed:

type Common struct {
    ID    string
}

type Foo struct {
    Common
    FooSpecificField  int64
}

type Bar struct {
    Common
    BarOnly    string
}

Now add a setter on the common type:

func (c *Common) SetID(id string) {
    c.ID = id
}

Now all types that embed Common have an ID field, and an setter to go with it:

f := Foo{}
f.SetID("fooID")
fmt.Println(f.ID) // fooID
b := Bar{}
b.SetID("barID")
fmt.Println(b.ID) // barID

To accept a map of all types that allow you to set an ID, all you really need to do is make fillIds accept the required interface:

type IDs interface {
    SetID(string)
}

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k, v := range vals {
        v.SetID(k)
        vals[k] = v
    }
    return vals
}

Because setters should, by definition, be pointer receivers, you could probably write the same function even shorter:

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k := range vals {
        vals[k].SetID(k)
    }
    return vals
}

Using interfaces indicates that this function wants to interact with the objects you pass to it, through a known/defined interface. Using generics indicates that you're expected to provide data that will be used as a whole. Setting fields is not using data as a whole, hence I'd argue generics aren't the right tool for the job. Generics can be very powerful, and extremely useful in certain cases. A while back, I posted a review on code review about generics to create a concurrent-safe map. That's an excellent use-case for generics, so much so that I ended up implementing such a type in response and put it up on github

I thought I'd mention this that I don't oppose generics at all. They can be very useful. The thing I object to is the over-use of the feature, which can -and often does- lead to code that is smelly, and harder to read/maintain.

答案3

得分: 2

type Definition struct {
	Id string
}
type Requirement struct {
	Id string
}
type Campaign struct {
	Id string
}

func (v Definition) WithId(id string) Definition   { v.Id = id; return v }
func (v Requirement) WithId(id string) Requirement { v.Id = id; return v }
func (v Campaign) WithId(id string) Campaign       { v.Id = id; return v }
type WithId[T any] interface {
	WithId(id string) T
}

func fillIds[T WithId[T]](values map[string]T) {
	for key, value := range values {
		values[key] = value.WithId(key)
	}
}
func main() {
	m1 := map[string]Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	fmt.Println(m1)

	m2 := map[string]Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	fmt.Println(m2)
}

https://go.dev/play/p/F3Qk0gcyKEa


如果使用映射的作为要求,可以使用@blackgreen的答案之外的另一种方法。

type Common struct {
	Id string
}

func (v *Common) SetId(id string) { v.Id = id }

type Definition struct {
	Common
}
type Requirement struct {
	Common
}
type Campaign struct {
	Common
}

type IdSetter[T any] interface {
	*T
	SetId(id string)
}

func fillIds[T any, U IdSetter[T]](values map[string]T) {
	for key, value := range values {
		U(&value).SetId(key)
		values[key] = value
	}
}
func main() {
	m1 := map[string]Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	fmt.Println(m1)

	m2 := map[string]Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	fmt.Println(m2)
}

https://go.dev/play/p/AG050b0peFw

英文:
type Definition struct {
	Id string
}
type Requirement struct {
	Id string
}
type Campaign struct {
	Id string
}

func (v Definition) WithId(id string) Definition   { v.Id = id; return v }
func (v Requirement) WithId(id string) Requirement { v.Id = id; return v }
func (v Campaign) WithId(id string) Campaign       { v.Id = id; return v }
type WithId[T any] interface {
	WithId(id string) T
}

func fillIds[T WithId[T]](values map[string]T) {
	for key, value := range values {
		values[key] = value.WithId(key)
	}
}
func main() {
	m1 := map[string]Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	fmt.Println(m1)

	m2 := map[string]Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	fmt.Println(m2)
}

https://go.dev/play/p/F3Qk0gcyKEa


An alternative to @blackgreen's answer if using a map of values is a requirement.

type Common struct {
	Id string
}

func (v *Common) SetId(id string) { v.Id = id }

type Definition struct {
	Common
}
type Requirement struct {
	Common
}
type Campaign struct {
	Common
}

type IdSetter[T any] interface {
	*T
	SetId(id string)
}

func fillIds[T any, U IdSetter[T]](values map[string]T) {
	for key, value := range values {
		U(&value).SetId(key)
		values[key] = value
	}
}
func main() {
	m1 := map[string]Definition{"foo": {}, "bar": {}}
	fillIds(m1)
	fmt.Println(m1)

	m2 := map[string]Campaign{"abc": {}, "def": {}}
	fillIds(m2)
	fmt.Println(m2)
}

https://go.dev/play/p/AG050b0peFw

huangapple
  • 本文由 发表于 2022年12月12日 19:39:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/74770559.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定