中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

gopl 方法和接口

發布時間:2020-07-18 20:16:54 來源:網絡 閱讀:338 作者:騎士救兵 欄目:編程語言

方法聲明

寫一個簡單的方法:

type Point struct{X, Y float64}

// 普通的函數
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// 同樣的作用,用方法實現
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

接收者:附加的參數 p 稱為方法的接收者。
調用方法的時候,接收者在方法名的前面。這樣就和聲明保持一致:

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // 函數調用
fmt.Println(p.Distance(q))  // 方法調用

選擇子:表達是 p.Distance 稱作選擇子(selector),因為它為接收者 p 選擇合適的 Distance 方法。

指針接收者的方法

對于函數,它會復制每一只實參變量。如果函數需要更新一個變量,或者是因為實參太大而需要避免復制整個實參,就需要使用指針來傳遞變量的地址。
對于方法的接受者,也可以將方法綁定到指針類型。習慣上遵循如果一個類型的任何一個方法使用指針接收者,那么所有該類型的方法都應該使用指針接收者,即使有些方法不一定需要。
另外,為了防止混淆,不允許本身是指針的類型進行方法聲明,會有編譯錯誤:

type p *int
func (p) f() { /*...*/ } // 編譯錯誤:非法的接收者類型

方法變量與表達式

方法變量(method value)

通常是在相同的表達式里使用和調用方法,但是把兩個操作分開也是可以的。選擇子 p.Distance 可以賦予一個方法變量,它是一個函數,把方法(Point.Distance)綁定到一個接收者 p 上。函數只需要提供實參而不需要提供接收者就能夠調用:

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法變量
fmt.Println(distanceFromP(q))

這里 p.Distance 是選擇子,把它賦值給變量 distanceFromP,這個變量就是方法變量,并且這個變量是一個函數。
如果包內的 API 調用一個函數值,并且使用者期望這個函數的行為是調用一個特定接收者的方法,方法變量就非常有用。使用方法變量還可以是代碼更加簡潔:

type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }

r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 如果沒有方法變量,那么要把執行一個方法包在一個函數里,等到函數被調用后執行
time.AfterFunc(10 * time.Second, r.Launch)  // 使用方法變量,這里 r.Launch 就是一個函數,只是沒有賦值給某個變量,沒有函數名

函數 time.AfterFunc 的作用是在指定的延遲后調用一個函數。上面說了,方法變量也是函數。

方法表達式(method expression)

調用方法的時候必須提供接收者,并且按照選擇子的語法進行調用。
方法表達式,寫成 T.f 或者 (*T.f)。
其中 T 是類型,是一種函數變量,把原來方法的接收者替換成函數的第一個形參,因此它可以像平常的函數一樣調用:

p := Point{1, 2}
q := Point{4, 6}
distance :=  Point.Distance  // 方法表達式
fmt.Println(distance(p, q))
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

如果需要一個值來代表多個方法中的一個,而方法都屬于同一個類型,方法表達式可以實現讓這個值所對應的方法來處理不同的接收者。就是可以把一個方法變成一個函數,函數的變量會增加一個,第一個變量就是原來方法中的接收者。其實各個參數的順序還是一樣的,原本第一個參數在 func 前,現在移動到了 func 后面。 p.Distance(q) 變成了 distance(p, q)。

接口類型

io包定義了很多有用的接口:

  • io.Writer : 抽象了所有寫入字節的類型,下面會列舉
  • io.Reader : 抽象了所有可以讀取字節的類型
  • io.Closer : 抽象了所有可以關閉的類型,比如文件或者網絡連接

io.Writer 是一個廣泛使用的接口,它負責所有可以寫入字節的抽象,包括但不限于下面列舉的這些:

  • 文件
  • 內存緩沖區
  • 網絡連接
  • HTTP客戶端
  • 打包器(archiver)
  • 散列器(hasher)

接口值

接口值,就是一個接口類型的值。分兩個部分:

  • 動態類型 : 該接口的具體類型
  • 動態值 : 該具體類型的一個值
var w io.Writer  // 聲明接口,動態類型和動態值都是nil
w = os.Stdout  // 有動態類型,也有動態值
w = io.Writer(os.Stdout)  // 和上面這句等價,把一個具體類型顯式轉換為接口類型
w = new(bytes.Buffer)  // 有動態類型,也有動態值
w = nil  // 把動態類型和動態值都設置為nil,恢復到聲明時的狀態

比較接口值

接口值可以用 == 和 != 來比較。動態類型一致,然后動態值相等(使用動態類型的 == 來比較),那么接口值相等。接口值都是nil也是相等的。
可以作為map的key,也可以作為switch語句的操作數,因為可以比較。
動態值可能是不可比較的類型,比如切片。對這樣的接口進行比較,就會Panic。把這樣的接口用作map的key或者switch語句的操作數時也同樣會Panic。所以,僅在能確認接口值包含的動態值可以比較時,才比較接口值。
fmt 包的 %T 打印出來的就是動態類型。在內部實現中,fmt 用反射來拿到接口動態類型的名字。

注意:含有空指針的非空接口

空的接口值(動態類型和動態值都為空)和僅僅動態值為nil的接口值是不一樣的。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer)
    }
    f(buf)
    if debug {
        // ...使用 buf...
    }
}

// 如果 out 不是 nil,那么會向其寫入輸出的數據
func f(out io.Writer) {
    // ...其他代碼...
    if out != nil {
        out.Write([]byte("done\n"))
    }
}

這里,把一個類型為 *bytes.Buffer 的空指針賦給了 out 參數,此時 out 的動態值為空。但它的動態類型是 *bytes.Buffer。就是說 out 是一個包含空指針的非空接口,所以這里的檢查 out != nil 是 true,防御不了這種情況。
對于某些類型,比如 *os.File,空接收值是合法的。但是對于這里的 *buyes.Buffer,要求接收者不能為空,于是運行時會Panic。
這里的解決方案是,把 main 函數中的 buf 類型修改為 io.Writer,從而避免在最開始就把一個功能不完整的值賦給一個接口:

var buf io.Writer
if debug {
    buf = new(bytes.Buffer)
}
f(buf)

類型斷言

類型斷言是一個作用在接口值上的操作,代碼類似于x(T),x是一個接口類型的表達式,而T是一個類型(稱為斷言類型)。類型斷言會檢查操作數的動態類型是否滿足指定的斷言類型。
這里有兩種可能:

  • 斷言類型T是一個具體類型
  • 斷言類型T是一個接口類型

具體類型
如果斷言類型T是一個具體類型,斷言類型會檢查x的動態類型是否就是T。如果檢查成功,返回x的動態值,返回的類型就是T。如果檢查失敗,那么操作崩潰。

接口類型
如果斷言類型T是一個接口類型,斷言類型會檢查x的動態類型是否滿足T。如果檢查成功,動態值并沒有提取出來,仍然是一個接口值,接口值的類型和值部分也不會變,只是結果類型為接口類型T。就是說,這里類型斷言就是一個接口值表達式,從一個接口類型變為擁有另外一套方法的接口類型,但保留了接口值中動態類型和動態值部分。如果檢查失敗還是會崩潰。

類型斷言可以返回兩個結果,此時操作不會因為檢查失敗而崩潰。多出來的返回值是一個布爾型,用來指示斷言是否成功。按照慣例,一般變量名用ok。如果操作失敗,ok為false,而第一個返回值會是斷言類型的零值。

類型分支

接口有兩種不同的風格。
第一種風格下,典型的比如:io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler 和 error。接口上的各種方法突出了滿足這個接口的具體類型之間的相似性,但隱藏了各個具體類型的布局和各自特有的功能。這種風格強調了方法,而不是具體類型。
第二種風格則充分利用了接口值能夠容納各種具體類型的能力,它把接口作為這些類型的聯合(union)來使用。類型斷言用來在運行時區分這些類型并分別處理。這這種風格中,強調的是滿足這個接口的具體類型,而不是這個接口的方法(經常是沒變方法的空接口),也不注重信息隱藏。這種風格的接口使用方式稱為可識別聯合(discriminated union)。
如果對面向對象熟悉,這兩種風格分別對應:

  • 子類型多態(subtype polymorphism)
  • 特設多態(ad hoc polymorphism)

使用接口的一些建議

不要一開始就定義接口,每個接口卻只是一個單獨的實現。這種接口是不必要的抽象,還會有運行時的成本。僅在有兩個或多個具體類型需要按統一的方式處理時才需要接口。
上面的建議也有特例,如果接口和類型實現出于依賴的原因不能放在同一個包里邊,那么一個接口只有一個具體類型實現也是可以的。在這種情況下,接口是一種解耦兩個包的好方式。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

洪江市| 普定县| 赤峰市| 饶阳县| 班玛县| 普陀区| 井冈山市| 松滋市| 鄂州市| 德钦县| 营山县| 中江县| 那曲县| 泰顺县| 大理市| 明光市| 沙湾县| 湖北省| 万全县| 临泉县| 前郭尔| 永泰县| 兴化市| 两当县| 高阳县| 合肥市| 高唐县| 揭东县| 双峰县| 仪陇县| 晋城| 察哈| 库伦旗| 清涧县| 亚东县| 威海市| 辉县市| 五常市| 台北市| 广饶县| 潢川县|