<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 10.6 方法 ## 10.6.1 方法是什么 在 Go 語言中,結構體就像是類的一種簡化形式,那么面向對象程序員可能會問:類的方法在哪里呢?在 Go 中有一個概念,它和方法有著同樣的名字,并且大體上意思相同:Go 方法是作用在接收者(receiver)上的一個函數,接收者是某種類型的變量。因此方法是一種特殊類型的函數。 接收者類型可以是(幾乎)任何類型,不僅僅是結構體類型:任何類型都可以有方法,甚至可以是函數類型,可以是 int、bool、string 或數組的別名類型。但是接收者不能是一個接口類型(參考 第 11 章),因為接口是一個抽象定義,但是方法卻是具體實現;如果這樣做會引發一個編譯錯誤:**invalid receiver type…**。 最后接收者不能是一個指針類型,但是它可以是任何其他允許類型的指針。 一個類型加上它的方法等價于面向對象中的一個類。一個重要的區別是:在 Go 中,類型的代碼和綁定在它上面的方法的代碼可以不放置在一起,它們可以存在在不同的源文件,唯一的要求是:它們必須是同一個包的。 類型 T(或 *T)上的所有方法的集合叫做類型 T(或* T)的方法集。 因為方法是函數,所以同樣的,不允許方法重載,即對于一個類型只能有一個給定名稱的方法。但是如果基于接收者類型,是有重載的:具有同樣名字的方法可以在 2 個或多個不同的接收者類型上存在,比如在同一個包里這么做是允許的: ``` func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix ``` 別名類型不能有它原始類型上已經定義過的方法。 定義方法的一般格式如下: ``` func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } ``` 在方法名之前,`func` 關鍵字之后的括號中指定 receiver。 如果 `recv` 是 receiver 的實例,Method1 是它的方法名,那么方法調用遵循傳統的 `object.name` 選擇器符號:**recv.Method1()**。 如果 `recv` 一個指針,Go 會自動解引用。 如果方法不需要使用 `recv` 的值,可以用 **\_** 替換它,比如: ``` func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... } ``` `recv` 就像是面向對象語言中的 `this` 或 `self`,但是 Go 中并沒有這兩個關鍵字。隨個人喜好,你可以使用 `this` 或`self` 作為 receiver 的名字。下面是一個結構體上的簡單方法的例子: 示例 10.10 method .go: ``` package main import "fmt" type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("The sum is: %d\n", two1.AddThem()) fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20)) two2 := TwoInts{3, 4} fmt.Printf("The sum is: %d\n", two2.AddThem()) } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } func (tn *TwoInts) AddToParam(param int) int { return tn.a + tn.b + param } ``` 輸出: ``` The sum is: 22 Add them to the param: 42 The sum is: 7 ``` 下面是非結構體類型上方法的例子: 示例 10.11 method2.go: ``` package main import "fmt" type IntVector []int func (v IntVector) Sum() (s int) { for _, x := range v { s += x } return } func main() { fmt.Println(IntVector{1, 2, 3}.Sum()) // 輸出是6 } ``` **練習 10.6** employee\_salary.go 定義結構體 `employee`,它有一個 `salary` 字段,給這個結構體定義一個方法 `giveRaise` 來按照指定的百分比增加薪水。 **練習 10.7** iteration\_list.go 下面這段代碼有什么錯? ``` package main import "container/list" func (p *list.List) Iter() { // ... } func main() { lst := new(list.List) for _= range list.Iter() { } } ``` 類型和作用在它上面定義的方法必須在同一個包里定義,這就是為什么不能在 int、float 或類似這些的類型上定義方法。試圖在 int 類型上定義方法會得到一個編譯錯誤: ``` cannot define new methods on non-local type int ``` 比如想在 `time.Time` 上定義如下方法: ``` func (t time.Time) first3Chars() string { return time.LocalTime().String()[0:3] } ``` 類型在在其他的,或是非本地的包里定義,在它上面定義方法都會得到和上面同樣的錯誤。 但是有一個繞點的方式:可以先定義該類型(比如:int 或 float)的別名類型,然后再為別名類型定義方法。或者像下面這樣將它作為匿名類型嵌入在一個新的結構體中。當然方法只在這個別名類型上有效。 示例 10.12 method\_on\_time.go: ``` package main import ( "fmt" "time" ) type myTime struct { time.Time //anonymous field } func (t myTime) first3Chars() string { return t.Time.String()[0:3] } func main() { m := myTime{time.Now()} // 調用匿名Time上的String方法 fmt.Println("Full time now:", m.String()) // 調用myTime.first3Chars fmt.Println("First 3 chars:", m.first3Chars()) } /* Output: Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011 First 3 chars: Mon */ ``` ## 10.6.2 函數和方法的區別 函數將變量作為參數:**Function1(recv)** 方法在變量上被調用:**recv.Method1()** 在接收者是指針時,方法可以改變接收者的值(或狀態),這點函數也可以做到(當參數作為指針傳遞,即通過引用調用時,函數也可以改變參數的狀態)。 **不要忘記 Method1 后邊的括號 (),否則會引發編譯器錯誤:`method recv.Method1 is not an expression, must be called`** 接收者必須有一個顯式的名字,這個名字必須在方法中被使用。 **receiver\_type** 叫做 **(接收者)基本類型**,這個類型必須在和方法同樣的包中被聲明。 在 Go 中,(接收者)類型關聯的方法不寫在類型結構里面,就像類那樣;耦合更加寬松;類型和方法之間的關聯由接收者來建立。 **方法沒有和數據定義(結構體)混在一起:它們是正交的類型;表示(數據)和行為(方法)是獨立的。** ## 10.6.3 指針或值作為接收者 鑒于性能的原因,`recv` 最常見的是一個指向 receiver\_type 的指針(因為我們不想要一個實例的拷貝,如果按值調用的話就會是這樣),特別是在 receiver 類型是結構體時,就更是如此了。 如果想要方法改變接收者的數據,就在接收者的指針類型上定義該方法。否則,就在普通的值類型上定義方法。 下面的例子 `pointer_value.go` 作了說明:`change()`接受一個指向 B 的指針,并改變它內部的成員;`write()` 接受通過拷貝接受 B 的值并只輸出B的內容。注意 Go 為我們做了探測工作,我們自己并沒有指出是是否在指針上調用方法,Go 替我們做了這些事情。b1 是值而 b2 是指針,方法都支持運行了。 示例 10.13 pointer\_value.go: ``` package main import ( "fmt" ) type B struct { thing int } func (b *B) change() { b.thing = 1 } func (b B) write() string { return fmt.Sprint(b) } func main() { var b1 B // b1是值 b1.change() fmt.Println(b1.write()) b2 := new(B) // b2是指針 b2.change() fmt.Println(b2.write()) } /* 輸出: {1} {1} */ ``` 試著在 `write()` 中改變接收者b的值:將會看到它可以正常編譯,但是開始的 b 沒有被改變。 我們知道方法不需要指針作為接收者,如下面的例子,我們只是需要 `Point3` 的值來做計算: ``` type Point3 struct { x, y, z float } // A method on Point3 func (p Point3) Abs float { return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z) } ``` 這樣做稍微有點昂貴,因為 `Point3` 是作為值傳遞給方法的,因此傳遞的是它的拷貝,這在 Go 中合法的。也可以在指向這個類型的指針上調用此方法(會自動解引用)。 假設 `p3` 定義為一個指針:`p3 := &Point{ 3, 4, 5}`。 可以使用 `p3.Abs()` 來替代 `(*p3).Abs()`。 像例子 10.11(method1.go)中接收者類型是 `*TwoInts` 的方法 `AddThem()`,它能在類型 `TwoInts` 的值上被調用,這是自動間接發生的。 因此 `two2.AddThem` 可以替代 `(&two2).AddThem()`。 在值和指針上調用方法: 可以有連接到類型的方法,也可以有連接到類型指針的方法。 但是這沒關系:對于類型 T,如果在 \*T 上存在方法 `Meth()`,并且 `t` 是這個類型的變量,那么 `t.Meth()` 會被自動轉換為 `(&t).Meth()`。 **指針方法和值方法都可以在指針或非指針上被調用**,如下面程序所示,類型 `List` 在值上有一個方法 `Len()`,在指針上有一個方法 `Append()`,但是可以看到兩個方法都可以在兩種類型的變量上被調用。 示例 10.14 methodset1.go: ``` package main import ( "fmt" ) type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } func main() { // 值 var lst List lst.Append(1) fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1) // 指針 plst := new(List) plst.Append(2) fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1) } ``` ## 10.6.4 方法和未導出字段 考慮 `person2.go` 中的 `person` 包:類型 `Person` 被明確的導出了,但是它的字段沒有被導出。例如在 `use_person2.go`中 `p.firsetname` 就是錯誤的。該如何在另一個程序中修改或者只是讀取一個 `Person` 的名字呢? 這可以通過面向對象語言一個眾所周知的技術來完成:提供 getter 和 setter 方法。對于 setter 方法使用 Set 前綴,對于 getter 方法只適用成員名。 示例 10.15 person2.go: ``` package person type Person struct { firstName string lastName string } func (p *Person) FirstName() string { return p.firstName } func (p *Person) SetFirstName(newName string) { p.firstName = newName } ``` 示例 10.16—use\_person2.go: ``` package main import ( "./person" "fmt" ) func main() { p := new(person.Person) // p.firstName undefined // (cannot refer to unexported field or method firstName) // p.firstName = "Eric" p.SetFirstName("Eric") fmt.Println(p.FirstName()) // Output: Eric } ``` **并發訪問對象** 對象的字段(屬性)不應該由 2 個或 2 個以上的不同線程在同一時間去改變。如果在程序發生這種情況,為了安全并發訪問,可以使用包 `sync`(參考第 9.3 節)中的方法。在第 14.17 節中我們會通過 goroutines 和 channels 探索另一種方式。 ## 10.6.5 內嵌類型的方法和繼承 當一個匿名類型被內嵌在結構體中時,匿名類型的可見方法也同樣被內嵌,這在效果上等同于外層類型 **繼承** 了這些方法:**將父類型放在子類型中來實現亞型**。這個機制提供了一種簡單的方式來模擬經典面向對象語言中的子類和繼承相關的效果,也類似 Ruby 中的混入(mixin)。 下面是一個示例(可以在練習 10.8 中進一步學習):假定有一個 `Engine` 接口類型,一個 `Car` 結構體類型,它包含一個`Engine` 類型的匿名字段: ``` type Engine interface { Start() Stop() } type Car struct { Engine } ``` 我們可以構建如下的代碼: ``` func (c *Car) GoToWorkIn() { // get in car c.Start() // drive to work c.Stop() // get out of car } ``` 下面是 `method3.go` 的完整例子,它展示了內嵌結構體上的方法可以直接在外層類型的實例上調用: ``` package main import ( "fmt" "math" ) type Point struct { x, y float64 } func (p *Point) Abs() float64 { return math.Sqrt(p.x*p.x + p.y*p.y) } type NamedPoint struct { Point name string } func main() { n := &NamedPoint{Point{3, 4}, "Pythagoras"} fmt.Println(n.Abs()) // 打印5 } ``` 內嵌將一個已存在類型的字段和方法注入到了另一個類型里:匿名字段上的方法“晉升”成為了外層類型的方法。當然類型可以有只作用于本身實例而不作用于內嵌“父”類型上的方法, 可以覆寫方法(像字段一樣):和內嵌類型方法具有同樣名字的外層類型的方法會覆寫內嵌類型對應的方法。 在示例 10.18 method4.go 中添加: ``` func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. } ``` 現在 `fmt.Println(n.Abs())` 會打印 `500`。 因為一個結構體可以嵌入多個匿名類型,所以實際上我們可以有一個簡單版本的多重繼承,就像:`type Child struct { Father; Mother}`。在第 10.6.7 節中會進一步討論這個問題。 結構體內嵌和自己在同一個包中的結構體時,可以彼此訪問對方所有的字段和方法。 **練習 10.8** inheritance\_car.go 創建一個上面 `Car` 和 `Engine` 可運行的例子,并且給 `Car` 類型一個 `wheelCount` 字段和一個 `numberOfWheels()` 方法。 創建一個 `Mercedes` 類型,它內嵌 `Car`,并新建 `Mercedes` 的一個實例,然后調用它的方法。 然后僅在 `Mercedes` 類型上創建方法 `sayHiToMerkel()` 并調用它。 ## 10.6.6 如何在類型中嵌入功能 主要有兩種方法來實現在類型中嵌入功能: A:聚合(或組合):包含一個所需功能類型的具名字段。 B:內嵌:內嵌(匿名地)所需功能類型,像前一節 10.6.5 所演示的那樣。 為了使這些概念具體化,假設有一個 `Customer` 類型,我們想讓它通過 `Log` 類型來包含日志功能,`Log` 類型只是簡單地包含一個累積的消息(當然它可以是復雜的)。如果想讓特定類型都具備日志功能,你可以實現一個這樣的 `Log` 類型,然后將它作為特定類型的一個字段,并提供 `Log()`,它返回這個日志的引用。 方式 A 可以通過如下方法實現(使用了第 10.7 節中的 `String()` 功能): 示例 10.19 embed\_func1.go: ``` package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string log *Log } func main() { c := new(Customer) c.Name = "Barak Obama" c.log = new(Log) c.log.msg = "1 - Yes we can!" // shorter c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}} // fmt.Println(c) &{Barak Obama 1 - Yes we can!} c.Log().Add("2 - After me the world will be a better place!") //fmt.Println(c.log) fmt.Println(c.Log()) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) Log() *Log { return c.log } ``` 輸出: ``` 1 - Yes we can! 2 - After me the world will be a better place! ``` 相對的方式 B 可能會像這樣: ``` package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string Log } func main() { c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}} c.Add("2 - After me the world will be a better place!") fmt.Println(c) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) String() string { return c.Name + "\nLog:" + fmt.Sprintln(c.Log) } ``` 輸出: ``` Barak Obama Log:{1 - Yes we can! 2 - After me the world will be a better place!} ``` 內嵌的類型不需要指針,`Customer` 也不需要 `Add` 方法,它使用 `Log` 的 `Add` 方法,`Customer` 有自己的 `String` 方法,并且在它里面調用了 `Log` 的 `String` 方法。 如果內嵌類型嵌入了其他類型,也是可以的,那些類型的方法可以直接在外層類型中使用。 因此一個好的策略是創建一些小的、可復用的類型作為一個工具箱,用于組成域類型。 ## 10.6.7 多重繼承 多重繼承指的是類型獲得多個父類型行為的能力,它在傳統的面向對象語言中通常是不被實現的(C++ 和 Python 例外)。因為在類繼承層次中,多重繼承會給編譯器引入額外的復雜度。但是在 Go 語言中,通過在類型中嵌入所有必要的父類型,可以很簡單的實現多重繼承。 作為一個例子,假設有一個類型 `CameraPhone`,通過它可以 `Call()`,也可以 `TakeAPicture()`,但是第一個方法屬于類型 `Phone`,第二個方法屬于類型 `Camera`。 只要嵌入這兩個類型就可以解個問題,如下所示: ``` package main import ( "fmt" ) type Camera struct{} func (c *Camera) TakeAPicture() string { return "Click" } type Phone struct{} func (p *Phone) Call() string { return "Ring Ring" } type CameraPhone struct { Camera Phone } func main() { cp := new(CameraPhone) fmt.Println("Our new CameraPhone exhibits multiple behaviors...") fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture()) fmt.Println("It works like a Phone too: ", cp.Call()) } ``` 輸出: ``` Our new CameraPhone exhibits multiple behaviors... It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring ``` **練習 10.9** point\_methods.go: 從 `point.go` 開始(第 10.1 節的聯系):使用方法來實現 `Abs()` 和 `Scale()`函數,`Point` 作為方法的接收者類型。也為 `Point3` 和 `Polar` 實現 `Abs()` 方法。完成了 `point.go` 中同樣的事情,只是這次通過方法。 **練習 10.10** inherit\_methods.go: 定義一個結構體類型 `Base`,它包含一個字段 `id`,方法 `Id()` 返回 `id`,方法 `SetId()` 修改 `id`。結構體類型 `Person`包含 `Base`,及 `FirstName` 和 `LastName` 字段。結構體類型 `Employee` 包含一個 `Person` 和 `salary` 字段。 創建一個 `employee` 實例,然后顯示它的 `id`。 **練習 10.11** magic.go: 首先預測一下下面程序的結果,然后動手實驗下: ``` package main import ( "fmt" ) type Base struct{} func (Base) Magic() { fmt.Println("base magic") } func (self Base) MoreMagic() { self.Magic() self.Magic() } type Voodoo struct { Base } func (Voodoo) Magic() { fmt.Println("voodoo magic") } func main() { v := new(Voodoo) v.Magic() v.MoreMagic() } ``` ## 10.6.8 通用方法和方法命名 在編程中一些基本操作會一遍又一遍的出現,比如打開(Open)、關閉(Close)、讀(Read)、寫(Write)、排序(Sort)等等,并且它們都有一個大致的意思:打開(Open)可以作用于一個文件、一個網絡連接、一個數據庫連接等等。具體的實現可能千差萬別,但是基本的概念是一致的。在 Go 語言中,通過使用接口(參考 第 11 章),標準庫廣泛的應用了這些規則,在標準庫中這些通用方法都有一致的名字,比如 `Open()`、`Read()`、`Write()`等。想寫規范的 Go 程序,就應該遵守這些約定,給方法合適的名字和簽名,就像那些通用方法那樣。這樣做會使 Go 開發的軟件更加具有一致性和可讀性。比如:如果需要一個 convert-to-string 方法,應該命名為 `String()`,而不是 `ToString()`(參考第 10.7 節)。 ## 10.6.9 和其他面向對象語言比較 Go 的類型和方法 在如 C++、Java、C# 和 Ruby 這樣的面向對象語言中,方法在類的上下文中被定義和繼承:在一個對象上調用方法時,運行時會檢測類以及它的超類中是否有此方法的定義,如果沒有會導致異常發生。 在 Go 語言中,這樣的繼承層次是完全沒必要的:如果方法在此類型定義了,就可以調用它,和其他類型上是否存在這個方法沒有關系。在這個意義上,Go 具有更大的靈活性。 下面的模式就很好的說明了這個問題: [![](https://box.kancloud.cn/81e3114c69c4f1b8a702c6d8de3afe7c_891x487.)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.6.9_fig10.4.jpg?raw=true) Go 不需要一個顯式的類定義,如同 Java、C++、C# 等那樣,相反地,“類”是通過提供一組作用于一個共同類型的方法集來隱式定義的。類型可以是結構體或者任何用戶自定義類型。 比如:我們想定義自己的 `Integer` 類型,并添加一些類似轉換成字符串的方法,在 Go 中可以如下定義: ``` type Integer int func (i *Integer) String() string { return strconv.Itoa(i) } ``` 在 Java 或 C# 中,這個方法需要和類 `Integer` 的定義放在一起,在 Ruby 中可以直接在基本類型 int 上定義這個方法。 **總結** 在 Go 中,類型就是類(數據和關聯的方法)。Go 不知道類似面向對象語言的類繼承的概念。繼承有兩個好處:代碼復用和多態。 在 Go 中,代碼復用通過組合和委托實現,多態通過接口的使用來實現:有時這也叫 **組件編程**。 許多開發者說相比于類繼承,Go 的接口提供了更強大、卻更簡單的多態行為。 **備注** 如果真的需要更多面向對象的能力,看一下 [`goop`](https://github.com/losalamos/goop) 包(Go Object-Oriented Programming),它由 Scott Pakin 編寫: 它給 Go 提供了 JavaScript 風格的對象(基于原型的對象),并且支持多重繼承和類型獨立分派,通過它可以實現你喜歡的其他編程語言里的一些結構。 **問題 10.1** 我們在某個類型的變量上使用點號調用一個方法:`variable.method()`,在使用 Go 以前,在哪兒碰到過面向對象的點號? **問題 10.2** a)假設定義: `type Integer int`,完成 `get()` 方法的方法體: `func (p Integer) get() int { ... }`。 b)定義: `func f(i int) {}; var v Integer` ,如何就 v 作為參數調用f? c)假設 `Integer` 定義為 `type Integer struct {n int}`,完成 `get()` 方法的方法體:`func (p Integer) get() int { ... }`。 d)對于新定義的 `Integer`,和 b)中同樣的問題。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看