This commit is contained in:
chai2010
2016-01-02 21:17:21 +08:00
parent 8772a9c000
commit ba03c527c0
17 changed files with 63 additions and 29 deletions
+5 -2
View File
@@ -44,6 +44,7 @@ fmt.Println(p) // "{2, 4}"
```
不過後面兩種方法有些笨拙。幸運的是,go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量,併且其方法需要一個Point指針作爲接收器,我們可以用下面這種簡短的寫法:
```go
p.ScaleBy(2)
```
@@ -61,21 +62,23 @@ pptr.Distance(q)
(*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
Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
```
或者接收器形參是類型T,但接收器實參是類型`*T`,這種情況下編譯器會隱式地爲我們取變量的地址:
```go
p.ScaleBy(2) // implicit (&p)
```
或者接收器形參是類型`*T`,實參是類型T。編譯器會隱式地爲我們解引用,取到指針指向的實際變量:
```go
pptr.Distance(q) // implicit (*pptr)
```
@@ -83,6 +86,7 @@ pptr.Distance(q) // implicit (*pptr)
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麽拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會産生一個值的拷貝。比如time.Duration的這個類型,在調用其方法時就會被全部拷貝一份,包括在作爲參數傳入函數的時候。但是如果一個方法使用指針作爲接收器,你需要避免對其進行拷貝,因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝,那麽可能會引起原始對象和拷貝對象隻是别名而已,但實際上其指向的對象是一致的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
譯註:作者這里説的比較繞,其實有兩點:
1.不管你的method的receiver是指針類型還是非指針類型,都是可以通過指針/非指針類型進行調用的,編譯器會幫你做類型轉換
2.在聲明一個method的receiver該是指針還是非指針類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特别大,如果聲明爲非指針變量時,調用會産生一次拷貝;第二方面是如果你用指針類型作爲receiver,那麽你一定要註意,這種指針類型指向的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艹的人這里應該很快能明白。
@@ -109,7 +113,6 @@ func (list *IntList) Sum() int {
下面是net/url包里Values類型定義的一部分。
```go
net/url
package url
+3 -3
View File
@@ -1,6 +1,7 @@
## 6.3. 通過嵌入結構體來擴展類型
來看看ColoredPoint這個類型:
```go
gopl.io/ch6/coloredpoint
import "image/color"
@@ -33,6 +34,7 @@ p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
```
Point類的方法也被引入了ColoredPoint。用這種方式,內嵌可以使我們定義字段特别多的複雜類型,我們可以將字段先按小類型分組,然後定義小類型的方法,之後再把它們組合起來。
讀者如果對基於類來實現面向對象的語言比較熟悉的話,可能會傾向於將Point看作一個基類,而ColoredPoint看作其子類或者繼承類,或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型,但q併不是一個Point類,所以盡管q有着Point這個內嵌類型,我們也必鬚要顯式地選擇它。嚐試直接傳q的話你會看到下面這樣的錯誤:
@@ -72,6 +74,7 @@ fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
```
一個struct類型也可能會有多個匿名字段。我們將ColoredPoint定義爲下面這樣:
```go
type ColoredPoint struct {
Point
@@ -118,6 +121,3 @@ func Lookup(key string) string {
```
我們給新的變量起了一個更具表達性的名字:cache。因爲sync.Mutex字段也被嵌入到了這個struct里,其Lock和Unlock方法也就都被引入到了這個匿名結構中了,這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
+11 -5
View File
@@ -6,10 +6,11 @@
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", ;5
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
@@ -30,6 +31,7 @@ time.AfterFunc(10 * time.Second, func() { r.Launch() })
```go
time.AfterFunc(10 * time.Second, r.Launch)
```
譯註:省掉了上面那個例子里的匿名函數。
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
@@ -41,7 +43,6 @@ p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
//譯註:這個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"
@@ -49,6 +50,11 @@ scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
// 譯註:這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance()
// 但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數,
// 卽其需要用第一個額外參數指定接收器,後面排列Distance方法的參數。
// 看起來本書中函數和方法的區别是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
```
當你根據一個變量來決定調用同一個類型的哪個函數時,方法表達式就顯得很有用了。你可以根據選擇來調用接收器各不相同的方法。下面的例子,變量op代表Point類型的addition或者subtraction方法,Path.TranslateBy方法會爲其Path數組中的每一個Point來調用對應的方法:
+4
View File
@@ -69,6 +69,7 @@ func (s *IntSet) String() string {
這里留意一下String方法,是不是和3.5.4節中的intsToString方法很相似;bytes.Buffer在String方法里經常這麽用。當你爲一個複雜的類型定義了一個String方法時,fmt包就會特殊對待這種類型的值,這樣可以讓這些類型在打印的時候看起來更加友好,而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言,在第7章中我們會詳細介紹。
現在我們就可以在實戰中直接用上面定義好的IntSet了:
```go
var x, y IntSet
x.Add(1)
@@ -86,14 +87,17 @@ fmt.Println(x.Has(9), x.Has(123)) // "true false"
```
這里要註意:我們聲明的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指針上可能會更合適一些,不過這也需要具體問題具體分析。
練習6.1: 爲bit數組實現下面這些方法
```go
func (*IntSet) Len() int // return the number of elements
func (*IntSet) Remove(x int) // remove x from the set
+2 -1
View File
@@ -5,6 +5,7 @@
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
這也就是前面的小節中IntSet被定義爲struct類型的原因,盡管它隻有一個字段:
```go
type IntSet struct {
words []uint64
@@ -12,6 +13,7 @@ type IntSet struct {
```
當然,我們也可以把IntSet定義爲一個slice類型,盡管這樣我們就需要把代碼中所有方法里用到的s.words用*s替換掉了:
```go
type IntSet []uint64
```
@@ -35,7 +37,6 @@ type Buffer struct {
// Grow expands the buffer's capacity, if necessary,
// to guarantee space for another n bytes. [...]
func (b *Buffer) Grow(n int) {
if b.buf == nil {
b.buf = b.initial[:0] // use preallocated space initially
+5 -6
View File
@@ -6,16 +6,15 @@
在早些的章節中,我們已經使用了標準庫提供的一些方法,比如time.Duration這個類型的Seconds方法:
```
const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"
```Go
const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"
```
併且在2.5節中,我們定義了一個自己的方法,Celsius類型的String方法:
```go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
```Go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
```
在本章中,OOP編程的第一方面,我們會向你展示如何有效地定義和使用方法。我們會覆蓋到OOP編程的兩個關鍵點,封裝和組合。