Go语言圣经 - 第6章 方法 - 6.5 示例:bit数组

第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指针上可能更合适些,不过这也需要具体问题具体分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值