2
\$\begingroup\$

I am working with System.Array and I am trying to convert System.Array objects to array of specific type (such as int[]) in C#. A series of array downcasting methods is designed as follows.

The experimental implementation

The experimental implementation of array downcasters are as below.

class ArrayDowncasters
{
    public static int[] ToIntArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(int)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new int[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (int)input.GetValue(i);
        }
        return output;
    }

    public static sbyte[] ToSbyteArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(sbyte)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new sbyte[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (sbyte)input.GetValue(i);
        }
        return output;
    }

    public static byte[] ToByteArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(byte)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new byte[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (byte)input.GetValue(i);
        }
        return output;
    }

    public static short[] ToShortArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(short)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new short[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (short)input.GetValue(i);
        }
        return output;
    }

    public static ushort[] ToUshortArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(ushort)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new ushort[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (ushort)input.GetValue(i);
        }
        return output;
    }

    public static char[] ToCharArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(char)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new char[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (char)input.GetValue(i);
        }
        return output;
    }

    public static float[] ToFloatArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(float)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new float[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (float)input.GetValue(i);
        }
        return output;
    }

    public static double[] ToDoubleArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(double)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new double[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (double)input.GetValue(i);
        }
        return output;
    }

    public static string[] ToStringArray1(Array input)
    {
        Type elementType = input.GetType().GetElementType();
        if (input.Rank != 1)
        {
            throw new System.InvalidOperationException();
        }

        if (!elementType.Equals(typeof(string)))
        {
            throw new System.InvalidOperationException();
        }

        var output = new string[input.GetLength(0)];
        for (int i = 0; i < input.GetLength(0); i++)
        {
            output[i] = (string)input.GetValue(i);
        }
        return output;
    }
}

Test cases

The Test cases for ToIntArray1 and ToDoubleArray1 methods are as below.

//    One Dimensional Array with int element convert to int[]

var array1 = Array.CreateInstance(typeof(int), 10);
for (int i = 0; i < array1.Length; i++)
{
    array1.SetValue(3, i);
}

var intArray = ArrayDowncasters.ToIntArray1(array1);
for (int i = 0; i < intArray.Length; i++)
{
    Console.WriteLine(intArray[i]);
}

//    One Dimensional Array with double element convert to double[]

var array2 = Array.CreateInstance(typeof(double), 10);
for (int i = 0; i < array2.Length; i++)
{
    array2.SetValue(3.1, i);
}

var doubleArray = ArrayDowncasters.ToDoubleArray1(array2);
for (int i = 0; i < doubleArray.Length; i++)
{
    Console.WriteLine(doubleArray[i]);
}

All suggestions are welcome.

If there is any possible improvement about:

  • Potential drawback or unnecessary overhead
  • Error handling

please let me know.

\$\endgroup\$
3
\$\begingroup\$

There are a few alternative solutions which might work

LINQ

It is possible to use Enumerable.Cast<T>() to achieve the same results (at least for a 1D array)

[TestMethod]
public void CheckConversion()
{
    //    One Dimensional Array with int element convert to int[]
    var array1 = Array.CreateInstance(typeof(int), 10);
    for (int i = 0; i < array1.Length; i++)
    {
        array1.SetValue(3, i);
    }

    var intArray1 = ArrayDowncasters.ToIntArray1(array1);
    var intArray2 = array1.Cast<int>().ToArray();
    CollectionAssert.AreEqual(intArray1, intArray2);

    //    One Dimensional Array with double element convert to double[]
    var array2 = Array.CreateInstance(typeof(double), 10);
    for (int i = 0; i < array2.Length; i++)
    {
        array2.SetValue(3.1, i);
    }

    var doubleArray1 = ArrayDowncasters.ToDoubleArray1(array2);
    var doubleArray2 = array2.Cast<double>().ToArray();
    CollectionAssert.AreEqual(doubleArray1, doubleArray2);

}

Generics

The naming of the functions e.g. ToDoubleArray1() indicates that there might be 2 or 3 or n dimensional versions to be added. I don't know if a LINQ solution can be made to work for these but the current code can be re-written as a single generic function

public static T[] Convert<T>(Array input)
{
    Type elementType = input.GetType().GetElementType();
    if (input.Rank != 1)
    {
        throw new System.InvalidOperationException();
    }

    if (!elementType.Equals(typeof(T)))
    {
        throw new System.InvalidOperationException();
    }

    var output = new T[input.GetLength(0)];
    for (int i = 0; i < input.GetLength(0); i++)
    {
        output[i] = (T)input.GetValue(i);
    }
    return output;
}

which means that a single generic function per dimension may suffice

[TestMethod]
public void CheckConversionGeneric()
{
    //    One Dimensional Array with int element convert to int[]
    var array1 = Array.CreateInstance(typeof(int), 10);
    for (int i = 0; i < array1.Length; i++)
    {
        array1.SetValue(3, i);
    }

    var intArray1 = ArrayDowncasters.ToIntArray1(array1);
    var intArray2 = ArrayDowncasters.Convert<int>(array1);
    CollectionAssert.AreEqual(intArray1, intArray2);

    //    One Dimensional Array with double element convert to double[]
    var array2 = Array.CreateInstance(typeof(double), 10);
    for (int i = 0; i < array2.Length; i++)
    {
        array2.SetValue(3.1, i);
    }

    var doubleArray1 = ArrayDowncasters.ToDoubleArray1(array2);
    var doubleArray2 = ArrayDowncasters.Convert<double>(array2);
    CollectionAssert.AreEqual(doubleArray1, doubleArray2);

}
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for the answer. I am wondering if it is possible to deduce T as elementType automatically so that ArrayDowncasters.Convert(array2); can be used directly and the type check if (!elementType.Equals(typeof(T)))... isn't needed. \$\endgroup\$
    – JimmyHu
    Feb 19 at 8:41
  • 1
    \$\begingroup\$ If I understand the question correctly, the problem will be on the left hand side. var is just a way of telling the compiler, 'you determine the type based upon the rest of the code' which means we need something in the code at compile time to tell it the target type. Take the original test code. The compiler knows that doubleArray is of type double[] because that is the return type of ToDoubleArray1() is a double[]. In the generic version, the Convert<**double**>() tells us it is a double[]. The elementType of the array is a runtime characteristic and no use to the compiler. \$\endgroup\$
    – AlanT
    Feb 19 at 9:19
3
\$\begingroup\$

After reading the code in question it seems every method is doing exactly the same hence I will focus only on one method and tell you what I would change:

public static int[] ToIntArray1(Array input)
{
    Type elementType = input.GetType().GetElementType();
    if (input.Rank != 1)
    {
        throw new System.InvalidOperationException();
    }

    if (!elementType.Equals(typeof(int)))
    {
        throw new System.InvalidOperationException();
    }

    var output = new int[input.GetLength(0)];
    for (int i = 0; i < input.GetLength(0); i++)
    {
        output[i] = (int)input.GetValue(i);
    }
    return output;
}

If this method is called and this would result in an InvalidOperationException one wouldn't know the exact cause of that exception because it could either be that the passed Array hasn't a Rank==1 or the type of the elements doesn't match the expected type.
What I would change here is to have the InvalidException being created using a message indicating what went wrong.

The line

Type elementType = input.GetType().GetElementType();  

should be placed after the check for Rank because its nearer to its usage and would be superflous if e.g Rank == 2.

Usually I would suggest to introduce another variable to hold the value of input.GetLength(0) because you are calling this method twice, but because we know at this point that the array isn't multidimensional we can just use the Length property of the array.

This would sum up to look like so

public static int[] ToIntArray1(Array input)
{
    
    if (input.Rank != 1)
    {
        throw new System.InvalidOperationException("Only one-dimensional array allowed");
    }

    Type elementType = input.GetType().GetElementType();
    if (!elementType.Equals(typeof(int)))
    {
        throw new System.InvalidOperationException("Wrong type");
    }

    var output = new int[input.Length];
    for (int i = 0; i < input.Length; i++)
    {
        output[i] = (int)input.GetValue(i);
    }
    return output;
}

Because all the methods are doing the same, you could just make one generic method like so

public static T[] ToArray<T>(Array input)
{

    if (input.Rank != 1)
    {
        throw new System.InvalidOperationException("Only one-dimensional array allowed");
    }

    Type elementType = input.GetType().GetElementType();
    if (!elementType.Equals(typeof(T)))
    {
        throw new System.InvalidOperationException("Wrong type");
    }

    var output = new T[input.Length];
    for (var i = 0; i < input.Length; i++)
    {
        output[i] = (T)input.GetValue(i);
    }
    return output;
}
\$\endgroup\$
3
\$\begingroup\$

Just use Array.ConvertAll that is already exist.

For instance, you want to convert array from string[] to int[] you can do this :

var converted = Array.ConvertAll(array , item => int.TryParse(item?.ToString() , out int result) ? result : 0);

Another point is, if you only targeting one-dimension array, then you don't need to use the abstract class Array, instead use one-dimension array types such as int[].

So, if you for instance use int[] as an argument then you will not need these lines :

 Type elementType = input.GetType().GetElementType();
if (input.Rank != 1)
{
    throw new System.InvalidOperationException();
}

if (!elementType.Equals(typeof(int)))
{
    throw new System.InvalidOperationException();
}

Note, you are missing null validation, you need to always check for nulls input whenever possible.

finally, if you don't need to use Array.ConvertAll directly, or you need to reduce the repetition of some converting logic, you could create a method and include your application business logic, then use Array.ConvertAll in the method.

For instance :

public static class ArrayDowncasters
{
    private static readonly Type[] _acceptableTypes =
    {
        typeof(int),
        typeof(sbyte),
        typeof(byte),
        typeof(short),
        typeof(ushort),
        typeof(char)
    };

    public static TOutput[] Convert<TInput, TOutput>(TInput[] array, Converter<TInput , TOutput> converter)
    {
        if(array == null)
        { return null; }

        var type = typeof(TOutput);

        if(!_acceptableTypes.Contains(type))
        {
            throw new NotSupportedException($"{type.Name}");
        }

        return Array.ConvertAll<TInput, TOutput>(array , converter);
    }
}

In the example above, It made things easy to control by restricting the types using _acceptableTypes. if there is a requirement for a new type, it's possible to only add the type to the _acceptableTypes, and adjust the method to that new type if necessary.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.