基礎技能樹-26 方法集

来源:http://www.cnblogs.com/lyj/archive/2017/11/28/foundation_26_method_set.html
-Advertisement-
Play Games

本節內容 - 什麼是方法集 - 方法集區分基礎類型T和指針類型*T - 匿名嵌入對方法集的影響 - 方法集調用 ...


本節內容

  • 什麼是方法集
  • 方法集區分基礎類型T和指針類型*T
  • 匿名嵌入對方法集的影響[付費閱讀]
  • 方法集調用[付費閱讀]

重點理解介面和介面相關的概念,介面在不同語言里有不同的做法,靜態語言往往會有顯式的介面,就是聲明一個介面類型,至於哪些類型是否實現了這個介面類型,不同語言有不同處理方法,C#或者Java必須指定類型實現了介面,go語言只要符合條件就可以了,不需要顯式的說實現。動態語言很多時候沒有介面這樣的概念,只要你有對應的名字就可以了,它們把介面稱之為協議,概念都是類似的,今天是要瞭解靜態語言介面是怎麼實現的,在瞭解介面之前,需要準備一些相關的概念。

什麼是方法集

go語言里的方法集,個人認為實現起來不是特別優雅,但是並不影響我們理解概念。什麼是方法集?假如說類型A實現了a1方法,B繼承自A,B實現了b1方法,C繼承B,C實現了c1方法。那麼A的方法集是什麼?什麼是方法集,就是說A能調用的方法集合。A的方法集是a1,B的方法集是a1,b1,就是還包含父類的方法,C的方法集是a1,b1,c1。問題是go語言很大的問題是沒有繼承的概念,它用的是組合的概念,這時候變得麻煩了。

假如說C裡面包含了B和A,C的方法集包含哪些呢?go語言編譯器就做了比較投機取巧的事情,它認為包含了某個東西,就除了訪問它的欄位以外還可以訪問它的方法,就是C.c1,C.B.b1,C.A.a1,按照正常訪問是訪問3個方法,在語法糖上把C.B.b1做了一次縮寫C.b1,C.A.a1縮寫成C.a1,編譯器負責查找,最後的方法集變成了c1,b1,a1。很顯然這個是編譯器替我們完成的這種東西。

正常情況下我們自己寫偽碼:

struct A{
    a1()
}

struct B{
    b1()
}

struct C{
    A
    B
    c1()
}

C::a1{
    C.A.a1()
}

C::b1{
    C.B.b1()
}

如果編譯器不幫我們做,我們實際上需要自己去寫C.a1調用C.A.a1,這樣一來,c的方法集就包含a1,b1,c1,因為理論上組合是沒有辦法繼承它內部欄位成員的,必須是顯式實現,區別在於是我們自己寫還是編譯器替我們寫。

方法集區分基礎類型T和指針類型*T

看看編譯器怎麼做這事情的?

$ cat test.go
package main

import (
    "fmt"
    "reflect"
    "strconv"
)

type N int

func (n *N) Inc() {
    *n++
}

func (n N) String() string {
    return strconv.Itoa(int(n))
}

func listMethods(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("\n--- %v ---------\n", t)

    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}

func main() {
    var n N

    listMethods(n)
    listMethods(&n)
}

listMethods方法是利用反射把當前方法的方法集全部列出來。方法集具體的區別在於go語言很大的不同在於可以顯式的提供參數,提供參數可以指定類型的*N或者N。這地方就會形成這樣一個概念。

比如聲明類型N,假如說有兩個方法A、B,但是它們的this參數可以是N也可以是指針*N,這樣的話A和B就會屬於不同的類型,一個是N,一個是N的指針。我們知道一個類型和一個類型的指針屬於兩種類型,*N長度是8,N長度是32,64之類的,雖然類型不是一回事,一個是指針一個是普通類型,這就造成實現方法的時候,是針對N實現的還是針對*N實現的,這就造成了N的方法集和*N的方法集是不一樣的。

上面代碼定義類型N,實現了兩個方法,一個方法針對N本身實現,一個方法針對*N實現。我們分別列出它們的方法集究竟有哪些。在main函數中定義了N實例,分別列出N*N有哪些方法集。

$ go run test.go

輸出:

--- main.N ---------
String: func(main.N) string

--- *main.N ---------
Inc: func(*main.N)
String: func(*main.N) string

我們可以看到N方法集就有1個N基礎類型的方法,*N方法集除了*N類型的方法以外還包含N類型的方法。func(*main.N) string做了類型轉換。

簡單的來說,我們有個類型TT的方法集只包含T自身的,但是*T方法集等於T+*T的方法,這就是差別。

不同的語言在這塊的做法會有一些細微的差別。Java和C#為什麼沒有這東西,因為它們預設的話this就有一種引用方式,沒有說把this分為引用和值兩種方式。就是你引用實例,說白了就相當於只有*T沒有T,指針類型和指針的基礎類型不是一回事。

當我們拿到一個對象指針*T的時候,調用對象T的方法是不是安全的呢?因為我們可以把指針裡面數據取出來,然後作為T參數。但是我們擁有T,未必就能獲得T的指針*T,因為它有可能是個臨時對象,我們知道臨時對象是沒有辦法取得它的指針的,你有指針也就意味著這個對象肯定是在棧上或者堆上分配過的,但是你擁有臨時對象的實例未必能拿到臨時對象的指針,不見得是合法的。我們假如訪問字典裡面一個元素,如果編譯器對字典元素本身做了不允許訪問地址,那你訪問元素的時候拿不到指針的,這時候獲取到它的指針沒有意義,還有跨棧幀獲取指針也沒有意義。所以說用指針獲取指針目標是安全的,用目標未必能獲得它的指針。這是因為記憶體安全模型決定的,因為go語言並不完全區分值類型和引用類型,它是由編譯器決定對象到底分配到哪。

String: func(*main.N) string方法哪裡來的?

編譯

$ go build -gcflags "-N -l" -o test test.go

輸出符號

$ nm test | grep "[^\.]main\."

輸出

00000000004b1ee0 T main.init
0000000000595204 B main.initdone.
00000000004b1a30 T main.listMethods
00000000004b1e20 T main.main
00000000004b1980 T main.(*N).Inc
00000000004b1f60 T main.(*N).String
00000000004b19c0 T main.N.String

我們註意到String有兩個,main.(*N).Stringmain.N.Stringmain.N.String是我們自己定義的,main.(*N).String是程式執行時候輸出的,兩個地址都不一樣,這表明最終生成機器代碼的時候是存在兩個這樣函數,很顯然main.(*N).String是編譯器生成的。

反彙編看看到底什麼樣的:

$ go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
TEXT main.(*N).String(SB) <autogenerated>

main.(*N).String(SB)是機器生成的,地址是00000000004b1f60和符號表裡面一致,實際上在符號表裡面已經打上了<autogenerated>標記。為什麼打上這個標記,因為我們自己寫的代碼在符號表裡面有信息可以對應到哪一行,但是很顯然有些東西不是我們寫的,所以從源碼上沒有辦法對應關係,所以符號表標記這些信息由編譯器生成的。

現在知道,當我們想實現一個方法集的時候,源碼層面和機器碼層面其實是不一樣的,因為源碼層面當我嵌入一個類型的時候,我會自動擁有它的方法。對於機器碼來說,你想調用函數,必須給一個合法的地址,這個合法的地址必鬚生成對應的代碼,這個代碼高級語言稱之為規則,規則就是編譯器支持這種理論,編譯器替你完成這種東西。

所謂的方法集就是當你嵌入一個類型的時候,你擁有它的方法,準確的說,編譯器自動生成嵌入類型的方法。

go語言雖然沒有繼承的概念,編譯器替我們補全了這種間接調用。這樣一來有點類似於A繼承B的方法,但是這不是繼承。因為是繼承的話就不會有自動代碼生成,直接通過類型表去調用。go語言所謂的自動擁有方法集不是繼承而是語法糖層面上的代碼補全。

匿名嵌入對方法集的影響[付費閱讀]

當你匿名嵌入一個對象的時候,編譯器會幫我們自動生成間接代碼調用,所以看上去擁有了對象的方法,實際上你不是擁有,而是編譯器幫你做了代碼補全。這個語法糖實際上是代碼補全而不是動態行為是靜態行為。

$ cat embed.go
package main

import (
    "strconv"
    "reflect"
    "fmt"
)

type N int

type X struct {
    N
}

type Y struct {
    *N
}

func (n *N) Inc() {
    *n++
}

func (n N) String() string {
    return strconv.Itoa(int(n))
}

func listMethods(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("\n--- %v ---------\n", t)

    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}

func main() {
    var x X
    listMethods(x)
    listMethods(&x)

    var y Y
    listMethods(y)
    listMethods(&y)
}

X嵌入了N,Y嵌入了*N。

$ go run embed.go
--- main.X ---------
String: func(main.X) string

--- *main.X ---------
Inc: func(*main.X)
String: func(*main.X) string

--- main.Y ---------
Inc: func(main.Y)
String: func(main.Y) string

--- *main.Y ---------
Inc: func(*main.Y)
String: func(*main.Y) string

我們註意到X只擁有X自身的方法,X指針擁有X和X指針的方法。Y因為嵌入了X,只能獲得對應值類型的方法,Y指針擁有對應的指針類型。

我們看看匿名嵌入的時候用哪些東西是自動生成的,我們註意到這裡面有很多自動生成的方法來實現方法集。

$ go build -gcflags "-N -l" -o test embed.go
$ nm test | grep "[^\.]main\."
$ go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
nm test | grep "[^\.]main\."
00000000004b1f60 T main.init
0000000000596204 B main.initdone.
00000000004b1a30 T main.listMethods
00000000004b1e20 T main.main
00000000004b1980 T main.(*N).Inc
00000000004b1fe0 T main.(*N).String
00000000004b19c0 T main.N.String
00000000004b2080 T main.(*X).Inc
00000000004b20a0 T main.(*X).String
00000000004b2130 T main.X.String
00000000004b21c0 T main.(*Y).Inc
00000000004b2270 T main.Y.Inc
00000000004b21e0 T main.(*Y).String
00000000004b22d0 T main.Y.String
go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
TEXT main.init(SB) <autogenerated>
TEXT main.(*N).String(SB) <autogenerated>
TEXT main.(*X).Inc(SB) <autogenerated>
TEXT main.(*X).String(SB) <autogenerated>
TEXT main.X.String(SB) <autogenerated>
TEXT main.(*Y).Inc(SB) <autogenerated>
TEXT main.(*Y).String(SB) <autogenerated>
TEXT main.Y.Inc(SB) <autogenerated>
TEXT main.Y.String(SB) <autogenerated>

方法集調用[付費閱讀]

$ cat call.go

N類型,有兩個方法,當我們用N類型調用的時候,理論上只執行String,但是如果我們用N類型調用Inc是不是合法的呢?

package main

import (
    "strconv"
)

type N int

func (n *N) Inc() {
    *n++
}

func (n N) String() string {
    return strconv.Itoa(int(n))
}

func main() {
    var n N = 100

    (*N).Inc(&n)

    s := (*N).String(&n)
    println(s)
}
$ go build -gcflags "-l" -o test call.go

反彙編

$ go tool objdump -s "main\.main" test
  call.go:17        0x451600        64488b0c25f8ffffff  MOVQ FS:0xfffffff8, CX
  call.go:17        0x451609        483b6110        CMPQ 0x10(CX), SP
  call.go:17        0x45160d        0f868b000000        JBE 0x45169e
  call.go:17        0x451613        4883ec38        SUBQ $0x38, SP
  call.go:17        0x451617        48896c2430      MOVQ BP, 0x30(SP)
  call.go:17        0x45161c        488d6c2430      LEAQ 0x30(SP), BP
  call.go:17        0x451621        488d0538e40000      LEAQ 0xe438(IP), AX
  call.go:18        0x451628        48890424        MOVQ AX, 0(SP)
  call.go:18        0x45162c        e84faefbff      CALL runtime.newobject(SB)
  call.go:18        0x451631        488b442408      MOVQ 0x8(SP), AX
  call.go:18        0x451636        4889442428      MOVQ AX, 0x28(SP)
  call.go:18        0x45163b        48c70064000000      MOVQ $0x64, 0(AX)
  call.go:20        0x451642        48890424        MOVQ AX, 0(SP)
  call.go:20        0x451646        e855ffffff      CALL main.(*N).Inc(SB)
  call.go:20        0x45164b        488b442428      MOVQ 0x28(SP), AX
  call.go:22        0x451650        48890424        MOVQ AX, 0(SP)
  call.go:22        0x451654        e8b7000000      CALL main.(*N).String(SB)
  call.go:22        0x451659        488b442408      MOVQ 0x8(SP), AX
  call.go:22        0x45165e        4889442420      MOVQ AX, 0x20(SP)
  call.go:22        0x451663        488b c2410      MOVQ 0x10(SP), CX
  call.go:22        0x451668        48894c2418      MOVQ CX, 0x18(SP)
  call.go:23        0x45166d        e81e22fdff      CALL runtime.printlock(SB)
  call.go:23        0x451672        488b442420      MOVQ 0x20(SP), AX
  call.go:23        0x451677        48890424        MOVQ AX, 0(SP)
  call.go:23        0x45167b        488b442418      MOVQ 0x18(SP), AX
  call.go:23        0x451680        4889442408      MOVQ AX, 0x8(SP)
  call.go:23        0x451685        e8a62bfdff      CALL runtime.printstring(SB)
  call.go:23        0x45168a        e8b124fdff      CALL runtime.printnl(SB)
  call.go:23        0x45168f        e88c22fdff      CALL runtime.printunlock(SB)
  call.go:24        0x451694        488b6c2430      MOVQ 0x30(SP), BP
  call.go:24        0x451699        4883c438        ADDQ $0x38, SP
  call.go:24        0x45169d        c3          RET
  call.go:17        0x45169e        e8ed70ffff      CALL runtime.morestack_noctxt(SB)
  call.go:17        0x4516a3        e958ffffff      JMP main.main(SB)

我們註意到CALL main.(*N).Inc(SB),它的調用方式並沒有運行期的行為,都是靜態綁定。就是很明確的你要調用哪些東西。因為我們知道你調用的哪些方法雖然源碼不存在,但是編譯器幫你生成了,這個方法實際上已經存在了,我們剛剛看到編譯器實際上替我們生成了這些間接調用,那j既然說這個函數或者方法已經存在了,那直接用call間接方法就可以了。

如果A嵌入B,B有個方法叫x,那麼編譯器會自動幫我們生成A.X方法,方法內部調用A.B.X方法,這實際上是編譯器自動生成的。如果我們源碼里寫A.X的話實際上會被編譯器翻譯成對自動包裝函數的調用,所以從語法上A.X看上去好像A繼承X方法,實際上是因為編譯器翻譯成A.B.X的調用。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 如圖,左圖是效果,右圖是原理,右圖X軸代表圖像一個像素點的灰度,Y軸代表RGB三個顏色對應的偽彩色圖顏色。代碼如下: for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { Color c = bmp.GetPixel ...
  • 針對WCF分散式消息隊列MSMQ大大提高了處理能力,無論是發送方還是接收方都不用等待對方返回成功消息,但是不適合Client與Server端的實時交互。WCF分散式消息隊列,在處理日誌方面,效果還是很顯著的。當然,針對消息隊列的處理技術,有很多種,例如:ActiveMQ、RabbitMQ、ZeroM... ...
  • 演示產品下載地址:http://www.jinhusns.com ...
  • 返回總目錄 本小節目錄 Consolidate Duplicate Conditional Fragments(合併重覆的條件片段) Remove Control Flag(移除控制標記) 3Consolidate Duplicate Conditional Fragments(合併重覆的條件片段) ...
  • 使用jdbcTemplate 原理是把載入驅動Class.forName("com.mysql.jdbc.Driver"); 和連接資料庫Connection conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/sw_datab ...
  • 1.首先要下載PHPExcel放到vendor文件夾下,我的路徑是:項目/vendor/PHPExcel/,把下載的PHPExcel文件放在這裡 2.前端代碼 3.後臺代碼 輸出結果: 註意: 引入第三方類庫使用vendor();是按照命名空間的形式。底層代碼會把“ . ”自動替換成" / ",所以 ...
  • 1 非對稱加密演算法 1.1 概述 1976年,美國學者Dime和Henman為解決信息公開傳送和密鑰管理問題,提出一種新的密鑰交換協議,允許在不安全的媒體上的通訊雙方交換信息,安全地達成一致的密鑰,這就是“公開密鑰系統”。 與對稱加密演算法不同,非對稱加密演算法需要兩個密鑰:公開密鑰(publickey ...
  • 原文:http://blog.csdn.net/u012152619/article/details/51485297 一般來說,上面的幾個配置項對任何項目都是必不可少的,定義了項目的基本屬性。 這裡有必要對一個不太常用的屬性classifier做一下解釋,因為有時候引用某個jar包,classif ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...