最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

Go語言的接口實現(xiàn)了鴨子類型嗎?- 談協(xié)變和抗變

2023-06-11 21:23 作者:冒-_-泡  | 我要投稿

上篇文章講了非入侵式接口,以及如何在沒有原生支持非入侵接口的語言中用適配器模式模擬實現(xiàn)它,即最后的例子(C++):

在最后這行實現(xiàn)中,我們可以注意到一點,實現(xiàn)類(T)的Size方法的返回,并不需要和接口類Container的Size一樣返回int,從語法上說,只要能隱式轉換即可,而從設計上說,只要業(yè)務含義無損即可,因為適配器只是簡單地做了一層轉接,而不涉及語法上的精確匹配。例如,T的Size如果返回short(一個小容器),由于short可以無損轉int,且含義都是數(shù)值量,那么顯然是匹配Container的

但是,如果我們換成Go語言,要想原生地將一個Container接口和這樣一個T直接匹配是不行的,因為接口簽名不同,Go對于接口匹配的規(guī)范,是要求簽名嚴格相同

一直以來,對于Go的非入侵接口設計,很多資料將其描述為“鴨子類型”,但Go的官方文檔spec倒是沒出現(xiàn)duck相關詞匯,我個人更傾向于它就是一個普通接口語法的非入侵式改造

當然,鴨子類型這個概念本身就沒有特別嚴謹?shù)亩x,如果從“一個實現(xiàn)類只要實現(xiàn)了一個接口的所有方法,就等于實現(xiàn)了這個接口”這個邏輯來說,非入侵接口倒是體現(xiàn)了這種思想,不過,鴨子類型在我看來更貼近使用層面,即“一個東西只要在需求范圍內使用起來(有存取、調用交互)跟鴨子一樣,那么就是鴨子,但不需要糾結于其和鴨子長得是否一樣”,從這個角度說,Go的接口匹配要更嚴格一些

舉個例子,Go程序員基本都用過net庫開發(fā)網(wǎng)絡程序,對Listener接口應該比較熟悉:

Listener表示一個通用的監(jiān)聽對象,它可以通過Accept方法調用來返回一個通用的Conn對象(Conn也是接口),這沒有任何問題

然后,假設你開發(fā)的是一個TCP服務器,那么Listener的對象實際是一個TCPListener,它的Accept方法自然也是這樣:

看到這個,有強迫癥的人就可能覺得不太對勁了,Accept返回一個Conn連接對象,這沒有問題,但是,TCPListener的Accept顯然只能返回一個TCP連接,既然不可能返回其他類型的連接,那么似乎改成這樣更加精確:

實際上,TCPListener是有這樣一個方法的,只是名字不同:

而Accept的實現(xiàn)大家也很容易想到,直接內部調用AcceptTCP就行了,相當于自己給自己做了個適配器(不過官方代碼是將一份代碼拷貝了兩次,可能是不想白白做一次嵌套調用吧)

看到這里,我們的問題就是:為什么Go不能使用這個“精確版”的Accept接口方法呢?

從語法角度,這個問題很顯然,因為方法簽名不同,它會導致TCPListener沒有實現(xiàn)Listener接口

但是我們更進一步問:如果不考慮Go的語法限制,而是我們來設計語法,“精確版”是否合理?是否能實現(xiàn)?

如果你從使用角度來看,那么它是合理的。為什么呢?Listener的Accept的含義是:調用我,返回一個Conn,那么精確版Accept滿足這個要求了嗎?答案是滿足了,因為*TCPConn本身就是一個Conn,從OOP角度看他倆就是“is”的關系,不沖突,所以返回*TCPConn,不就是返回了一個Conn嗎,完全滿足要求

那么能實現(xiàn)嗎?也是可以的,雖然這個需求會讓編譯器和運行時更加復雜,但無論是靜態(tài)分析還是運行時的處理,都算不上特別難,解決辦法也不唯一

這種設計可能存在的問題,一個是會讓接口適配更加靈活,理解難度高些;另一個,對于允許隱式轉換類型的語言有隱患(比如C++的long轉int有損),但是,Go沒有非接口類型的隱式轉換(強類型語言),所以不存在這個問題

返回值如此,參數(shù)也是類似,只不過關系需要反過來:

這個例子中,TCPSender是一個接口,接收一個TCP連接作為參數(shù),在其上做一些發(fā)送工作,那么如果我們有一個XXXSender的類型,它可以在任何連接上做同樣的工作,是否就實現(xiàn)了TCPSender接口呢?

答案也是肯定的,分析一下這個接口,Send方法需要傳入一個TCP連接來工作,而實現(xiàn)類的Send方法需要傳入一個Conn接口來工作,一個TCP連接自然也是一個Conn接口可以引用的對象,所以沒有邏輯問題,當你用一個TCPSender接口來操作XXXSender對象的時候,接收*TCPConn并傳入給下層,自然隱式轉換的(當然,這個例子不是真實的Go代碼,Go不支持這種)

抽象地總結一下,就是:如果一個實現(xiàn)類的方法的所有返回類型可以無損轉為接口的對應方法的對應返回類型,同時接口的所有參數(shù)類型可以無損轉為實現(xiàn)類的對應方法的對應參數(shù)類型,那么從使用角度說,這個實現(xiàn)類就是實現(xiàn)了這個接口(盡管方法簽名可能不同)

而這,就是我們一般意義上的協(xié)變和抗變概念(也叫順變和逆變)

需要注解一下的是,協(xié)變抗變從字面意思,一開始是指派生類到基類(自然)和基類到派生類(動態(tài))的轉換,只不過這個概念太平凡了,現(xiàn)在一般是說上面論述的這種套一層的情況

顯然Go的接口并不直接支持自動的協(xié)變抗變,它要求方法簽名完全匹配,C++和Java也不行,在這方面有比較完美支持的語言,叫C#

很多相關資料會用Java的基類和派生類的數(shù)組轉換來討論這個問題,其實是上面說的情況的特化版本:

這里B是D的派生類,那么顯然一個D對象可以自然地轉B(也就是用B的變量引用),而反過來如果一個D的變量去引用原本用B的變量引用的對象,則需要運行時檢查,看它是不是D,這個是大家都懂的OOP理論

那么它倆的數(shù)組能互相轉換嗎:

答案是都不行,雖然你可以用強轉的辦法來賦值,但是總會有運行時的問題:

但是,如果將讀寫顛倒過來,就可以了:

換句話說,如果你做了這類數(shù)組轉換后,只從里面讀數(shù)據(jù),那么派生類的數(shù)組轉為基類數(shù)組是ok的,反之,如果轉換后你只寫不讀,那么基類數(shù)組轉派生類數(shù)組是安全的,如果讀寫都有,那么兩個方向都無法安全轉換,總會出問題

結合上面講的接口內容,將數(shù)組看做是這種接口:

一個支持安全的協(xié)變轉換(T作為返回類型),另一個則支持抗變(T作為參數(shù)類型),這樣看就比較清楚了

Go語言的接口實現(xiàn)了鴨子類型嗎?- 談協(xié)變和抗變的評論 (共 條)

分享到微博請遵守國家法律
方正县| 新野县| 乌海市| 全南县| 安庆市| 衡东县| 贵溪市| 玉山县| 彭州市| 保德县| 屏南县| 西藏| 玉林市| 五寨县| 崇仁县| 本溪| 余干县| 盘锦市| 来宾市| 揭西县| 邵阳县| 麻江县| 光山县| 政和县| 雷山县| 永福县| 固原市| 滦南县| 仙游县| 志丹县| 南平市| 理塘县| 绵阳市| 平原县| 石首市| 蓝田县| 吉水县| 信阳市| 夏邑县| 广宁县| 巴林右旗|