天天看點

07 面向對象程式設計-結構、封裝、繼承、多态、接口Struct定義struct 方法工廠模式面向對象三大特性

文章目錄

  • Struct定義
  • struct 方法
  • 工廠模式
  • 面向對象三大特性
    • 封裝
    • 繼承
    • 接口
    • 多态
  1. Golang支援面向對象程式設計,但是和傳統面向對象有差別,并不是純粹面向對象的語言,隻能說Golang支援面向對象程式設計特性
  2. Golang沒有類,通過結構體struct 來實作OOP,非常簡潔。

Struct定義

  1. Struct字段聲明文法同變量,建立一個結構體後如果沒有為字段指派 則字段取值為預設值
  2. 結構體可以進行type 重新定義,Golang預設是新類型
  3. 結構體是使用者單獨定義的類型,和其他類型轉換時需要有完全相同的字段(名字、個數、類型)
  4. struct 每個字段 都可以寫上一個tag,該tag 可以通過反射機制擷取
  5. 結構體所有字段在記憶體中是連續的

示例代碼

//聲明一個結構體
type Cat struct{
	Name string
	Age int
	Color string
	Hobby string
}
func test01(){
	
	//建立結構體并指派1
	var cat1 Cat
	cat1.Name = "小白"
	cat1.Age = 10
	cat1.Color = "白色"
	cat1.Hobby = "吃 魚"
	var cat2 Cat
	fmt.Println(cat1,cat2)//{小白 10 白色 吃 魚} { 0  }
    //建立結構體并指派2
	c3 := Cat{"小黑",20,"黑色","shuijiao"}
	 //建立結構體并指派3
	var c4 *Cat = new(Cat)
	(*c4).Name = "小花" //通過索引通路值 再通路屬性
	c4.Age = 10 //也可以直接通過索引通路屬性(goLang做了優化)
    //建立結構體并指派4
	var c5 *Cat  = &Cat{}
    c5.Name = "c5"
	fmt.Println(cat1,cat2,c3,c4,c5)
	c6 := c3
	c6.Name ="c6"
	//指派後會直接克隆一個變量給C6,修改c6的值 c3 不受影響
	//{小黑 20 黑色 shuijiao} {c6 20 黑色 shuijiao}
	fmt.Println(c3,c6)
	//建立結構體變量時直接指定字段值
	var c7 = Cat{
				Name:"小綠",
				Age:40,
				}
	fmt.Println(c7)	

}
           
type Point struct{
	x int
	y int
}
type Rect struct{
	leftUp, rightDown Point
}
type Rect2 struct{
	leftUp, rightDown *Point
}


func test02(){

	//結構體所有字段在記憶體中是連續的
	r1 := Rect{Point{1,2} , Point{3,4}}
	fmt.Printf("r1.rd.x位址= %p r1.rd.y位址= %p r1.lu.x位址= %p r1.lu.y位址= %p  \n",
	&r1.rightDown.x,&r1.rightDown.y,&r1.leftUp.x,&r1.leftUp.y)
	//	fmt.Printf("r1.rd.x位址= %p r1.rd.y位址= %p r1.lu.x位址= %p r1.lu.y位址= %p  \n",
	//&r1.rightDown.x,&r1.rightDown.y,&r1.leftUp.x,&r1.leftUp.y)
	r2 := Rect2{&Point{1,2} , &Point{3,4}}
	fmt.Printf("r2.rd.x位址= %p r2.rd.y位址= %p r2.lu.x位址= %p r2.lu.y位址= %p  \n",
	&r2.rightDown.x,&r2.rightDown.y,&r2.leftUp.x,&r2.leftUp.y)

	//結構體是使用者單獨定義的類型,和其他類型轉換時需要有完全相同的字段(名字、個數、類型)
	
	type A struct{
		Num int
	}
	type B struct{
		Num int
	}
	var a A
	var b B
	a = A(b)
	a.Num =10
	fmt.Println(a,b)
	
	//結構體進行type 重新定義,Golang預設是新類型,
	type AA A
	var aa AA
	//var a A = aa 報錯需要強轉
	var a3 A = A(aa)
	fmt.Println(aa,a3)
	//struct 每個字段 都可以寫上一個tag,該tag 可以通過反射機制擷取
	//常見的場景就是 序列化和反序列化
	type Monster struct {
		Name string `json:"name"`
		Age int `json:age`
	}
	m1 := Monster{"牛魔王",300}
	jsonstr ,err := json.marshal(m1)
	if err != nil {
		fmt.Println("json 字元串處理錯誤")
	}
	fmt.Println("json 字元串=",jsonstr)
}
           

struct 方法

struct 方法聲明文法 表示 A結構體有一個test 方法 (a A)辨別這個方式是和 A類型綁定的

func( a A)test(){} 
func( a A)test2(參數清單)(傳回值清單){
		方法體
	return 傳回值
} 
           

注意點

  1. 在通過一個結構變量去調用方法時,調用機制和函數相同,隻不過調用結構體方法時,結構體變量也會作為一個參數傳遞到方法中。
  2. Golang中的方法是指定在資料類型上的,是以所有自定義類型都可以有方法,包括 struct、int 、float32
  3. 如果一個類型實作了 String() 這個方法,那麼 fmt.Println 預設會調用變量的這個方法作為結果進行輸出
  4. 通過變量去調用方法時,其(參數傳遞)機制和函數一樣,不一樣的地方是變量本身也會作為參數傳遞給該方法(如果變量是值類型則進行值拷貝,如果是引用類型則進行地質拷貝,如果希望在方法中修改結構體的值可以使用指針來傳遞)。
  5. 方法的通路範圍控制和函數一樣,方法名首字母小寫隻在本包通路,大寫可以在包外通路。

示例代碼

//定義結構體
type Person struct{
	Name string
}
//定義方法
func (p Person) speak(){
	fmt.Println(p.Name,"是一個好人")
}
func test03(){
	p := Person{"zhangsan"}
	p.speak()
}
           

工廠模式

工廠模式用來通過指定的方法來建立(隻在包内可用的/首字母小寫的結構體)執行個體。

同時如果結構體的屬性 首字母小寫也可以通過方法類擷取(類似于java中的set/get 方法)

執行個體代碼

func test04(){
	var stu = mode.NewStudent("tom",12.0)
	fmt.Println(*stu)
	fmt.Println(stu.Name,stu.GetScore())
}
package model

type student struct{
	Name string
	score float64
}
//通過工廠方法擷取 隐藏結構的執行個體
func NewStudent(n string,s float64) *student{
	return &student{Name:n,score:s}
}
//通過方法或去被封裝的屬性
func( stu *student) GetScore () float64{
	return stu.score
}
           

面向對象三大特性

Golang也有面向對象程式設計的三大特性,隻不過實作方式和其他OOP語言不同

封裝

将抽象出來的字段封裝在一起

好處:隐藏實作細節 同時借助統一通路方法,可以對資料進行驗證,保證資料合理性

實作方式

  1. 将結構體、字段首字母小寫(類似java private)
  2. 為封裝的結構體提供工廠模式函數,首字母大寫,類似一個構造函數
  3. 為結構體提供首字母大寫的Set/Get 方法 用于屬性讀寫

    示例代碼

func test05(){
	var per = mode.NewPerson("TOM")
	per.SetAge(50)
	per.SetSal(10000.0)
	fmt.Println(per)
	fmt.Println(per.GetAge(),per.GetSal(),per.Name)

}
package model
import (
	"fmt"
)
type person struct{
	Name string
	age int
	sal float64
}
// 封裝工廠方法 建立結構體執行個體
func NewPerson(name string) *person{
	return &person{
		Name:name,
	}
}
func (p *person)SetAge(age int){
	if age>0 && age<150{
		p.age = age 
	} else{
		fmt.Println("年齡不在合法範圍内")
	}

}

func (p *person)SetSal(sal float64){
	if sal>3000 && sal<30000{
		p.sal = sal 
	} else{
		fmt.Println("薪資不在合法範圍内")
	}
}
func (p *person)GetAge() int {
	fmt.Println("====>",p.age)
	return p.age
}

func (p *person)GetSal() float64 {
	fmt.Println("====>",p.sal)
	return p.sal
}
           

繼承

繼承可以解決代碼的複用,當多個結構體存在相同的字段和方法時,可以抽象出一個結構體包含公有的屬性和方法。其他結構體不用定義這些屬性和方法,隻需要嵌套這個結構體即可。

就是說如果一個結構體嵌套了另外一個匿名結構體,那麼這個結構體可以直接通路匿名結構體的方法和字段,如此就實作了繼承。

基本文法形式

type Goods struct{
	Name string
	Price int
}
type Book struct{
	Goods //嵌套匿名結構體
	Writer string
}
           

示例代碼

func test06(){
	pu := &Pupil{}
	pu.Student.Name = "TOM"
	pu.Student.Age =12
	pu.Age=13//對繼承的字段可以簡化通路
	pu.testing();
	pu.Student.ShowInfo()
	pu.ShowInfo()//方法簡化通路
	//聲明結構體時直接為嵌套結構體指派
	pu2 := &Pupil{
		Student{Name: "Jack",
			Age:19,
			Score:100,
		},
	}
	pu2.ShowInfo()
}
//定義學生類
type Student struct{
	Name string
	Age int
	Score int
}
//顯示學生資訊
func (stu *Student)ShowInfo(){
	fmt.Println(stu)
}
func (stu *Student) SetCore(score int){
	stu.Score = score
}
//定義國小生類
type Pupil struct{
	Student//嵌入 學生結構體
}
func (p *Pupil) testing(){
	fmt.Println("國小生正在考試...")
}
           

注意點

  1. 結構體可以使用匿名結構體的所有字段和方法,無論首字母大寫還是小寫
  2. 結構體通路匿名結構體方法和屬性可以簡化,簡化通路時通路邏輯:如果目前結構體有通路屬性/方法則直接調用本結構體的該屬性/方法,如果目前結構體沒有該屬性/方法,則到嵌套的匿名結構體中找該屬性/方法,找到就調用,如果找不到就報錯。
  3. 當結構體和嵌套結構體有相同的屬性或方法時,編譯器采用就近通路原則。如果想調用匿名結構體中的屬性/方法這個時候就必須 通過匿名結構體的名字進行調用(不能進行簡化調用)
  4. 如果一個結構體嵌套了多個結構體,且多個嵌套結構體包含相同的屬性/方法,那麼在調用時就必須指明匿名結構體的名字。否則編譯器報錯
  5. 如果一個結構體嵌套了一個有名字的結構體,那麼二者是組合關系,這個時候通路組合結構體的屬性/方法時必須帶上 結構體的名字
  6. 嵌套結構體變量後,可以在建立結構體變量時直接為嵌套結構體指派。
  7. 如果一個結構體中嵌套了基本資料類型如int,通路方式 A.int=12, 如果要有多個int字段必須制定名字

多重繼承

如果一個struct 嵌套了多個匿名結構體,那麼它可以通路所有結構體的字段和方法,這就是Golang的多重繼承

*如果嵌套的多個結構體中包含相同的屬性/方法,那麼在調用方法時必須明确嵌套結構體的名字

接口

interface 類型可以定義一組方法,但是不需要實作,并且interface 不能包含任何變量,在具體類型中根據實際情況實作這些方法。

type 接口名 interface{
	method1()
	method2()
}
           

示例程式

type Gun interface{
	Fire()
}
type Gun51 struct{
	
}
func(g Gun51) Fire(){
	fmt.Println("Gun51連續發射7mm子彈...")
}
type GunRPG struct{
	
}
func(g GunRPG) Fire(){
	fmt.Println("RPC發射火箭彈...")
}
func test07(){
	var g51 Gun51
	var rpg GunRPG
	var gun Gun = g51
	gun.Fire();
	gun = rpg
	gun.Fire()
}
           

注意點

  1. 接口裡所有的方法都沒有方法體,展現了程式 高内聚低耦合的思想
  2. Golang 中不顯式的實作接口,隻要一個變量中包含接口聲明的所有方法,就認為這個變量實作了這個接口。
  3. 接口本身不建立執行個體,但是可以指向一個實作了該接口的執行個體
  4. 一個自定義類型隻有實作了某個接口,才可以将該類型變量指派給這個接口。
  5. 任何自定義類型都可以實作接口 不一定是結構體,且一個變量可以實作多個接口
  6. 接口A可以繼承多個其他接口B、C,這個時候如果變量實作A接口也必須同時實作B和C接口。如果接口(包括繼承的接口)中包含重複的方法,那麼編譯器報錯
  7. interface 預設是一個指針類型,如果沒有初始化預設輸出nil
  8. 空接口 沒有任何方法,所有類型都實作了空接口,即所有類型都可以指派給空接口

繼承和接口比較

繼承主要解決代碼的複用性和可維護性。強調共性複用。

接口主要是設計規範,讓其他類型遵守規範 。強調共性能力 各自實作。

接口一定程度上實作代碼解耦

多态

變量具有多種形态,Golang中多态通過接口實作。按照同一個接口在不同變量中做不同實作,這時接口就呈現多種不同狀态。(見接口示例)

一般通過方法參數展現多态

類型斷言

如何将一個接口變量指派給自定義類型變量?需要先确定變量的類型,即類型斷言

示例代碼

func test08(){
	var x interface{}
	var b2 int = 1
	x = b2
	// 将x 轉換為int  
	y, ok := x.(int); 
	if ok {
		fmt.Printf("轉換成功 y的類型是%T 值是=%v \n",y,y);
	}else{
		fmt.Println("轉換失敗")
	}
	fmt.Println("x.(int)==>",x.(int))
	//fmt.Println("x.(type)==>",x.(type)) 隻能在switch 中使用 這裡使用報錯
	//調用斷言方法
	TypeJudge(b2)
}

func TypeJudge(items... interface{}){
	for index,x := range items{
		switch x.(type){
		case bool :
			fmt.Printf("第%v個參數是 bool 類型,值是 %v \n",index,x)
		case float32:
			fmt.Printf("第%v個參數是 float32 類型,值是 %v \n",index,x)
		case float64:
			fmt.Printf("第%v個參數是 float64 類型,值是 %v \n",index,x)
		case int,int32,int64:
			fmt.Printf("第%v個參數是 int 類型,值是 %v \n",index,x)
		case string:
			fmt.Printf("第%v個參數是 string 類型,值是 %v \n",index,x)
		default:
			fmt.Printf("第%v個參數 類型不确定,值是 %v \n",index,x)

		}
	}
}
           

繼續閱讀