使用LINQ将多个列表合并为一个列表

17
有没有一种巧妙的方法可以使用 LINQ 将多个列表合并为一个列表,以有效地复制这个过程?
public class RGB
{
    public int Red { get; set; }
    public int Green { get; set; }
    public int Blue { get; set; }
    public RGB(int red, int green, int blue) { Red = red; Green = green; Blue = blue; }
}

public void myFunction()
{
    List<int> red = new List<int> { 0x00, 0x03, 0x06, 0x08, 0x09 };
    List<int> green = new List<int> { 0x00, 0x05, 0x06, 0x07, 0x0a };
    List<int> blue = new List<int> { 0x00, 0x02, 0x03, 0x05, 0x09 };

    List<RGB> colors = new List<RGB>();

    colors.Add(new RGB(red[0], green[0], blue[0]));
    colors.Add(new RGB(red[1], green[1], blue[1]));
    colors.Add(new RGB(red[2], green[2], blue[2]));
    colors.Add(new RGB(red[3], green[3], blue[3]));
    colors.Add(new RGB(red[4], green[4], blue[4]));
}

或者,由于列表是分开到达的,所以更有效的方法是按照以下方式将它们依次合并。

public class RGB
{
    public int Red { get; set; }
    public int Green { get; set; }
    public int Blue { get; set; }

    public RGB(int red, int green, int blue) { Red = red; Green = green; Blue = blue; }
}

public void myFunction()
{
    List<int> red = new List<int> { 0x00, 0x03, 0x06, 0x08, 0x09 };

    List<RGB> colors = new List<RGB>();

    colors.Add(new RGB(red[0], 0, 0));
    colors.Add(new RGB(red[1], 0, 0));
    colors.Add(new RGB(red[2], 0, 0));
    colors.Add(new RGB(red[3], 0, 0));
    colors.Add(new RGB(red[4], 0, 0));

    List<int> green = new List<int> { 0x00, 0x05, 0x06, 0x07, 0x0a };

    colors[0].Green = green[0];
    colors[1].Green = green[1];
    colors[2].Green = green[2];
    colors[3].Green = green[3];
    colors[4].Green = green[4];

    List<int> blue = new List<int> { 0x00, 0x02, 0x03, 0x05, 0x09 };

    colors[0].Blue = blue[0];
    colors[1].Blue = blue[1];
    colors[2].Blue = blue[2];
    colors[3].Blue = blue[3];
    colors[4].Blue = blue[4];
}

可能是重复的问题,参见Linq使用3个集合创建项 - Pat
类似但不同。前者专注于内存性能和资源优化。这不是关于资源的问题,答案并不针对特定的性能指标,并提供了一系列广泛的可能性,不受性能因素的限制。 - tlum
另一个问题写得很差,答案也不如这里的好,但它并没有特别关注性能(OP只是提到他/她的内存不足),它是一个简单而最小的问题,并且它比较旧,因此我认为它是一个“可能”的重复。无论如何,其他人应该知道存在类似的问题,并有一个链接。 - Pat
8个回答

28

您基本上正在尝试压缩三个集合。如果只有LINQ的Zip()方法支持同时压缩多达两个,但不幸的是,它一次仅支持两个。但我们可以让它正常工作:

var reds = new List<int> { 0x00, 0x03, 0x06, 0x08, 0x09 };
var greens = new List<int> { 0x00, 0x05, 0x06, 0x07, 0x0a };
var blues = new List<int> { 0x00, 0x02, 0x03, 0x05, 0x09 };

var colors =
    reds.Zip(greens.Zip(blues), (red, tuple) =>
        new RGB(red, tuple.First, tuple.Second)
    )
    .ToList();

当然,编写一个扩展方法来执行三个(或更多)操作并不是非常痛苦。

public static IEnumerable<TResult> Zip<TFirst, TSecond, TThird, TResult>(
    this IEnumerable<TFirst> first,
    IEnumerable<TSecond> second,
    IEnumerable<TThird> third,
    Func<TFirst, TSecond, TThird, TResult> resultSelector)
{
    using (var enum1 = first.GetEnumerator())
    using (var enum2 = second.GetEnumerator())
    using (var enum3 = third.GetEnumerator())
    {
        while (enum1.MoveNext() && enum2.MoveNext() && enum3.MoveNext())
        {
            yield return resultSelector(
                enum1.Current,
                enum2.Current,
                enum3.Current
            );
        }
    }
}

这可以让事情变得更加美好:

var colors =
    reds.Zip(greens, blues, (red, green, blue) =>
        new RGB(red, green, blue)
    )
    .ToList();

1
在我看来,reds.Zip(greens.Zip(blues, (g, b) => new {g,b}), (r, gb) => new RGB(r, gb.g, gb.b)) 更易读。 - lc.
这也可以,我同意,肯定更易读(尽管我个人会写出名称)。但这已足以表达我的观点。 - Jeff Mercado

16

是的 - 你可以这样做:

List<int> red = new List<int> { 0x00, 0x03, 0x06, 0x08, 0x09 };
List<int> green = new List<int> { 0x00, 0x05, 0x06, 0x07, 0x0a };
List<int> blue = new List<int> { 0x00, 0x02, 0x03, 0x05, 0x09 };

List<RGB> colors = Enumerable
    .Range(0, red.Count)
    .Select(i => new RGB(red[i], green[i], blue[i]))
    .ToList();

如果列表的大小不同,则它无法工作。在这种情况下,使用zip更可靠。 - nverinaud

6

像这样使用SelectMany:

List_A.Select(a => a.List_B).SelectMany(s => s).ToList();

4

这里是一个简化版,可以将 任何 相同类型的序列(作为数组)一起压缩:

public static IEnumerable<TResult> Zip<T, TResult>(this IEnumerable<T>[] sequences, Func<T[], TResult> resultSelector)
{
    var enumerators = sequences.Select(s => s.GetEnumerator()).ToArray();
    while(enumerators.All(e => e.MoveNext()))
        yield return resultSelector(enumerators.Select(e => e.Current).ToArray());
}

优点

  • 任意数量的序列
  • 只需四行代码
  • .Zip()方法的另一个重载,用于LINQ
  • 一次性压缩所有序列,而不是每次使用.Zip添加一个序列

缺点

  • 所有序列必须是相同类型(在您的情况下不是问题)
  • 没有检查相同列表长度(如果需要,请添加一行)

用法

Zipping colors


3
var colours = red.Select((t, i) => new RGB(t, green[i], blue[i])).ToList();

这个解决方案唯一的问题是它假设了red数组的长度。如果红色元素比其他元素多,它将会失败并导致灾难性后果。 - Jeff Mercado
3
同样适用于楼主的解决方案 #1。 - Sergey Kalinichenko
是的,关注它们保持同步非常重要。幸运的是,在这个应用程序中,如果列表大小不正确和/或校验和错误,您已经抛出了异常。即使它们大小相同,答案最终也会出现严重错误,即使它们并不都按正确顺序排列。尝试组合相关但明显分离的对象从来不是一件漂亮的事情。 - tlum

1
你可以使用Aggregate和Zip在一次操作中压缩任意数量的IEnumerables。
以下是如何在您的示例中执行此操作的方法:
var colorLists = new List<int>[] { red, green, blue };
var rgbCount = red.Count;
var emptyTriples =
    Enumerable.Repeat<Func<List<int>>>(() => new List<int>(), rgbCount)
    .Select(makeList => makeList());

var rgbTriples = colorLists.Aggregate(
    emptyTriples,
    (partialTriples, channelValues) =>
        partialTriples.Zip(
            channelValues,
            (partialTriple, channelValue) =>
            {
                partialTriple.Add(channelValue);
                return partialTriple;
            }));

var rgbObjects = rgbTriples.Select(
    triple => new RGB(triple[0], triple[1], triple[2]));

通常情况下,依靠Zip作为底层合并器可以避免因输入长度不同而产生的问题。

0

就我个人而言,我喜欢LINQ并经常使用它,但有时老派的方法是最好的。请注意以下示例:

        const int Max = 100000;
        var rnd = new Random();
        var list1 = Enumerable.Range(1, Max).Select(r => rnd.Next(Max)).ToList();
        var list2 = Enumerable.Range(1, Max).Select(r => rnd.Next(Max)).ToList();

        DateTime start;

        start = DateTime.Now;
        var r1 = list1.Zip(list2, (a, b) => new { a, b }).ToList();
        var time1 = DateTime.Now - start;

        start = DateTime.Now;
        var r2 = list1.Select((l1, i) => new { a = l1, b = list2[i]}).ToList();
        var time2 = DateTime.Now - start;

        start = DateTime.Now;
        var r3 = new int[0].Select(i => new { a = 0, b = 0 }).ToList();
        //  Easy out-of-bounds prevention not offered in solution #2 (if list2 has fewer items)
        int max = Math.Max(list1.Count, list2.Count);
        for (int i = 0; i < max; i++)
            r3.Add(new { a = list1[i], b = list2[i] });
        var time3 = DateTime.Now - start;

        Debug.WriteLine("r1 == r2: {0}", r1.SequenceEqual(r2));
        Debug.WriteLine("r1 == r3: {0}", r1.SequenceEqual(r3));
        Debug.WriteLine("time1 {0}", time1);
        Debug.WriteLine("time2 {0}", time2);
        Debug.WriteLine("time3 {0}", time3);

输出结果为:

r1 == r2: True
r1 == r3: True
time1 00:00:00.0100071
time2 00:00:00.0170138
time3 00:00:00.0040028

当然,在这种情况下,时间几乎无法被人类感知,所以这取决于个人喜好。但是,由于#3明显是最快的,因此我倾向于在类型更复杂或可枚举对象较大的关键性能区域中使用它。

另外,请注意使用3时的差异:

        const int Max = 100000;
        var rnd = new Random();
        var list1 = Enumerable.Range(1, Max).Select(r => rnd.Next(Max)).ToList();
        var list2 = Enumerable.Range(1, Max).Select(r => rnd.Next(Max)).ToList();
        var list3 = Enumerable.Range(1, Max).Select(r => rnd.Next(Max)).ToList();

        DateTime start;

        start = DateTime.Now;
        var r1 = list1.Zip(list2, (a, b) => new { a, b }).Zip(list3, (ab, c) => new { ab.a, ab.b, c }).ToList();
        var time1 = DateTime.Now - start;

        start = DateTime.Now;
        var r2 = list1.Select((l1, i) => new { a = l1, b = list2[i], c = list3[i] }).ToList();
        var time2 = DateTime.Now - start;

        start = DateTime.Now;
        var r3 = new int[0].Select(i => new { a = 0, b = 0, c = 0 }).ToList();
        //  Easy out-of-bounds prevention not offered in solution #2 (if list2 or list3 have fewer items)
        int max = new int[] { list1.Count, list2.Count, list3.Count }.Max();
        for (int i = 0; i < max; i++)
            r3.Add(new { a = list1[i], b = list2[i], c = list3[i] });
        var time3 = DateTime.Now - start;

        Debug.WriteLine("r1 == r2: {0}", r1.SequenceEqual(r2));
        Debug.WriteLine("r1 == r3: {0}", r1.SequenceEqual(r3));
        Debug.WriteLine("time1 {0}", time1);
        Debug.WriteLine("time2 {0}", time2);
        Debug.WriteLine("time3 {0}", time3);

输出结果:

r1 == r2: True
r1 == r3: True
time1 00:00:00.0280393
time2 00:00:00.0089870
time3 00:00:00.0050041

正如预期的那样,.zip 方法需要进行多次迭代,因此变得最慢。


0

Jeff Mercado提供了一个将三个序列压缩的答案。这可以推广到任意数量的序列,但有一个限制,那就是所有序列都必须具有相同的项类型。

这里是一个通用的zip运算符,它处理不同长度的输入,并具有适当的错误处理和枚举器的正确处理:

static class EnumerableExtensions {

  public static IEnumerable<TResult> Zip<TSource, TResult>(
    this IEnumerable<IEnumerable<TSource>> source,
    Func<IEnumerable<TSource>, TResult> resultSelector
  ) {
    if (source == null)
      throw new ArgumentNullException("source");
    if (resultSelector == null)
      throw new ArgumentNullException("resultSelector");

    var enumerators = new List<IEnumerator<TSource>>();
    try {
      foreach (var enumerable in source) {
        if (enumerable == null)
          throw new ArgumentNullException();
        enumerators.Add(enumerable.GetEnumerator());
      }

      while (enumerators.Aggregate(true, (moveNext, enumerator) => moveNext && enumerator.MoveNext()))
        yield return resultSelector(enumerators.Select(enumerator => enumerator.Current));
    }
    finally {
      foreach (var enumerator in enumerators)
        enumerator.Dispose();
    }
  }

}

然后可以使用这个通用的zip操作符计算颜色:

var reds = new[] { 0x00, 0x03, 0x06, 0x08, 0x09 };
var greens = new[] { 0x00, 0x05, 0x06, 0x07, 0x0a };
var blues = new[] { 0x00, 0x02, 0x03, 0x05, 0x09 };
var colors = new[] { reds, greens, blues }
  .Zip(rgb => new RGB(rgb.First(), rgb.Skip(1).First(), rgb.Skip(2).First()));

这段代码可能不像其他解决方案那样优雅,但通用的 zip 操作符在某些情况下可能很有用,而我提供的代码非常高效,因为它只迭代每个源序列一次。


.Skip(n).First() == .ElementAt(n) - Robert Synoradzki

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