新编程范式之数据总是有效

备注:本文中所有的示例代码均使用golang实现

在软件编程中,方法是被使用得最广泛的结构;也是出现问题最多的结构。
方法接收一些参数(0个或多个),返回一些值(0个或多个)。
对于方法的输入参数,程序员很少会有疑问,在使用中也很少出现错误;但是对于方法的返回值,程序员却经常犯错。我们将常见错误分为以下2类:
1、具有多义性的单返回值,在使用前未进行有效性的判断
2、意义明确的多返回值,在使用前未进行有效性的判断
在进行代码的展示之前,我们先定义一些基础的数据类型和变量。

type Player struct {
  Id int64
  Lv int32
}

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

让我们先看看第一类错误:具有多义性的单返回值,在使用前未进行有效性的判断。
相信大家对于以下的代码都习以为常了。

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

  return playerPtr
}

以上的方法GetPlayer的返回值具有二义性,或为空,或为玩家对象引用。调用方在使用返回值之前必须先判断其是否为空。

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

playerPtr.Lv++

如果忘记判断返回值的有效性,则可能出现空引用从而导致程序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

那么如果判断了返回值的有效性,是不是就一定不会出现问题了呢?还有一种常见的出错场景。

25 playerPtr := GetPlayer(1024)
26 if playerPtr == nil {
27  log.Printf("Player: %d is not exists.", playerPtr.Id) // 此处playerPtr是空值,但是却被用于记录日志了,从而导致 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

接下来,让我们再看看第二类错误:意义明确的多返回值,在使用前未进行有效性的判断。
为了解决第一类问题,我们可以引入另外一个返回值来标识数据是否存在,如下实例代码所示:

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

调用方在使用返回值之前必须先判断第二个参数是否有效。

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

playerPtr++

现在GetPlayer方法的两个返回值不再具有二义性,而是各自表示一个明确的含义;但是方法的调用方依然可能由于不小心或者在代码的维护中未对第二个返回值进行判断,如下代码所示:

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

又或者,虽然对返回值进行了正确的判断,但是却错误地使用了无效的数据,如下代码所示:

20 playerPtr, exists := GetPlayer(1024)
21 if !exists {
22  log.Printf("Player: %d is not exists.", playerPtr.Id) // 此处playerPtr是空值,但是却被用于记录日志了,从而导致 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

我们已经非常小心地判断方法的返回值,但是为什么还是可能出现错误呢?这是因为,无论是否有效,被调用的方法已经返回了所有的数据;而调用方可能由于各种原因误用了无效的返回值。
从软件工程的角度来说,代码只会被写一次,但是会被维护(阅读和修改)无数次;也许第一次写的时候是正确的,但是在维护的过程中可能被错误地使用了。因为维护者可能没有准确地理解上下文,或者只是单纯地想要记录一行日志。
管理学中的墨菲定律说:一件事情如果可能出错,那么就一定会出错。虽然这中说法不够严谨,但只要我们把时间线拉长,把范围扩大,再加上程序员的水平参差不齐;在一个项目的整个生命周期中,在成百上千的同类型代码中,就一定会出错。

那有没有办法可以彻底解决这个问题呢?号称内存安全的编程语言Rust给出了它的解决方案:保证给出的返回值总是有效的数据。那如何才能保证返回值总是有效的数据呢?让我们引入一个新的数据类型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
}

通过引入新的类型Option,将真正的数据和数据的有效性信息隐藏起来,然后通过对外提供方法来达到保证返回值都是有效的数据的目的。我们可以通过实际的代码来体会这种思想:


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行代码之前,我们并没有获得真正的Player数据;而在我们获得Player数据时,我们知道它一定是有效的数据。无论我们如何使用,都不会再出现问题了。
那我们有没有可能在判断不存在的时候误用了返回值呢?让我们添加一行代码;

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,导致编译失败;我们再也无法错误地使用方法的返回值了。

总结:
在新的编程思想的指引下,我们终于可以放心地使用方法的返回值了。这种思想的应用范围其实非常广泛,在Rust中就有Option/Result/Mutex等类型应用了该思想。感兴趣的同学可以自行去研究一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容