简体   繁体   中英

In C#, how can I force iteration over IEnumerable within nested foreach loops?

I have two IEnumerable s:

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

I want to create a second IEnumerable<string> that is the concatenation of each element of each IEnumerable .

For example:

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

This would produce:

"ac"; "ad"; "bc"; "bd";

The problem is, sometimes one of the two IEnumerable s is empty:

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

In this case, the nested foreach construct never reaches the yield return statement. When either IEnumerable is empty, I would like the result to just be the list of the non-empty IEnumerable .

How can I produce the combinations I am looking for?

EDIT : In reality, I have three different IEnumerable s I am trying to combine, so adding if conditions for every possible permutation of empty IEnumerable seems bad. If that's the only way, then I guess I'll have to do it that way.

You can simply check that first enumerable is not empty:

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

To eliminate double IEnumerable evaluation in positive cases just convert first enumerable to list

Your current approach should work until any of the collections is empty. If this is the case you need some check in front:

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

However you should consider making an immediate execution using ToList in front to avoid multiple iterations of the same collection.

Assuming you're output for case :

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

would be :

c
d

This would work :

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

Less code , easier to maintain and debug !

Edit : If you have any other IEnumerable is just 1 extra line per IEnumerable ( includes the check )

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;

Edit 2 : you can just add the spaces at the end :

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

Simply use Enumerable.DefaultIfEmpty() to enumerate collection even if there is no items.

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

Note: I have used String.Join to join items which are not null or empty and method to select non-empty items to be joined (you can inline this code if you don't want to have a separate method):

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

Output for sample above is

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

For two collections and foreach loops (though I would prefere LINQ as above):

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
}

Output:

[ "a", "b" ]

If you have more than a couple lists, you can setup a recursive iterator. You'll want to be mindful of the stack, and I think the string concatenation is less than ideal, and passing lists of lists is rather clunky, but this should get you started.

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

console output:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM