如何避免为类似的golang结构重新实现sort.Interface

9
有一个问题一直困扰着我,与Golang有关。假设我有两个结构体:
type Dog struct {
   Name string
   Breed string
   Age int
}

type Cat struct {
    Name string
    FavoriteFood string
    Age int
}

当我尝试按年龄[]*Dog[]*Cat进行排序时,我需要定义2个不同的排序结构,如下:

type SortCat []*Cat
func (c SortCat) Len() int {//..}
func (c SortCat) Swap(i, j int) {//..}
func (c SortCat) Less(i, j int) bool {//..}

type SortDog []*Dog
func (c SortDog) Len() int {//..}
func (c SortDog) Swap(i, j int) {//..}
func (c SortDog) Less(i, j int) bool {//..}

一个自然的想法是实现一些 SortableByAge 接口,然后使用接口函数创建一个 Less 函数。例如:

type SortableByAge interface {
    AgeValue() int
}

然后:

type SortAnimal []SortableByAge
func (c SortDog) Less(i, j int) bool {
    return c[i].AgeValue() < c[j].AgeValue() 
}

然而,根据: http://golang.org/doc/faq#convert_slice_of_interface
dogs := make([]*Dogs, 0 , 1)
//add dogs here
sort.Sort(SortAnimal(dogs))

以上是不可能的。

因此,我想知道这种情况下最佳实践是什么,

是否有其他技术可以减少需要再次为类似结构实现sort.Interface的需求,我错过了什么?

编辑: 我意识到我的示例很糟糕:(

在实际情况中,这两个结构非常不同,它们之间唯一的共同点是我希望按一个共同的数值对它们进行排序。

一个更好的例子是:

type Laptop {//...}
type Pizza {//...}

这两个结构体唯一共同之处是我想通过价格对它们的切片进行排序(啊...在示例中不应该使用Pizza)。

因此,将它们合并为一个通用结构体对许多情况并不起作用。 但会研究go generate。


6
建议重新实现它,因为只有三行代码。 - twotwotwo
1
由于没有泛型 -> 重新实现 - 0x434D53
1
另一件事是,如果它们非常相似,也许你真的想要 type Animal { Species, Name string; Age int }。并不是所有不同的现实类别都必须映射到程序中的不同类型。 - twotwotwo
感谢@twotwotwo!我认为我的示例很糟糕,在实际项目中,这两个结构在许多方面都是不同的,我只希望通过数字字段使它们都可排序。我首先想到的是接口,但发现它不受支持。所以我来StackOverflow查看是否有我错过的任何东西。 - Lich
3个回答

11

这个特定情况

在这种特定情况下,由于它们是相同的,您不应该使用两种不同的类型,只需使用常见的Animal类型:

type Animal struct {
    Name string
    Age  int
}

func (a Animal) String() string { return fmt.Sprintf("%s(%d)", a.Name, a.Age) }

type SortAnim []*Animal

func (c SortAnim) Len() int           { return len(c) }
func (c SortAnim) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
func (c SortAnim) Less(i, j int) bool { return c[i].Age < c[j].Age }

func main() {
    dogs := []*Animal{&Animal{"Max", 4}, &Animal{"Buddy", 3}}
    cats := []*Animal{&Animal{"Bella", 4}, &Animal{"Kitty", 3}}

    fmt.Println(dogs)
    sort.Sort(SortAnim(dogs))
    fmt.Println(dogs)

    fmt.Println(cats)
    sort.Sort(SortAnim(cats))
    fmt.Println(cats)
}

输出 (Go Playground):

[Max(4) Buddy(3)]
[Buddy(3) Max(4)]
[Bella(4) Kitty(3)]
[Kitty(3) Bella(4)]

通用情况

通常情况下,如果你愿意放弃具体类型并使用接口类型,你只能使用通用的排序实现。

创建你想让切片持有的接口类型:

type Animal interface {
    Name() string
    Age() int
}

您可以使用一种常见的实现方式:

type animal struct {
    name string
    age  int
}

func (a *animal) Name() string  { return a.name }
func (a *animal) Age() int      { return a.age }
func (a animal) String() string { return fmt.Sprintf("%s(%d)", a.name, a.age) }

您具体的动物类型:

type Dog struct {
    animal  // Embed animal (its methods and fields)
}

type Cat struct {
    animal // Embed animal (its methods and fields)
}

你在SortAnim上实现了 sort.Interface:
type SortAnim []Animal

func (c SortAnim) Len() int           { return len(c) }
func (c SortAnim) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
func (c SortAnim) Less(i, j int) bool { return c[i].Age() < c[j].Age() }

使用方法:

dogs := SortAnim{&Dog{animal{"Max", 4}}, &Dog{animal{"Buddy", 3}}}
cats := SortAnim{&Cat{animal{"Bella", 4}}, &Cat{animal{"Kitty", 3}}}

fmt.Println(dogs)
sort.Sort(SortAnim(dogs))
fmt.Println(dogs)

fmt.Println(cats)
sort.Sort(SortAnim(cats))
fmt.Println(cats)

输出 (Go Playground):

[Max(4) Buddy(3)]
[Buddy(3) Max(4)]
[Bella(4) Kitty(3)]
[Kitty(3) Bella(4)]

10
注意:如提交记录ad26bb5所示,在Go 1.8(2017年第一季度),您无需实现Len()Swap()Less(),因为问题16721已得到解决。只需要Less(),其余部分由反射完成。
问题是:
  1. 绝大多数sort.Interface使用切片
  2. 必须定义一个新的顶级类型
  3. LenSwap方法总是相同的
  4. 希望通过对性能影响最小来简化常见情况
请参见new sort.go
// Slice sorts the provided slice given the provided less function.
//
// The sort is not guaranteed to be stable. For a stable sort, use
// SliceStable.
//
// The function panics if the provided interface is not a slice.
func Slice(slice interface{}, less func(i, j int) bool) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
}

只要您拥有一个比较两个实例的Less()函数并遵守接口,就可以对任何符合该共同接口的结构体进行排序。

Go 1.18 泛型将增加另一个选项,如nwillc/genfuncs/container/sort.go所示。

// Sort sorts a slice by the LessThan order.
func (s GSlice[T]) Sort(lessThan genfuncs.BiFunction[T, T, bool]) {
    n := len(s)
    s.quickSort(0, n, maxDepth(n), lessThan)
}

使用BiFunction

// BiFunction accepts two arguments and produces a result.
type BiFunction[T, U, R any] func(T, U) R

这只是一个示例,说明如何实现通用排序:这不是官方的(因为泛型在撰写时仍然非常新,即2022年第一季度)。

1
此外,您可以创建一个Less(匿名)lambda。 sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) 这将按升序排序,如果您想要相反的顺序,只需在lambda中编写a[i]>a[j]即可。 - Franck Jeannin

0

对于这种情况,最佳实践是定义

type Animal struct{
    Species,Name string
    Age int
}

根据twotwotwo的建议。如果猫和狗足够相似以便以相同方式排序,它们也足够相似以成为相同的结构体。如果它们在某些方面不同,则应为每种类型重新实现接口。

另一种选择是将您的[]*Cat切片中的所有指针复制到相同大小的[]SortableByAge切片中。如果您要对切片进行排序,那么这将需要O(n*log(n)),因此额外的O(n)不应该影响性能。

第三种选择,在罕见情况下,如果您有许多类型由于某种原因必须是不同的,但仍具有非常简单的排序函数,则可以使用go generate自动生成它们。


谢谢@johan,我也对go generate感兴趣!您能否详细说明一下在这种情况下我们如何使用go generate? - Lich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接