172

随着在 .net 4 中添加Tuple类,我一直在尝试确定在我的设计中使用它们是否是一个糟糕的选择。在我看来,元组可以成为编写结果类的捷径(我相信还有其他用途)。

所以这:

public class ResultType
{
    public string StringValue { get; set; }
    public int IntValue { get; set; }
}

public ResultType GetAClassedValue()
{
    //..Do Some Stuff
    ResultType result = new ResultType { StringValue = "A String", IntValue = 2 };
    return result;
}

相当于这个:

public Tuple<string, int> GetATupledValue()
{
    //...Do Some stuff
    Tuple<string, int> result = new Tuple<string, int>("A String", 2);
    return result;
}

所以撇开我错过元组的可能性不谈,元组的例子是一个糟糕的设计选择吗?对我来说,它似乎不那么混乱,但不像自我记录和干净。这意味着使用 type ResultType,稍后将非常清楚类的每个部分的含义,但是您需要维护额外的代码。有了它,Tuple<string, int>您将需要查找并弄清楚每个Item代表什么,但您编写和维护的代码更少。

您在此选择方面的任何经验将不胜感激。

4

13 回答 13

167

如果您控制创建和使用元组,那么元组非常棒——您可以维护上下文,这对于理解它们至关重要。

但是,在公共 API 上,它们的效果较差。消费者(不是您)必须猜测或查找文档,尤其是对于Tuple<int, int>.

我会将它们用于私有/内部成员,但将结果类用于公共/受保护成员。

这个答案也有一些信息。

于 2010-06-10T18:49:17.563 回答
79

在我看来,元组是编写结果类的捷径(我相信还有其他用途)。

确实还有其他有价值的用途Tuple<>- 其中大多数涉及抽象出共享相似结构的特定类型组的语义,并将它们简单地视为有序的值集。在所有情况下,元组的一个好处是它们可以避免将您的命名空间与只公开属性但不公开方法的纯数据类混淆。

这是一个合理使用的示例Tuple<>

var opponents = new Tuple<Player,Player>( playerBob, playerSam );

在上面的示例中,我们想要表示一对对手,元组是配对这些实例的便捷方式,而无需创建新类。这是另一个例子:

var pokerHand = Tuple.Create( card1, card2, card3, card4, card5 );

扑克手可以被认为只是一组卡片 - 元组(可能是)表达该概念的合理方式。

撇开我错过元组的可能性不谈,元组的例子是一个糟糕的设计选择吗?

Tuple<>作为公共类型的公共 API 的一部分返回强类型实例并不是一个好主意。正如您自己所认识到的,元组要求相关各方(图书馆作者、图书馆用户)提前就所使用的元组类型的目的和解释达成一致。创建直观清晰的 API 具有足够的挑战性,Tuple<>公开使用只会掩盖 API 的意图和行为。

匿名类型也是一种元组- 但是,它们是强类型的,允许您为属于该类型的属性指定清晰、信息丰富的名称。但是匿名类型很难在不同的方法中使用——它们主要是为了支持像 LINQ 这样的技术,在这种技术中,投影会产生我们通常不想为其分配名称的类型。(是的,我知道编译器会合并具有相同类型和命名属性的匿名类型)。

我的经验法则是: 如果您要从公共接口返回它 - 将其设为命名类型

我使用元组的另一个经验法则是: 尽可能清楚地命名方法参数和局部变量类型- 使名称代表元组元素之间关系的含义。Tuple<>想想我的var opponents = ...例子。

这是一个真实案例的示例,我曾经Tuple<>避免声明仅在我自己的程序集中使用的纯数据类型。TryGetValue()这种情况涉及这样一个事实,当使用包含匿名类型的通用字典时,使用该方法在字典中查找项目变得困难,因为该方法需要一个out无法命名的参数:

public static class DictionaryExt 
{
    // helper method that allows compiler to provide type inference
    // when attempting to locate optionally existent items in a dictionary
    public static Tuple<TValue,bool> Find<TKey,TValue>( 
        this IDictionary<TKey,TValue> dict, TKey keyToFind ) 
    {
        TValue foundValue = default(TValue);
        bool wasFound = dict.TryGetValue( keyToFind, out foundValue );
        return Tuple.Create( foundValue, wasFound );
    }
}

public class Program
{
    public static void Main()
    {
        var people = new[] { new { LastName = "Smith", FirstName = "Joe" },
                             new { LastName = "Sanders", FirstName = "Bob" } };

        var peopleDict = people.ToDictionary( d => d.LastName );

        // ??? foundItem <= what type would you put here?
        // peopleDict.TryGetValue( "Smith", out ??? );

        // so instead, we use our Find() extension:
        var result = peopleDict.Find( "Smith" );
        if( result.First )
        {
            Console.WriteLine( result.Second );
        }
    }
}

PS还有另一种(更简单)的方法可以解决字典中匿名类型引起的问题,那就是使用var关键字让编译器为您“推断”类型。这是那个版本:

var foundItem = peopleDict.FirstOrDefault().Value;
if( peopleDict.TryGetValue( "Smith", out foundItem ) )
{
   // use foundItem...
}
于 2010-06-10T19:31:21.203 回答
18

元组可能很有用……但以后它们也可能会很痛苦。如果您有一个返回的方法,Tuple<int,string,string,int>那么您以后如何知道这些值是什么。是他们ID, FirstName, LastName, Age还是他们UnitNumber, Street, City, ZipCode

于 2010-06-10T18:48:29.450 回答
9

从 C# 程序员的角度来看,元组对 CLR 的补充是相当平庸的。如果您有一组长度不同的项目,则不需要它们在编译时具有唯一的静态名称。

但是,如果您有一个恒定长度的集合,这意味着集合中的固定位置每个都有特定的预定义含义。在这种情况下,最好给它们适当的静态名称,而不是记住 , 等的Item1意义Item2

C# 中的匿名类已经为最常见的元组私有使用提供了极好的解决方案,它们为项目提供了有意义的名称,因此在这个意义上它们实际上是优越的。唯一的问题是它们不能从命名方法中泄漏出来。我更愿意看到解除限制(也许只针对私有方法)而不是对 C# 中的元组有特定的支持:

private var GetDesserts()
{
    return _icecreams.Select(
        i => new { icecream = i, topping = new Topping(i) }
    );
}

public void Eat()
{
    foreach (var dessert in GetDesserts())
    {
        dessert.icecream.AddTopping(dessert.topping);
        dessert.Eat();
    }
}
于 2010-06-11T15:08:48.163 回答
8

与关键字类似var,它的目的是为了方便 - 但也很容易被滥用。

以我最卑微的观点,不要暴露Tuple为返回班。如果服务或组件的数据结构需要它,请私下使用它,但从公共方法返回格式良好的知名类。

// one possible use of tuple within a private context. would never
// return an opaque non-descript instance as a result, but useful
// when scope is known [ie private] and implementation intimacy is
// expected
public class WorkflowHost
{
    // a map of uri's to a workflow service definition 
    // and workflow service instance. By convention, first
    // element of tuple is definition, second element is
    // instance
    private Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> _map = 
        new Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> ();
}
于 2010-06-10T18:49:23.943 回答
5

在装饰-排序-未装饰模式中使用元组怎么样?(Perl 人的 Schwartzian 变换)。可以肯定的是,这是一个人为的例子,但元组似乎是处理这种事情的好方法:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("C:\\Windows")
                    .Select(x => new Tuple<string, string>(x, FirstLine(x)))
                    .OrderBy(x => x.Item2)
                    .Select(x => x.Item1).ToArray();
        }
        static string FirstLine(string path)
        {
            using (TextReader tr = new StreamReader(
                        File.Open(path, FileMode.Open)))
            {
                return tr.ReadLine();
            }
        }
    }
}

现在,我可以使用两个元素的 Object[],或者在这个特定示例中,使用两个元素的字符串 []。关键是我可以使用任何东西作为内部使用的元组中的第二个元素,并且非常容易阅读。

于 2010-06-11T14:42:15.020 回答
5

使用类似的类ResultType更清楚。您可以为类中的字段赋予有意义的名称(而使用元组它们将被称为Item1and Item2)。如果两个字段的类型相同,这一点更为重要:名称清楚地区分它们。

于 2010-06-10T18:47:50.640 回答
4

IMO 这些“元组”基本上都是具有未命名成员struct的公共访问匿名类型。

唯一会使用元组的地方是当您需要在非常有限的范围内快速组合一些数据时。 数据的语义应该是显而易见的,所以代码不难阅读。因此,对 (row,col) 使用元组 ( int, int) 似乎是合理的。但是我很难找到struct与命名成员相比的优势(因此不会犯错误,并且不会意外互换行/列)

如果您将数据发送回调用者,或从调用者接收数据,您确实应该使用struct带有命名成员的 a。

举个简单的例子:

struct Color{ float r,g,b,a ; }
public void setColor( Color color )
{
}

元组版本

public void setColor( Tuple<float,float,float,float> color )
{
  // why?
}

我认为使用元组代替具有命名成员的结构没有任何优势。使用未命名的成员对于代码的可读性和可理解性来说是一种倒退。

元组让我觉得这是一种避免struct使用实际命名成员创建的懒惰方式。过度使用元组,你真的觉得你/或其他人遇到你的代码需要命名成员,如果我见过的话,这是一件坏事™。

于 2013-05-10T21:37:19.613 回答
4

不要评判我,我不是专家,但现在使用 C# 7.x 中的新元组,您可以返回如下内容:

return (string Name1, int Name2)

至少现在您可以命名它,开发人员可能会看到一些信息。

于 2018-03-09T19:00:47.787 回答
2

当然,这取决于!正如您所说,当您想将一些项目组合在一起以供本地使用时,元组可以节省您的代码和时间。如果您传递一个具体的类,您还可以使用它们来创建更通用的处理算法。我不记得有多少次我希望我有一些超越 KeyValuePair 或 DataRow 的东西来快速将某个日期从一种方法传递到另一种方法。

另一方面,很可能过度使用并传递元组,您只能猜测它们包含什么。如果您要跨类使用元组,那么创建一个具体类可能会更好。

当然,适度使用元组可以产生更简洁和可读的代码。您可以查看 C++、STL 和 Boost 以了解元组如何在其他语言中使用的示例,但最后,我们都必须进行试验以找出它们如何最适合 .NET 环境。

于 2010-06-10T18:49:21.490 回答
1

元组在 .NET 4 中是一个无用的框架特性。我认为 C# 4.0 错过了一个很好的机会。我本来希望拥有具有命名成员的元组,因此您可以按名称访问元组的各个字段,而不是Value1Value2等...

它需要更改语言(语法),但它会非常有用。

于 2010-06-10T20:12:38.737 回答
1

我个人永远不会使用元组作为返回类型,因为没有指示值代表什么。元组有一些有价值的用途,因为与对象不同,它们是值类型,因此可以理解相等性。因此,如果我需要一个多部分键,我会将它们用作字典键,或者如果我想按多个变量分组并且不想要嵌套分组(谁想要嵌套分组?),我会将它们用作 GroupBy 子句的键。要克服极其冗长的问题,您可以使用辅助方法创建它们。请记住,如果您经常访问成员(通过 Item1、Item2 等),那么您可能应该使用不同的构造,例如结构或匿名类。

于 2014-10-09T16:11:47.387 回答
0

我在许多不同的场景中都使用了元组 theTuple和 new ValueTuple,并得出以下结论:不要使用.

每次,我都会遇到以下问题:

  • 由于缺乏强命名,代码变得不可读;
  • 无法使用类层次特性,如基类 DTO 和子类 DTO 等;
  • 如果它们在多个地方使用,您最终会复制和粘贴这些丑陋的定义,而不是一个干净的类名。

我的观点是元组是 C# 的一个缺陷,而不是一个特性。

Func<>我对和的批评有些相似,但不那么苛刻Action<>。这些在许多情况下都很有用,尤其是简单ActionFunc<type>变体,但除此之外,我发现创建委托类型更优雅、可读、可维护,并为您提供更多功能,例如ref/out参数。

于 2018-06-01T00:39:15.643 回答