您现在的位置是:首页 >学无止境 >Golang 1.18 泛型:零值判断网站首页学无止境

Golang 1.18 泛型:零值判断

恋喵大鲤鱼 2023-05-14 19:27:12
简介Golang 1.18 泛型:零值判断

1.背景

如果我想实现一个函数,其功能是清除一个切片中所有零值元素,该如何实现呢?

从 Golang 1.18 开始支持泛型,我们可以考虑使用泛型来实现支持任意类型的切片,那么需要判断泛型切片的元素是否为零值。

下面是我实现的一个清除切片零值元素的函数。

// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E any](s S) S {
	r := make([]E, 0, len(s))
	for i := range s {
		if !IsZero(s[i]) {
			r = append(r, s[i])
		}
	}
	return r
}

这里的问题是如何判断泛型切片元素是否为零值,也就是实现上面代码中的函数 IsZero()

2.可能的实现

Go 原生支持类型零值,我们使用var v T申明一个变量 v,那么变量 v 便是类型 T 的零值。所以你可能会这么实现 IsZero()

func IsZero[T any](v T) bool {
	var zero T
	return v == zero // 此处有语法错误:invalid operation: cannot compare v == zero (incomparable types in type set)
}

从语法错误提示可以看出,我们没有对类型参数做可比较的限制,即没有将类型参数 T 限制为comparable。所以改为下面这样就可以了。

func IsZero[T comparable](v T) bool {
	var zero T
	return v == zero
}

对应的,ClearZero 的元素类型 E 也要限定为comparable

// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E comparable](s S) S {
	r := make([]E, 0, len(s))
	for i := range s {
		if !IsZero(s[i]) {
			r = append(r, s[i])
		}
	}
	return r
}

上面的实现可以满足大部业务场景下的需要,因为日常使用的切片元素均是可比较大小的(comparable),比如 booleans, numbers, strings, pointers, channels 等。但是一旦切片元素类型不可比较时,便无法使用上面的ClearZero()。比如切片元素是个 map 时。

var ms []map[string]string
ClearZero(ms) // 此处有语法错误:map[string]string does not implement comparable

3.通用的实现

要想实现一个满足所有元素类型的 ClearZero(),那么将切片元素和类型参数的零值比较便不能满足要求,有没有其他更好的办法完成零值判断呢?

虽然 Go 支持了泛型,但是我们也不能忘记了反射。标准库包 reflect 有一个函数用于判断一个值是否是其对应类型的零值。

// IsZero reports whether v is the zero value for its type.
// It panics if the argument is invalid.
func (v Value) IsZero() bool

有了 reflect Value.IsZero 我们便可以改写我们的 IsZero()

func IsZeroRef[T any](v T) bool {
	return reflect.ValueOf(v).IsZero()
}

// 或者
func IsZeroRef[T any](v T) bool {
	return reflect.ValueOf(&v).Elem().IsZero()
}

推荐使用后者,因为ValueOf接受一个interface{}参数,如果 v 恰好是一个接口,你就会丢失这个信息。也就是说,使用ValueOf(v)时,当 v 是一个 interface 时会有问题。

然后再改写一下ClearZero()

// ClearZeroRef creates a slice with all zero values removed.
func ClearZeroRef[S ~[]E, E any](s S) S {
	r := make([]E, 0, len(s))
	for i := range s {
		if !IsZeroRef(s[i]) {
			r = append(r, s[i])
		}
	}
	return r
}

测试如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	bs := []bool{true, false, true}
	fmt.Println(ClearZeroRef(bs))

	is := []int{1, 2, 0, 3}
	fmt.Println(ClearZeroRef(is))

	strs := []string{"foo", "bar", "", "baz"}
	fmt.Println(ClearZeroRef(strs))

	ms := []map[string]string{
		{"foo": "foo"},
		nil,
		{"bar": "bar"},
	}
	fmt.Println(ClearZeroRef(ms))
}

运行如下:

[true true]
[1 2 3]
[foo bar baz]
[map[foo:foo] map[bar:bar]]

4.go-huge-util

本文实现的两个函数对应的两个版本已放置开源仓库 dablelv/go-huge-util,欢迎大家使用。

// IsZero reports whether v is the zero value for its type.
func IsZero[T comparable](v T) bool {
	var zero T
	return v == zero
}

// IsZeroRef reports whether v is the zero value for its type.
// IsZeroRef is implemented base on reflection. 
func IsZeroRef[T any](v T) bool {
	return reflect.ValueOf(v).IsZero()
}

// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E comparable](s S) S {
	r := make([]E, 0, len(s))
	for i := range s {
		if !IsZero(s[i]) {
			r = append(r, s[i])
		}
	}
	return r
}

// ClearZeroRef creates a slice with all zero values removed.
// ClearZeroRef is implemented base on reflection. 
func ClearZeroRef[S ~[]E, E any](s S) S {
	r := make([]E, 0, len(s))
	for i := range s {
		if !reflect.ValueOf(s[i]).IsZero() {
			r = append(r, s[i])
		}
	}
	return r
}

使用示例:

package main

import (
	"fmt"

	"github.com/dablelv/go-huge-util/cond"
	"github.com/dablelv/go-huge-util/slice"
)

type ILvlv interface {
	Name() string
}

type Lvlv struct{}

func (l Lvlv) Name() string {
	return "lvlv"
}

func main() {
	fmt.Println(cond.IsZero(false)) // true
	fmt.Println(cond.IsZero(true))  // false
	fmt.Println(cond.IsZero(0))     // true
	fmt.Println(cond.IsZero(1))     // false
	fmt.Println(cond.IsZero(""))    // true
	fmt.Println(cond.IsZero("foo")) // false

	fmt.Println(cond.IsZeroRef(map[string]string(nil)))          // true
	fmt.Println(cond.IsZeroRef(map[string]string{}))             // false
	fmt.Println(cond.IsZeroRef(map[string]string{"foo": "foo"})) // false
	
	ifcSlice := []ILvlv{Lvlv{}, nil, Lvlv{}}
	fmt.Println(cond.IsZeroRef(ifcSlice[0])) // false
	fmt.Println(cond.IsZeroRef(ifcSlice[1])) // true

	bs := []bool{true, false, true}
	fmt.Println(slice.ClearZero(bs))

	is := []int{1, 2, 0, 0, 3}
	fmt.Println(slice.ClearZero(is))

	strs := []string{"", "foo", "bar", "baz"}
	fmt.Println(slice.ClearZero(strs))

	ms := []map[string]string{
		{"foo": "foo"},
		nil,
		{"bar": "bar"},
	}
	fmt.Println(slice.ClearZeroRef(ms))
}

运行输出:

true
false
true
false
true
false
true
false
false
false
true
[true true]
[1 2 3]
[foo bar baz]
[map[foo:foo] map[bar:bar]]

注意,在删除切片零值元素时,如果切片元素是可比较的(comparable),建议使用ClearZero,因为其性能略好于ClearZeroRef

参考文献

dablelv/go-huge-util - GitHub
How to check if the value of a generic type is the zero value? - stackoverflow.com

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。