通常在糊业务代码的时候,不论是函数、办法,还是宏,都只会有一个返回值。比方在C语言用于查看一个字符是否为阿拉伯数字的isdigit函数就只会返回是(1)或否(0)

#include <ctype.h>#include <stdio.h>intmain(int argc, char *argv[]){    char c = 'a';    printf("isdigit('%c') is %d\n", c, isdigit(c));    return 0;}

但有时候如果一个函数、办法,或宏能够返回多个值的话会更加不便。例如,在Python中dict类型有一个实例办法get,它能够获得dict实例中与给定的键对应的值。但如果有一个键在字典中的值为None,那么光凭get的返回值无奈精确判断这个键是否存在——除非你给它一个非None的默认值

# -*- coding: utf8 -*-def test(d, key):    print("d.get('{0}') is {1}\t'{0}' in d is {2}".format(key, d.get(key), key in d))if __name__ == '__main__':    d = {        'foo': 'bar',        'baz': None,    }    test(d, 'foo')    test(d, 'baz')

倒退了这么多年的编程语言,又怎么会连一次调用、多值返回这么简略的事件都做不到呢。事实上,有各种各样、各显神通的返回多个值的办法,我给其中的一些做了个分类

Lisp的multiple-value-bind

Common Lisp(简称为CL)的多重返回值当之无愧是其中最正统、最好用的实现形式。以它的内置函数truncate为例,它的第一个返回值为第一个参数除以第二个参数的商,第二个返回值为对应的余数

CL-USER> (truncate 10 3)31

如果不加润饰地调用truncate,就像其它只返回一个值的函数一样,也只会拿到一个返回值

CL-USER> (let ((q (truncate 10 3)))           (format t "q = ~D~%" q))q = 3

除非用multiple-value-bind来捕捉一个函数产生的所有返回值

CL-USER> (multiple-value-bind (q r)             (truncate 10 3)           (format t "q = ~D~8Tr = ~D~%" q r))q = 3   r = 1

CL的计划的长处在于它非常灵便。即便将一个函数从返回单个值改为返回多个值,也不会导致本来调用该函数的地位要全副批改一遍——对批改关闭,对扩大凋谢(误)。

Go的多重返回值

踩在C语言肩膀上的Go也可能从函数中返回多个值。在io/ioutil包的官网文档中有大量的例子,比方用ReadAll办法从字符串衍生的流中读取全部内容,就会返回两个值

package mainimport (    "fmt"    "io/ioutil"    "log"    "strings")func main() {    s := "Hello, world!"    reader := strings.NewReader(s)    bytes, err := ioutil.ReadAll(reader)    if err != nil {        log.Fatal(err)    }    fmt.Printf("bytes is %s", bytes)}

Go以这种形式取代了C语言中用返回值表白胜利与否、再通过指针传出读到的数据的格调。因为这个模式在有用的Go程序中到处呈现,因而Gopher们用的都是定制的键盘(误)

不同于前文的multiple-value-bind,如果一个函数或办法返回多个值,那么调用者必须捕捉每一个值,否则编译无奈通过

➜  try cat try_read_all_ignore_err.gopackage mainimport (    "fmt"    "io/ioutil"    "strings")func main() {    s := "Hello, world!"    reader := strings.NewReader(s)    bytes := ioutil.ReadAll(reader)    fmt.Printf("bytes is %s", bytes)}➜  try go build try_read_all_ignore_err.go# command-line-arguments./try_read_all_ignore_err.go:12:8: assignment mismatch: 1 variable but ioutil.ReadAll returns 2 values

这一要求也是正当的,毕竟多重返回值机制次要用于向调用者传递出错起因——既然可能出错,那么就必须要查看一番。

Python和Rust的解构

就像CL的truncate函数一样,Python中的函数divmod也能够同时返回两个数相除的商和余数,并且咋看之下也是返回多个值的模式

# -*- coding: utf8 -*-if __name__ == '__main__':    q, r = divmod(10, 3)    print('q = {}\tr = {}'.format(q, r))

但实质上,这是因为Python反对解构,同时divmod返回的是一个由商和余数组成的元组。这样的做法与CL的真·奥义·多重返回值的差别在于,如果只想要divmod的第一个值,那么等号左侧也要写成对应的构造

# -*- coding: utf8 -*-if __name__ == '__main__':    q, _ = divmod(10, 3)    print('q = {}'.format(q))

在反对解构的语言中都能够模拟出多重返回值,例如Rust

fn divmod(a: u32, b: u32) -> (u32, u32) {    (a / b, a % b)}fn main() {    let (q, r) = divmod(10, 3);    println!("q = {}\tr = {}", q, r);}

Prolog的归一

到了Prolog这里,画风就有点不一样了。首先Prolog既没有函数,也没有办法,更没有宏。在Prolog中,像length/2member/2这样的货色叫做functor,它们之于Prolog中的列表,就犹如CL的lengthmember之于列表、Python的len函数和in操作符之于列表,JavaScript的length属性和indexOf办法之于数组……

其次,Prolog并不“返回”一个functor的“调用后果”,它只是判断输出的查问是否成立,以及给出使查问成立的变量值。在第一个查问中,length/2的第二个参数为变量L,因而Prolog给出了使这个查问成立的L的值4;第二个查问中没有变量,Prolog只是简略地给出查问是否成立;第三个查问中,Prolog给出了四个可能使查问成立的变量X的值。

因为Prolog会给出查问中每一个变量的值,能够用这个个性来模仿多重返回值。例如,能够让Prolog一次性给出两个数字的和、差、积,和商

麻烦之处在于就算只想要失去两数之和,也必须用占位符填在后三个参数上:jjcc(10, 3, S, _, _, _)

舞弊的指针与全局变量

只管在开篇的时候提到了C语言中的函数无奈返回多个值,但如果像上文的Prolog那般容许批改参数的话,C语言也是能够做到的,谁让它有指针这个强力个性呢。例如,stat(2)函数就会将对于一个文件的信息填充到参数中所指向的构造体的内存中

#include <stdio.h>#include <sys/stat.h>intmain(int argc, char *argv[]){    char *path = "./try_stat.c";    struct stat buf;    stat(path, &buf);    printf("inode's number of %s is %llu\n", path, buf.st_ino);    return 0;}

查看man 2 stat能够晓得struct stat类型中有十分多的内容,这显然也是一种多重返回值。同样的手法,在Go中也能够使用,例如用于把从数据库中读取进去的行的数据写入指标数据结构的Scan办法。

最初,如果只有能让调用者感知就行,那么全局变量未尝不是一种通用的多重返回值机制。例如在C语言中的strtol函数,就会在无奈转换出任何数字的时候返回0并设置errno,因而查看errno是必须的步骤

#include <stdio.h>#include <stdlib.h>#include <sys/errno.h>void try_conversion(const char *str){    long num = strtol(str, NULL, 10);    if (errno == EINVAL || errno == ERANGE)    {        char message[64];        snprintf(message, sizeof(message), "strtol(\"%s\")", str);        perror(message);        return;    }    printf("strtol(\"%s\") is %ld\n", str, num);}intmain(int argc, char *argv[]){    try_conversion("233");    try_conversion("0");    try_conversion("lisp");    return 0;}

鉴于errno是一个全局变量,strtol的使用者齐全有可能遗记要查看。相比之下,Go的strconv包的函数都将转换过程中的谬误以第二个参数的模式返回给调用者,用起来更平安。

后记

依照《代码写得不好,不要总感觉是本人形象得不好》这篇文章的说法,代码写成什么样子齐全是由产品经理决定的。但产品经理又怎么会在意你用的技术是怎么实现多重返回值的呢。综上所述,这个个性没用(误)。

全文完。