官术网_书友最值得收藏!

1.4.2 方法

方法一般是面向對象編程(object-oriented programming,OOP)的一個特性,在C++語言中方法對應一個類對象的成員函數,是關聯到具體對象上的虛表中的。但是Go語言的方法卻是關聯到類型的,這樣可以在編譯階段完成方法的靜態綁定。一個面向對象的程序會用方法來表達其屬性對應的操作,這樣使用這個對象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。面向對象編程進入主流開發領域一般認為是從C++開始的,C++就是在兼容C語言的基礎上支持了類等面向對象的特性。Java編程則號稱是純粹的面向對象語言,因為Java中函數是不能獨立存在的,每個函數都必然是屬于某個類?的。

面向對象編程更多的只是一種思想,很多號稱支持面向對象編程的語言只是將經常用到的特性內置到語言中而已。Go語言的祖先C語言雖然不是一個支持面向對象的語言,但是C語言的標準庫中與文件相關的函數也用到了面向對象編程的思想。下面我們實現一組C語言風格的與文件相關的函數:

// 文件對象
type File struct {
    fd int
}
// 打開文件
func OpenFile(name string) (f *File, err error) {
    // ...
}
// 關閉文件
func CloseFile(f *File) error {
    // ...
}
// 讀文件數據
func ReadFile(f *File, offset int64, data []byte) int {
    // ...
}

其中OpenFile()類似于構造函數,用于打開文件對象,CloseFile()類似于析構函數,用于關閉文件對象,ReadFile()則類似于普通的成員函數,這3個函數都是普通函數。CloseFile()ReadFile()作為普通函數,需要占用包級空間中的名字資源。不過CloseFile()ReadFile()函數只是針對File類型對象的操作,這時候我們更希望這類函數和操作對象的類型緊密綁定在一?起。

Go語言的做法是將函數CloseFile()ReadFile()的第一個參數移到函數名的開頭:

// 關閉文件
func (f *File) CloseFile() error {
    // ...
}
// 讀文件數據
func (f *File) ReadFile(offset int64, data []byte) int {
    // ...
}

這樣的話,函數CloseFile()ReadFile()就成了File類型獨有的方法了(而不是File對象方法)。它們也不再占用包級空間中的名字資源,同時File類型已經明確了它們的操作對象,因此方法名字一般簡化為CloseRead

// 關閉文件
func (f *File) Close() error {
    // ...
}
// 讀文件數據
func (f *File) Read(offset int64, data []byte) int {
    // ...
}

將第一個函數參數移到函數前面,從代碼角度看雖然只是一個小的改動,但是從編程哲學角度看,Go語言已經是進入面向對象語言的行列了。我們可以給任何自定義類型添加一個或多個方法。每種類型對應的方法必須和類型的定義在同一個包中,因此是無法給int這類內置類型添加方法的(因為方法的定義和類型的定義不在一個包中)。對于給定的類型,每個方法的名字必須是唯一的,同時方法和函數一樣也不支持重?載。

方法由函數演變而來,只是將函數的第一個對象參數移到了函數名前面而已。因此,我們依然可以按照原始的過程式思維來使用方法。通過稱為方法表達式的特性可以將方法還原為普通類型的函數:

// 不依賴具體的文件對象
// func CloseFile(f *File) error
var CloseFile = (*File).Close
// 不依賴具體的文件對象
// func ReadFile(f *File, offset int64, data []byte) int
var ReadFile = (*File).Read
// 文件處理
f, _ := OpenFile("foo.dat")
ReadFile(f, 0, data)
CloseFile(f)

有些場景更關心一組相似的操作。例如,Read()讀取一些數組,然后調用Close()關閉。在此種場景中,用戶并不關心操作對象的類型,只要能滿足通用的Read()Close()行為就可以了。不過在方法表達式中,因為得到的ReadFile()CloseFile()函數參數中含有File這個特有的類型參數,這使得File相關的方法無法與其他不是File類型但是有著相同Read()Close()方法的對象無縫適配。這種小困難難不倒Go語言程序員,我們可以結合閉包特性來消除方法表達式中第一個參數類型的差異:

// 打開文件對象
f, _ := OpenFile("foo.dat")
// 綁定到f對象
// func Close() error
var Close = func Close() error {
    return (*File).Close(f)
}
// 綁定到f對象
// func Read(offset int64, data []byte) int
var Read = func(offset int64, data []byte) int {
    return (*File).Read(f, offset, data)
}
// 文件處理
Read(0, data)
Close()

這剛好是方法值也要解決的問題。我們可以用方法值特性簡化實現:

// 打開文件對象
f, _ := OpenFile("foo.dat")
// 方法值:綁定到f對象
// func Close() error
var Close = f.Close
// 方法值:綁定到f對象
// func Read(offset int64, data []byte) int
var Read = f.Read
// 文件處理
Read(0, data)
Close()

Go語言不支持傳統面向對象中的繼承特性,而是以其特有的組合方式支持了方法的繼承。Go語言中,通過在結構體內置匿名的成員來實現繼承:

import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {
    Point
    Color color.RGBA
}

雖然我們可以將ColoredPoint定義為一個有3個字段的扁平結構的結構體,但是這里將Point嵌入ColoredPoint來提供XY這兩個字段:

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y)       // "2"

通過嵌入匿名的成員,不僅可以繼承匿名成員的內部成員,而且可以繼承匿名成員類型所對應的方法。我們一般會將Point看作基類,把ColoredPoint看作Point的繼承類或子類。不過這種方式繼承的方法并不能實現C++中虛函數的多態特性。所有繼承來的方法的接收者參數依然是那個匿名成員本身,而不是當前的變?量。

type Cache struct {
    m map[string]string
    sync.Mutex
}
func (p *Cache) Lookup(key string) string {
    p.Lock()
    defer p.Unlock()
    return p.m[key]
}

Cache結構體類型通過嵌入一個匿名的sync.Mutex來繼承它的方法Lock()Unlock()。但是在調用p.Lock()p.Unlock()時,p并不是方法Lock()Unlock()的真正接收者,而是會將它們展開為p.Mutex.Lock()p.Mutex.Unlock()調用。這種展開是編譯時完成的,并沒有運行時代?價。

在傳統的面向對象語言(如C++或Java)的繼承中,子類的方法是在運行時動態綁定到對象的,因此基類實現的某些方法看到的this可能不是基類類型對應的對象,這個特性會導致基類方法運行的不確定性。而在Go語言通過嵌入匿名的成員來“繼承”的基類方法,this就是實現該方法的類型的對象,Go語言中方法是編譯時靜態綁定的。如果需要虛函數的多態特性,我們需要借助接口來實?現。

主站蜘蛛池模板: 徐州市| 老河口市| 滨州市| 墨脱县| 江安县| 集安市| 衡东县| 罗平县| 景泰县| 华安县| 绵竹市| 西城区| 望江县| 罗山县| 墨竹工卡县| 舞阳县| 南木林县| 阳原县| 碌曲县| 乌鲁木齐市| 图片| 新昌县| 蚌埠市| 新郑市| 明水县| 图们市| 淮北市| 长沙市| 喀喇沁旗| 漳州市| 德江县| 德州市| 伊通| 寻乌县| 南部县| 伽师县| 资溪县| 建湖县| 独山县| 五家渠市| 宁阳县|