備注:本文中所有的示例代碼均使用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é)可以自行去研究一下牲剃。