mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
fmr code
This commit is contained in:
@@ -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)
|
||||
```
|
||||
|
||||
Let’s 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
|
||||
|
||||
@@ -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方法也就都被引入到了這個匿名結構中了,這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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來調用對應的方法:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
ch6/ch6.md
11
ch6/ch6.md
@@ -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編程的兩個關鍵點,封裝和組合。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user