基礎技能樹-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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...