• 第四章 面向对象


    第一天: go对象的基础. 如何创建结构体, 方法, 构造方法(工厂函数), 接收者模式

    第二天: 包, 如何引入外部包和系统包(定义别名或组合)

    第三天: 每个目录定义一个main方法.  


    一. 面向对象介绍

    1. go语言仅支持封装, 不支持继承和多态. 

      那么继承和多态所做的事情, 怎么来做呢? 使用接口来实现, go语言是面向接口编程.

    2. go语言只支持封装, 所以, go语言没有class, 只有struct

    二. 结构体的用法

    1. 结构体的创建方法

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func main()  {
        //创建结构体的方法
        var root TreeNode
        root = TreeNode{Value:4}
        root.Left = &TreeNode{}
        root.Right = &TreeNode{5, nil, nil}
        root.Left.Right = new(TreeNode)
    
        fmt.Println(root)
    }
    • 创建实例的几种方法
      • var root TreeNode
      • 变量名 := TreeNode{}
      • 使用内建函数new
    • 无论地址还是结构本身, 都使用.来访问成员
      •  这句话很重要, 之前就一直不明白, 为什么结构体也是打点就能访问呢

    2. slice中实例化结构体的方法

    func main()  {
        //创建结构体的方法
        var root TreeNode
        root = TreeNode{Value:4}
        root.Left = &TreeNode{}
        root.Right = &TreeNode{5, nil, nil}
        root.Left.Right = new(TreeNode)
    
        fmt.Println(root)
    
    
        nodes := []TreeNode{
            {4, nil,nil},
            {},
            {Value:3},
            {5, nil, &root},
        }
        fmt.Println(nodes)
    }

    在slice中构建结构体的时候, 可以省去结构体名

    nodes := []TreeNode{
            {4, nil,nil},
            {},
            {Value:3},
            {5, nil, &root},
    }

    3. go语言构造函数?

    root = TreeNode{Value:4}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Right = new(TreeNode)
    • go语言没有构造函数的说法. 但是从上面的例子可以看出, 他已经给出了各种各样的构造函数, 无参的, 一个参数的, 多个参数的
    • 如果我们还是想定义一个自己的构造方法怎么办?我们可以加工厂函数.  

      

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func  NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    • 看看这个构造函数, 入参是一个value, 出参是一个TreeNode的地址. 返回值是new了一个局部变量. 这就是工厂函数.
    • 工厂函数返回的是一个地址

    问题: 在NewTreeNode函数里面返回了一个局部变量的地址. 这种java里是不允许的. 但在go中是允许的.

    那么这个局部的TreeNode到底是放在堆里了还是放在栈里了呢?

    c语言, 局部变量是放在栈上的, 如果想要呗别人访问到就要放在堆上, 结束后需要手动回收.

    java语言, 类是放在堆上的, 使用的时候new一个, 用完会被自动垃圾回收

    而go语言, 我们不需要知道他是创建在堆上还是栈上. 这个是由go语言的编译器和运行环境来决定的. 他会判断, 如果TreeNode没有取地址, 他的值不需要给别人用,那就在栈上分配, 如果取地址返回了, 那就是要给别人用, 他就在堆上分配. 在堆上分配完, 会被垃圾回收

    如上: 我们定义了一个这样的结构

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    
    func main()  {
        //创建结构体的方法
        var root TreeNode
        root = TreeNode{Value:3}
        root.Left = &TreeNode{}
        root.Right = &TreeNode{5, nil, nil}
        root.Left.Left = new(TreeNode)
        root.Left.Right = NewTreeNode(2)
        
    }

     4. 如何给结构体定义方法

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func  NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    
    func (node TreeNode) Print() {
        fmt.Println(node.Value)
    }

    如上就定义了一个Print方法, 

    • 有一个接收者(node TreeNode), 相当于其他语言的this. 其实go语言的这种定义方法的方式就和普通的方法定义是一样的
      func Print(node TreeNode) {
          fmt.Println(node.Value)
      }

      功能都是相同的, 只不过, 写在前面表示是这个结构体的方法.使用上略有区别

      // 结构体函数方法调用
      root.print()
      
      //谱图函数方法调用
      print(root)

      问题: 既然(node TreeNode)放在前面这种形式的写法和普通函数一样, 那么他是传值还是传引用呢? 答案是传值. 我们来验证一下

      type TreeNode struct {
          Value int
          Left, Right *TreeNode
      }
      
      func  NewTreeNode(value int) *TreeNode {
          return &TreeNode{Value:value}
      }
      
      func (node TreeNode) Print() {
          fmt.Println(node.Value)
      }
      
      func (node TreeNode) setValue() {
          node.Value = 200
      }
      
      func main()  {
          //创建结构体的方法
          var root TreeNode
          root = TreeNode{Value:3}
          root.Left = &TreeNode{}
          root.Right = &TreeNode{5, nil, nil}
          root.Left.Left = new(TreeNode)
          root.Left.Right = NewTreeNode(2)
          root.Print()
          root.setValue()
          root.Print()
      }

      输出结果:

      33

      由此, 可以看出, setValue()方法中修改了Value值为200,但是方法外打印依然是3. 说明: 接收者方法的方法定义是值拷贝的方式, 内部修改, 不会影响外面

      那么,如何让他成功set呢, 我们给他传一个地址

      func (node *TreeNode) setValue() {
          node.Value = 200
      }

      和上一个方法的区别是: 接收者传的是一个地址. 用法和原来一样. 这样就实现了地址拷贝, 内部修改, 外部有效.

         总结: 

        1. 调用print()方法是将值拷贝一份进行打印

        2. 调用setValue()方法是地址拷贝一份, 给地址中的对象赋值.

    4. nil指针也能调用方法

      注意: 这里的重点是nil指针. 而不是nil对象

      这里为什么拿出来单写呢? 是因为, 他和我之前学得java是不同的. null对象调用方法, 调用属性都会报错, 而nil可以调用方法.

      我们先来看这个demo

      

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func  NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    
    func (node TreeNode) Print() {
        fmt.Println(node.Value)
    }
    
    func (node *TreeNode) setValue() {
        node.Value = 200
    }
    
    
    func main()  {
        var node TreeNode
        fmt.Println(node)
        node.Print()
        node.setValue()
        node.Print()
    }

    输出结果: 

    {0 <nil> <nil>}
    0
    200

    这里main中的treeNode是对象, 不是地址. 他在初始化的时候如果没有值, 会给一个默认的值. 所以, 使用它来调用, 肯定都没问题. 我们这里要讨论的是空指针. 来看看空指针的情况

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func (node *TreeNode) Print() {
        if node == nil {
            fmt.Println("node为空指针")
            return
        }
        fmt.Println(node.Value)
    }
    
    func main()  {
        var node *TreeNode
        fmt.Println(node)
        node.Print()
    }

    和上一个的区别是, 这里的TreeNode是一个指针.

    来看看结果

    <nil>
    node为空指针

    确实, 成功调用了Print方法, 并且捕获到node对象是空对象

    但这里需要注意, 对nil对象调用属性, 依然是会报错的. 

    type TreeNode struct {
        Value int
        Left, Right *TreeNode
    }
    
    func (node *TreeNode) Print() {
        if node == nil {
            fmt.Println("node为空指针")
            // return
        }
        fmt.Println(node.Value)
    }
    
    func main()  {
        var node *TreeNode
        fmt.Println(node)
        node.Print()
    }

    把return注释掉. 看结果

    报了panic异常.

    那么, 指针接收者是不是上来都要判断这个指针是否是nil呢? 这不一定, 要看使用场景.

    5. 结构体函数的遍历

    func(node *TreeNode) traveres() {
        if node == nil{
            return
        }
        node.Left.traveres()
        node.Print()
        node.Right.traveres()
    }

    遍历左子树, 打印出来, 在遍历又子树, 打印出来

    结果: 

    0
    0
    3
    5
    4

    注意: 这里的node.Left.traveres()的写法. 我们只判断了node是否为nil. 如果在java中, 我们还需要判断node.Left是否为null. 否则会抛异常, 但是go不会, nil指针也可以调用方法

    到底应该使用值接受者还是指针接收者?

    • 要改变内容, 必须使用指针接收者
    • 结构过大也考虑使用指针接收者: 因为结构过大耗费很多内存
    • 一致性: 如果有指针接收者, 最好使用指针接收者 (建议)
    • 值接收者是go语言特有的. 指针接收者其他语言也有, c有this指针, java的this不是指针,他是对对象的一个引用, python有self.
    • 值/指针接收者均可接收值/指针: 这句话的含义是, 我定义一个对象, 或者是指针对象, 都可以调用Print方法
      func main()  {
          //创建结构体的方法
          var root TreeNode
          root = TreeNode{Value:3}
          root.Left = &TreeNode{}
          root.Right = &TreeNode{5, nil, nil}
          root.Left.Left = new(TreeNode)
          root.Right.Right = NewTreeNode(4)
      
          root.traveres()
          
          var node *TreeNode
          node.traveres()
      
      }

      root是一个值, node是一个指针, 都可以调用指针接收者traveres. 同样, root和node也都可以调用一个值接收者

     三. 包

    包里面重点说明的是

    1. 首字母大写表示public, 首字母小写表示private

    2. 包的定义: 一个目录下只能有一个包.  比如, 定义了一个文件夹叫tree. 那么他里面所有的文件的包名都是tree. 或者都是main(这样也是允许的). 不能既有tree又有main.  

    四. 如何扩展系统包或者别人定义的包?

    假如有一个别人写的结构体, 我想用, 但是还不满足我的需求, 我想扩展, 怎么扩展呢?

    在其他语言, 比如c++和java都是继承, 但继承有很多不方便的地方. 所以go取消了继承. 用以下两种方法实现

    • 定义别名
    • 使用组合

    1. 定义别名: 比如上面treeNode的例子. 如果我想在另外一个包里扩展, 使用定义别名的方式如何实现呢?

    package main
    
    
    import (
        "aaa/tree"
        "fmt"
    )
    
    // 原来遍历方式是前序遍历. 现在想扩展一个后序遍历. 怎么办呢? 我们使用组合的方式来实现一下
    // 第一步: 自己定义一个类型, 然后引用外部类型. 引用的时候最好使用指针, 不然要对原来的结构体进行一个值拷贝
    // 第二步: 扩展自己的方法
    type myTreeNode struct {
        node *tree.TreeNode
    }
    
    func (myNode *myTreeNode) postorder() {
        if myNode == nil || myNode.node == nil{
            return
        }
        left := myTreeNode{myNode.node.Left}
        left.postorder()
        right := myTreeNode{myNode.node.Right}
        right.postorder()
        fmt.Print(myNode.node.Value)
    }
    
    func main(){
        //创建结构体的方法
    
        var root tree.TreeNode
        root = tree.TreeNode{Value:3}
        root.Left = &tree.TreeNode{}
        root.Right = &tree.TreeNode{5, nil, nil}
        root.Left.Left = new(tree.TreeNode)
        root.Right.Right = tree.NewTreeNode(4)
    
        root.Traveres()
    
        var node *tree.TreeNode
        node.Traveres()
    
        treeNode := myTreeNode{&root}
        treeNode.postorder()
    }

    第一步: 先定义一个自己的类型, 然后引入外部结构. 这里组好引入的是指针类型, 不然对外部结构还要进行一份值拷贝

    type myTreeNode struct {
        node *tree.TreeNode
    }

    这样做, 当前这个对象已经拥有了原来定义的TreeNode结构体. 想象一下使用的时候, 传递进来了一个TreeNode类型的结构体. 然后我们对这个TreeNode结构体进行操作

    第二步: 实现自己的方法, 后序遍历

    func (myNode *myTreeNode) postorder() {
       // 这里需要注意的是myNode.node可能是空节点.
    if myNode == nil || myNode.node == nil{ return } left := myTreeNode{myNode.node.Left} left.postorder() right := myTreeNode{myNode.node.Right} right.postorder() fmt.Print(myNode.node.Value) }

    取出外部结构体, 然后获取结构体的左子树. 在获取结构体的右子树, 在打印出来, 这样就实现了对原来结构体的调用了.

    第三步: 调用

    func main(){
        //创建结构体的方法
    
        var root tree.TreeNode
        root = tree.TreeNode{Value:3}
        root.Left = &tree.TreeNode{}
        root.Right = &tree.TreeNode{5, nil, nil}
        root.Left.Left = new(tree.TreeNode)
        root.Right.Right = tree.NewTreeNode(4)
    
        root.Traveres()
    
        var node *tree.TreeNode
        node.Traveres()
    
        treeNode := myTreeNode{&root}
        treeNode.postorder()
    }

    调用也很简单. 吧root传进来地址, 然后调用方法即可

    2. 定义别名的方式实现外部结构体或系统结构体的调用

    下面我们给切片定义一个别名. --- 队列

    package main
    
    import "fmt"
    
    type Queue []int
    
    func(q *Queue) add(v int){
        *q = append(*q, v)
    }
    
    func(q *Queue) pop() int{
        tail := (*q)[len(*q)-1]
        *q = (*q)[:len(*q)-1]
        return tail
    }
    
    func(q *Queue) isEmpty() bool {
        return len(*q) == 0
    }
    
    func main() {
        q := Queue{1}
        q.add(2)
        q.add(3)
        fmt.Println(q.pop())
        fmt.Println(q.pop())
        fmt.Println(q.isEmpty())
        fmt.Println(q.pop())
        fmt.Println(q.isEmpty())
    }

    第一步: 给切片定义一个别名

    type Queue []int

    然后对这个切片进行操作, 添加一个元素

    func(q *Queue) add(v int){
        *q = append(*q, v)
    }

    这里需要注意: 在add方法里. 我们上面说了接收者这种写法类似于this, 但是这个方法里, *q 对地址的值进行修改了. 也就是说add以后, 他已经不是原来的地址了. 

    我们运算完以后的地址也不是原来的地址了

    func main() {
        q := Queue{1}
        fmt.Printf("地址: 0x%x 
    ", &q[0])
        q.add(2)
        fmt.Printf("地址: 0x%x 
    ", &q[1])
        q.add(3)
        fmt.Println(q.pop())
        fmt.Println(q.pop())
        fmt.Println(q.isEmpty())
        fmt.Println(q.pop())
        fmt.Println(q.isEmpty())
    }
    地址: 0xc000096008 
    地址: 0xc000096028 
    3
    2
    false
    1
    true

    两次打印出来的地址是不同的. 说明他的地址变了

    五. 包名的定义, 每一个文件夹下面只能有一个main

    我们用系统包来举例

    所以,我们在定义文件的时候, 在每一个文件夹下定义一个main函数.  

  • 相关阅读:
    .net 日期格式化
    grunt 上手
    设计模式的认识
    顺时针打印矩阵
    WCF 框架运行时类图
    Python闭包详解
    软件用了那些技术
    zoj 1610 Count the Colors(线段树延迟更新)
    快速提高自己的技术的办法?有两个方法
    纯win32实现PNG图片透明窗体
  • 原文地址:https://www.cnblogs.com/ITPower/p/12289770.html
Copyright © 2020-2023  润新知