单元素元组和零元素元组有什么用



C# 7.0 引入了值元组,并且还引入了一些语言级别的支持。他们还添加了对单元素元组和零元素元组的支持;但是,我找不到它们有用的任何情况。

通过ValueTuple.Create重载,我可以创建任何类型的元组,但 C# 7.0 语法只允许至少两个元素:

Microsoft (R) Roslyn C# Compiler version 2.8.3.62923
Loading context from 'CSharpInteractive.rsp'.
Type "#help" for more information.
> ValueTuple.Create()
[()]
> ValueTuple.Create(1)
[(1)]
> ValueTuple.Create(1, "x")
[(1, x)]

通过元组语法:

> var t2 = (1, 2);
> var t1 = (1); // t1 is now int (of course)
> ValueTuple<int> t1 = (1);
(1,23): error CS0029: Cannot implicitly convert type 'int' to 'ValueTuple<int>'
> ValueTuple<int> t1 = new ValueTuple<int>(1);
> t1
[(1)]

我想我找到了请求此功能的线程,但现在没有任何代码示例在 C# 中有效,并且在 C# 8.0 的计划功能中也找不到任何引用,甚至在递归元组模式中也是如此。

在请求线程中提到了函数式编程语言。现在是否有任何函数式语言使用它们?我不是 F# 专家,但它的元组参考没有提到单元素元组和零元素元组的任何使用。

所以TL;灾难恢复问题:

  • 单元素元组和零元素元组是否在任何(可能是功能性).NET 语言中使用?我的意思不是Tuple.Create或构造函数,而是母语支持。
  • 它们是否计划在未来版本的 C# 中使用?
  • 还是它们是为了"以防万一",为了未来的兼容性?

0 元组有什么用?

2 元组或 3 元组表示一组相关项。(2D 空间中的点、颜色的 RGB 值等)1 元组不是很有用,因为它可以很容易地替换为单个int

0元组似乎更无用,因为它绝对不包含任何东西。然而,它具有使其在函数式语言(如 F#)中非常有用的属性。例如,0 元组类型只有一个值,通常表示为()。所有 0 元组都具有此值,因此它本质上是单例类型。在大多数函数式编程语言(包括 F#)中,这称为unit类型。

在 C# 中返回void的函数将返回 F# 中的unit类型:

let printResult = printfn "Hello"

在 F# 交互式解释器中运行它,你将看到:

val printResult : unit = ()

这意味着值printResult的类型为unit,并且具有值()(空元组,unit类型的唯一值)。

函数也可以将unit类型作为参数。在 F# 中,函数可能看起来不带任何参数。但实际上,他们采用的是unit类型的单个参数。此函数:

let doMath() = 2 + 4

实际上相当于:

let doMath () = 2 + 4

也就是说,一个函数,它采用一个类型unit的参数并返回int值 6。如果查看定义此函数时 F# 交互式解释器打印的类型签名,您将看到:

val doMath : unit -> int

事实上,所有函数都将接受至少一个参数并返回一个值,即使该值有时是像()这样的"无用"值,这意味着函数组合在 F# 中比在没有unit类型的语言中要容易得多。但这是一个更高级的主题,我们将在后面讨论。现在,请记住,当您在函数签名中看到unit,或在函数的参数中看到()时,这是 0 元组类型,可以作为说"此函数不获取或返回任何有意义的值"的方式。

我想不出一个元素元组的用例。编辑 - 正如所指出的,引用的页面提到了一个元素元组作为命名返回参数的一种方式,这听起来实际上很有用,尽管这可能是特定于 C# 的。

零元素元组在函数式语言中也称为单位。这有点相当于 C# 中的 void,而某些(大多数?)函数式语言没有。

不同之处在于()是一个实际值,你可以用它来做一些事情,比如保持一个列表,模式匹配等。 函数式语言需要这个,因为函数必须返回一些东西,如果你想返回"什么",你必须通过返回()来显式地做到这一点。您还可以编写一个接受单位作为参数的函数,这基本上意味着它是一个执行延迟的值,很可能有副作用:

let x = getFromDb() // will read DB
stream.Flush() // will change state

在 C# 中不支持 o-tuple (nople) 和 1-tuple(oneple)。

但是,在基类库(BCL)上,为了连贯性,以防万一将来有人发现它的用途,有ValueTupleValueTuple<T>。例如,具有 0 元组和 1 元组构造的语言。

ValueTuple有一些附加值。从文档中:

结构表示没有元素的元组。它主要用于其静态方法,这些方法允许您创建和比较值元组类型的实例。它的帮助程序方法允许您实例化值元组,而无需显式指定每个值元组组件的类型。通过调用其静态 Create 方法,可以创建具有 0 到 8 个组件的值元组。对于具有八个以上组件的值元组,必须调用 ValueTuple 构造函数。

1 元组可以用作可空值周围的非空包装器。这在尝试添加到字典时很有用,例如:

void Process<TKey>(TKey[] keys)
{
var dict = new Dictionary<TKey, Something>();
var firstKey = keys[0];
dict[firstKey] = new Something();
...
}

如果启用了nullable(创建dict时),将显示警告,因为TKey可能是可为空的类型(API 可能愿意接受),并且Dictionary<TKey, TValue>不接受null; 而:

void Process<TKey>(TKey[] keys)
{
var dict = new Dictionary<ValueTuple<TKey>, Something>();
var firstKey = ValueTuple.Create(keys[0]);
dict[firstKey] = new Something();
...
}

不会显示警告。

1 元组可以用作委托的单个参数列表。元组非常适合用作委托参数列表,因为您可以更改其结构而不会破坏委托的实现。您也可以通过这种方式命名委托的参数。但是由于不能使用 1 元组,因此如果参数数更改为 1,则必须恢复为常规参数。

例如:

int DoSomething(
Func<(int First, int Second, int Third), int> func
) =>
func((1, 2, 3));
// notice that you could add more parameters or change the order
// of the values and not break the lambda
DoSomething(args => 
args.First + args.Second - args.Third
);
// DOESN'T WORK
int DoSomethingElse(
Func<(int First), int> func
) =>
func((1));
// does work but not as nice
int DoSomethingElse2(
Func<int, int> func
) =>
func(1);
// can do it this way; no naming
int DoSomethingElse3(
Func<ValueTuple<int>, int> func
) =>
func(new(1));
DoSomethingElse3(args =>
args.Item1
);

1 元组的另一个用途是允许嵌套的可为空性。如果您直接使用ValueTuple结构,您实际上可以执行此操作。

ValueTuple<string?>? maybeTupleOfMaybeString = new(null);
Assert.IsTrue(maybeTupleOfMaybeString.HasValue);
ValueTuple<string?> tupleOfMaybeString = maybeTupleOfMaybeString.Value;
string? maybeString = tupleOfMaybeString.Item1;
Assert.IsNull(maybeString);

另一种用途是,如果要通常指定值集合,然后在某些情况下可以指定 1(或 0)个这样的值。

abstract class GenericBase<TTuple>
where TTuple : struct, ITuple {
public void DoSomething(
TTuple tuple
) {
}
}
...
class OneTupleImpl : GenericBase<ValueTuple<int>> {
...
DoSomething(new(5));
...
}

目前,ValueTuple'1仅用于长元组的表示,即使 1 元组没有 C# 元组语法:ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8>>是 8 元组的基础类型。

至于 0 元组和ValueTuple'0,我们推测它们可能会出现在未来的场景中(如果我没记错的话,可能是一些递归模式)。

单值元组可用于描述方法的返回值。 有时,提供有关返回值是什么的其他信息很有用,例如:

(int ScreenNumber) AddingTarget(string stockItemCode); 

我在处理 HTTP GET 查询字符串时经常遇到 1 元组类型。

泛型查询解析器通常会将查询字符串转换为具有数组/元组成员的某种结构。例如,查询字符串?format=pill&color=red&color=blue可以转换为以下中间结构。

{
format: ["pill"],
color: ["red", "blue"],
}

在类型级别上,我们可以使用 1 元组类型Tuple<Format>来指示应该只有一个Format类型的值,并且我们可以使用数组类型Array<Color>来指示Color类型的几个值是可接受的。

{
format: Tuple<Format>,
color: Array<Color>,
}

1 元组仅在我们处理泛型中间表示时才有意义。我们通常希望完全删除 1 元组,并使用以下同构结构,也许不那么容易混淆。

{
format: Format,
color: Array<Color>,
}

但是,只有在我们将泛型结构转换为以下特定于应用程序的结构之后,才能使用上述类型签名。

{
format: "pill",
color: ["red", "blue"],
}

根据我的经验,完成删除 1 元组的额外步骤通常是值得的。但是,基于 1 元组的替代方法在执行转换时可以用作健全性检查。

请注意,可以反向执行相同的步骤,以从解析的结构构造查询字符串。可以在两个方向上使用相同的数据结构。

空元组由一对空括号构造;具有一个项目的元组是通过在值后面加上逗号来构造的(将单个值括在括号中是不够的)

最新更新