为什么要使用元组而不是对象?

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的许多其他用途也同样令人不满。

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

10个回答

35

首先,元组(tuple)非常快捷和简单:不需要为每个想要组合两个元素的情况编写一个类,可以使用模板来实现。

其次,它们是通用的。例如,在C++中,std::map使用了std::pair表示键和值。因此,可以使用任何一对类型组合,而不必为每种类型的组合制作某种包装器类。

最后,它们非常有用于返回多个值。没有理由专门为函数的多个返回值创建一个类,并且如果这些值是无关的,则不应将其视为一个对象。

公正地说,您粘贴的代码是对pair的不良使用。


5
只是在扮演魔鬼的辩手,但是你真的需要一个返回多个无关值的方法吗?我经常看到的一种情况是 C# 内置数据类型的 TryParse 方法。在这里,你需要返回一个 bool 值(表示解析是否成功),以及一个 int 值(如果解析成功则为该值)。为了允许第二个返回值,使用了 out 关键字。我想用元组可能更好一些,但我不确定它是否比声明一个具有命名为 ParseSuccessfulbool 和命名为 Value 的通用 T 的类型更好。 - devuxer
1
@DanThMan:是的,虽然它们并不无关。例如,C ++算法equal_range返回一对迭代器。在C ++中的映射相当于pair <key,value>集合。还有其他情况,但我现在想不起来了。 - rlbond
1
DanThMan:从支持多个返回值的语言的角度来看,我们也可以问同样的问题:如果你有多个返回值,你是否需要“out”参数?你想要返回两个东西,那么为什么一个看起来像是我在传递它呢? :-) - Ken
对我来说,人们如何捍卫元组等糟糕的编程实践真是令人惊讶。当然,对于一种没有对象的语言来说,这可能是一种方便的方式来组合两个东西。但是数字2有什么特别之处呢?为什么不是三、四、五个东西?固执地只关注2是对问题域的狭隘看法。除此之外,人们希望一个函数返回多个值。有人停下来想过吗?这不是函数应该做的事情。解决这个问题的方法是传递可变参数(即通过引用)。老实说,这并不是很难。 - Bungles
创建类型安全的模板类以实现代码重用并没有什么问题。但是说“这样很快很容易,因为我不必创建一个带有所有访问器等的类”是幼稚的。你确实正在创建一个类,只是通过语法糖隐藏了它。如果在C++中你没有意识到这一点,可能会导致几种不同类型的难以发现的错误。明确你的代码通常有助于避免这些类型的错误。混淆则会促进它们的出现。 - Bungles
显示剩余5条评论

17

元组在Python中被广泛使用,已经融入语言并非常有用(它们允许返回多个值)。

有时候,你只需要将事物配对起来,创建一个真正的、诚实的类是过度设计。另一方面,当你应该使用类时却使用元组同样是个坏主意。


1
+1 返回多个值是我使用 Pair 类的主要原因。Pair 类可能是在返回 Object[] 或全新类型之间的一个很好的折中方案。 - Tim Frey

11

这个代码示例存在几个问题:

  • 造轮子

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

  • 做出正方形的车轮

如果你有一组键值对,最好使用 KeyValuePair 结构而不是一个具有相同目的的类,因为它会导致更少的内存分配。

  • 隐藏意图

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


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# 的语法过于繁琐,难以充分利用它们。


你可以稍微整理一下。这个辅助类怎么样? 静态类 Tuple { 静态 Tuple<T0, T1> Build<T0, T1>(T0 t0, T1 t1); } 然后你就可以像这样创建一个元组:Tuple.Build(1, "US"); 利用泛型类型推断函数。 :) - jalf
虽然还不如Python或ML/F#那样简洁,但至少你可以避免指定泛型参数。 - jalf
你的元组代码可以使用字典初始化器来完成,类似于这样 (new Dictionary<int, string>() { {1, "美国"} {2, "加拿大" } }).ToTuple()? - Paul Stovell
回复:传递ID:实际上,传递ID非常方便,特别是当您从一层到另一层时。但在这种情况下,您必须反向工程代码才能弄清楚第一个整数是ID。 - Mr. Shiny and New 安宇

5

这是代码复用。不要再写一个与我们之前创建的5个类完全相同结构的类,而是创建一个元组类,并在需要元组时使用它。

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


3

这只是原型代码,很可能是随意拼凑而成,从未重构过。不修复它只是懒惰。

元组的真正用途是用于通用功能,实际上并不关心组成部分是什么,而是在元组级别上运行。


他所建议的确实可行。如果没有更多上下文,我无法对此发表评论。 - Eclipse

2

Scala拥有元组类型,从2元组(对)一直到具有20个以上元素的元组。请参见Scala入门第九步

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

如果您需要将一些相对零散的值捆绑在一起,元组非常有用。例如,如果您有一个需要返回两个不太相关对象的函数,而不是创建一个新类来保存这两个对象,您可以从该函数返回一个Pair。

我完全同意其他帖子中提到的元组可能被滥用的观点。如果元组对于您的应用程序有任何重要的语义,您应该使用适当的类。


2

已经提到了很多事情,但我认为还应该提到一些编程风格,这些风格与面向对象编程不同,对于这些风格来说,元组非常有用。

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


1

一个明显的例子是坐标对(或三元组)。标签是无关紧要的;使用X和Y(以及Z)只是一种惯例。使它们统一可以清楚地表明它们可以以相同的方式处理。


@JohnGibb - Tuple 中的项目应具有不同的含义。这就是为什么您必须单独指定它们的类型:Item1 可以是字符串,但 Item2 可以是整数等。 - Daniel Earwicker
事实上,xy实际上具有对称性(通常在坐标系中,我们将每个维度视为相同),因此更好的做法是进一步将它们放入一个数组中! - Daniel Earwicker

0

如果你正在使用Schwartzian transform按特定键排序(重复计算代价高),或者类似的操作,那么使用一个类可能有点过度设计:

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}

在这里使用一个名为 DataAndKey 的类似乎有点多余。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接