make loop

This commit is contained in:
chai2010
2015-12-18 14:49:31 +08:00
parent 9fde1ff772
commit f9ac065e47
106 changed files with 725 additions and 725 deletions

View File

@@ -1,6 +1,6 @@
## 6.1. 方法聲明
在函數聲明時,在其名字之前放上一個變量,卽是一個方法。這個附加的參數會將該函數附加到這種類型上,卽相當於爲這種類型定義了一個獨的方法。
在函數聲明時,在其名字之前放上一個變量,卽是一個方法。這個附加的參數會將該函數附加到這種類型上,卽相當於爲這種類型定義了一個獨的方法。
下面來寫我們第一個方法的例子這個例子在package geometry下
@@ -25,9 +25,9 @@ func (p Point) Distance(q Point) float64 {
```
上面的代碼那個附加的參數p叫做方法的接收器(receiver),早期的面向對象語言留下的遺將調用一個方法稱爲“向一個對象發送消息”。
上面的代碼那個附加的參數p叫做方法的接收器(receiver),早期的面向對象語言留下的遺將調用一個方法稱爲“向一個對象發送消息”。
在Go語言中我們不會像其它語言那樣用this或者self作爲接收器我們可以任意的選擇接收器的名字。由於接收器的名字經常會被使用到所以保持其在方法間傳遞時的一性和簡短性是不錯的主意。這的建議是可以使用其類型的第一個字母,比如這使用了Point的首字母p。
在Go語言中我們不會像其它語言那樣用this或者self作爲接收器我們可以任意的選擇接收器的名字。由於接收器的名字經常會被使用到所以保持其在方法間傳遞時的一性和簡短性是不錯的主意。這的建議是可以使用其類型的第一個字母,比如這使用了Point的首字母p。
在方法調用過程中,接收器參數一般會在方法名之前齣現。這和方法聲明是一樣的,都是接收器參數在方法名字之前。下面是例子:
@@ -38,11 +38,11 @@ fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call
```
可以看到上面的兩個函數調用都是Distance但是卻沒有發生突。第一個Distance的調用實際上用的是包級的函數geometry.Distance而第二個則是使用剛剛聲明的Point調用的是Point類下聲明的Point.Distance方法。
可以看到上面的兩個函數調用都是Distance但是卻沒有發生突。第一個Distance的調用實際上用的是包級的函數geometry.Distance而第二個則是使用剛剛聲明的Point調用的是Point類下聲明的Point.Distance方法。
這種p.Distance的表達式叫做選擇器因爲他會選擇合適的對應p這個對象的Distance方法來執行。選擇器也會被用來選擇一個struct類型的字段比如p.X。由於方法和字段都是在同一命名空間所以如果我們在這聲明一個X方法的話編譯器會報錯因爲在調用p.X時會有歧義(譯註:這確實挺奇怪的)。
這種p.Distance的表達式叫做選擇器因爲他會選擇合適的對應p這個對象的Distance方法來執行。選擇器也會被用來選擇一個struct類型的字段比如p.X。由於方法和字段都是在同一命名空間所以如果我們在這聲明一個X方法的話編譯器會報錯因爲在調用p.X時會有歧義(譯註:這確實挺奇怪的)。
因爲每種類型都有其方法的命名空間我們在用Distance這個名字的時候不同的Distance調用指向了不同類型的Distance方法。讓我們來定義一個Path類型這個Path代表一個段的集合,且也給這個Path定義一個叫Distance的方法。
因爲每種類型都有其方法的命名空間我們在用Distance這個名字的時候不同的Distance調用指向了不同類型的Distance方法。讓我們來定義一個Path類型這個Path代表一個段的集合,且也給這個Path定義一個叫Distance的方法。
```Go
// A Path is a journey connecting the points with straight lines.
@@ -59,9 +59,9 @@ func (path Path) Distance() float64 {
}
```
Path是一個命名的slice類型而不是Point那樣的struct類型然而我們依然可以爲它定義方法。在能夠給任意類型定義方法這一點上Go和很多其它的面向對象的語言不太一樣。因此在Go語言我們爲一些簡單的數值、字符串、slice、map來定義一些附加行爲很方便。方法可以被聲明到任意類型隻要不是一個指或者一個interface。
Path是一個命名的slice類型而不是Point那樣的struct類型然而我們依然可以爲它定義方法。在能夠給任意類型定義方法這一點上Go和很多其它的面向對象的語言不太一樣。因此在Go語言我們爲一些簡單的數值、字符串、slice、map來定義一些附加行爲很方便。方法可以被聲明到任意類型隻要不是一個指或者一個interface。
兩個Distance方法有不同的類型。他們兩個方法之間沒有任何關繫管Path的Distance方法會在內部調用Point.Distance方法來計算每個連接鄰接點的段的長度。
兩個Distance方法有不同的類型。他們兩個方法之間沒有任何關繫管Path的Distance方法會在內部調用Point.Distance方法來計算每個連接鄰接點的段的長度。
讓我們來調用一個新方法,計算三角形的週長:
@@ -77,7 +77,7 @@ fmt.Println(perim.Distance()) // "12"
在上面兩個對Distance名字的方法的調用中編譯器會根據方法的名字以及接收器來決定具體調用的是哪一個函數。第一個例子中path[i-1]數組中的類型是Point因此Point.Distance這個方法被調用在第二個例子中perim的類型是Path因此Distance調用的是Path.Distance。
對於一個給定的類型,其內部的方法都必鬚有唯一的方法名,但是不同的類型卻可以有同樣的方法名,比如我們這Point和Path就都有Distance這個名字的方法所以我們沒有必要非在方法名之前加類型名來消除歧義比如PathDistance。這我們已經看到了方法比之函數的一些好處:方法名可以簡短。當我們在包外調用的時候這種好處就會被放大,因爲我們可以使用這個短名字,而可以省略掉包的名字,下面是例子:
對於一個給定的類型,其內部的方法都必鬚有唯一的方法名,但是不同的類型卻可以有同樣的方法名,比如我們這Point和Path就都有Distance這個名字的方法所以我們沒有必要非在方法名之前加類型名來消除歧義比如PathDistance。這我們已經看到了方法比之函數的一些好處:方法名可以簡短。當我們在包外調用的時候這種好處就會被放大,因爲我們可以使用這個短名字,而可以省略掉包的名字,下面是例子:
```Go
import "gopl.io/ch6/geometry"
@@ -87,4 +87,4 @@ fmt.Println(geometry.PathDistance(perim)) // "12", standalone function
fmt.Println(perim.Distance()) // "12", method of geometry.Path
```
譯註如果我們要用方法去計算perim的distance還需要去寫全geometry的包名和其函數名但是因爲Path這個變量定義了一個可以直接用的Distance方法所以我們可以直接寫perim.Distance()。相當於可以少打很多字作者應該是這個意思。因爲在Go包外調用函數需要帶上包名,還是挺麻煩的。
譯註如果我們要用方法去計算perim的distance還需要去寫全geometry的包名和其函數名但是因爲Path這個變量定義了一個可以直接用的Distance方法所以我們可以直接寫perim.Distance()。相當於可以少打很多字作者應該是這個意思。因爲在Go包外調用函數需要帶上包名,還是挺麻煩的。

View File

@@ -1,6 +1,6 @@
## 6.2. 基於指對象的方法
## 6.2. 基於指對象的方法
當調用一個函數時,會對其每一個參數值進行拷貝,如果一個函數需要更新一個變量,或者函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝,這種情況下我們就需要用到指了。對應到我們這用來更新接收器的對象的方法,當這個接受者變量本身比較大時,我們就可以用其指而不是對象來聲明方法,如下:
當調用一個函數時,會對其每一個參數值進行拷貝,如果一個函數需要更新一個變量,或者函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝,這種情況下我們就需要用到指了。對應到我們這用來更新接收器的對象的方法,當這個接受者變量本身比較大時,我們就可以用其指而不是對象來聲明方法,如下:
```go
func (p *Point) ScaleBy(factor float64) {
@@ -9,18 +9,18 @@ func (p *Point) ScaleBy(factor float64) {
}
```
這個方法的名字是`(*Point).ScaleBy`。這的括號是必鬚的;沒有括號的話這個表達式可能會被理解爲`*(Point.ScaleBy)`
這個方法的名字是`(*Point).ScaleBy`。這的括號是必鬚的;沒有括號的話這個表達式可能會被理解爲`*(Point.ScaleBy)`
在現實的程序一般會約定如果Point這個類有一個指作爲接收器的方法那麽所有Point的方法都必鬚有一個指接收器,卽使是那些不需要這個指接收器的函數。我們在這打破了這個約定隻是爲了展示一下兩種方法的異同而已。
在現實的程序一般會約定如果Point這個類有一個指作爲接收器的方法那麽所有Point的方法都必鬚有一個指接收器,卽使是那些不需要這個指接收器的函數。我們在這打破了這個約定隻是爲了展示一下兩種方法的異同而已。
隻有類型(Point)和指向他們的指(*Point),纔是可能會齣現在接收器聲明的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指的話,是不允許其齣現在接收器中的,比如下面這個例子:
隻有類型(Point)和指向他們的指(*Point),纔是可能會齣現在接收器聲明的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指的話,是不允許其齣現在接收器中的,比如下面這個例子:
```go
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
```
想要調用指類型方法`(*Point).ScaleBy`隻要提供一個Point類型的指卽可,像下面這樣。
想要調用指類型方法`(*Point).ScaleBy`隻要提供一個Point類型的指卽可,像下面這樣。
```go
r := &Point{1, 2}
@@ -43,18 +43,18 @@ p := Point{1, 2}
fmt.Println(p) // "{2, 4}"
```
不過後面兩種方法有些笨拙。運的是go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量且其方法需要一個Point指作爲接收器,我們可以用下面這種簡短的寫法:
不過後面兩種方法有些笨拙。運的是go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量且其方法需要一個Point指作爲接收器,我們可以用下面這種簡短的寫法:
```go
p.ScaleBy(2)
```
編譯器會隱式地幫我們用&p去調用ScaleBy這個方法。這種簡寫方法隻適用於“變量”包括struct的字段比如p.X以及array和slice內的元素比如perim[0]。我們不能通過一個無法取到地址的接收器來調用指方法,比如臨時變量的內存地址就無法取得到:
編譯器會隱式地幫我們用&p去調用ScaleBy這個方法。這種簡寫方法隻適用於“變量”包括struct的字段比如p.X以及array和slice內的元素比如perim[0]。我們不能通過一個無法取到地址的接收器來調用指方法,比如臨時變量的內存地址就無法取得到:
```go
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
```
但是我們可以用一個`*Point`這樣的接收器來調用Point的方法因爲我們可以通過地址來找到這個變量隻要用解引用符號`*`來取到該變量卽可。編譯器在這也會給我們隱式地插入`*`這個操作符,所以下面這兩種寫法等價的:
但是我們可以用一個`*Point`這樣的接收器來調用Point的方法因爲我們可以通過地址來找到這個變量隻要用解引用符號`*`來取到該變量卽可。編譯器在這也會給我們隱式地插入`*`這個操作符,所以下面這兩種寫法等價的:
```Go
pptr.Distance(q)
@@ -62,7 +62,7 @@ pptr.Distance(q)
```
Lets summarize these three cases again, since they are a frequent point of confusion. In every valid method call expression, exactly one of these three statements is true.
的幾個例子可能讓你有些睏惑,所以我們總結一下:在每一個合法的方法調用表達式中,也就是下面三種情況的任意一種情況都是可以的:
的幾個例子可能讓你有些睏惑,所以我們總結一下:在每一個合法的方法調用表達式中,也就是下面三種情況的任意一種情況都是可以的:
不論是接收器的實際參數和其接收器的形式參數相同比如兩者都是類型T或者都是類型`*T`
```go
@@ -75,19 +75,19 @@ pptr.ScaleBy(2) // *Point
p.ScaleBy(2) // implicit (&p)
```
或者接收器形參是類型`*T`實參是類型T。編譯器會隱式地爲我們解引用取到指指向的實際變量:
或者接收器形參是類型`*T`實參是類型T。編譯器會隱式地爲我們解引用取到指指向的實際變量:
```go
pptr.Distance(q) // implicit (*pptr)
```
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麽拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會生一個值的拷貝。比如time.Duration的這個類型在調用其方法時就會被全部拷貝一份包括在作爲參數傳入函數的時候。但是如果一個方法使用指作爲接收器你需要避免對其進行拷貝因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝那麽可能會引起原始對象和拷貝對象隻是名而已,但實際上其指向的對象是一的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麽拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會生一個值的拷貝。比如time.Duration的這個類型在調用其方法時就會被全部拷貝一份包括在作爲參數傳入函數的時候。但是如果一個方法使用指作爲接收器你需要避免對其進行拷貝因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝那麽可能會引起原始對象和拷貝對象隻是名而已,但實際上其指向的對象是一的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
譯註:作者這裡說的比較繞,其實有兩點:
1.不管你的method的receiver是指類型還是非指類型,都是可以通過指/非指類型進行調用的,編譯器會幫你做類型轉換
2.在聲明一個method的receiver該是指還是非指類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特大,如果聲明爲非指變量時,調用會生一次拷貝;第二方面是如果你用指類型作爲receiver那麽你一定要註意這種指類型指向的始終是一塊內存地址就算你對其進行了拷貝。熟悉C或者C艹的人這應該很快能明白。
譯註:作者這里説的比較繞,其實有兩點:
1.不管你的method的receiver是指類型還是非指類型,都是可以通過指/非指類型進行調用的,編譯器會幫你做類型轉換
2.在聲明一個method的receiver該是指還是非指類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特大,如果聲明爲非指變量時,調用會生一次拷貝;第二方面是如果你用指類型作爲receiver那麽你一定要註意這種指類型指向的始終是一塊內存地址就算你對其進行了拷貝。熟悉C或者C艹的人這應該很快能明白。
###6.2.1. Nil也是一個合法的接收器類型
就像一些函數允許nil指作爲參數一樣方法理論上也可以用nil指作爲其接收器尤其當nil對於對象來是合法的零值時比如map或者slice。在下面的簡單int鏈表的例子nil代表的是空鏈表
就像一些函數允許nil指作爲參數一樣方法理論上也可以用nil指作爲其接收器尤其當nil對於對象來是合法的零值時比如map或者slice。在下面的簡單int鏈表的例子nil代表的是空鏈表
```go
// An IntList is a linked list of integers.
@@ -105,9 +105,9 @@ func (list *IntList) Sum() int {
}
```
當你定義一個允許nil作爲接收器值的方法的類型時在類型前面的註釋中指齣nil變量代表的意義是很有必要的就像我們上面例子做的這樣。
當你定義一個允許nil作爲接收器值的方法的類型時在類型前面的註釋中指齣nil變量代表的意義是很有必要的就像我們上面例子做的這樣。
下面是net/url包Values類型定義的一部分。
下面是net/url包Values類型定義的一部分。
```go
@@ -131,7 +131,7 @@ func (v Values) Add(key, value string) {
}
```
這個定義向外部暴露了一個map的類型的變量且提供了一些能夠簡單操作這個map的方法。這個map的value字段是一個string的slice所以這個Values是一個多維map。客戶端使用這個變量的時候可以使用map固有的一些操作(make切片m[key]等等),也可以使用這提供的操作方法,或者兩者用,都是可以的:
這個定義向外部暴露了一個map的類型的變量且提供了一些能夠簡單操作這個map的方法。這個map的value字段是一個string的slice所以這個Values是一個多維map。客戶端使用這個變量的時候可以使用map固有的一些操作(make切片m[key]等等),也可以使用這提供的操作方法,或者兩者用,都是可以的:
```go
gopl.io/ch6/urlvalues
@@ -149,6 +149,6 @@ fmt.Println(m.Get("item")) // ""
m.Add("item", "3") // panic: assignment to entry in nil map
```
對Get的最後一次調用中nil接收器的行爲卽是一個空map的行爲。我們可以等價地將這個操作寫成Value(nil).Get("item")但是如果你直接寫nil.Get("item")的話是無法通過編譯的因爲nil的字面量編譯器無法判斷其準備類型。所以相比之下最後的那行m.Add的調用就會生一個panic因爲他試更新一個空map。
對Get的最後一次調用中nil接收器的行爲卽是一個空map的行爲。我們可以等價地將這個操作寫成Value(nil).Get("item")但是如果你直接寫nil.Get("item")的話是無法通過編譯的因爲nil的字面量編譯器無法判斷其準備類型。所以相比之下最後的那行m.Add的調用就會生一個panic因爲他試更新一個空map。
由於url.Values是一個map類型且間接引用了其key/value對因此url.Values.Add對這個map的元素做任何的更新、刪除操作對調用方都是可見的。實際上就像在普通函數中一樣雖然可以通過引用來操作內部值但在方法想要脩改引用本身是不會影響原始值的比如把他置爲nil或者讓這個引用指向了其它的對象調用方都不會受影響。(譯註因爲傳入的是存儲了內存地址的變量你改變這個變量是影響不了原始的變量的想想C語言是差不多的)
由於url.Values是一個map類型且間接引用了其key/value對因此url.Values.Add對這個map的元素做任何的更新、刪除操作對調用方都是可見的。實際上就像在普通函數中一樣雖然可以通過引用來操作內部值但在方法想要脩改引用本身是不會影響原始值的比如把他置爲nil或者讓這個引用指向了其它的對象調用方都不會受影響。(譯註因爲傳入的是存儲了內存地址的變量你改變這個變量是影響不了原始的變量的想想C語言是差不多的)

View File

@@ -11,7 +11,7 @@ type ColoredPoint struct {
}
```
我們完全可以將ColoredPoint定義爲一個有三個字段的struct但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式使其包含Point類型所具有的一切字段然後再定義一些自己的。如果我們想要的話我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段而完全不需要在調用時指齣Point比如下面這樣。
我們完全可以將ColoredPoint定義爲一個有三個字段的struct但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式使其包含Point類型所具有的一切字段然後再定義一些自己的。如果我們想要的話我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段而完全不需要在調用時指齣Point比如下面這樣。
```go
var cp ColoredPoint
@@ -21,7 +21,7 @@ cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
```
對於Point中的方法我們也有類似的用法我們可以把ColoredPoint類型當作接收器來調用Point的方法卽使ColoredPoint沒有聲明這些方法:
對於Point中的方法我們也有類似的用法我們可以把ColoredPoint類型當作接收器來調用Point的方法卽使ColoredPoint沒有聲明這些方法:
```go
red := color.RGBA{255, 0, 0, 255}
@@ -33,15 +33,15 @@ p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
```
Point類的方法也被引入了ColoredPoint。用這種方式內嵌可以使我們定義字段特多的雜類型,我們可以將字段先按小類型分組,然後定義小類型的方法,之後再把它們組合起來。
Point類的方法也被引入了ColoredPoint。用這種方式內嵌可以使我們定義字段特多的雜類型,我們可以將字段先按小類型分組,然後定義小類型的方法,之後再把它們組合起來。
讀者如果對基於類來實現面向對象的語言比較熟悉的話可能會傾向於將Point看作一個基類而ColoredPoint看作其子類或者繼承類或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型但q不是一個Point類所以管q有着Point這個內嵌類型我們也必鬚要顯式地選擇它。試直接傳q的話你會看到下面這樣的錯誤
讀者如果對基於類來實現面向對象的語言比較熟悉的話可能會傾向於將Point看作一個基類而ColoredPoint看作其子類或者繼承類或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型但q不是一個Point類所以管q有着Point這個內嵌類型我們也必鬚要顯式地選擇它。試直接傳q的話你會看到下面這樣的錯誤
```go
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
```
一個ColoredPoint不是一個Point但他"has a"Point且它有從Point類引入的Distance和ScaleBy方法。如果你喜歡從實現的角度來考慮問題內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法和下面的形式是等價的
一個ColoredPoint不是一個Point但他"has a"Point且它有從Point類引入的Distance和ScaleBy方法。如果你喜歡從實現的角度來考慮問題內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法和下面的形式是等價的
```go
func (p ColoredPoint) Distance(q Point) float64 {
@@ -53,9 +53,9 @@ func (p *ColoredPoint) ScaleBy(factor float64) {
}
```
當Point.Distance被第一個包裝方法調用時它的接收器值是p.Point而不是p當然了在Point類的方法你是訪問不到ColoredPoint的任何字段的。
當Point.Distance被第一個包裝方法調用時它的接收器值是p.Point而不是p當然了在Point類的方法你是訪問不到ColoredPoint的任何字段的。
在類型中內嵌的匿名字段也可能是一個命名類型的指,這種情況下字段和方法會被間接地引入到當前的類型中(譯註:訪問需要通過該指指向的對象去取)。添加這一層間接關繫讓我們可以共享通用的結構動態地改變對象之間的關繫。下面這個ColoredPoint的聲明內嵌了一個*Point的指
在類型中內嵌的匿名字段也可能是一個命名類型的指,這種情況下字段和方法會被間接地引入到當前的類型中(譯註:訪問需要通過該指指向的對象去取)。添加這一層間接關繫讓我們可以共享通用的結構動態地改變對象之間的關繫。下面這個ColoredPoint的聲明內嵌了一個*Point的指
```go
type ColoredPoint struct {
@@ -78,11 +78,11 @@ type ColoredPoint struct {
color.RGBA
}
```
然後這種類型的值便會擁有Point和RGBA類型的所有方法以及直接定義在ColoredPoint中的方法。當編譯器解析一個選擇器到方法時比如p.ScaleBy它會首先去找直接定義在這個類型的ScaleBy方法然後找被ColoredPoint的內嵌字段們引入的方法然後去找Point和RGBA的內嵌字段引入的方法然後一直遞歸向下找。如果選擇器有二義性的話編譯器會報錯比如你在同一級有兩個同名的方法。
然後這種類型的值便會擁有Point和RGBA類型的所有方法以及直接定義在ColoredPoint中的方法。當編譯器解析一個選擇器到方法時比如p.ScaleBy它會首先去找直接定義在這個類型的ScaleBy方法然後找被ColoredPoint的內嵌字段們引入的方法然後去找Point和RGBA的內嵌字段引入的方法然後一直遞歸向下找。如果選擇器有二義性的話編譯器會報錯比如你在同一級有兩個同名的方法。
方法隻能在命名類型(像Point)或者指向類型的指上定義但是多虧了內嵌有些時候我們給匿名struct類型來定義方法也有了手段。
方法隻能在命名類型(像Point)或者指向類型的指上定義但是多虧了內嵌有些時候我們給匿名struct類型來定義方法也有了手段。
下面是一個小trick。這個例子展示了簡單的cache其使用兩個包級的變量來實現一個mutex互斥量(§9.2)和它所操作的cache
下面是一個小trick。這個例子展示了簡單的cache其使用兩個包級的變量來實現一個mutex互斥量(§9.2)和它所操作的cache
```go
var (
@@ -98,7 +98,7 @@ func Lookup(key string) string {
}
```
下面這個版本在功能上是一但將兩個包級吧的變量放在了cache這個struct一組內
下面這個版本在功能上是一但將兩個包級吧的變量放在了cache這個struct一組內
```go
var cache = struct {
@@ -117,7 +117,7 @@ func Lookup(key string) string {
}
```
我們給新的變量起了一個更具表達性的名字cache。因爲sync.Mutex字段也被嵌入到了這個struct其Lock和Unlock方法也就都被引入到了這個匿名結構中了這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
我們給新的變量起了一個更具表達性的名字cache。因爲sync.Mutex字段也被嵌入到了這個struct其Lock和Unlock方法也就都被引入到了這個匿名結構中了這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。

View File

@@ -1,6 +1,6 @@
## 6.4. 方法值和方法表達式
我們經常選擇一個方法,且在同一個表達式執行比如常見的p.Distance()形式實際上將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”選擇器會返迴一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器卽可被調用;卽調用時不需要指定接收器(譯註:因爲已經在前文中指定過了),隻要傳入函數的參數卽可:
我們經常選擇一個方法,且在同一個表達式執行比如常見的p.Distance()形式實際上將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”選擇器會返迴一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器卽可被調用;卽調用時不需要指定接收器(譯註:因爲已經在前文中指定過了),隻要傳入函數的參數卽可:
```go
p := Point{1, 2}
@@ -16,7 +16,7 @@ scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)
```
在一個包的API需要一個函數值、且調用方希望操作的是某一個綁定了對象的方法的話方法"值"會非常實用(=_=眞是繞)。舉例來下面例子中的time.AfterFunc這個函數的功能是在指定的延遲時間之後來執行一個(譯註:另外的)函數。且這個函數操作的是一個Rocket對象r
在一個包的API需要一個函數值、且調用方希望操作的是某一個綁定了對象的方法的話方法"值"會非常實用(=_=眞是繞)。舉例來下面例子中的time.AfterFunc這個函數的功能是在指定的延遲時間之後來執行一個(譯註:另外的)函數。且這個函數操作的是一個Rocket對象r
```go
type Rocket struct { /* ... */ }
@@ -30,7 +30,7 @@ time.AfterFunc(10 * time.Second, func() { r.Launch() })
```go
time.AfterFunc(10 * time.Second, r.Launch)
```
譯註:省掉了上面那個例子的匿名函數。
譯註:省掉了上面那個例子的匿名函數。
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
@@ -41,7 +41,7 @@ p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
//譯註這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance()但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數卽其需要用第一個額外參數指定接收器後面排列Distance方法的參數。看起來本書中函數和方法的區是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
//譯註這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance()但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數卽其需要用第一個額外參數指定接收器後面排列Distance方法的參數。看起來本書中函數和方法的區是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

View File

@@ -1,8 +1,8 @@
## 6.5. 示例: Bit數組
Go語言的集合一般會用map[T]bool這種形式來表示T代表元素類型。集合用map類型來表示雖然非常靈活但我們可以以一種更好的形式來表示它。例如在數據流分析領域集合元素通常是一個非負整數集合會包含很多元素且集合會經常進行集、交集操作這種情況下bit數組會比map表現更加理想。(譯註:這再補充一個例子比如我們執行一個http下載任務把文件按照16kb一塊劃分爲很多塊需要有一個全變量來標識哪些塊下載完成了這種時候也需要用到bit數組)
Go語言的集合一般會用map[T]bool這種形式來表示T代表元素類型。集合用map類型來表示雖然非常靈活但我們可以以一種更好的形式來表示它。例如在數據流分析領域集合元素通常是一個非負整數集合會包含很多元素且集合會經常進行集、交集操作這種情況下bit數組會比map表現更加理想。(譯註:這再補充一個例子比如我們執行一個http下載任務把文件按照16kb一塊劃分爲很多塊需要有一個全變量來標識哪些塊下載完成了這種時候也需要用到bit數組)
一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示每一個元素的每一位都表示集合的一個值。當集合的第i位被設置時我們纔這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型且實現了三個函數來對這個bit數組來進行操作
一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示每一個元素的每一位都表示集合的一個值。當集合的第i位被設置時我們纔這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型且實現了三個函數來對這個bit數組來進行操作
```go
gopl.io/ch6/intset
@@ -39,9 +39,9 @@ func (s *IntSet) UnionWith(t *IntSet) {
}
```
因爲每一個字都有64個二進製位所以爲了定位x的bit位我們用了x/64的商作爲字的下標且用x%64得到的值作爲這個字內的bit的所在位置。UnionWith這個方法用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。(在練習6.5中我們還會程序用到這個64位字的例子。)
因爲每一個字都有64個二進製位所以爲了定位x的bit位我們用了x/64的商作爲字的下標且用x%64得到的值作爲這個字內的bit的所在位置。UnionWith這個方法用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。(在練習6.5中我們還會程序用到這個64位字的例子。)
當前這個實現還缺少了很多必要的特性我們把其中一些作爲練習題列在本小節之後。但是有一個方法如果缺失的話我們的bit數組可能會比較難混將IntSet作爲一個字符串來打印。這我們來實現它讓我們來給上面的例子添加一個String方法類似2.5節中做的那樣:
當前這個實現還缺少了很多必要的特性我們把其中一些作爲練習題列在本小節之後。但是有一個方法如果缺失的話我們的bit數組可能會比較難混將IntSet作爲一個字符串來打印。這我們來實現它讓我們來給上面的例子添加一個String方法類似2.5節中做的那樣:
```go
// String returns the set as a string of the form "{1 2 3}".
@@ -66,7 +66,7 @@ func (s *IntSet) String() string {
}
```
留意一下String方法是不是和3.5.4節中的intsToString方法很相似bytes.Buffer在String方法經常這麽用。當你爲一個雜的類型定義了一個String方法時fmt包就會特殊對待這種類型的值這樣可以讓這些類型在打印的時候看起來更加友好而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言在第7章中我們會詳細介紹。
留意一下String方法是不是和3.5.4節中的intsToString方法很相似bytes.Buffer在String方法經常這麽用。當你爲一個雜的類型定義了一個String方法時fmt包就會特殊對待這種類型的值這樣可以讓這些類型在打印的時候看起來更加友好而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言在第7章中我們會詳細介紹。
現在我們就可以在實戰中直接用上面定義好的IntSet了
```go
@@ -85,13 +85,13 @@ fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x.Has(9), x.Has(123)) // "true false"
```
要註意我們聲明的String和Has兩個方法都是以指類型*IntSet來作爲接收器的但實際上對於這兩個類型來,把接收器聲明爲指類型也沒什麽必要。不過另外兩個函數就不是這樣了因爲另外兩個函數操作的是s.words對象如果你不把接收器聲明爲指對象那麽實際操作的是拷貝對象而不是原來的那個對象。因此因爲我們的String方法定義在IntSet指所以當我們的變量是IntSet類型而不是IntSet指時,可能會有下面這樣讓人意外的情況:
要註意我們聲明的String和Has兩個方法都是以指類型*IntSet來作爲接收器的但實際上對於這兩個類型來,把接收器聲明爲指類型也沒什麽必要。不過另外兩個函數就不是這樣了因爲另外兩個函數操作的是s.words對象如果你不把接收器聲明爲指對象那麽實際操作的是拷貝對象而不是原來的那個對象。因此因爲我們的String方法定義在IntSet指所以當我們的變量是IntSet類型而不是IntSet指時,可能會有下面這樣讓人意外的情況:
```go
fmt.Println(&x) // "{1 9 42 144}"
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"
```
在第一個Println中我們打印一個*IntSet的指,這個類型的指確實有自定義的String方法。第二Println我們直接調用了x變量的String()方法這種情況下編譯器會隱式地在x前插入&操作符這樣相當遠我們還是調用的IntSet指的String方法。在第三個Println中因爲IntSet類型沒有String方法所以Println方法會直接以原始的方式理解打印。所以在這種情況下&符號是不能忘的。在我們這種場景下你把String方法綁定到IntSet對象上而不是IntSet指上可能會更合適一些,不過這也需要具體問題具體分析。
在第一個Println中我們打印一個*IntSet的指,這個類型的指確實有自定義的String方法。第二Println我們直接調用了x變量的String()方法這種情況下編譯器會隱式地在x前插入&操作符這樣相當遠我們還是調用的IntSet指的String方法。在第三個Println中因爲IntSet類型沒有String方法所以Println方法會直接以原始的方式理解打印。所以在這種情況下&符號是不能忘的。在我們這種場景下你把String方法綁定到IntSet對象上而不是IntSet指上可能會更合適一些,不過這也需要具體問題具體分析。
練習6.1: 爲bit數組實現下面這些方法
```go
@@ -103,7 +103,7 @@ func (*IntSet) Copy() *IntSet // return a copy of the set
練習6.2: 定義一個變參方法(*IntSet).AddAll(...int)這個方法可以爲一組IntSet值求和比如s.AddAll(1,2,3)。
練習6.3: (*IntSet).UnionWith會用|操作符計算兩個集合的交集我們再爲IntSet實現另外的幾個函數IntersectWith(交集元素在A集合B集合均齣現),DifferenceWith(差集元素齣現在A集合未齣現在B集合),SymmetricDifference(差集元素齣現在A但沒有齣現在B或者齣現在B沒有齣現在A)。
練習6.4: 實現一個Elems方法返迴集合中的所有元素用於做一些range之類的遍操作。
練習6.3: (*IntSet).UnionWith會用|操作符計算兩個集合的交集我們再爲IntSet實現另外的幾個函數IntersectWith(交集元素在A集合B集合均齣現),DifferenceWith(差集元素齣現在A集合未齣現在B集合),SymmetricDifference(差集元素齣現在A但沒有齣現在B或者齣現在B沒有齣現在A)。
練習6.4: 實現一個Elems方法返迴集合中的所有元素用於做一些range之類的遍操作。
練習6.5: 我們這章定義的IntSet的每個字都是用的uint64類型但是64位的數值可能在32位的平上不高效。脩改程序使其使用uint類型這種類型對於32位平檯來說更合適。當然了,這我們可以不用簡單粗暴地除64可以定義一個常量來決定是用32還是64你可能會用到平的自動判斷的一個智能表達式32 << (^uint(0) >> 63)
練習6.5: 我們這章定義的IntSet的每個字都是用的uint64類型但是64位的數值可能在32位的平上不高效。脩改程序使其使用uint類型這種類型對於32位平颱來説更合適。當然了,這我們可以不用簡單粗暴地除64可以定義一個常量來決定是用32還是64你可能會用到平的自動判斷的一個智能表達式32 << (^uint(0) >> 63)

View File

@@ -4,27 +4,27 @@
Go語言隻有一種控製可見性的手段大寫首字母的標識符會從定義它們的包中被導齣小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象我們必鬚將其定義爲一個struct。
這也就是前面的小節中IntSet被定義爲struct類型的原因管它隻有一個字段:
這也就是前面的小節中IntSet被定義爲struct類型的原因管它隻有一個字段:
```go
type IntSet struct {
words []uint64
}
```
當然我們也可以把IntSet定義爲一個slice類型管這樣我們就需要把代碼中所有方法用到的s.words用*s替換掉了
當然我們也可以把IntSet定義爲一個slice類型管這樣我們就需要把代碼中所有方法用到的s.words用*s替換掉了
```go
type IntSet []uint64
```
管這個版本的IntSet在本質上是一樣的他也可以允許其它包中可以直接讀取編輯這個slice。換句話,相對*s這個表達式會齣現在所有的包中s.words隻需要在定義IntSet的包中齣現(譯註:所以還是推薦後者吧的意思)。
管這個版本的IntSet在本質上是一樣的他也可以允許其它包中可以直接讀取編輯這個slice。換句話,相對*s這個表達式會齣現在所有的包中s.words隻需要在定義IntSet的包中齣現(譯註:所以還是推薦後者吧的意思)。
這種基於名字的手段使得在語言中最小的封裝單元是package而不是像其它語言一樣的類型。一個struct類型的字段對同一個包的所有代碼都有可見性無論你的代碼是寫在一個函數還是一個方法
這種基於名字的手段使得在語言中最小的封裝單元是package而不是像其它語言一樣的類型。一個struct類型的字段對同一個包的所有代碼都有可見性無論你的代碼是寫在一個函數還是一個方法
封裝提供了三方面的優點。首先,因爲調用方不能直接脩改對象的變量值,其隻需要關註少量的語句且隻要弄懂少量變量的可能的值卽可。
封裝提供了三方面的優點。首先,因爲調用方不能直接脩改對象的變量值,其隻需要關註少量的語句且隻要弄懂少量變量的可能的值卽可。
第二隱藏實現的細節可以防止調用方依賴那些可能變化的具體實現這樣使設計包的程序員在不破壞對外的api情況下能得到更大的自由。
把bytes.Buffer這個類型作爲例子來考慮。這個類型在做短字符串疊加的時候很常用所以在設計的時候可以做一些預先的優化比如提前預留一部分空間來避免反的內存分配。又因爲Buffer是一個struct類型這些額外的空間可以用附加的字節數組來保存且放在一個小寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提,但不會得到這個附加變量。Buffer和其增長算法我們列在這,爲了簡潔性稍微做了一些精簡:
把bytes.Buffer這個類型作爲例子來考慮。這個類型在做短字符串疊加的時候很常用所以在設計的時候可以做一些預先的優化比如提前預留一部分空間來避免反的內存分配。又因爲Buffer是一個struct類型這些額外的空間可以用附加的字節數組來保存且放在一個小寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提,但不會得到這個附加變量。Buffer和其增長算法我們列在這,爲了簡潔性稍微做了一些精簡:
```go
type Buffer struct {
@@ -48,7 +48,7 @@ func (b *Buffer) Grow(n int) {
}
```
封裝的第三個優點也是最重要的優點是阻止了外部調用方對對象內部的值任意地進行脩改。因爲對象內部變量隻可以被同一個包內的函數脩改所以包的作者可以讓這些函數確保對象內部的一些值的不變性。比如下面的Counter類型允許調用方來增加counter變量的值且允許將這個值reset爲0但是不允許隨便設置這個值(譯註:因爲壓根就訪問不到)
封裝的第三個優點也是最重要的優點是阻止了外部調用方對對象內部的值任意地進行脩改。因爲對象內部變量隻可以被同一個包內的函數脩改所以包的作者可以讓這些函數確保對象內部的一些值的不變性。比如下面的Counter類型允許調用方來增加counter變量的值且允許將這個值reset爲0但是不允許隨便設置這個值(譯註:因爲壓根就訪問不到)
```go
type Counter struct { n int }
@@ -57,7 +57,7 @@ func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset() { c.n = 0 }
```
隻用來訪問或脩改內部變量的函數被稱爲setter或者getter例子如下比如log包的Logger類型對應的一些函數。在命名一個getter方法時我們通常會省略掉前面的Get前綴。這種簡潔上的偏好也可以推廣到各種類型的前綴比如FetchFind或者Lookup。
隻用來訪問或脩改內部變量的函數被稱爲setter或者getter例子如下比如log包的Logger類型對應的一些函數。在命名一個getter方法時我們通常會省略掉前面的Get前綴。這種簡潔上的偏好也可以推廣到各種類型的前綴比如FetchFind或者Lookup。
```go
package log
@@ -72,9 +72,9 @@ func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)
```
Go的編碼風格不禁止直接導齣字段。當然一旦進行了導齣就沒有辦法在保API兼容的情況下去除對其的導齣所以在一開始的選擇一定要經過深思熟慮且要考慮到包內部的一些不變量的保,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
Go的編碼風格不禁止直接導齣字段。當然一旦進行了導齣就沒有辦法在保API兼容的情況下去除對其的導齣所以在一開始的選擇一定要經過深思熟慮且要考慮到包內部的一些不變量的保,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
封裝不總是理想的。
封裝不總是理想的。
雖然封裝在有些情況是必要的但有時候我們也需要暴露一些內部內容比如time.Duration將其表現暴露爲一個int64數字的納秒使得我們可以用一般的數值操作來對時間進行對比甚至可以定義這種類型的常量
```go
@@ -82,8 +82,8 @@ const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"
```
另一個例子將IntSet和本章開頭的geometry.Path進行對比。Path被定義爲一個slice類型這允許其調用slice的字面方法來對其內部的points用range進行迭代遍在這一點上IntSet是沒有辦法讓你這麽做的。
另一個例子將IntSet和本章開頭的geometry.Path進行對比。Path被定義爲一個slice類型這允許其調用slice的字面方法來對其內部的points用range進行迭代遍在這一點上IntSet是沒有辦法讓你這麽做的。
這兩種類型決定性的不同geometry.Path的本質是一個坐標點的序列不多也不少我們可以預見到之後也不會給他增加額外的字段所以在geometry包中將Path暴露爲一個slice。相比之下IntSet僅僅是在這用了一個[]uint64的slice。這個類型還可以用[]uint類型來表示或者我們甚至可以用其它完全不同的用更小內存空間的東西來表示這個集合所以我們可能還會需要額外的字段來在這個類型中記録元素的個數。也正是因爲這些原因我們讓IntSet對調用方透明。
這兩種類型決定性的不同geometry.Path的本質是一個坐標點的序列不多也不少我們可以預見到之後也不會給他增加額外的字段所以在geometry包中將Path暴露爲一個slice。相比之下IntSet僅僅是在這用了一個[]uint64的slice。這個類型還可以用[]uint類型來表示或者我們甚至可以用其它完全不同的用更小內存空間的東西來表示這個集合所以我們可能還會需要額外的字段來在這個類型中記録元素的個數。也正是因爲這些原因我們讓IntSet對調用方透明。
在這章中,我們學到了如何將方法與命名類型進行組合,且知道了如何調用這些方法。管方法對於OOP編程來至關重要但他們隻是OOP編程的半邊天。爲了完成OOP我們還需要接口。Go的接口會在下一章中介紹。
在這章中,我們學到了如何將方法與命名類型進行組合,且知道了如何調用這些方法。管方法對於OOP編程來至關重要但他們隻是OOP編程的半邊天。爲了完成OOP我們還需要接口。Go的接口會在下一章中介紹。

View File

@@ -1,8 +1,8 @@
# 第六章 方法
從90年代早期開始面向對象編程(OOP)就成爲了稱霸工程界和敎育界的編程所以之後幾乎所有大規模被應用的語言都包含了對OOP的支持go語言也不例外。
從90年代早期開始面向對象編程(OOP)就成爲了稱霸工程界和敎育界的編程所以之後幾乎所有大規模被應用的語言都包含了對OOP的支持go語言也不例外。
管沒有被大衆所接受的明確的OOP的定義從我們的理解來講一個對象其實也就是一個簡單的值或者一個變量在這個對象中會包含一些方法而一個方法則是一個一個和特殊類型關聯的函數。一個面向對象的程序會用方法來表達其屬性和對應的操作這樣使用這個對象的用戶就不需要直接去操作對象而是借助方法來做這些事情。
管沒有被大衆所接受的明確的OOP的定義從我們的理解來講一個對象其實也就是一個簡單的值或者一個變量在這個對象中會包含一些方法而一個方法則是一個一個和特殊類型關聯的函數。一個面向對象的程序會用方法來表達其屬性和對應的操作這樣使用這個對象的用戶就不需要直接去操作對象而是借助方法來做這些事情。
在早些的章節中我們已經使用了標準庫提供的一些方法比如time.Duration這個類型的Seconds方法
@@ -11,7 +11,7 @@
fmt.Println(day.Seconds()) // "86400"
```
且在2.5節中我們定義了一個自己的方法Celsius類型的String方法:
且在2.5節中我們定義了一個自己的方法Celsius類型的String方法:
```go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }