zoukankan      html  css  js  c++  java
  • 第四章 面向对象

    第一天: 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函数.  

  • 相关阅读:
    IntelliJ IDEA 14.03 java 中文文本处理中的编码格式设置
    应聘感悟
    STL string分析
    CUDA SDK VolumeRender 分析 (1)
    BSP
    CUDA SDK VolumeRender 分析 (3)
    CUDA SDK VolumeRender 分析 (2)
    Windows软件发布时遇到的一些问题
    Ten Commandments of Egoless Programming (转载)
    复习下光照知识
  • 原文地址:https://www.cnblogs.com/ITPower/p/12289770.html
Copyright © 2011-2022 走看看