开发者

Better way to consume an IEnumerable<IEnumerable<string>>

开发者 https://www.devze.com 2023-02-20 10:23 出处:网络
I\'m calling a stored procedure with multiple results sets (always 2) and writing the results to seperate files (in pipe delimited format).I can\'t split the results sets into seperate stored procs.I\

I'm calling a stored procedure with multiple results sets (always 2) and writing the results to seperate files (in pipe delimited format). I can't split the results sets into seperate stored procs. I'm using IDataReader and IEnumerable to keep as little in memor开发者_运维问答y in the process.

Is there a cleaner way of consuming my IEnumerable<IEnumerable<string>> than using GetEnumerator/MoveNext/Current to get to the inner IEnumerable<string> to pass to File.AppendAllLines?

    public void Execute()
    {
        var reader = GetLines();

        using (var enumerator = reader.GetEnumerator())
        {
            enumerator.MoveNext();

            File.AppendAllLines("file1.dat", enumerator.Current);
            enumerator.MoveNext();

            File.AppendAllLines("file2.dat", enumerator.Current);
        }
    }

    public IEnumerable<IEnumerable<string>> GetLines()
    {
        Database db = DatabaseFactory.CreateDatabase("connectionStringKey");
        using (var command = db.GetStoredProcCommand("getdata_sp"))
        {
            var reader = db.ExecuteReader(command);
            yield return GetInnerEnumerable(reader);
            reader.NextResult();
            yield return GetInnerEnumerable(reader);
        }
    }

    private IEnumerable<string> GetInnerEnumerable(IDataReader reader)
    {
        while (reader.Read())
        {
            object[] rowValues = new object[reader.FieldCount];
            reader.GetValues(rowValues);
            yield return String.Join("|", rowValues);
        }
    }


Why not a foreach loop? That is the most basic.


Personally, I would just use a foreach loop with a separate variable for tracking which file to write to, something like:

public void Execute()
{
    var reader = GetLines();

    int i = 0;
    foreach (var inner in reader)
    {
        if (i % 2 == 0)
            File.AppendAllLines("file1.dat", inner);
        else
            File.AppendAllLines("file2.dat", inner);
        ++i;
    }
}


You could use SelectMany() to flatten the enumeration, since you are only interested in the values themselves.

Edit:

As per comment SelectMany() is unsuitable given the use case, so best is to use a foreach loop:

var reader = GetLines();
int index = 0;
foreach(var lines in reader)
    File.AppendAllLines(string.Format("file{0}.dat", index++%2 + 1), lines);


Maybe turn the result of GetLines() into an array and access it by index (since you said there will always be 2 result sets)?

public void Execute()
{
    IEnumerable<string>[] rows = GetLines().ToArray();

    File.AppendAllLines("file1.dat", rows[0]);
    File.AppendAllLines("file2.dat", rows[1]);
}


I would just change my GetLines method to the following

public IEnumerable<string> GetLines()
{
    Database db = DatabaseFactory.CreateDatabase("connectionStringKey");
    using (var command = db.GetStoredProcCommand("getdata_sp"))
    {
        var reader = db.ExecuteReader(command);
        for (var i = 0; i < 2; i++) 
        {  
          foreach(var cur in GetInnerEnumerable(reader))
          {
            yield return cur;
          }
          reader.NextResult();
        }
    }
}

Having it return an IEnumerable<IEnumerable<string>> will produce an unnecessary burden to consumers of the API. My guess is they will all just prefer to see this as an IEnumerable<string>.


foreach is implicitly supported by IEnumerable. So:

public void Execute()
{
    var reader = GetLines();

    using (var enumerator = reader.GetEnumerator())
    {
        enumerator.MoveNext();

        File.AppendAllLines("file1.dat", enumerator.Current);
        enumerator.MoveNext();

        File.AppendAllLines("file2.dat", enumerator.Current);
    }
}

Becomes:

public void Execute()
{
    var reader = GetLines();

    int index = 0;

    foreach (string line in reader)
    {
        if ((index % 2) == 0)
            File.AppendAllLines("file1.dat", line);

        else
            File.AppendAllLines("file2.dat", line);

        index++;
    }
}

Or:

public void Execute()
{
    var reader = GetLines();

    var evenLines = reader.Where((str, i) => i % 2 == 0);
    var oddLines = reader.Where((str, i) => i % 2 != 0);

    foreach (string line in evenLines)
        File.AppendAllLines("file1.dat", line);

    foreach (string line in oddLines)
        File.AppendAllLines("file2.dat", line);
}


You can zip your results into a Tuple<> with your filenames like so:

using System.Linq;
using FileZip = System.Tuple<
    System.String,
    System.Collections.Generic.IEnumerable<
        System.String>>;

public void Execute()
{
     var files = new string[] { "file1.dat", "file2.dat" };
     var results = GetLines();

     foreach (var file in files.Zip(results, (f, r) => new FileZip(f, r)))
     {
         File.AppendAllLines(file.Item1, file.Item2);
     }
}

Of course, I'm pretty sure this is going to throw as soon as you return a different number of rows, but it will do what you're looking for.

0

精彩评论

暂无评论...
验证码 换一张
取 消