在C#代码中使用.NET 4.0元组是否是糟糕的设计决策?


166

在.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;
}

所以,撇开,我很想念元组点的可能性,是一个例子元组设计不好的选择呢?在我看来,这似乎不太杂乱,但没有自我记录和干净。这意味着与类型ResultType,这是非常以后清除类的手段对各部分,但你有额外的代码维护。使用,Tuple<string, int>您将需要查找并弄清楚每个Item代表什么,但是您只需编写和维护较少的代码。

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


16
@Boltbait:因为元组来自集合论en.wikipedia.org/wiki/
Matthew Whited 2010年

16
@BoltBait:“元组”是一组有序的值,不一定与数据库中的行有任何关系。
FrustratedWithFormsDesigner

19
如果有一些好的元组拆箱操作,则C#中的元组会更好:)
Justin 2010年

4
@John Saunders我的问题专门与c#中的设计决策有关。我没有使用任何其他.net语言,所以我不确定元组如何影响它们。所以我将其标记为c#。对我来说,这似乎是合理的,但我想也可以将.net更改为正常。
杰森·韦伯

13
@John Saunders:我很确定他正在询问有关纯粹用C#编写的应用程序中使用或不使用Tuples的设计决策。我不相信他在问有关制作C#语言的设计决策。
Brett Widmeier 2010年

Answers:


163

如果您同时控制创建和使用元组,则元组非常有用-您可以维护上下文,这对于理解它们至关重要。

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

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

这个答案也有一些信息。


19
感谢您提出来,公共/私人之间的区别非常重要。在公共API中返回元组确实不是一个好主意,但是在内部可能紧密耦合的地方(但这没关系)就可以了。
克林顿·皮尔斯

36
是紧密耦合还是紧密元组?;)
contactmatt

适当的文档在这里有用吗?例如,Scala StringOps(基本上是Java的“扩展方法”类String)具有一个方法parition(Char => Boolean): (String, String)(采用谓词,返回2个字符串的元组)。很明显输出是什么(显然,一个将是谓词为true的字符,另一个是谓词为false的字符,但不清楚是哪个)。这是清除此问题的文档(可以使用模式匹配来明确使用哪个)。
2015年

@Mike:这很难做到正确,容易出错,API的标志会在某处给某人带来痛苦:-)
Bryan Watts 2015年

79

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

实际上,还有其他有价值的用途Tuple<> -其中大多数涉及抽象化共享相似结构的特定类型组的语义,并将它们简单地视为有序的值集。在所有情况下,元组的好处是它们避免使用仅公开数据类的属性(而不是方法)使名称空间混乱。

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

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

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

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

扑克手可以看作只是一组纸牌-元组(可能是)表达该概念的一种合理方法。

抛开我遗漏Tuples的可能性,Tuple的示例是否是错误的设计选择?

将强类型Tuple<>实例作为公共类型的公共API的一部分返回并不是一个好主意。如您所知,元组要求相关各方(图书馆作者,图书馆用户)提前就所使用的元组类型的目的和解释达成协议。创建直观而清晰的API充满挑战,Tuple<>仅公开使用会掩盖API的意图和行为。

匿名类型也是元组的一种 -但是,它们是强类型的,并且允许您为属于该类型的属性指定清晰,有用的名称。但是匿名类型很难跨不同的方法使用-匿名类型主要是为支持LINQ等技术而添加的,其中投影会产生我们通常不希望分配名称的类型。(是的,我知道编译器会合并具有相同类型和命名属性的匿名类型)。

我的经验法则是: 如果要从公共接口返回它,请将其命名为type

使用元组的另一个经验法则是: 尽可能清楚地命名方法参数和类型的localc变量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...
}

5
@stakx:是的,我同意。但请记住-该示例主要用于说明使用a Tuple<> 可能有意义的情况。总是有其他选择...
LBushkin 2010年

1
我认为,像在TryGetValue或TryParse中那样,用Tuple替换参数是少数几个可能需要公共API返回Tuple的情况之一。实际上,至少一种.NET语言会自动为您更改此API。
Joel Mueller 2010年

1
@stakx:元组也是不可变的,而数组则不是。
罗伯特·保尔森

2
我了解到,元组仅用作不变的扑克玩家对象。
Gennady VaninГеннадийВанин2013年

2
+1不错的答案-我喜欢您的两个最初的例子,但实际上我的想法有所改变。我以前从未见过这样的示例,在这些示例中,元组至少与自定义结构或类型一样合适。但是,对我来说,您的最后一个“现实生活中的例子”似乎不再增加任何价值。前两个示例是完美而简单的。最后一个有点难以理解,并且您的“ PS”类型使它变得无用,因为您的替代方法更简单并且不需要元组。
chiccodoro 2014年

18

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


9

从C#程序员的角度来看,元组对于CLR来说是相当强大的。如果您拥有长度不等的项目集合,则不需要它们在编译时具有唯一的静态名称。

但是,如果您有一个固定长度的集合,这意味着该集合中固定的位置分别具有特定的预定义含义。而且它总是不如给他们适当的静态的名字在这种情况下,而不是记住的意义Item1Item2等等。

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();
    }
}

作为一般功能,我想看到的是.net为几种特殊类型的匿名类型定义标准,例如,struct var SimpleStruct {int x, int y;}将以名称开头的struct定义与简单结构关联的guid的结构。{System.Int16 x}{System.Int16 y}附有类似内容;要求以该GUID开头的任何名称表示仅包含那些元素和特定指定内容的结构;如果同一名称的多个定义在范围内并且匹配,则所有这些都将被视为同一类型。
2013年

这样的事情对于几种类型将是有帮助的,包括可变且不可变的类和结构元组,以及在应将匹配结构视为暗示匹配语义的情况下使用的接口和委托。虽然有某些原因使具有相同参数的两个委托类型不被认为可以互换是有用的,但如果可以指定一种方法希望一个委托使用参数的某种组合但不包括这样的委托,这也将很有帮助。不管是什么样的代表。
2013年

不幸的是,如果两个不同的人想要编写将输入委托作为一个函数的函数,而该函数需要一个ref参数,则唯一可以传递给两个函数的委托的唯一方法就是一个人产生并编译一个委托类型,并将其提供给其他对象。双方都无法表明这两种类型应被视为同义词。
2013年

您是否有更具体的示例说明将在何处使用匿名类而不是Tuple?我只是在代码库中浏览了50个使用元组的地方,而这些地方要么都是返回值(通常是yield return),要么是在其他地方使用的集合的元素,因此不适合使用匿名类。

2
@JonofAllTrades-意见可能会有所不同,但是我尝试设计公共方法,就好像其他人会使用它们一样,即使那个人是我,所以我也给自己提供良好的客户服务!与编写函数相比,调用函数的次数往往更多。:)
Daniel Earwicker 2013年

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>> ();
}

2
主流“ RAD”语言(Java是最好的例子)始终选择安全性,并避免在脚踩式场景中自杀。我很高兴Microsoft能够抓住机会使用C#,并为C#提供一些强大的功能,即使它可能被滥用。
Matt Greer 2010年

4
var关键字不可滥用。也许您是说动态的?
克里斯·马里西奇

@克里斯·马里西克(Chris Marisic),只是为了澄清我在同一情况下的意思-您是完全正确的,var不能被传回或存在于其声明的范围之外。但是,仍然有可能超出预期目的使用它并被“滥用”
johnny g 2010年

5
我仍然坚持认为var不能被滥用。它只是使变量定义更短的关键字。您的论点可能是关于匿名类型的,但即使是那些真的不能被滥用的原因也是如此,因为它们仍然是正常的静态链接类型,而动态的则完全不同。
克里斯·马里西奇

4
var在某种意义上可以被滥用,如果过度使用它有时会给阅读代码的人造成问题。特别是如果您不在Visual Studio中并且缺乏智能感知。
Matt Greer 2010年

5

使用like这样的类ResultType更加清晰。您可以给类中的字段赋予有意义的名称(而使用元组时,它们将被称为Item1Item2)。如果两个字段的类型相同,这一点就尤为重要:名称清楚地区分了它们。


一个小的附加优点:您可以安全地对类的参数重新排序。对元组执行此操作将破坏代码,并且如果字段具有相似的类型,则在编译时可能无法检测到。

我同意,当开发人员缺乏精力将有意义的类名归为有意义的属性名时,就会使用Tuples。编程是关于沟通的。元组是通信的反模式。我本来希望Microsoft不使用它来强迫开发人员做出良好的设计决策。
麦克金

5

如何在decorate-sort-unecorate模式中使用Tuples?(适用于Perl人的Schwartzian转换)。当然,这是一个人为的示例,但是Tuples似乎是处理此类问题的好方法:

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 []或在此特定示例中使用两个元素的字符串[]。关键是我可以将任何东西用作内部使用且非常易于阅读的元组的第二个元素。


3
您可以使用Tuple.Create代替构造函数。它更短。
Kugel

匿名类型将更具可读性;如使用真实变量名,而不是“X”
戴维高柏灵参赞

现在就可以。那是一个SO问题,几年前发布的答案从未针对新技术进行过清理或更新。
克林顿·皮尔斯

4

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

唯一的地方,我会用元组是当你需要快速团块一些数据一起,在非常有限的范围。 数据的语义应该很明显,因此代码不难阅读。因此intint对(row,col)使用元组(,)似乎是合理的。但是,我很难找到struct具有命名成员的A的优势(因此不会出错,并且行/列不会被意外地互换)

如果您要将数据发送回呼叫者,或者接受来自呼叫者的数据,则实际上应该使用struct带有命名成员的。

举一个简单的例子:

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

元组版本

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

我没有看到使用元组代替具有命名成员的结构的任何优势。使用未命名成员对于代码的可读性和可理解性而言是一个后退。

元组给我打招呼是一种避免struct使用实际的命名成员创建的懒惰方式。元组的过度使用,在您真正感觉到您/或其他遇到您的代码的人将需要命名成员的地方,如果我看到过,则是Bad Bading™。


3

不要判断我,我不是专家,但是现在有了C#7.x中的新元组,您可以返回类似以下内容:

return (string Name1, int Name2)

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


2

当然要看情况!如您所说,当您希望将某些项目组合在一起以供本地使用时,元组可以节省代码和时间。与传递具体类相比,您还可以使用它们来创建更多的通用处理算法。我不记得有多少次希望我拥有KeyValuePair或DataRow之外的东西可以将某个日期从一种方法快速传递给另一种方法。

另一方面,很可能会过度使用它并绕过元组,您只能在其中猜测它们所包含的内容。如果要在各个类之间使用元组,则最好创建一个具体的类。

当然,在适度使用元组可以导致更简洁和可读的代码。您可以参考C ++,STL和Boost来获取在其他语言中如何使用元组的示例,但最后,我们所有人都必须进行实验以找到它们最适合.NET环境的方式。


1

元组是.NET 4中无用的框架功能。我认为C#4.0错失了很大的机会。我本来希望有具有命名成员的元组,所以您可以按名称而不是Value1Value2等访问元组的各个字段。

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


元组如何成为“语言功能”?它是所有.net语言都可以使用的类,不是吗?
杰森·韦伯

11
也许对C#没用。但是由于C#,未将System.Tuple添加到框架中。添加它是为了简化F#与其他语言之间的互操作性。
Joel Mueller 2010年

@Jason:我没有说这是语言功能(我确实打错了它,但在您发表此评论之前我已将其纠正)。
Philippe Leybaert 2010年

2
@Jason-直接支持元组的语言暗含支持将元组解构为它们的组件值。用这些语言编写的代码通常永远不会访问Item1,Item2属性。let success, value = MethodThatReturnsATuple()
Joel Mueller 2010年

@Joel:这个问题被标记为C#,所以它与C#有关。我仍然认为他们可以将其转变为该语言的一流功能。
Philippe Leybaert 2010年

1

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


0

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

每次,我遇到以下问题:

  • 由于缺乏强有力的命名,代码变得不可读;
  • 无法使用类层次结构功能,例如基类DTO和子类DTO等;
  • 如果在多个地方使用它们,您最终将复制并粘贴这些难看的定义,而不是使用干净的类名。

我的看法是,元组是C#的缺点,而不是功能。

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


我提到了命名元组-- ValueTuple我也不喜欢它们。首先,命名不强,更像是一个建议,很容易弄乱,因为它可以隐式地分配给一个TupleValueTuple具有相同数量和类型的项目。其次,缺乏继承以及需要从一个地方到另一个地方复制粘贴整个定义仍然是不利的。
TA先生

我同意这些观点。当我控制生产者和消费者时,如果他们不太“遥远”,我尝试只使用元组。意思是,生产者和消费者以相同的方法=“关闭”,而生产者和消费者在不同的程序集中=“光年”。那么,如果我在方法的开头创建元组,并在结尾使用它们?当然可以,我可以。我也记录这些条款。但是,是的,我同意,通常它们不是正确的选择。
迈克·克里斯蒂安森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.