何时使用:C# 7.0 中的元组与 Class

When to use: Tuple vs Class in C# 7.0

在元组之前,我曾经创建一个 class 及其变量,然后从这个 class 创建对象,并使该对象成为某些函数的 return 类型。

现在,对于元组,我可以做同样的事情,在 C# 7.0 中,我们可以为元组属性分配易于理解的名称(在此之前,它是 item1item2 等。 )

所以现在我想知道,什么时候使用元组以及什么时候在 C# 7.0 中创建 class?

一般来说,命名 classes 在您的系统设计中具有一定的意义。它们写起来也更冗长。例如,您可能有一个名为 MediaFileOpener 的 class。我们知道这个 class 的作用对设计很重要 - 我们正在处理媒体文件!

匿名类型和元组在没有设计意义且您只需要一个轻量级数据传输对象 (DTO) 来移动信息时使用。

通常,如果您的 class 需要一些文档来描述它的用途,或者如果它提供了一些行为,请使用完整的 class。如果您只需要临时存储或某种分组,请使用元组。考虑一种情况,您希望 return 来自异步方法的多个值。元组旨在解决该问题。

元组旨在表示多个值,例如当一个方法打算 return 多个值时。 C# 7 中的元组支持使用 System.ValueTuple<...> 实例来表示该组值。这些值的名称仅在使用它们的上下文中有效,并不强制执行。

类 旨在表示具有多个属性的单个值。

使用一个Class

如果您的对象是在整个应用程序中广泛使用的实体,并且还存储在某种持久性存储中,例如关系数据库(SQL服务器,MySQL,SQL ite),NoSQL 数据库或缓存(Redis、Azure DocumentDB),甚至是简单的文本文件或 CSV。

所以是的,任何持久的东西都应该有自己的 class。

使用元组

如果您的对象寿命很短,对您的应用程序没有特殊意义。例如,如果你需要快速 return 一对坐标,最好有这样的东西:

(double Latitude, double Longitude) getCoordinates()
{
    return (144.93525, -98.356346);
}

而不是单独定义 class

class Coordinates
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

元组将节省您使用 new 在堆上分配内存的时间,这样一个简单的操作。

我发现元组有用的另一次是在对某些操作数执行多项数学运算时

(double Result1, double Result2, double Result3) performCalculations(int operand1, int operand 2)

在这种情况下定义 class 没有意义。不管是什么计算结果都不属于class。所以另一种选择是使用 out 变量,但我相信元组更具表现力并提高可读性。

通常,当您的对象将在其他地方使用或者它代表您所在领域中的真实对象或概念时,您希望有一个 class。您可能会创建一个 class 来表示汽车或汽车商店,而不是元组。

另一方面,有时您只想从一个方法中 return 几个对象。也许它们并不代表任何特殊的东西,只是你需要在那个特定的方法中将它们 return 在一起。有时,即使它们确实代表了您领域中的概念(假设您正在 returning (Car, Store),它可以表示为 Sale 对象),您实际上并不打算在任何地方使用它们——您只是在移动数据。在这些情况下,可以使用元组。

现在,具体谈到 C#,您还应该知道一件事。 C# 7 的元组类型实际上是 ValueTuple,它是一个结构。与引用类型 classes 不同,结构是值类型。您可以在 msdn 上阅读更多相关信息。最重要的是,要知道它们可能涉及大量复制,所以要小心。

我认为这将成为一个被问到很多的问题。目前没有 "best practice" 何时使用新值元组与 class.

不过,值得一读what came up during previous conversations about the previous versions of tuples vs a class

在我看来,值元组应该尽量少用,最多不超过三个值。我认为这很好地平衡了 "returning some values without the need for a class" 和 "horrific mess of values"。如果 return 的值超过三个,请创建 class.

我也永远不会使用消费者必须使用的 public 面向 API 的 return 的元组。同样,只需使用 class.

这是我使用过的一些真实代码:

public async Task<(double temperature, double humidity, string description)> DownloadTodaysForecast()

一旦我想要 return 更复杂的数据,我就制作一个 class。

首先我要提到 C# 已经支持 anonymous types。哪些是引用类型。因此,您已经有了一个合适的替代方法来创建命名的 class.

命名 class 的一个优点是更易于重用(例如,如果您在多个地方需要相同的类型)和文档。由于匿名类型是匿名的,如果你可以使用 var,你只能得到输入给它的变量,这限制了匿名类型有用的上下文(例如,你不能将它们用作字段类型,return类型或参数类型)。

当然,您可以使用 System.Tuple 来克服匿名类型的一些限制。这也是一种引用类型,您可以显式使用它。缺点是它缺少成员的自定义名称。


C# 7 元组 (ValueTuple) 可以被认为类似于匿名类型。第一个区别是它们是值类型。这意味着只要这些元组留在本地范围内或在堆栈中移动(由于其限制,这是匿名类型的常见用法),它们将具有性能优势。

第二个区别是新语法允许元组出现在比匿名类型更多的地方,如你所知,你有语法糖将 return 类型定义为 ValueTuple(同时使用匿名类型你必须 return object).

第三个区别是ValueTuple支持开箱即用的解构。引用 What’s New in C# 7.0:

Another way to consume tuples is to deconstruct them. A deconstructing declaration is a syntax for splitting a tuple (or other value) into its parts and assigning those parts individually to fresh variables:

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");

您还可以通过添加 Deconstruct method.

来使用自定义类型

摘要:

  • ValueTuple在C#7.0中有语法糖,可读性要考虑。
  • ValueTuple 是值类型。使用 classstruct 之间的所有优缺点均适用。
  • ValueTuple 可以显式使用(有或没有语法糖)允许它在保留命名成员的同时具有 System.Tuple 的多功能性。
  • ValueTuple支持解构

鉴于它必须是语法糖,我会说选择 ValueTuple 的更强论据与选择 struct 的论据相同。这对于主要存在于堆栈中的小型不可变类型来说是理想的选择(因此您不需要大量的装箱和拆箱)。

ValueTuple 与完整的结构进行比较,考虑到语法糖,我建议默认使用 ValueTuple,除非您需要 explicit layout 或需要添加方法

我还想说句法糖不一定能提高可读性。主要原因是您没有命名类型,类型的名称为代码提供了意义。除此之外,您可以将文档添加到 structclass 声明中,以便于理解。

总而言之,ValueTuple 真正出色的地方是 return 从一个方法中获取多个值。在这种情况下,无需创建新的 out 参数。所使用的 ValueTuple 的文档可以存在于方法的文档中。如果您发现需要使用 ValueTuple 做一些其他事情(例如定义扩展方法),我建议您考虑创建一个命名类型。

由于这个答案在这里引起了一些人的混淆,我应该澄清一下 - 根据问题 - 这里对 "tuple" 的所有引用都指的是 ValueTuple 类型和新的元组语法糖C# 7 的特性,绝不会引用旧的 System.Tuple 引用类型。

So now I am wondering, when Should I use tuples and when Should I create a class in c# 7.0?

只有您才能真正回答这个问题,因为它实际上取决于您的代码。

但是,您可以遵循一些准则和规则来指导您在它们之间进行选择:

元组是值,因此按值复制,而不是按引用复制。

大多数时候,这应该不是问题。但是,如果您传递大型结构的元组,这可能会对性能产生影响。不过,Ref locals/returns 可用于解决这些性能问题。

此外,因为它们是值,所以远程修改副本不会更改原始副本。这是一件好事,但可能会引起一些人的注意。

不保留元组元素名称

元素的名称由编译器使用,并且(在大多数情况下)在 运行 时不可用。这意味着不能使用反射来发现它们的名字;它们不能动态访问,也不能在剃刀视图中使用。

这也是 API 的一个重要考虑因素。从方法 return 编辑的元组是有关编译后名称可发现性规则的例外。编译器将属性添加到保存有关元组名称的信息的方法。这意味着您可以安全地 return 来自一个程序集中的 public 方法的元组,并在另一个程序集中访问它的名称。

元组是轻量级的

元组比类型更容易编写,因为它们不那么冗长并且声明可以是"inlined"(即在使用时声明)。例如,在声明 return 多个值的方法时,这很有效。

但是,因为它们是在使用时声明的,所以如果您有 MethodA 调用 MethodB 调用 MethodC 并且每个 return 是一个元组,您每个阶段都需要重新定义元组。没有(yet)创建元组别名并在多个方法中重复使用它的方法。

只要运用常识

对于您可能考虑使用元组的任何情况:只需问自己这个问题:"will a tuple simplify the code here"。如果答案是 "yes",则使用一个。这最终是使用元组还是自定义 class.

的主要考虑因素

我会避免使用元组作为 return 类型的 public 方法。在这种情况下,我更愿意定义一个 class 或 struct.

当您想将多个值(可以是不同类型)组合到一个对象中而不创建自定义 class 时,元组是一个很好的选择。在这种情况下,Tuple 将是一个快速且完美的选择。

我最近比较了一个案例的性能差异,我涉及一个包含 2(或 4)个基元的元组和一个包含 2(或 4)个相同基元类型的 class。我在这里分享,以防对任何人有帮助。

对于只使用一次的快速代码,使用元组。如果必须维护代码,请使用 类。查看代码并看到如下表达式令人沮丧:

if (t.Item4 == x.Item3)