泛型和可空类型

9

假设我有一个方法,它接受一个字符串形式的整数,并在解析成功时返回整数,否则返回 null 值。

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

如何重新编写此方法,使其不仅适用于int类型,还适用于long、decimal和DateTime类型?

7个回答

15

有趣的是你提到这个,因为我前几天也在玩类似的东西:

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);
    }
}

这是一种类似的方法,但可以将其视为更好的TryParse方法,它返回一个 Tuple<Boolean, T?>(这需要.NET 4)。元组的第一个属性是一个布尔值,表示分析尝试的成功或失败,第二个属性是可为空的值,类型为泛型类型参数,如果解析失败则为null,如果解析成功则为值。

它通过使用反射从泛型类型参数中检索静态的Parse(String)方法,并为传入的字符串调用该方法来工作。我将其构建为扩展方法,以允许您执行以下操作:

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

很遗憾,这种方法不能用于enums,因为它们的解析方法签名不同,所以您无法使用此扩展来解析enum,但是将其修改为enums的特殊情况并不难。
这种方法的好处之一是,通过反射检索Parse方法的成本仅在第一次使用时发生,因为对于所有后续使用都创建了静态委托。
还有一件事——这种方法唯一笨拙的地方就是没有语言扩展或语法糖可以使其易于处理。我希望通过这段代码实现的是一种更简便的使用BCL中存在的标准TryParse方法的方式。
我个人认为这种模式相当丑陋:
Int32 value;
if (Int32.TryParse(someString, out value))
    // do something with value

主要是因为它需要提前声明变量并使用一个out参数。我上面的方法并没有更好:
var result = someString.TryParse<Int32>();
if (result.Item1)
    // do something with result.Item2

一个真正酷炫的想法是创建一个 C# 语言扩展,使其能够与 Tuple<Boolean, T?> 无缝协作,但我越写越觉得这个想法并不切实际。


@Andrew:你捕获的 FormatException 就是我所说的那个异常。捕获异常并不会改变抛出异常的成本。你应该查找并调用一个 TryParse 方法而不是 Parse 方法。 - Sam Harwell
是的,我可以尝试查找一个TryParse方法来调用,但由于这些方法的签名因类型而异,这将使整个过程无效。再次强调,这只是一个半成品的想法,当我阅读问题时记起来的。 - Andrew Hare
与“static T Parse(string)”方法相对应的TryParse签名为“static bool T.TryParse(string, out T)”,这是“始终如一”的。 - Sam Harwell
好的,出于某种原因,我以为签名是不同的(特别是对于DateTime这样的类型),但实际上我是错误的!谢谢! - Andrew Hare
@Philip - 很高兴听到这个消息!需要指出的是,280Z28所说的更好的解决方案实际上会在底层使用TryParse方法,记得牢记这一点。 - Andrew Hare
显示剩余4条评论

3

最好实现一个扩展方法,甚至可以解析枚举。这样你就可以像这样获得 Nullable<任何值类型>:

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);
    }

3

不必使用问号,您可以明确使用Nullable关键字:

例如,

int?等同于Nullable<int>

因此,将原始设计更改为Nullable<T> ParseValue(string valueAsString)应该就可以解决问题:在此之后只需进行通用实现即可。


2

如果您能等待C# 4.0版本,您可以使用dynamic关键字来解决此类情况。


1
我并不真正明白为什么在安德鲁的解决方案中要使用元组,只要我们返回一个可空类型,似乎做了两次同样的事情。我编辑了他的解决方案,使用TryParse并允许返回可空类型或指定为参数的某个默认值。
    /// <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);
        }
    }

0

你列出的这些类型都有一个名为TryParse的静态方法。它们看起来很相似,但实际上签名完全不同。它们遵循类似的“模式”,但编译器无法检测到。

你可以尝试像这样做:

    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");
    }

然而,你不能这样做。编译器在编译时无法知道如何将类型'T'转换为int(或任何其他类型)。

您可以使用双重转换使其正常工作。(感谢Dotson)

用法:

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

先将i转换为对象: (T)(object)i - Matt Dotson

0

实际上,您可以更新Matt的代码并使其生效,以下是代码:

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;
    }

而且,你可以像这样使用它: double? i = TryParse("111.111"); int? a = TryParse("111");


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接