Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert string to generic type (basic or array) in C#

I want to convert a string to a given generic type T. It may be either basic type or string (e.g. int or string), or an array of a basic types or strings (e.g. int[] or string[]). I have the following function:

T Str2Val<T>(string str)
{
  return (T)Convert.ChangeType(str, typeof(T));
}

It works well for basic types. But it fails for T being an array E[] and str being a comma-separated list of values.

I can easily check whether T is an array with typeof(T).IsArray. Then I have two solutions: parse an array in the same function with a scalar, like the following:

  if (!typeof(T).IsArray)
  {
    return (T)Convert.ChangeType(str, typeof(T));
  }
  else
  {
    // Handle an array
  }

or implement two overloaded functions: one for generic T and second for generic E[]. However, both of solutions fail. I cannot use any array-specific code in the else-clause since it must be compatible with a scalar. And C# cannot pick proper overload with E[] when T is actually an array.

What should I do?

like image 500
Mikhail Avatar asked Jun 28 '26 09:06

Mikhail


1 Answers

I would create some listing of custom parsers that you register then later leverage since you seem to be wanting to use custom rules anyway:

public static class StringParsers
{
    private static Dictionary<Type, object> Parsers = new Dictionary<Type, object>();

    public static void RegisterParser<T>(Func<string, T> parseFunction)
    {
        Parsers[typeof(T)] = parseFunction;
    }

    public static T Parse<T>(string input)
    {
        object untypedParser;
        if (!Parsers.TryGetValue(typeof(T), out untypedParser))
            throw new Exception("Could not find a parser for type " + typeof(T).FullName);

        Func<string, T> parser = (Func<string, T>)untypedParser;

        return parser(input);
    }
}

During your application initialization, you would register the types you intend to use later in your application (I'm guessing this is known since you're using generics):

StringParsers.RegisterParser<string[]>(input => input.Split(','));
StringParsers.RegisterParser<int[]>(input => input.Split(',').Select(i => Int32.Parse(i)).ToArray());
StringParsers.RegisterParser<int>(input => Int32.Parse(input));

Finally, you can call it simply:

string testArrayInput = "1,2,8";

int[] integers = StringParsers.Parse<int[]>(testArrayInput); // {1, 2, 8}

string[] strings = StringParsers.Parse<string[]>(testArrayInput); // {"1", "2", "8"}

int singleInt = StringParsers.Parse<int>("9999"); //9999

Now, this is a pretty simple implementation. You may wish to extend it so instead of using type Func<string, T> it might use an IStringParser interface and you can provide deeper implementations of the parsing if necessary. Furthermore, you may wish to make it thread safe (unless you're sure that won't be an issue, or if you are sure your registration on startup is before any usages)

EDIT: If you really, really, really want it all in one function just accounting for your comma delimited array, then you can use this:

public static T Str2Val<T>(string str)
{
    if (!typeof(T).IsArray)
        return (T)Convert.ChangeType(str, typeof(T));

    Type elementType = typeof(T).GetElementType();

    string[] entries = str.Split(',');
    int numberOfEntries = entries.Length;

    System.Array array = Array.CreateInstance(elementType, numberOfEntries);

    for(int i = 0; i < numberOfEntries; i++)
        array.SetValue(Convert.ChangeType(entries[i], elementType), i);

    return (T)(object)array;
}

But this feels so wrong. There must be a better way and to avoid the double generic input in Alexander's answer, but there you go.

like image 86
Chris Sinclair Avatar answered Jun 30 '26 02:06

Chris Sinclair