新編程范式之?dāng)?shù)據(jù)總是有效

備注:本文中所有的示例代碼均使用golang實(shí)現(xiàn)

在軟件編程中,方法是被使用得最廣泛的結(jié)構(gòu)填抬;也是出現(xiàn)問(wèn)題最多的結(jié)構(gòu)窘问。
方法接收一些參數(shù)(0個(gè)或多個(gè))辆童,返回一些值(0個(gè)或多個(gè))。
對(duì)于方法的輸入?yún)?shù)惠赫,程序員很少會(huì)有疑問(wèn)把鉴,在使用中也很少出現(xiàn)錯(cuò)誤;但是對(duì)于方法的返回值儿咱,程序員卻經(jīng)常犯錯(cuò)庭砍。我們將常見(jiàn)錯(cuò)誤分為以下2類(lèi):
1、具有多義性的單返回值混埠,在使用前未進(jìn)行有效性的判斷
2怠缸、意義明確的多返回值,在使用前未進(jìn)行有效性的判斷
在進(jìn)行代碼的展示之前钳宪,我們先定義一些基礎(chǔ)的數(shù)據(jù)類(lèi)型和變量揭北。

type Player struct {
  Id int64
  Lv int32
}

var (
  playerMap = make(map[int64]*Player, 1024) // key: Player's Id
)

讓我們先看看第一類(lèi)錯(cuò)誤:具有多義性的單返回值,在使用前未進(jìn)行有效性的判斷吏颖。
相信大家對(duì)于以下的代碼都習(xí)以為常了搔体。

func GetPlayer(id int64) *Player {
  playerPtr, exists := playerMap[id]
  if !exists {
    return nil
  }

  return playerPtr
}

以上的方法GetPlayer的返回值具有二義性,或?yàn)榭瞻胱恚驗(yàn)橥婕覍?duì)象引用疚俱。調(diào)用方在使用返回值之前必須先判斷其是否為空。

playerPtr := GetPlayer(1024)
if playerPtr == nil {
  return
}

playerPtr.Lv++

如果忘記判斷返回值的有效性缩多,則可能出現(xiàn)空引用從而導(dǎo)致程序panic呆奕。

22 playerPtr := GetPlayer(1024)
23 playerPtr.Lv++

PS D:\GoProject\testFunc> go run .\main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x18 pc=0x79cb55]                                                                                            
goroutine 1 [running]:                                                 
main.main()                                                            
        D:/GoProject/testFunc/main.go:23 +0x35                         
exit status 2

那么如果判斷了返回值的有效性养晋,是不是就一定不會(huì)出現(xiàn)問(wèn)題了呢?還有一種常見(jiàn)的出錯(cuò)場(chǎng)景登馒。

25 playerPtr := GetPlayer(1024)
26 if playerPtr == nil {
27  log.Printf("Player: %d is not exists.", playerPtr.Id) // 此處playerPtr是空值匙握,但是卻被用于記錄日志了,從而導(dǎo)致 panic陈轿。
28  return
29 }

31 playerPtr.Lv++

PS D:\GoProject\testFunc> go run .\main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x5e6e31]
goroutine 1 [running]:
main.main()
        D:/GoProject/testFunc/main.go:27 +0x51
exit status 2

接下來(lái)圈纺,讓我們?cè)倏纯吹诙?lèi)錯(cuò)誤:意義明確的多返回值,在使用前未進(jìn)行有效性的判斷麦射。
為了解決第一類(lèi)問(wèn)題蛾娶,我們可以引入另外一個(gè)返回值來(lái)標(biāo)識(shí)數(shù)據(jù)是否存在,如下實(shí)例代碼所示:

func GetPlayer(id int64) (*Player, bool) {
  playerPtr, exists := playerMap[id]
  return  playerPtr, exists
}

調(diào)用方在使用返回值之前必須先判斷第二個(gè)參數(shù)是否有效潜秋。

playerPtr, exists := GetPlayer(1024)
if !exists {
  return
}

playerPtr++

現(xiàn)在GetPlayer方法的兩個(gè)返回值不再具有二義性蛔琅,而是各自表示一個(gè)明確的含義;但是方法的調(diào)用方依然可能由于不小心或者在代碼的維護(hù)中未對(duì)第二個(gè)返回值進(jìn)行判斷峻呛,如下代碼所示:

playerPtr, _ := GetPlayer(1024)
playerPtr++

又或者罗售,雖然對(duì)返回值進(jìn)行了正確的判斷,但是卻錯(cuò)誤地使用了無(wú)效的數(shù)據(jù)钩述,如下代碼所示:

20 playerPtr, exists := GetPlayer(1024)
21 if !exists {
22  log.Printf("Player: %d is not exists.", playerPtr.Id) // 此處playerPtr是空值寨躁,但是卻被用于記錄日志了,從而導(dǎo)致 panic牙勘。
23  return
24 }

26 playerPtr++

PS D:\GoProject\testFunc> go run .\main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x7b6e2a]
goroutine 1 [running]:
main.main()
        D:/GoProject/testFunc/main.go:22 +0x4a
exit status 2

我們已經(jīng)非常小心地判斷方法的返回值职恳,但是為什么還是可能出現(xiàn)錯(cuò)誤呢?這是因?yàn)榉矫妫瑹o(wú)論是否有效放钦,被調(diào)用的方法已經(jīng)返回了所有的數(shù)據(jù);而調(diào)用方可能由于各種原因誤用了無(wú)效的返回值恭金。
從軟件工程的角度來(lái)說(shuō)操禀,代碼只會(huì)被寫(xiě)一次,但是會(huì)被維護(hù)(閱讀和修改)無(wú)數(shù)次蔚叨;也許第一次寫(xiě)的時(shí)候是正確的床蜘,但是在維護(hù)的過(guò)程中可能被錯(cuò)誤地使用了。因?yàn)榫S護(hù)者可能沒(méi)有準(zhǔn)確地理解上下文蔑水,或者只是單純地想要記錄一行日志邢锯。
管理學(xué)中的墨菲定律說(shuō):一件事情如果可能出錯(cuò),那么就一定會(huì)出錯(cuò)搀别。雖然這中說(shuō)法不夠嚴(yán)謹(jǐn)丹擎,但只要我們把時(shí)間線(xiàn)拉長(zhǎng),把范圍擴(kuò)大,再加上程序員的水平參差不齊蒂培;在一個(gè)項(xiàng)目的整個(gè)生命周期中再愈,在成百上千的同類(lèi)型代碼中,就一定會(huì)出錯(cuò)护戳。

那有沒(méi)有辦法可以徹底解決這個(gè)問(wèn)題呢翎冲?號(hào)稱(chēng)內(nèi)存安全的編程語(yǔ)言Rust給出了它的解決方案:保證給出的返回值總是有效的數(shù)據(jù)。那如何才能保證返回值總是有效的數(shù)據(jù)呢媳荒?讓我們引入一個(gè)新的數(shù)據(jù)類(lèi)型Option:

import "fmt"

type Option[T any] struct {
    // none and data are mutual exclusive
    none bool
    data T
}

func NewNoneOption[T any]() Option[T] {
    return Option[T]{
        none: true,
    }
}

func NewDataOption[T any](data T) Option[T] {
    return Option[T]{
        data: data,
    }
}

func (this Option[T]) HasNone() bool {
    return this.none
}

func (this Option[T]) HasData() bool {
    return !this.none
}

// Data returns the underlying data.
// Panic if there is no data.
func (this Option[T]) Data() T {
    if this.none {
        panic(fmt.Errorf("check validity first"))
    }

    return this.data
}

通過(guò)引入新的類(lèi)型Option抗悍,將真正的數(shù)據(jù)和數(shù)據(jù)的有效性信息隱藏起來(lái),然后通過(guò)對(duì)外提供方法來(lái)達(dá)到保證返回值都是有效的數(shù)據(jù)的目的钳枕。我們可以通過(guò)實(shí)際的代碼來(lái)體會(huì)這種思想:


func GetPlayer(id int64) Option[*Player] {
    type OptionDataType = *Player

    playerPtr, exists := playerMap[id]
    if !exists {
        return NewNoneOption[OptionDataType]()
    }

    return NewDataOption(playerPtr)
}

25 playerOption := GetPlayer(1024)
26 if playerOption.HasNone() {
27  return
28 }

29 playerPtr := playerOption.Data()
30 playerPtr.Lv++

在第29行代碼之前缴渊,我們并沒(méi)有獲得真正的Player數(shù)據(jù);而在我們獲得Player數(shù)據(jù)時(shí)鱼炒,我們知道它一定是有效的數(shù)據(jù)衔沼。無(wú)論我們?nèi)绾问褂茫疾粫?huì)再出現(xiàn)問(wèn)題了昔瞧。
那我們有沒(méi)有可能在判斷不存在的時(shí)候誤用了返回值呢指蚁?讓我們添加一行代碼;

PS D:\GoProject\testFunc> go build
# testFunc
.\main.go:29:56: playerOption.Id undefined (type Option[*Player] has no field or method Id)

由于方法的返回值是Option自晰,而不是*Player欣舵,導(dǎo)致編譯失敗缀磕;我們?cè)僖矡o(wú)法錯(cuò)誤地使用方法的返回值了。

總結(jié):
在新的編程思想的指引下劣光,我們終于可以放心地使用方法的返回值了袜蚕。這種思想的應(yīng)用范圍其實(shí)非常廣泛,在Rust中就有Option/Result/Mutex等類(lèi)型應(yīng)用了該思想绢涡。感興趣的同學(xué)可以自行去研究一下牲剃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市雄可,隨后出現(xiàn)的幾起案子凿傅,更是在濱河造成了極大的恐慌,老刑警劉巖数苫,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聪舒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡虐急,警方通過(guò)查閱死者的電腦和手機(jī)箱残,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人被辑,你說(shuō)我怎么就攤上這事燎悍。” “怎么了盼理?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵谈山,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我宏怔,道長(zhǎng)奏路,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任举哟,我火速辦了婚禮思劳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妨猩。我一直安慰自己潜叛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布壶硅。 她就那樣靜靜地躺著威兜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪庐椒。 梳的紋絲不亂的頭發(fā)上椒舵,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音约谈,去河邊找鬼笔宿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棱诱,可吹牛的內(nèi)容都是我干的泼橘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼迈勋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼炬灭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起靡菇,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤重归,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后厦凤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鼻吮,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年较鼓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狈网。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拓哺,靈堂內(nèi)的尸體忽然破棺而出勇垛,到底是詐尸還是另有隱情,我是刑警寧澤士鸥,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布闲孤,位于F島的核電站,受9級(jí)特大地震影響烤礁,放射性物質(zhì)發(fā)生泄漏讼积。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一脚仔、第九天 我趴在偏房一處隱蔽的房頂上張望勤众。 院中可真熱鬧,春花似錦鲤脏、人聲如沸们颜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)窥突。三九已至,卻和暖如春硫嘶,著一層夾襖步出監(jiān)牢的瞬間阻问,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工沦疾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留称近,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓哮塞,卻偏偏與公主長(zhǎng)得像煌茬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子彻桃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容