开发者

Generics and nullable type

开发者 https://www.devze.com 2022-12-13 12:59 出处:网络
Say I have a method that takes an int as a string and returns the int if the parse succeeds or a null value otherw开发者_运维百科ise.

Say I have a method that takes an int as a string and returns the int if the parse succeeds or a null value otherw开发者_运维百科ise.

    int? ParseValue(string intAsString)
    {
        int i;
        if (int.TryParse(intAsString, out i))
            return i;
        return null;
    }

How can this method be re-written so that it works not only with int?, but also long?, decimal? and DateTime? ?


It's funny you should mention it because I was messing around with something just like this the other day:

using System;
using System.Reflection;

static class Example
{
    public static Tuple<Boolean, T?> TryParse<T>(this String candidate)
        where T : struct
    {
        T? value = null;
        Boolean success = false;

        var parser = ParsingBinder<T>.GetParser();

        try 
        { 
                value = parser(candidate);
                success = true;
        } 
        catch (FormatException) { }

        return new Tuple<Boolean,T?>(success, value);
    }
}

static class ParsingBinder<T>
{
    static Func<String, T> parser;

    public static Func<String, T> GetParser()
    {
        if (parser == null)
                parser = getParser();

        return parser;
    }

    static Func<String, T> getParser()
    {
        MethodInfo methodInfo 
            = typeof(T).GetMethod(
                    "Parse", new [] { typeof(String) });

        if (methodInfo == null)
                throw new Exception(
                        "Unable to retrieve a \"Parse\" method for type.");

        return (Func<String, T>)Delegate
        .CreateDelegate(typeof(Func<String, T>), methodInfo);
    }
}

It is a similar approach but think of it like a better TryParse method that returns a Tuple<Boolean, T?> (this requires .NET 4). The first property of the tuple is a boolean value indicating the success or failure of the parsing attempt and the second property is a nullable value typed to the generic type argument that will be null if parsing fails and the value if parsing succeeds.

It works by using reflection to retrieve a static Parse(String) method from the generic type argument and invokes that method for the string that is passed in. I built it as an extension method to allow you to do stuff like this:

var intValue = "1234".TryParse<Int32>();
var doubleValue = "1234".TryParse<Double>();

Unfortunately this won't work on enums since they don't have the same signature for the parse method so you couldn't use this extension to parse an enum but it wouldn't be hard to hack this up to make a special case for enums.

One of the nice things about this approach is that the cost of retrieving the Parse method via reflection is only incurred on the first use since a static delegate is created for all subsequent uses.


One more thing - the only thing that is clunky about this approach is that there is no language extensions or syntactic sugar that would make this easy to work with. What I was hoping to achieve with this code was a less clunky way of using the standard TryParse methods that exist in the BCL.

I personally find this pattern rather ugly:

Int32 value;
if (Int32.TryParse(someString, out value))
    // do something with value

mainly because it requires a variable declaration ahead of time and the use of an out parameter. My approach above isn't really that much better:

var result = someString.TryParse<Int32>();
if (result.Item1)
    // do something with result.Item2

What would be really cool would be to see a C# language extension that was built to work with a Tuple<Boolean, T?> that would allow us to work with this type smoothly but I get the feeling the more I write about this that it doesn't really seem that feasible.


Instead of using the question mark, you can explicitly use the Nullable keyword: for example,

int? equals Nullable<int>

Therefore switching your original design to Nullable<T> ParseValue(string valueAsString) should do the trick: just do the generic implementation after this.


It is best to implement an Extension method and you can even parse Enumerations. This way you can get a Nullable<ForAnyValueType> like this:

public static T? Parse<T>(this string text) where T: struct
    {
        object o = null;
        try { 
            var ttype = typeof(T);
            if (ttype.IsEnum)
            {
                T n = default(T);
                if (Enum.TryParse<T>(text, true, out n))
                    return n;
            }
            else
            o = Convert.ChangeType(text, ttype); 
        }
        catch { }

        if (o == null)
            return new Nullable<T>();

        return new Nullable<T>((T)o);
    }


If you can wait for C# 4.0, you can use the dynamic keyword, which solves such scenario.


I don't really see why use the Tuple in Andrews solution, as long as we're returning a Nullable anyway, seems like doing the same thing twice. I edited his solution to use TryParse and allow to return either a Nullable or some default value specified as argument.

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="aText"></param>
    /// <returns></returns>
    public static Nullable<T> TryParse<T>(this string aText) where T : struct {
        T value;
        if (ParsingBinder<T>.TryParse(aText, out value)) {
            return value;
        }
        return null;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="aText"></param>
    /// <param name="aDefault"></param>
    /// <returns></returns>
    public static T TryParse<T>(this string aText, T aDefault) where T : struct {
        T value;
        if (!ParsingBinder<T>.TryParse(aText, out value)) {
            value = aDefault;
        }
        return value;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    static class ParsingBinder<T> where T : struct {

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="aText"></param>
        /// <param name="aOutput"></param>
        /// <returns></returns>
        public delegate bool Delegate_TryParse<T>(string aText, out T aOutput) where T : struct;

        /// <summary>
        /// 
        /// </summary>
        static Delegate_TryParse<T> methodTryParse;

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public static Delegate_TryParse<T> TryParse {
            get {
                if (methodTryParse == null) {
                    methodTryParse = GetParserMethod();
                }
                return methodTryParse;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        static Delegate_TryParse<T> GetParserMethod() {
            var typeT = typeof(T);
            var paramTypes = new Type[] { typeof(string), typeT.MakeByRefType() };
            var methodInfo = typeT.GetMethod("TryParse", BindingFlags.Static | BindingFlags.Public, null, paramTypes, null);
            if (methodInfo == null) {
                var message = String.Format("Unable to retrieve a 'TryParse' method for type '{0}'.", typeT.Name);
                throw new Exception(message);
            }
            return (Delegate_TryParse<T>) Delegate.CreateDelegate(typeof(Delegate_TryParse<T>), methodInfo);
        }
    }


Actually, You can update what Matt's code has done and make it, and here is the code:

enter code here:static T? TryParse<T>(string parse)
        where T : struct
    {
        Type t=typeof(T);
        if (t==typeof(int))
        {
            int i;
            if (int.TryParse(parse, out i))
                return (T)(object)i;
            return null;
            //Console.WriteLine(t.Name);
        }
        if (t == typeof(double))
        {
            double i;
            if (double.TryParse(parse, out i))
                return (T)(object)i;
            return null;
        }
        //blabla, more logic like datetime and other data types
        return null;
    }

And also, you can use it like this: double? i = TryParse("111.111"); int? a = TryParse("111");


These types that you have listed all have a static method called TryParse. They look similar, but in fact the signatures are completely different. They follow a similar 'pattern', but that is not detectable by the compiler.

You might try to do something like this:

    public T? ParseValue<T>(string value) where T : struct
    {
        if (typeof(T) == typeof(int))
        {
            int i;
            if (int.TryParse(value, out i))
                return (T)(object)i;
            return null;
        }
        if (typeof(T) == typeof(decimal)) 
        {
            decimal d;
            if (decimal.TryParse(value, out d))
                return (T)(object)d;
            return null;
        }
        // other supported types...
        throw new ArgumentException("Type not supported");
    }

However, you can't. The compiler has no way to know (at compile time) how to convert type 'T' to an int (or any other type).

You can double-cast to make this work. (Thanks, Dotson)

Usage:

        var mydecimal = ParseValue<decimal>("12.1");
        var myint = ParseValue<int>("-22");
        var badint = ParseValue<int>("Bad");
        // badint.HasValue == false
0

精彩评论

暂无评论...
验证码 换一张
取 消