如何轻松初始化一个元组列表?

402

我喜欢元组。它们可以让你快速地将相关信息组合在一起,而不必为此编写结构体或类。这在重构本地化代码时非常有用。

然而,初始化它们的列表似乎有些冗余。

var tupleList = new List<Tuple<int, string>>
{
    Tuple.Create( 1, "cow" ),
    Tuple.Create( 5, "chickens" ),
    Tuple.Create( 1, "airplane" )
};

难道没有更好的办法吗?我希望有一种类似于字典初始化器的解决方案。

Dictionary<int, string> students = new Dictionary<int, string>()
{
    { 111, "bleh" },
    { 112, "bloeh" },
    { 113, "blah" }
};

我们不能使用类似的语法吗?


2
在这种情况下,你为什么不使用字典而是使用元组列表呢? - Ed S.
25
СИђСИф Dictionary СИЇтЁЂУ«ИТюЅжЄЇтцЇуџёжћ«сђѓ - Steven Jeuris
4
每次不一定都是由一个可哈希/可排序且唯一的值组成的二元组。 - user395760
8个回答

491

在 c# 7.0 中,您可以这样做:

  var tupleList = new List<(int, string)>
  {
      (1, "cow"),
      (5, "chickens"),
      (1, "airplane")
  };
如果你不需要一个 `List`,只需要一个数组,你可以这样做:
  var tupleList = new(int, string)[]
  {
      (1, "cow"),
      (5, "chickens"),
      (1, "airplane")
  };

如果你不喜欢 "Item1" 和 "Item2",你可以这样做:

  var tupleList = new List<(int Index, string Name)>
  {
      (1, "cow"),
      (5, "chickens"),
      (1, "airplane")
  };

或者对于一个数组:

  var tupleList = new (int Index, string Name)[]
  {
      (1, "cow"),
      (5, "chickens"),
      (1, "airplane")
  };

这使您可以执行:tupleList[0].IndextupleList[0].Name

框架4.6.2及以下版本

您必须从Nuget包管理器中安装System.ValueTuple

框架4.7及以上版本

它已内置于框架中。不要安装System.ValueTuple。实际上,将其删除并从bin目录中删除。

注意:在现实生活中,我不能在牛、鸡或飞机之间进行选择。我会感到真的很纠结。


2
这个能在 .net core 2.0 上使用吗? - Алекса Јевтић
3
@АлексаЈевтић,System.ValueTuple支持核心2.0。但是首先尝试不使用Nuget,因为不必要的包可能会导致问题。所以无论如何都可以使用。如果可能,请使用c#v7或更高版本。 - toddmo
1
绝对完美的答案!谢谢! - Vitox
这里有一个相关的 Microsoft 链接,讲述了本回答中提到的集合初始化器。 - pmcnamee
好的,我现在准备把这些点赞转换成现金。我该怎么做? - toddmo
Error CS1503 Argument 1: cannot convert from '(string, string)' to 'System.Tuple<string, string>' MauiApp1 (net7.0-windows10.0.19041.0) C:\projects\MauiApp1\MainPage.xaml.cs - ruffin

238

是的!这是可能的

{ }集合初始化程序的语法适用于任何具有正确数量参数的Add方法的类型。不必费心考虑它在内部是如何工作的,这意味着您可以简单地从List<T>扩展,添加自定义Add方法来初始化您的T,然后完成!

public class TupleList<T1, T2> : List<Tuple<T1, T2>>
{
    public void Add( T1 item, T2 item2 )
    {
        Add( new Tuple<T1, T2>( item, item2 ) );
    }
}

这使您能够执行以下操作:
var groceryList = new TupleList<int, string>
{
    { 1, "kiwi" },
    { 5, "apples" },
    { 3, "potatoes" },
    { 1, "tomato" }
};

43
缺点是需要编写类。和你一样,我喜欢元组,因为我不需要编写类或结构体。 - goodeye
2
@BillW 我不太确定这是否是确切的帖子,但我知道Jon Skeet以前写过关于它的文章。 - Steven Jeuris
1
这个代码 groceryList[0] == groceryList[1] 能够工作吗?还是需要实现一个比较函数? - Byyo
我创建了一个 Nuget 包,其中包含 ICollection 上的 Add 方法,以节省其他人维护此代码的时间。请查看此处的包:https://nuget.org/packages/Naos.Recipes.TupleInitializers 请查看此处的代码:https://github.com/NaosProject/Naos.Recipes/blob/master/Naos.Recipes.TupleInitializers/.Naos.Recipes/TupleInitializers/Initializers.cs 这将在您的解决方案中包括一个 cs 文件,位于“.Naos.Recipes”文件夹下,因此您无需拖动程序集依赖项。 - Suraj
每个 C# 版本语言的新特性:C# 5、C# 6、C# 7、C# 8、C# 9、C# 10... - Kiquenet

89

C# 6新增了一个针对此问题的新功能:扩展Add方法。虽然VB.net一直支持,但现在也适用于C#。

现在您不必直接向类中添加Add()方法,可以将其实现为扩展方法。当用Add()方法扩展任何可枚举类型时,您将能够在集合初始化表达式中使用它。因此,您不再需要明确从列表中派生(如另一个答案中所述),而是可以简单地进行扩展。

public static class TupleListExtensions
{
    public static void Add<T1, T2>(this IList<Tuple<T1, T2>> list,
            T1 item1, T2 item2)
    {
        list.Add(Tuple.Create(item1, item2));
    }

    public static void Add<T1, T2, T3>(this IList<Tuple<T1, T2, T3>> list,
            T1 item1, T2 item2, T3 item3)
    {
        list.Add(Tuple.Create(item1, item2, item3));
    }

    // and so on...
}

这将使您可以在实现 IList<> 接口的任何类上执行此操作:

Translated sentence:

这将使您可以在实现 IList<> 接口的任何类上执行此操作:

var numbers = new List<Tuple<int, string>>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
    { 4, "four" },
    { 5, "five" },
};
var points = new ObservableCollection<Tuple<double, double, double>>
{
    { 0, 0, 0 },
    { 1, 2, 3 },
    { -4, -2, 42 },
};

当然你不仅可以扩展元组集合,还可以为任何你想要的特定类型集合使用特殊的语法。

public static class BigIntegerListExtensions
{
    public static void Add(this IList<BigInteger> list,
        params byte[] value)
    {
        list.Add(new BigInteger(value));
    }

    public static void Add(this IList<BigInteger> list,
        string value)
    {
        list.Add(BigInteger.Parse(value));
    }
}

var bigNumbers = new List<BigInteger>
{
    new BigInteger(1), // constructor BigInteger(int)
    2222222222L,       // implicit operator BigInteger(long)
    3333333333UL,      // implicit operator BigInteger(ulong)
    { 4, 4, 4, 4, 4, 4, 4, 4 },               // extension Add(byte[])
    "55555555555555555555555555555555555555", // extension Add(string)
};

C# 7将内置元组支持到语言中,不过它们将是不同类型的(System.ValueTuple)。因此,最好添加值元组重载以便您也可以选择使用它们。不幸的是,在两者之间没有定义隐式转换。


public static class ValueTupleListExtensions
{
    public static void Add<T1, T2>(this IList<Tuple<T1, T2>> list,
        ValueTuple<T1, T2> item) => list.Add(item.ToTuple());
}

这样列表的初始化看起来会更加漂亮。

var points = new List<Tuple<int, int, int>>
{
    (0, 0, 0),
    (1, 2, 3),
    (-1, 12, -73),
};

但是,与其费尽心思地进行所有这些操作,不如完全转而使用ValueTuple

var points = new List<(int, int, int)>
{
    (0, 0, 0),
    (1, 2, 3),
    (-1, 12, -73),
};

我最终在将我的库更新到C# 6.0时,好好地看了一眼这个。虽然乍一看它看起来不错,但我仍然更喜欢我之前发布的解决方案,因为我认为TupleList<int, string>.List<Tuple<int, string>>更易读。 - Steven Jeuris
2
那是类型别名可以解决的问题。尽管可能更好的是,别名本身不能是通用的。using TupleList = System.Collections.Generic.List<System.Tuple<int, string>>; - Jeff Mercado
我创建了一个 Nuget 包,其中包含 ICollection 上的 Add 方法,以节省其他人维护此代码的时间。请在此处查看软件包:https://nuget.org/packages/Naos.Recipes.TupleInitializers 请在此处查看代码:https://github.com/NaosProject/Naos.Recipes/blob/master/Naos.Recipes.TupleInitializers/.Naos.Recipes/TupleInitializers/Initializers.cs 这将在您的解决方案中包括一个 cs 文件,在 ".Naos.Recipes" 文件夹下,因此您无需拖动程序集依赖项。 - Suraj

47

你可以每次调用构造函数来实现这一点,这样会稍微好一些。

var tupleList = new List<Tuple<int, string>>
{
    new Tuple<int, string>(1, "cow" ),
    new Tuple<int, string>( 5, "chickens" ),
    new Tuple<int, string>( 1, "airplane" )
};

7
原代码无法运行,所以我做了修改,改为我认为应该是这样的......这可能会改变你对语法是否“略微更好”的看法 :) - onedaywhen
5
请至少使用 Tuple.Create,您可以推断类型参数。 - Dave Cousineau
这就是我一直在寻找的。谢谢 @onedaywhen - Rod Talingting

30

这是一个老问题,但以下是我通常所做的使得事情变得更易读的方法:

Func<int, string, Tuple<int, string>> tc = Tuple.Create;

var tupleList = new List<Tuple<int, string>>
{
    tc( 1, "cow" ),
    tc( 5, "chickens" ),
    tc( 1, "airplane" )
};

也适用于旧版本的.NET! - Ed Bayiates

4

虽然我知道这些技术已经有点老了,但我想谈谈使用LINQ和续行Lambda在使用C# 7的方法上。我尝试使用命名元组来替换DTO和匿名投影,当它们在类中被重复使用时。是的,为了模拟和测试,你仍需要类,但在类中处理内联对象并传递它们是一个很好的选择,以我的个人看法。你可以通过以下方式实例化它们:

  1. 直接实例化(Direct Instantiation)
var items = new List<(int Id, string Name)> { (1, "Me"), (2, "You")};
  1. 从现有的集合中,你现在可以返回良好类型化的元组,类似于以前如何进行匿名投影。
public class Hold
{
    public int Id { get; set; }
    public string Name { get; set; }
}

//In some method or main console app:
var holds = new List<Hold> { new Hold { Id = 1, Name = "Me" }, new Hold { Id = 2, Name = "You" } };
var anonymousProjections = holds.Select(x => new { SomeNewId = x.Id, SomeNewName = x.Name });
var namedTuples = holds.Select(x => (TupleId: x.Id, TupleName: x.Name));

可以使用分组方法重复使用元组,或者使用内联方法在其他逻辑中构建它们:
//Assuming holder class above making 'holds' object
public (int Id, string Name) ReturnNamedTuple(int id, string name) => (id, name);
public static List<(int Id, string Name)> ReturnNamedTuplesFromHolder(List<Hold> holds) => holds.Select(x => (x.Id, x.Name)).ToList();
public static void DoSomethingWithNamedTuplesInput(List<(int id, string name)> inputs) => inputs.ForEach(x => Console.WriteLine($"Doing work with {x.id} for {x.name}"));

var namedTuples2 = holds.Select(x => ReturnNamedTuple(x.Id, x.Name));
var namedTuples3 = ReturnNamedTuplesFromHolder(holds);
DoSomethingWithNamedTuplesInput(namedTuples.ToList());

你让我感觉老了。 :) 我可能在这里漏掉了什么,但初始化“holds”一点也不简洁。这正是问题的关键。 - Steven Jeuris
@Steven Jeuris 不,你是对的,我复制并粘贴了错误的代码部分来处理第一点。在点击“提交”之前需要慢下来一点。 - djangojazz

1

我认为有一种技术比较简单,这里之前还没有提到过:

var asdf = new [] { 
    (Age: 1, Name: "cow"), 
    (Age: 2, Name: "bird")
}.ToList();

我认为这比以下代码更简洁:

var asdf = new List<Tuple<int, string>> { 
    (Age: 1, Name: "cow"), 
    (Age: 2, Name: "bird")
};

这更多取决于个人喜好,无论你是偏向于显式地提及类型还是不提,就像使用var或不使用一样。我个人更喜欢显式地进行类型定义(当它不会在构造函数中重复出现时)。 - Steven Jeuris

-6
    var colors = new[]
    {
        new { value = Color.White, name = "White" },
        new { value = Color.Silver, name = "Silver" },
        new { value = Color.Gray, name = "Gray" },
        new { value = Color.Black, name = "Black" },
        new { value = Color.Red, name = "Red" },
        new { value = Color.Maroon, name = "Maroon" },
        new { value = Color.Yellow, name = "Yellow" },
        new { value = Color.Olive, name = "Olive" },
        new { value = Color.Lime, name = "Lime" },
        new { value = Color.Green, name = "Green" },
        new { value = Color.Aqua, name = "Aqua" },
        new { value = Color.Teal, name = "Teal" },
        new { value = Color.Blue, name = "Blue" },
        new { value = Color.Navy, name = "Navy" },
        new { value = Color.Pink, name = "Pink" },
        new { value = Color.Fuchsia, name = "Fuchsia" },
        new { value = Color.Purple, name = "Purple" }
    };
    foreach (var color in colors)
    {
        stackLayout.Children.Add(
            new Label
            {
                Text = color.name,
                TextColor = color.value,
            });
        FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
    }

this is a Tuple<Color, string>

如何使用元组获取值/名称语法? - Andreas Reiff
编译器提示匿名类型不能隐式转换为元组。 - cthepenier

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