.NET 4.0 中元组(Tuple)的实际应用示例是什么?

98

我见过在 .Net 4 中引入的元组(Tuple),但我无法想象它可以在哪里使用。我们总是可以制作自定义类或结构体。


13
也许这是个值得注意的地方,这个话题非常古老了。看看C# 7发生了什么! - maf-soft
19个回答

83

这就是重点——并不总是需要创建自定义类或结构体更加方便。这就像ActionFunc一样的改进……你可以自己创建这些类型,但是它们在框架中存在非常方便。


5
可能值得指出的是,从MSDN上可以看到:“此类型的任何公共静态成员均为线程安全。不能保证任何实例成员都是线程安全的。 - Matt Borja
也许语言设计者应该让动态创建自定义类型变得更容易,这样我们就可以保持相同的语法,而不是引入另一种语法? - Thomas Eyde
@ThomasEyde 这正是他们过去15年一直在做的事情。这就是表达式主体成员和现在的值元组是如何添加的。记录样式类在8月份(此评论9个月前)差点没能进入C# 7版本,可能会在C# 8中推出。还要注意,元组提供了值相等性,而普通类则没有。在2002年引入所有这些将需要先见之明。 - Panagiotis Kanavos
你所说的“方便不创建自定义类”是什么意思?你是指使用=new..()来创建自定义类实例吗? - user786

75

使用元组,你可以轻松实现一个二维字典(或者n维字典)。例如,你可以使用这样的字典来实现货币兑换映射:

var forex = new Dictionary<Tuple<string, string>, decimal>();
forex.Add(Tuple.Create("USD", "EUR"), 0.74850m); // 1 USD = 0.74850 EUR
forex.Add(Tuple.Create("USD", "GBP"), 0.64128m);
forex.Add(Tuple.Create("EUR", "USD"), 1.33635m);
forex.Add(Tuple.Create("EUR", "GBP"), 0.85677m);
forex.Add(Tuple.Create("GBP", "USD"), 1.55938m);
forex.Add(Tuple.Create("GBP", "EUR"), 1.16717m);
forex.Add(Tuple.Create("USD", "USD"), 1.00000m);
forex.Add(Tuple.Create("EUR", "EUR"), 1.00000m);
forex.Add(Tuple.Create("GBP", "GBP"), 1.00000m);

decimal result;
result = 35.0m * forex[Tuple.Create("USD", "EUR")]; // USD 35.00 = EUR 26.20
result = 35.0m * forex[Tuple.Create("EUR", "GBP")]; // EUR 35.00 = GBP 29.99
result = 35.0m * forex[Tuple.Create("GBP", "USD")]; // GBP 35.00 = USD 54.58

我宁愿使用枚举来表示国家缩写,这可行吗? - Zack
1
当然,它应该可以正常工作,因为枚举是值类型。 - MarioVW
@MarioVW 这也可以通过使用多维数组来实现。元组有什么不同之处? - user786

26

有一篇在MSDN杂志上的优秀文章讲述了向BCL添加Tuple时所进行的抱怨和设计考虑。 在值类型和引用类型之间进行选择特别有趣。

正如文章所清楚表明的那样,Tuple背后的推动力是Microsoft内部许多团队都有使用它的需求,尤其是F#团队。 虽然没有提到,但我认为C#(和VB.NET)中的新“dynamic”关键字也与此有关,元组在动态语言中非常常见。

除此之外,它并不特别优于创建自己的POCO类,至少您可以给成员一个更好的名称。


更新:由于将在C#版本7中进行大修订,现在将获得更多的语法支持。 初步公告在此博客文章中。


24

以下是一个简单的例子 - 假设你有一个方法需要查找用户ID对应的用户名和电子邮件地址。你可以创建一个包含这些数据的自定义类,或使用ref / out参数来传递这些数据,或者你可以返回一个Tuple,从而拥有良好的方法签名,而无需创建新的POCO。

public static void Main(string[] args)
{
    int userId = 0;
    Tuple<string, string> userData = GetUserData(userId);
}

public static Tuple<string, string> GetUserData(int userId)
{
    return new Tuple<string, string>("Hello", "World");
}

10
这是一个很好的例子,然而并不能证明使用元组的必要性。 - Amitabh
7
在这种情况下,元组是一个不错的选择,因为它返回不同的值;但当你返回多个不同类型的值时,元组更加突出。 - Mark Rushakoff
22
另一个很好的例子是int.TryParse,您可以消除输出参数,改用元组。因此,您可以使用Tuple<bool, T> TryParse<T>(string input),而无需使用输出参数,在元组中获取两个值返回。 - Tejs
3
实际上,当你从F#调用任何TryParse方法时,就会发生这种情况。 - Joel Mueller

23

我用一个元组来解决欧拉计划第11题

class Grid
{
    public static int[,] Cells = { { 08, 02, 22, // whole grid omitted

    public static IEnumerable<Tuple<int, int, int, int>> ToList()
    {
        // code converts grid to enumeration every possible set of 4 per rules
        // code omitted
    }
}

现在,我可以通过以下方法解决整个问题:
class Program
{
    static void Main(string[] args)
    {
        int product = Grid.ToList().Max(t => t.Item1 * t.Item2 * t.Item3 * t.Item4);
        Console.WriteLine("Maximum product is {0}", product);
    }
}

对于这个,我本来可以使用自定义类型的,但它看起来会和元组一模一样


16

C#的元组语法非常臃肿,因此声明元组很麻烦。而且它没有模式匹配,所以使用起来也很麻烦。

但有时候,你只是想要一个临时的对象组合,而不想为它创建一个类。例如,假设我想聚合一个列表,但我想要两个值而不是一个:

// sum and sum of squares at the same time
var x =
    Enumerable.Range(1, 100)
    .Aggregate((acc, x) => Tuple.Create(acc.Item1 + x, acc.Item2 + x * x));

与其将一系列值组合成单个结果,不如将单个结果扩展为一系列值。编写此函数的最简单的方法是:

static IEnumerable<T> Unfold<T, State>(State seed, Func<State, Tuple<T, State>> f)
{
    Tuple<T, State> res;
    while ((res = f(seed)) != null)
    {
        yield return res.Item1;
        seed = res.Item2;
    }
}

f将某些状态转换为元组。我们返回元组的第一个值,并将新状态设置为第二个值。这使我们能够在整个计算过程中保留状态。

您可以这样使用它:

// return 0, 2, 3, 6, 8
var evens =
    Unfold(0, state => state < 10 ? Tuple.Create(state, state + 2) : null)
    .ToList();

// returns 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
var fibs =
    Unfold(Tuple.Create(0, 1), state => Tuple.Create(state.Item1, Tuple.Create(state.Item2, state.Item1 + state.Item2)))
    .Take(10).ToList();

evens 相对比较简单,但是 fibs 则更加巧妙。它的 state 实际上是一个元组,分别保存着 fib(n-2) 和 fib(n-1)。


4
Tuple.Create是一个方便的简写方式,用于创建new Tuple<Guid, string, ...> - AaronLS
@Juliet State关键字的用途是什么? - irfandar

7

我不喜欢滥用它们,因为它们生成的代码无法自我解释,但是它们非常适合实现即时复合键,因为它们实现了IStructuralEquatable和IStructuralComparable(用于查找和排序)。

它们内部组合了所有项的哈希码;例如,这是Tuple的GetHashCode(摘自ILSpy):

    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer)
    {
        return Tuple.CombineHashCodes(comparer.GetHashCode(this.m_Item1), comparer.GetHashCode(this.m_Item2), comparer.GetHashCode(this.m_Item3));
    }

7

元组非常适用于同时执行多个异步IO操作并一起返回所有值。以下是使用元组进行和不使用元组进行的示例。实际上,元组可以使您的代码更清晰!

没有元组(嵌套可怕!):

Task.Factory.StartNew(() => data.RetrieveServerNames())
    .ContinueWith(antecedent1 =>
        {
            if (!antecedent1.IsFaulted)
            {
                ServerNames = KeepExistingFilter(ServerNames, antecedent1.Result);
                Task.Factory.StartNew(() => data.RetrieveLogNames())
                    .ContinueWith(antecedent2 =>
                        {
                            if (antecedent2.IsFaulted)
                            {
                                LogNames = KeepExistingFilter(LogNames, antecedent2.Result);
                                Task.Factory.StartNew(() => data.RetrieveEntryTypes())
                                    .ContinueWith(antecedent3 =>
                                        {
                                            if (!antecedent3.IsFaulted)
                                            {
                                                EntryTypes = KeepExistingFilter(EntryTypes, antecedent3.Result);
                                            }
                                        });
                            }
                        });
            }
        });

使用元组

Task.Factory.StartNew(() =>
    {
        List<string> serverNames = data.RetrieveServerNames();
        List<string> logNames = data.RetrieveLogNames();
        List<string> entryTypes = data.RetrieveEntryTypes();
        return Tuple.Create(serverNames, logNames, entryTypes);
    }).ContinueWith(antecedent =>
        {
            if (!antecedent.IsFaulted)
            {
                ServerNames = KeepExistingFilter(ServerNames, antecedent.Result.Item1);
                LogNames = KeepExistingFilter(LogNames, antecedent.Result.Item2);
                EntryTypes = KeepExistingFilter(EntryTypes, antecedent.Result.Item3);
            }
        });

如果您已经使用了一个隐含类型的匿名函数,那么使用元组并不会使代码变得不清晰。从方法中返回元组?在我个人看来,当代码清晰度至关重要时,请谨慎使用。我知道在C#中进行函数式编程很难抗拒,但我们必须考虑所有那些古老而笨重的“面向对象”的C#程序员。


5

我倾向于避免在大多数情况下使用Tuple,因为它会影响可读性。然而,在需要组合不相关的数据时,Tuple非常有用。

举个例子,假设你有一份汽车清单以及它们购买的城市:

Mercedes, Seattle
Mustang, Denver
Mercedes, Seattle
Porsche, Seattle
Tesla, Seattle
Mercedes, Seattle

你想为每个城市的每辆汽车聚合计数:
Mercedes, Seattle [3]
Mustang, Denver [1]
Porsche, Seattle [1]
Tesla, Seattle [1]

为了实现这一点,您需要创建一个Dictionary,有几个选项:
  1. 创建一个Dictionary<string,Dictionary<string,int>>
  2. 创建一个Dictionary<CarAndCity,int>
  3. 创建一个Dictionary<Tuple<string, string>,int>
第一种选择会失去可读性,需要编写更多的代码。
第二个选项是行之有效的,但汽车和城市并没有真正相关,并且可能不应该在同一个类中。
第三个选项简洁明了。这是Tuple很好的使用方式。

5

元组在函数式语言中被广泛使用,可以做更多的事情。现在F#是一个“官方”的.NET语言,您可能想要从C#与之互操作,并在两种语言编写的代码之间传递它们。


元组也是一些流行脚本语言的内置类型,例如Python和Ruby(它们还具有用于交互操作的.Net实现...IronPython/Ruby)。 - MutantNinjaCodeMonkey

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