【问题标题】:In C#, how can I force iteration over IEnumerable within nested foreach loops?在 C# 中,如何在嵌套的 foreach 循环中强制迭代 IEnumerable?
【发布时间】:2017-09-30 02:28:35
【问题描述】:

我有两个IEnumerables:

IEnumerable<string> first = ...
IEnumerable<string> second = ...

我想创建第二个IEnumerable&lt;string&gt;,它是每个IEnumerable 的每个元素的串联。

例如:

IEnumerable<string> first = new [] {"a", "b"};
IEnumerable<string> second = new [] {"c", "d"};

foreach (string one in first)
{
   foreach (string two in second)
   {
      yield return string.Format("{0} {1}", one, two);
   }
}

这会产生:

"a c"; "a d"; "b c"; "b d";

问题是,有时两个IEnumerables 之一是空的:

IEnumerable<string> first = new string[0];
IEnumerable<string> second = new [] {"c", "d"};

在这种情况下,嵌套的foreach 构造永远不会到达yield return 语句。当IEnumerable 为空时,我希望结果只是非空IEnumerable 的列表。

我怎样才能产生我正在寻找的组合?

编辑: 实际上,我尝试组合三个不同的IEnumerables,因此为空IEnumerable 的每个可能排列添加if 条件似乎很糟糕。如果这是唯一的方法,那么我想我必须这样做。

【问题讨论】:

  • 查看 Zip linq 运算符。它需要两个枚举并允许您遍历两者。如果尺寸不同,请考虑到这一点。
  • @JohnPeters 他没有压缩序列。
  • “当任一 IEnumerable 为空时,我希望结果只是非空 IEnumerable 的列表”然后您应该在循环之前处理这种情况。
  • 使用索引,如果 zip 不适合你。
  • 为了清楚起见,您能否在问题中包含first 为空和second 为空的示例输出。

标签: c#


【解决方案1】:

如果您有多个列表,则可以设置递归迭代器。您需要注意堆栈,并且我认为字符串连接不太理想,并且传递列表列表相当笨拙,但这应该可以帮助您入门。

using System;
using System.Collections.Generic;
using System.Linq;

namespace en
{
    class Program
    {
        static void Main(string[] args)
        {
            // three sample lists, for demonstration purposes.
            var a = new List<string>() { "a", "b", "c" };
            var b = new List<string>() { "1", "2", "3" };
            var c = new List<string>() { "i", "ii", "iii" };

            // the function needs everything in one argument, so create a list of the lists.
            var lists = new List<List<string>>() { a, b, c };

            var en = DoStuff(lists).GetEnumerator();

            while (en.MoveNext())
            {
                Console.WriteLine(en.Current);
            }
        }

        // This is the internal function. I only made it private because the "prefix" variable
        // is mostly for internal use, but there might be a use case for exposing that ...
        private static IEnumerable<String> DoStuffRecursive(IEnumerable<String> prefix, IEnumerable<IEnumerable<String>> lists)
        {
            // start with a sanity check
            if (object.ReferenceEquals(null, lists) || lists.Count() == 0)
            {
                yield return String.Empty;
            }

            // Figure out how far along iteration is
            var len = lists.Count();

            // down to one list. This is the exit point of the recursive function.
            if (len == 1)
            {
                // Grab the final list from the parameter and iterate over the values.
                // Create the final string to be returned here.
                var currentList = lists.First();
                foreach (var item in currentList)
                {
                    var result = prefix.ToList();
                    result.Add(item);

                    yield return String.Join(" ", result);
                }
            }
            else
            {
                // Split the parameter. Take the first list from the parameter and 
                // separate it from the remaining lists. Those will be handled 
                // in deeper calls.
                var currentList = lists.First();
                var remainingLists = lists.Skip(1);

                foreach (var item in currentList)
                {
                    var iterationPrefix = prefix.ToList();
                    iterationPrefix.Add(item);

                    // here's where the magic happens. You can't return a recursive function
                    // call, but you can return the results from a recursive function call.
                    // http://stackoverflow.com/a/2055944/1462295
                    foreach (var x in DoStuffRecursive(iterationPrefix, remainingLists))
                    {
                        yield return x;
                    }
                }
            }
        }

        // public function. Only difference from the private function is the prefix is implied.
        public static IEnumerable<String> DoStuff(IEnumerable<IEnumerable<String>> lists)
        {
            return DoStuffRecursive(new List<String>(), lists);
        }
    }
}

控制台输出:

a 1 i
a 1 ii
a 1 iii
a 2 i
a 2 ii
a 2 iii
a 3 i
a 3 ii
a 3 iii
b 1 i
b 1 ii
b 1 iii
b 2 i
b 2 ii
b 2 iii
b 3 i
b 3 ii
b 3 iii
c 1 i
c 1 ii
c 1 iii
c 2 i
c 2 ii
c 2 iii
c 3 i
c 3 ii
c 3 iii

【讨论】:

    【解决方案2】:

    即使没有项目,也只需使用Enumerable.DefaultIfEmpty() 枚举集合。

    IEnumerable<string> first = new string[0]; 
    IEnumerable<string> second = new[] { "a", "b" };
    IEnumerable<string> third = new[] { "c", null, "d" };
    
    var permutations = 
         from one in first.DefaultIfEmpty()
         from two in second.DefaultIfEmpty()
         from three in third.DefaultIfEmpty()
         select String.Join(" ", NotEmpty(one, two, three));
    

    注意:我使用String.Join 来连接不为空或空的项目以及选择要连接的非空项目的方法(如果您不想使用单独的方法,可以内联此代码):

    private static IEnumerable<string> NotEmpty(params string[] items)
    {
        return items.Where(s => !String.IsNullOrEmpty(s));
    }
    

    上面示例的输出是

    [ "a c", "a", "a d", "b c", "b", "b d" ]
    

    对于两个集合和 foreach 循环(虽然我更喜欢上面的 LINQ):

    IEnumerable<string> first = new[] { "a", "b" };
    IEnumerable<string> second = new string[0];
    
    foreach(var one in first.DefaultIfEmpty())
    {
         foreach(var two in second.DefaultIfEmpty())
            yield return $"{one} {two}".Trim(); // with two items simple Trim() can be used
    }
    

    输出:

    [ "a", "b" ]
    

    【讨论】:

    • @doremifasolasido 你这是什么意思?如果您将第二个集合设为空,则结果将完全相同。
    • 啊,漂亮。基本上,与@doremifasolasido 的答案相同,但使用框架提供的方法而不是对空数组进行编码。你的两个答案都是我要找的。​​span>
    • Trim() 将只删除开始和结束多余的空格...而不是中间的
    • @michaelkoss 有一个显着的区别 - Any() 将开始枚举。以最简单的方式,它只会创建枚举器对象并尝试获取第一项。但是,如果您的字符串来自文件或数据库等慢速资源,那么它将再次打开文件或运行数据库查询
    • @SergeyBerezovskiy,我对DefaultIfEmpty() 的实现不太熟悉,但是确定IEnumerable 是否为空是否也不必这样做?
    【解决方案3】:

    假设你是 case 的输出:

    IEnumerable<string> first = new string[0];
    IEnumerable<string> second = new [] {"c", "d"};
    

    应该是:

    c
    d
    

    这可行:

    var query = from x in first.Any() ? first : new [] { "" }
                from y in second.Any() ? second : new[] { "" }
                select x + y;
    

    代码更少,更易于维护和调试!

    编辑:如果您有任何其他 IEnumerable,则每个 IEnumerable 只需多出 1 行(包括检查)

    var query = from x in first.Any() ? first : new [] { "" }
                from y in second.Any() ? second : new[] { "" }
                from z in third.Any() ? third : new[] { "" }
                select x + y + z;
    

    编辑 2 :您可以在末尾添加空格:

    select (x + y + z).Aggregate(string.Empty, (c, i) => c + i + ' ');
    

    【讨论】:

    • 到目前为止,这是我正在寻找的最接近的答案。
    • @michaelkoss 这在输出中的项目之间不会有空格。你还好吗?
    • 关闭,但如果我正确理解了 OP,则存在细微差别:如果第一个可枚举是 {""},则结果应该是 {" c", " d"}。如果为空,则结果应为{"c", "d"}
    • .Aggregate(string.Empty, (c, i) => c + i + ' ');只会在逻辑末尾添加空格
    • @SergeyBerezovskiy,我知道这不会有空格,我可以选择性地添加/删除它。
    【解决方案4】:

    您可以简单地检查第一个可枚举项是否为空:

    IEnumerable<string> first = new [] {"a", "b"};
    IEnumerable<string> second = new [] {"c", "d"};
    
    var firstList = first.ToList();
    
    if (!firstList.Any()) {
        return second;
    }
    
    foreach (string one in firstList)
    {
       foreach (string two in second)
       {
          yield return string.Format("{0} {1}", one, two);
       }
    }
    

    要在肯定的情况下消除双重 IEnumerable 评估,只需将第一个可枚举转换为列表

    【讨论】:

    • 这是正确的解决方案 - 如果列表为空,OP 想要一个特殊情况,因此正确的解决方案是检查特殊条件。为什么这被否决了?
    • 为什么要拨打ToList
    • 如果first不为空,将有两个IEnumerable评估没有列表转换
    • 可能还应该检查第二个是否为空并在这种情况下返回第一个,但这取决于 OP 的要求。
    • 您可以检查是否首先实现 IList,而不是调用 ToList。不确定实现未知大小的 IEnumerable 是一个好主意,因为 Any 最多会查找 1 个项目(以防首先不实现 IList)。实际上,我想 Any 无论如何都会为你做那个检查。
    【解决方案5】:

    在任何集合为空之前,您当前的方法都应该有效。如果是这种情况,您需要在前面进行一些检查:

    if(!first.Any())
        foreach(var e in second) yield return e;
    else if(!second.Any())
        foreach(var e in first) yield return e;
    
    foreach (string one in first)
    {
       foreach (string two in second)
       {
          yield return string.Format("{0} {1}", one, two);
       }
    }
    

    但是,您应该考虑在前面使用ToList 立即执行,以避免同一集合的多次迭代。

    【讨论】:

    • 小心,这会导致对您调用.Any() on..的 IEnumerable 进行双重评估。
    • 是的,我的实际代码在第二个上使用 .ToList() 以避免对 IEnumerable 进行多次迭代。如果我必须使用 .Any(),我也会在第一个中包含 .ToList()。
    猜你喜欢
    • 1970-01-01
    • 2021-07-21
    • 1970-01-01
    • 1970-01-01
    • 2016-04-07
    • 2017-11-04
    • 1970-01-01
    • 2015-07-06
    • 1970-01-01
    相关资源
    最近更新 更多