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

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

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方法也就都被引入到了這個匿名結構中了這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。

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來調用對應的方法

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

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

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編程的兩個關鍵點封裝和組合。