How to make a generic number parser in C#?

2019-01-25 09:42发布

To parse a string to an int, one calls Int32.Parse(string), for double, Double.Parse(string), for long, Int64.Parse(string), and so on..

Is it possible to create a method that makes it generic, for example, ParseString<T>(string)? where T can be Int32, Double, etc. I notice the number of types don't implement any common interface, and the Parse methods don't have any common parent.

Is there any way to achieve this or something similar to this?

5条回答
欢心
2楼-- · 2019-01-25 09:51

You'd basically have to use reflection to find the relevant static Parse method, invoke it, and cast the return value back to T. Alternatively, you could use Convert.ChangeType or get the relevant TypeDescriptor and associated TypeConverter.

A more limited but efficient (and simple, in some ways) approach would be to keep a dictionary from type to parsing delegate - cast the delegate to a Func<string, T> and invoke it. That would allow you to use different methods for different types, but you'd need to know the types you needed to convert to up-front.

Whatever you do, you won't be able to specify a generic constraint which would make it safe at compile-time though. Really you need something like my idea of static interfaces for that kind of thing. EDIT: As mentioned, there's the IConvertible interface, but that doesn't necessarily mean that you'll be able to convert from string. Another type could implement IConvertible without having any way of converting to that type from a string.

查看更多
该账号已被封号
3楼-- · 2019-01-25 10:06

This is very hackish, but it works using Newtonsoft.Json (Json.NET):

 JsonConvert.DeserializeObject<double>("24.11");
 // Type == System.Double - Value: 24.11

 JsonConvert.DeserializeObject<int>("29.4");
 // Type == System.Int32 - Value: 29
查看更多
再贱就再见
4楼-- · 2019-01-25 10:08

Actually, the standard number types do implement a common interface: IConvertible. This is the one that Convert.ChangeType use.

Unfortunately, there is no TryParse equivalent, it will throw exceptions if the string cannot be parsed.

As a side note, it seems this whole "conversion" area has been completely forgotten by the BCL team. There is nothing new there since .NET Framework 1 (except from TryParse methods).

查看更多
萌系小妹纸
5楼-- · 2019-01-25 10:09

I have written some code that uses reflection to find Parse/TryParse methods on a type and access these from generic functions:

private static class ParseDelegateStore<T>
{
    public static ParseDelegate<T> Parse;
    public static TryParseDelegate<T> TryParse;
}

private delegate T ParseDelegate<T>(string s);
private delegate bool TryParseDelegate<T>(string s, out T result);


public static T Parse<T>(string s)
{
    ParseDelegate<T> parse = ParseDelegateStore<T>.Parse;
    if (parse == null)
    {
        parse = (ParseDelegate<T>)Delegate.CreateDelegate(typeof(ParseDelegate<T>), typeof(T), "Parse", true);
        ParseDelegateStore<T>.Parse = parse;
    }
    return parse(s);
}

public static bool TryParse<T>(string s, out T result)
{
    TryParseDelegate<T> tryParse = ParseDelegateStore<T>.TryParse;
    if (tryParse == null)
    {
        tryParse = (TryParseDelegate<T>)Delegate.CreateDelegate(typeof(TryParseDelegate<T>), typeof(T), "TryParse", true);
            ParseDelegateStore<T>.TryParse = tryParse;
    }
    return tryParse(s, out result);
}

https://github.com/CodesInChaos/ChaosUtil/blob/master/Chaos.Util/Conversion.cs

But I haven't tested them too much, so they might stiff have some bugs/not work correctly with every type. The error handling is a bit lacking too.

And they have no overloads for culture invariant parsing. So you probably need to add that.

查看更多
爱情/是我丢掉的垃圾
6楼-- · 2019-01-25 10:11

Yes, the types that can be parsed from a string will most likely have static Parse and TryParse overloads that you can find via reflection like Jon suggested.

private static Func<string, T> GetParser<T>()
{
    // The method we are searching for accepts a single string.
    // You can add other types, like IFormatProvider to target specific overloads.
    var signature = new[] { typeof(string) };

    // Get the method with the specified name and parameters.
    var method = typeof(T).GetMethod("Parse", signature);

    // Initialize the parser delegate.
    return s => (T)method.Invoke(null, new[] { s });
}

For performance you can also build lambda expressions calling them since the Invoke method accepts the method parameters as an object[] which is an unnecessary allocation and if your parameters include value types, causes boxing. It also returns the result as an object which also causes boxing when your type is a value type.

private static Func<string, T> GetParser<T>()
{
    // Get the method like we did before.
    var signature = new[] { typeof(string) };
    var method = typeof(T).GetMethod("Parse", signature);

    // Build and compile a lambda expression.
    var param = Expression.Parameter(typeof(string));
    var call = Expression.Call(method, param);
    var lambda = Expression.Lambda<Func<string, T>>(call, param);
    return lambda.Compile();
}

Calling the compiled lambda expression is essentially as fast as calling the original parsing method itself, but building and compiling it in the first place is not. This is why, again like Jon suggested, we should cache the resulting delegate.

I use a static, generic class to cache parsers in ValueString.

private static class Parser<T>
{
    public static readonly Func<string, T> Parse = InitParser();

    private static Func<string, T> InitParser()
    {
        // Our initialization logic above.
    }
}

After that your parsing method can be written like this:

public static T Parse<T>(string s)
{
    return Parser<T>.Parse(s);
}
查看更多
登录 后发表回答