Search code examples
c#arraysgenericstype-conversiontype-parameter

How to convert a string array to a generic array type while allowing both non array and array as generic type parameter for the class?


I have an Option<T> that works great for any type that be converted from string, and now I am trying to extend that to cover Option<T[]> (ie, Option<int[]>). Afraid I may be coming at this problem with too many C++ templates under my belt. Having trouble wrapping my head around the seemingly inadequate C# generics. I can detect when T is an array, but then I can't make any use of typeof(T).GetElementType().

I think I may be in one of those XY problem valleys, where I am just coming at this from the wrong direction and can't see the path over the rise. Any ideas how to get unbocked? I've tried everything I can think of and spend the past couple of days trying to figure out to get unblocked. I would add that I can arrange to parse the comma delimited string into an array of strings prior to conversion. The code below is a simplified extract from some of what I've tried.

using System;
using System.Collections.Generic;

namespace StackOverflowCS
{
    internal static class ConversionExtensionMethods
    {
        internal static T ChangeType<T>(this object obj)
        {
            try
            {
                return (T)Convert.ChangeType(obj, typeof(T));
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        internal static T ChangeType<T>(this object[] objects)
        {
            try
            {
                if (!typeof(T).IsArray) throw new Exception("T is not an array type.");

                var converted = new object[objects.Length];

                foreach (var item in objects)
                {
                    // AFAIK, converstion requires compile time knowledge of T.GetElementType(),
                    // but this won't compile.
                    converted.Add(item.ChangeType<typeof(T).GetElementType())>
                }

                return (T)converted; // And this won't compile either.
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
    }


    internal class Option<T>
    {
        public T Value;

        public Option() {}

        // This works fine for non-arrays
        public bool SetValue(string valueString)
        {
            try
            {
                Value = valueString.ChangeType<T>();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return false;
            }

            return true;
        }

        // I think I am in an XY problem valley here.
        public bool SetValue(string[] valueStrings)
        {
            try
            {
                if (!typeof(T).IsArray)
                {
                    throw new Exception("T is not an array type.");
                }

                // The crux of my problem is I can't seem to write pure generic code in C#
                var convertedElements = new List<!!!Cannot use typeof(T).GetElementType() here!!!>();

                foreach (var item in valueStrings)
                {
                    // The crux of my problem is I can't seem to write pure generic code in C#
                    convertedElements.Add(!!!Cannot use typeof(T).GetElementType() here!!!);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return false;
            }

            return true;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            var opt = new Option<int>(); // Works fine.

            var integerList = new Option<int[]>();

            integerList.SetValue("this,that,whatever"); // This fails at run-time.

            foreach (var item in integerList.Value)
            {
                Console.WriteLine(item);
            }


            Console.ReadKey();
        }
        
    }
}

The parser (not shown) can detect arguments of the form
Opt:1,2,3 or
Opt:"short sentence",word,"string with quotes\" in it", etc.
I'd rather not have the parser try to figure out what type the Opt Option's array elements are. The Option<T>.SetValue(string[] strings) function should be able to handle that.
I haven't even tried test/implement `Options<List> yet, though I suspect that would be much easier.


Solution

  • You can try this to allow both non-array and array generic types parameters:

    using System;
    using System.Linq;
    

    SetValue(string valueString)

    public bool SetValue(string valueString)
    {
      try
      {
        if ( typeof(T).IsArray ) throw new Exception("T is an array type.");
        Value = (T)Convert.ChangeType(valueString, typeof(T));
      }
      catch ( Exception e )
      {
        Console.WriteLine(e);
        return false;
      }
      return true;
    }
    

    SetValue(string[] valueStrings)

    public bool SetValue(string[] valueStrings)
    {
      try
      {
        if ( !typeof(T).IsArray ) throw new Exception("T is not an array type.");
        var thetype = typeof(T).GetElementType();
        var list = valueStrings.Select(s => Convert.ChangeType(s, thetype)).ToList();
        var array = Array.CreateInstance(thetype, list.Count);
        for (int index = 0; index < list.Count; index++ )
          array.SetValue(list[index], index);
        Value = (T)Convert.ChangeType(array, typeof(T));
      }
      catch ( Exception e )
      {
        Console.WriteLine(e);
        return false;
      }
      return true;
    }
    

    The test

    static void Main(string[] args)
    {
      // Non array
      var opt = new Option<int>();
      opt.SetValue("10");
      Console.WriteLine(opt.Value);
      Console.WriteLine();
      // Array
      var integerList = new Option<int[]>();
      integerList.SetValue(new[] { "1", "2", "3" });
      foreach ( var item in integerList.Value )
        Console.WriteLine(item);
      // End
      Console.ReadKey();
    }
    

    Output

    10
    
    1
    2
    3