英文:
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 ints and strings 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 comparabledoesn't support string operations. It's just acomparable. So above I naively use[]interface{}andfmt.Sprintf, which just does the job
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论