第6章 方法
从90年代开始,面向对象编程(OOP)就成了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,Go语言也不例外
关于OOP,其实没有明确定义,但是大概的意思是,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型相关联的函数,一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情
在早些章节,其实我们已经使用过标准库提供的一些方法,比如time.Duration这个类型的Seconds方法
const day = 24*time.Hour
fmt.Println(day.Seconds())//86400
在2.5节中,我们定义了一个方法,Celsius类型的String方法
func (c Celsius) String() string{
return fmt.Sprintf("%g˚C",c)
}
在本章中,OOP编程的第一方面,我们会展示如何有效的定义和使用方法,我们会覆盖到OOP编程的两个关键点,封装和组合
6.5 示例:bit数组
Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。集合类型用map来表示虽然非常灵活,但是我们还有更好的形式来表示
例如在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现得更加理想(例如:我们执行一个http下载任务,把文件按照16kb一块划分为很多块,需要有一个全局变量来标识那些块已经下载完成了,这种时候也需要用到bit数组)
一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示,每一个元素的每一位都表示集合里的一个值,当集合的第i位被设置时,我们才是这个集合包含元素i
下面程序展示了一个简单的bit数组类型,并用了三个函数去操作它
type Insert struct {
words []uint64
}
func (s *Insert) Has(x int) bool {
word, bit := x/64,uint(x%64)
return word < len(s.words) && s.words[word]&(1<<bit) !=0
}
func (s *Insert) Add(x int) {
word,bit := x/64, uint(x%64)
for word >= len(s.words){
s.words = append(s.words,0)
}
s.words[word] |= 1 << bit
}
func (s *Insert) UnionWith(t *Insert){
for i, tword := range t.words {
if i< len(s.words){
s.words[i] |= tword
}else {
s.words = append(s.words,tword)
}
}
}
因为每个字都有64个二进制位,所以为了定位x的bit位,我们用了x/64的商作为字的下标,并且用x%64得到的值作为这个字内的bit所在的位置,UnionWith这个方法里用到了bit位的逻辑“或”逻辑操作符号|来一次完成64个元素的或计算
当前这个实现还少了很多必要的特性,我们来添加一个方法来实现它:
func (s *Insert) String() string{
var buf bytes.Buffer
buf.WriteByte('{')
for i,word := range s.words{
if word == 0 {
continue
}
for j:=0;j<64;j++{
if word&(1<<uint(j)) != 0 {
if buf.Len()> len("{") {
buf.WriteByte(' ')
}
fmt.Fprintf(&buf,"%d",64*i+j)
}
}
}
buf.WriteByte('}')
return buf.String()
}
这里留意一下String方法,是不是和3.5.4节中的intsToString方法很相似;bytes.Buffer在String方法里经常怎么用。当你为一个复杂的类型定义了一个String方法时发,fmt包就会特殊对待这种类型的值,这样可以让这些类型在打印的时候看起来就更加友好,而不是直接打印其原始的值,fmt会直接调用用户定义的String方法,这种机制依赖于接口和类型断言,在第七章我们会详细介绍
现在我们可以在实战中直接应用上面定义好的IntSet了
var x,y Insert
x.Add(1)
x.Add(100)
x.Add(9)
fmt.Println(x.String()) // {1 9 100}
y.Add(9)
y.Add(42)
fmt.Println(y.String()) // {9 42}
x.UnionWith(&y)
fmt.Println(x.String()) // {1 9 42 100}
fmt.Println(x.Has(9),x.Has(123)) // true false
这里须注意,String和Has两个方法都是以指针*Insert来作为接收器的,但是实际上来说,把接收器声明为指针类型也没什么必要,但另外两个函数操作的时s.words对象,如果不把接收器声明为指针对象,那么实际操作的是拷贝的对象,而不是原来那个对象。因此String方法定义在IntSet指针上,所以当我们的变量是InsSert类型而不是IntSet指针时,可能会有下面这种意外情况
fmt.Println(&x) // {1 9 42 100}
fmt.Println(x.String()) //{1 9 42 100}
fmt.Println(x) //{[4398046511618 68719476736]}
在第一个Println中,我们打印一个*InSet的指针,这个类型的指针的确有自定义的String方法,第二个Println,我们直接调用了x变量的String()方法,这种情况下编译器会隐式地在x前插入&操作符,这样相当于我们还是调用了InSet指针的String方法,在第三个println中,因为InSet类型没有方法,所以Println会直接以原始的方式理解并打印,在这种情况下&符号是不能忘的。在这种场景下,我们把String方法绑定到InSet对象上,而不是InSet指针上可能更合适些,不过这也需要具体问题具体分析