Search code examples
c#arraysserializationmultidimensional-array

How can I set the value of each element in a multidimensional System.Array of unknown rank?


I'm trying to serialize multidimensional arrays with any number of dimensions. Note that I'm referring to actual multidimensional arrays (float[,]), not jagged arrays (float[][]).

After serializing the length of each dimension, I can easily serialize each element using foreach (var item in array).

But when deserializing I can't use foreach because I need to assign the values. I know that I'll need to use array.SetValue(object value, params int[] indices), but I can't wrap my head around how to set up a loop to do what you would normally do with one nested for loop for each dimension.


Solution

  • First, serialize the original array using a foreach loop. It will flatten the array.

    Then, read the values in the same way the were serialized (code is based on the array's enumerator)

    var original = new int[2,2,2] { { { 1, 2 }, { 3, 4} }, { { 5, 6 }, { 7, 8 } } };
    
    var serialized = original.Cast<int>().ToArray();
    var originalBounds = Enumerable.Range(0, original.Rank)
        .Select(i => original.GetUpperBound(i) + 1).ToArray();
    
    var empty = Array.CreateInstance(typeof(int), originalBounds);
    
    var indices = new int[empty.Rank];
    indices[indices.Length - 1]--;
    var index = 0;
    while (IncArray(empty, indices))
    {
        empty.SetValue(serialized[index++], indices);
    }
    
    private bool IncArray(Array array, int[] indices)
    {
        int rank = array.Rank;
        indices[rank - 1]++;
        for (int i = rank - 1; i >= 0; i--)
        {
            if (indices[i] > array.GetUpperBound(i))
            {
                if (i == 0)
                {
                    return false;
                }
                for (int j = i; j < rank; j++)
                {
                    indices[j] = 0;
                }
                indices[i - 1]++;
            }
        }
        return true;
    }
    

    This approach would allow you to read the values from a stream - you don't need to create the serialized array like I have.

    Another way is to use Buffer.BulkCopy. Here you'd have to read the entire array, at least into bytes, and calculate the amount of bytes you need to copy. The advantage here is that you avoid all the boxing in the previous method, and that it's simpler:

    var bytes = new byte[...]; // read the entire byte array
    
    var empty = Array.CreateInstance(typeof(int), originalBounds);
    
    Buffer.BlockCopy(bytes, 0, empty, 0, bytes.Length);