45

我工作的代码库有一个名为 Pair 的对象,其中 A 和 B 是 Pair 中第一个和第二个值的类型。我觉得这个对象令人反感,因为它被用来代替具有明确命名成员的对象。所以我发现这个:

List<Pair<Integer, Integer>> productIds = blah();
// snip many lines and method calls

void doSomething(Pair<Integer, Integer> id) {
  Integer productId = id.first();
  Integer quantity = id.second();
}

代替

class ProductsOrdered {
  int productId;
  int quantityOrdered;
  // accessor methods, etc
}

List<ProductsOrderded> productsOrdered = blah();

代码库中 Pair 的许多其他用途也同样难闻。

我用谷歌搜索了元组,它们似乎经常被误解或以可疑的方式使用。是否有令人信服的论据支持或反对使用它们?我可以理解不想创建巨大的类层次结构,但是如果不使用元组,是否存在类层次结构会爆炸的现实代码库?

4

10 回答 10

36

首先,元组既快速又简单:不是每次你想把两件事放在一起时都编写一个类,而是有一个模板可以为你做这件事。

其次,它们是通用的。例如,在 C++ 中,std::map 使用 std::pair 键和值。因此,可以使用 ANY 对,而不必为两种类型的每个排列创建某种带有访问器方法的包装类。

最后,它们对于返回多个值很有用。确实没有理由专门为函数的多个返回值创建一个类,如果它们不相关,则不应将它们视为一个对象。

公平地说,您粘贴的代码是对的错误使用。

于 2009-03-23T21:11:51.050 回答
16

元组在 Python 中一直被使用,它们被集成到语言中并且非常有用(它们允许初学者有多个返回值)。

有时,你真的只需要配对并创建一个真实的、对上帝诚实的类是矫枉过正的。另一方面,当你真正应该使用一个类时使用元组与相反的想法一样糟糕。

于 2009-03-23T21:13:59.000 回答
10

代码示例有一些不同的味道:

  • 重新发明轮子

框架中已经有一个可用的元组;KeyValuePair结构。类使用Dictionary它来存储对,但您可以在任何合适的地方使用它。(并不是说它适合这种情况......)

  • 制作方轮

如果您有一个对列表,则最好使用KeyValuePair结构而不是具有相同目的的类,因为它会减少内存分配。

  • 隐藏意图

具有属性的类清楚地显示了值的含义,而Pair<int,int>类不会告诉您有关值代表什么的任何信息(只是它们可能以某种方式相关)。为了使代码用这样的列表合理地自我解释,你必须给列表一个非常具有描述性的名称,比如productIdAndQuantityPairs......

于 2009-03-23T21:48:10.307 回答
8

就其价值而言,OP 中的代码一团糟,不是因为它使用元组,而是因为元组中的值类型太弱。比较以下内容:

List<Pair<Integer, Integer>> products_weak = blah1();
List<Pair<Product, Integer>> products_strong = blah2();

如果我的开发团队传递的是 ID 而不是类实例,我也会感到不安,因为整数可以代表任何东西


话虽如此,当您正确使用元组时,它们非常有用:

  • 存在元组以将特定值组合在一起。它们肯定比创建过多的包装类要好。
  • 当您需要从函数返回多个值时,它是 out/ref 参数的有用替代方法。

然而,C# 中的元组让我眼花缭乱。许多语言(如 OCaml、Python、Haskell、F# 等)都有用于定义元组的特殊、简洁的语法。例如,在 F# 中,Map 模块定义了一个构造函数,如下所示:

val of_list : ('key * 'a) list -> Map<'key,'a>

我可以使用以下方法创建地图实例:

(* val values : (int * string) list *)
let values = 
    [1, "US";
     2, "Canada";
     3, "UK";
     4, "Australia";
     5, "Slovenia"]

(* val dict : Map<int, string> *)
let dict = Map.of_list values

C# 中的等效代码很可笑:

var values = new Tuple<int, string>[] {
     new Tuple<int, string>(1, "US"),
     new Tuple<int, string>(2, "Canada"),
     new Tuple<int, string>(3, "UK"),
     new Tuple<int, string>(4, "Australia"),
     new Tuple<int, string>(5, "Slovenia")
     }

 var dict = new Dictionary<int, string>(values);

我不相信元组原则上有什么问题,但是 C# 的语法太麻烦了,无法充分利用它们。

于 2009-03-23T21:33:10.030 回答
5

这是代码重用。与其编写与我们创建的最后 5 个类元组类具有完全相同结构的又一个类,不如创建...一个元组类,并在需要元组时使用它。

如果该类的唯一意义是“存储一对值”,那么我会说使用元组是一个明显的想法。如果您开始实现多个相同的类只是为了重命名这两个成员,我会说这是一种代码味道(尽管我讨厌这个术语)。

于 2009-03-23T21:49:54.890 回答
2

这只是原型质量的代码,很可能被拼凑在一起,从未被重构过。不修复它只是懒惰。

元组的真正用途是用于真正不关心组件部分是什么的通用功能,而只是在元组级别运行。

于 2009-03-23T21:11:51.923 回答
2

Scala 具有元组值类型,从 2 元组(对)到具有 20 多个元素的元组。请参阅Scala 的第一步(第 9 步):

val pair = (99, "Luftballons")
println(pair._1)
println(pair._2)

如果您需要将值捆绑在一起以用于某些相对特殊的目的,则元组很有用。例如,如果您有一个函数需要返回两个完全不相关的对象,而不是创建一个新类来保存这两个对象,您可以从该函数返回一个 Pair。

我完全同意其他关于元组可能被滥用的海报。如果一个元组有任何对你的应用程序很重要的语义,你应该使用一个适当的类来代替。

于 2009-12-08T21:23:58.370 回答
1

最明显的例子是坐标对(或三元组)。标签无关紧要;使用 X 和 Y(和 Z)只是一个约定。使它们统一表明可以以相同的方式对待它们。

于 2009-03-23T21:26:36.850 回答
1

已经提到了很多事情,但我认为还应该提到,有一些编程风格与 OOP 不同,并且对于那些元组非常有用。

例如,像 Haskell 这样的函数式编程语言根本没有类。

于 2009-03-23T21:35:49.307 回答
0

如果您正在执行Schwartzian 变换以按特定键排序(重复计算代价高昂)或类似的东西,那么一个类似乎有点矫枉过正:

val transformed = data map {x => (x.expensiveOperation, x)}
val sortedTransformed = transformed sort {(x, y) => x._1 < y._1}
val sorted = sortedTransformed map {case (_, x) => x}

在这里拥有一个class DataAndKey或任何东西似乎有点多余。

不过,我同意你的例子不是一个很好的元组例子。

于 2009-03-23T22:39:25.223 回答