英文:
How to refactor common methods of sets of int and string into a common base?
问题
考虑以下定义了两种类型IntSet
和StringSet
的程序,分别用于存储int
和string
类型的集合。
这些类型的Add()
、AddRange()
、Contains()
、ContainsAny()
和Length()
方法基本相同(只有参数类型不同)。
我可以定义独立的函数Add()
、AddRange()
等,而不使用方法接收器和interface{}
参数来处理IntSet
或StringSet
,但我希望这些方法与集合保持耦合。
如果我使用组合,那么基本结构体无法访问子结构体的map[...]bool
。
重构上述五个方法以消除代码重复的正确方法是什么?
程序:
package main
import (
"fmt"
"sort"
"strconv"
"strings"
)
type IntSet map[int]bool
type StringSet map[string]bool
func NewStringSet(vs []string) StringSet {
ss := StringSet{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (ss StringSet) Add(v string) bool {
_, found := ss[v]
ss[v] = true
return !found
}
func (ss StringSet) AddRange(vs []string) {
for _, v := range vs {
ss[v] = true
}
}
func (ss StringSet) Contains(v string) bool {
_, found := ss[v]
return found
}
func (ss StringSet) ContainsAny(vs []string) bool {
for _, v := range vs {
if _, found := ss[v]; found {
return true
}
}
return false
}
func (ss StringSet) Length() int {
return len(ss)
}
func (ss StringSet) Stringify() string {
vs := make([]string, len(ss))
i := 0
for v := range ss {
vs[i] = v
i++
}
return strings.Join(vs, ",")
}
func NewIntSet(vs []int) IntSet {
is := IntSet{}
for _, v := range vs {
is.Add(v)
}
return is
}
func (is IntSet) Add(v int) bool {
_, found := is[v]
is[v] = true
return !found
}
func (is IntSet) AddRange(vs []int) {
for _, v := range vs {
is[v] = true
}
}
func (is IntSet) Contains(v int) bool {
_, found := is[v]
return found
}
func (is IntSet) ContainsAny(vs []int) bool {
for _, v := range vs {
if _, found := is[v]; found {
return true
}
}
return false
}
func (is IntSet) Length() int {
return len(is)
}
func (is IntSet) Stringify() string {
vs := make([]int, 0)
for v := range is {
vs = append(vs, v)
}
sort.Ints(vs)
ws := make([]string, 0)
for _, v := range vs {
s := strconv.Itoa(v)
ws = append(ws, s)
}
return strings.Join(ws, ",")
}
英文:
Consider the following program that defines two types IntSet
and StringSet
for sets containing int
s and string
s respectively.
The Add()
, AddRange()
, Contains()
, ContainsAny
, and Length()
of these types are basically the same (only the argument types differ).
I could define standalone functions Add()
, AddRange()
, ... without method receivers and with interface{}
argument for IntSet
or StringSet
, but I would like these methods to stay coupled with the sets.
If I use composition, then the base struct can't access the map[...]bool
of the child struct.
What's the proper way of refactoring the five methods above to remove code duplication?
Program:
package main
import (
"fmt"
"sort"
"strconv"
"strings"
)
type IntSet map[int]bool
type StringSet map[string]bool
func NewStringSet(vs []string) StringSet {
ss := StringSet{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (ss StringSet) Add(v string) bool {
_, found := ss[v]
ss[v] = true
return !found
}
func (ss StringSet) AddRange(vs []string) {
for _, v := range vs {
ss[v] = true
}
}
func (ss StringSet) Contains(v string) bool {
_, found := ss[v]
return found
}
func (ss StringSet) ContainsAny(vs []string) bool {
for _, v := range vs {
if _, found := ss[v]; found {
return true
}
}
return false
}
func (ss StringSet) Length() int {
return len(ss)
}
func (ss StringSet) Stringify() string {
vs := make([]string, len(ss))
i := 0
for v := range ss {
vs[i] = v
i++
}
return strings.Join(vs, ",")
}
func NewIntSet(vs []int) IntSet {
is := IntSet{}
for _, v := range vs {
is.Add(v)
}
return is
}
func (is IntSet) Add(v int) bool {
_, found := is[v]
is[v] = true
return !found
}
func (is IntSet) AddRange(vs []int) {
for _, v := range vs {
is[v] = true
}
}
func (is IntSet) Contains(v int) bool {
_, found := is[v]
return found
}
func (is IntSet) ContainsAny(vs []int) bool {
for _, v := range vs {
if _, found := is[v]; found {
return true
}
}
return false
}
func (is IntSet) Length() int {
return len(is)
}
func (is IntSet) Stringify() string {
vs := make([]int, 0)
for v := range is {
vs = append(vs, v)
}
sort.Ints(vs)
ws := make([]string, 0)
for v := range vs {
s := strconv.Itoa(v)
ws = append(ws, s)
}
return strings.Join(ws, ",")
}
答案1
得分: 1
只保留重复的代码。从维护开销的角度来看,五种方法不是问题。
无论如何,这里有一个使用泛型的示例,也可以在Go2 playground上运行:
package main
import (
"fmt"
)
type Set[T comparable] map[T]bool
func NewSet[T comparable](vs []T) Set[T] {
ss := Set[T]{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (s Set[T]) Add(v T) bool {
_, found := s[v]
s[v] = true
return !found
}
func (s Set[T]) AddRange(vs []T) {
for _, v := range vs {
s[v] = true
}
}
func (s Set[T]) Contains(v T) bool {
_, found := s[v]
return found
}
func (s Set[T]) ContainsAny(vs []T) bool {
for _, v := range vs {
if _, found := s[v]; found {
return true
}
}
return false
}
func (s Set[T]) Length() int {
return len(s)
}
func (s Set[T]) Stringify() string {
vs := make([]interface{}, len(s))
i := 0
for v := range s {
vs[i] = v
i++
}
return fmt.Sprintf("%v", vs)
}
func main() {
sset := NewSet([]string{"foo", "bar"})
sset.Add("baz")
fmt.Println(sset.Stringify()) // [foo bar baz]
iset := NewSet([]int{12, 13, 14})
iset.Add(20)
fmt.Println(iset.Stringify()) // [12 13 14 20]
}
特别说明:
Set
的类型参数中使用的约束必须是comparable
,因为映射键必须支持比较运算符(==
,!=
)。- 所有接收器中必须显式重复类型参数,但约束不需要重复。因此,在所有方法中都有
func (s Set[T]) ...
的形式。 Stringify()
的实现有些麻烦,因为类型参数T comparable
不支持字符串操作。它只是一个comparable
。因此,我简单地使用了[]interface{}
和fmt.Sprintf
,这样就完成了任务。
英文:
Just keep the duplicate code. Five methods are a non-issue in terms of maintenance overhead.
Anyway here's an obligatory example with generics, which also works on the Go2 playground:
package main
import (
"fmt"
)
type Set[T comparable] map[T]bool
func NewSet[T comparable](vs []T) Set[T] {
ss := Set[T]{}
for _, v := range vs {
ss.Add(v)
}
return ss
}
func (s Set[T]) Add(v T) bool {
_, found := s[v]
s[v] = true
return !found
}
func (s Set[T]) AddRange(vs []T) {
for _, v := range vs {
s[v] = true
}
}
func (s Set[T]) Contains(v T) bool {
_, found := s[v]
return found
}
func (s Set[T]) ContainsAny(vs []T) bool {
for _, v := range vs {
if _, found := s[v]; found {
return true
}
}
return false
}
func (s Set[T]) Length() int {
return len(s)
}
func (s Set[T]) Stringify() string {
vs := make([]interface{}, len(s))
i := 0
for v := range s {
vs[i] = v
i++
}
return fmt.Sprintf("%v", vs)
}
func main() {
sset := NewSet([]string{"foo", "bar"})
sset.Add("baz")
fmt.Println(sset.Stringify()) // [foo bar baz]
iset := NewSet([]int{12, 13, 14})
iset.Add(20)
fmt.Println(iset.Stringify()) // [12 13 14 20]
}
In particular:
- the constraint used in
Set
's type parameter must becomparable
, as map keys must support comparison operators (==
,!=
) - the type param must be repeated explicitly in all receivers, but the constraint need not be repeated. So you have
func (s Set[T]) ...
in all methods - the implementation of
Stringify()
is pesky because the type parameterT comparable
doesn't support string operations. It's just acomparable
. So above I naively use[]interface{}
andfmt.Sprintf
, which just does the job
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论