Java 语言的几个缺陷之五: 多返回值问题

希望 Java 能支持动态对象(匿名对象) 的特性是源于想要 Java 方法能优雅的返回多个值. 目前如果希望 Java 方法返回多个值的做法有返回一个自定义对象, 数组或列表, 或 Map. 这种需求多发生在私有方法上, 但目前的解决办法有如下弊端:


  1. 如果用自定义类来作为返回类型的话, 会使得类过于杂乱, 而且这些自定义类的复用性不高
  2. 数组或列表有太强的顺序依赖, 没有属性名告知每个位置上值的意义, 而且类型都必须为 Object
  3. 返回 Map 的, 在维护 Key 上容易出错(或须为 Key 定义很多字符串常量), 且类型也是为 Object.

说了上面许多, 各位可能还不定清楚我想要的是什么, 其实就是类似于 C# 的匿名对象(或曰动态对象), 看一段 C#  的方法返回一个匿名对象的例子(.NET3.5 开始引入匿名对象, .NET 4.0 后匿名对象可以作为函数值)
 1//MyClass.cs
 2class MyClass {
 3    public dynamic Person()  //返回匿名对象的方法用 dynamic 关键字声明
 4    {
 5        return new {name = "Yanbin", city = "Chicago" };
 6    }
 7}
 8
 9//Program.cs
10class MainClass
11{
12    public static void Main (string[] args)
13    {
14        var person = new MyClass().Person();  //C# 还是有赖于 var 进行类型推断
15        Console.WriteLine("Name: {0}, City: {1}", person.name, person.city);  //person 为动态类型时, 编译器不对 name, city 属性有无进行检查
16    }
17}

上面的代码输出为
Name: Yanbin, City: Chicago
注意: 对于方法返回的动态类型, 如 person, C# 编译器无法判断它有什么属性, 所以写 person.ab, person.bb 都能通过, 只会在运行期报错. 这就像返回 Map 的方法一样, 编译器是不知道某个 Key 是否存在的. 如果是在可见作用域下使用匿名对象编译器是可以检测属性是否存在, 如
1var person = new { name = "Yanbin" };
2Console.WriteLine(person.abc);  //这行代码无法通过编译, 因为 abc 属性不存在

很多其他语言的方法是通过  Tuple 来返回多个值, 在接收 Tuple 时可以进行拆解, 后面会说.

Play Framework 受到 Scala Tuple 的启迪, 它自己实现了几个 Java 版 Tuple(见 F.java), 包括 Tuple, Tuple3, Tuple4 和 Tuple5. 如果一个方法同时返回值不超过 5 个的话可以选择其中一种类型, 使用例子:
 1public Tuple3<Integer, String, String> getPerson() {
 2  return new Tuple3(100, "Yanbin", "Chicago");
 3}
 4
 5//调用代码
 6Tuple3<Integer, String, String> person = getPerson();
 7
 8//使用每一个属性值
 9System.out.printf("Id: %s, Name: %s, City: %s", person._1, person._2, person._3);
10
11//如果多次经常性的用 ._1, ._2, ._3 这样来获得属性会造成难于理解, 可以像下面那样拆解
12int id = person._1;
13String name = person._2;
14String city = person._3;
15System.out.printf("Id: %s, Name: %s, City: %s", id, name, city);

这就是 Java, 只能看到一大片的 ._1, ._2, 有时候一段代码要同时多个 Tuple, 于是一堆  person._1, customer._2, consumer._2, 这时就是眼花缭乱, 甚至是六神无主了.

假如换做其他几门语言

Scala (它有类库有 Tuple1, Tuple2, ..... Tuple22, 22  个类型来支持)
1scala> def person() = (100, "Yanbin", "Chicago")
2person: ()(Int, String, String)
3
4scala> val (id, name, city) = person()
5id: Int = 100
6name: String = Yanbin
7city: String = Chicago

Swift(它在拆解 Tuple  时只与顺序有关)
1let person = (firstName: "John", lastName: "Smith")
2print(person.firstName)               //John
3
4let (firstName, familyName) = person  
5print(firstName, familyName)          //John Smith
6
7let (a, b, c) = (100, "Name", "City")
8print(a, b, c)                        //100, Name, City

Swift 的 Tuple 给属性命上名, 和 C# 的匿名对象基本是一样了.

JavaScript(ES6), JavaScript 都不干示弱了
1[a, b] = [1, 2];   //a=1, b=2
2[a, b, ...rest] = [1, 2, 3, 4, 5];  //a=1, b=2, rest = [3, 4, 5]
3{a, b} = {a:1, b:2};  //a=1, b=2, 与 Swift 不同, 这里的变量名 a 要与键 a 匹配, {a, b} = {b:2, c:3}, a 将是 undefined
4{a, b, ...rest} = {a:1, b:2, c:3, d:4}; //a=1, b=2, rest = {c:3, d:4}, ES7 才支持

Perl(与 Scala, Swift 语法差不多
1($a, $b) = (100, "Chicago");
2print $a, $b;        //100Chicago

Ruby(它是以数组形式返回多个值)
 1irb(main):013:0> def multiple
 2irb(main):014:1> return 1, 2
 3irb(main):015:1> end
 4=> nil
 5irb(main):016:0> x, y = multiple
 6=> [1, 2]
 7irb(main):017:0> x
 8=> 1
 9irb(main):018:0> y
10=> 2
11irb(main):019:0> x = multiple
12=> [1, 2]
13irb(main):020:0> x
14=> [1, 2]

Python
1>>> def multiple():
2... return (100, "Yanbin")
3...
4>>> id, name = multiple()
5>>> id
6100
7>>> name
8'Yanbin'

Clojure(最近在学这个)
1user=> (defn multiple [] [100 "Yanbin"])
2#'user/multiple
3user=> (let [[id name] (multiple)]
4 #_=> (println id)
5 #_=> (println name))
6100
7Yanbin
8nil

参考: 1. https://rosettacode.org/wiki/Return_multiple_values 永久链接 https://yanbin.blog/java-language-defect-no-multiple-returns/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。