通用的 TryParse

223

我正在尝试创建一个通用扩展,使用'TryParse'检查字符串是否为给定类型:

public static bool Is<T>(this string input)
{
    T notUsed;
    return T.TryParse(input, out notUsed);
}

由于无法解析符号'TryParse',因此此代码将无法编译。

据我所知,'TryParse'不是任何接口的一部分。

这是否有可能实现?

更新:

根据以下答案,我想出了:

public static bool Is<T>(this string input)
{
    try
    {
        TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(input);
    }
    catch
    {
        return false;
    }

    return true;
}

它的表现相当不错,但我认为以这种方式使用异常不太合适。

更新2:

修改为传递类型而不是使用泛型:

public static bool Is(this string input, Type targetType)
{
    try
    {
        TypeDescriptor.GetConverter(targetType).ConvertFromString(input);
        return true;
    }
    catch
    {
        return false;
    }
}

1
我认为在这种一般情况下,你只能处理异常的丑陋代码。你可以添加检查整数或双精度浮点数等内容的情况,并使用特定的TryParse方法,但你仍然必须退回到这个方法来捕获其他类型的异常。 - luke
1
使用泛型是不必要的。只需将Type作为参数传递即可。public static bool Is(this string input, Type targetType)。这样调用看起来更漂亮:x.Is(typeof(int)) -VS- x.Is<int>()。 - mikesigs
2
转换器上有一个 IsValid 方法,供您检查转换是否会出现问题。我使用了下面的方法,似乎工作正常。`protected Boolean TryParse(Object value, out T result) { result = default(T); var convertor = TypeDescriptor.GetConverter(typeof(T)); if (convertor == null || !convertor.IsValid(value)) { return false; } result = (T)convertor.ConvertFrom(value); return true; }` - CastroXXL
@CastroXXL 感谢您对这个问题的关注,但是您的方法并不完全符合我的要求,因为我想检查字符串值是否属于某种类型,而不是对象。虽然您的方法对于对象类型会很有用(但必须将ConvertFrom(value)方法包装在try-catch块中以捕获异常)。 - Piers Myers
2
你应该检查 (targetType == null),因为在你的代码中第一次使用它可能会抛出异常,但是这个异常会被你的 catch 捕获。 - Nick Strupat
显示剩余3条评论
25个回答

226
您应该使用TypeDescriptor类:

TypeDescriptor

public static T Convert<T>(this string input)
{
    try
    {
        var converter = TypeDescriptor.GetConverter(typeof(T));
        if(converter != null)
        {
            // Cast ConvertFromString(string text) : object to (T)
            return (T)converter.ConvertFromString(input);
        }
        return default(T);
    }
    catch (NotSupportedException)
    {
        return default(T);
    }
}

3
抱歉打扰,但是 GetConverter 方法返回空值吗?如果是的话,可能应该抛出异常,而不是默默地失败并返回其他内容。当我在自己的类上尝试使用它(其中我没有定义类型转换器)时,从 GetConverter 获取了一个转换器,但 ConvertFromString 抛出了 NotSupportedException 异常。 - user420667
3
在尝试从字符串转换之前,我认为您应该检查CanConvertFrom(typeof(string))的结果。TypeConverter可能不支持从字符串进行转换。 - Reuben Bond
3
在获取转换器之前,您可以添加 if (typeof(T).IsEnum) { return (T)Enum.Parse(typeof(T), input); } 作为所有枚举类型的通用快捷方式。我想这要取决于您处理枚举类型的频率与更复杂类型的比较。 - Jesse Chisholm
15
我不明白为什么这个回答被标记为最佳答案并且得到了很多赞,因为它没有实现所要求的通用的TryParse功能。TryParse方法的主要目的是在尝试执行解析时不抛出异常,并且在解析失败时对性能影响较小,而这个解决方案无法提供这一点。 - Florin Dumitrescu
2
其中一个问题是,如果T是int类型且输入大于int.MaxValue,则会抛出System.Exception,内部异常为System.OverFlowException。因此,如果您期望出现OverflowException,除非您询问所抛出的异常,否则您将无法获得它。原因是ConvertFromString会抛出OverflowException,然后转换为T时会抛出System.Exception。 - Trevor
显示剩余2条评论

86
我最近也需要一个通用的TryParse。这是我想出来的方法:;
public static T? TryParse<T>(string value, TryParseHandler<T> handler) where T : struct
{
    if (String.IsNullOrEmpty(value))
        return null;
    T result;
    if (handler(value, out result))
        return result;
    Trace.TraceWarning("Invalid value '{0}'", value);
    return null;
}

public delegate bool TryParseHandler<T>(string value, out T result);

然后只需要这样调用即可:
var value = TryParse<int>("123", int.TryParse);
var value2 = TryParse<decimal>("123.123", decimal.TryParse);

3
几个月后再次发现这篇帖子时,我注意到在重新使用它时,该方法无法从处理程序中推断出 T,因此我们必须在调用时明确指定 T。我很好奇,为什么它不能推断出 T - Nick Strupat
换句话说,为什么我们不能使用 TryParse("42", Int32.TryParse) 呢? - Nick Strupat
40
为什么你想要使用这个函数?如果你知道调用哪个函数来解析值,为什么不直接调用它呢?它已经知道正确的输入类型,并且不需要泛型。但对于没有 TryParseHandler 的类型,这种解决方案将不起作用。 - xxbbcc
4
@xxbbcc:我想使用这个函数,因为TryParse返回一个布尔值来表示解析是否成功,通过输出参数返回解析后的值。有时候我只想像这样做:SomeMethod(TryParse<int>(DollarTextbox.Text, int.TryParse)),而不用创建一个输出变量来接收int.TryParse的结果。不过,我同意Nick关于让函数推导类型的观点。 - Walter Stabosz
3
非常高效的方法。强烈推荐。 - Vladimir Kocjancic
4
我建议将默认值作为第三个参数。这样可以解决无法推断出T的问题 。此外,它还允许一个人决定如果字符串值无效时他们想要什么值。例如,-1 可能表示无效。public static T TryParse<T>(string value, TryParseHandler<T> handler, T defaultValue) - Rhyous

35

使用try/catch作为流程控制是一种糟糕的策略。抛出异常会导致性能延迟,因为运行时需要处理异常。相反,在转换之前应该先验证数据。

var attemptedValue = "asdfasdsd";
var type = typeof(int);
var converter = TypeDescriptor.GetConverter(type);
if (converter != null &&  converter.IsValid(attemptedValue))
    return converter.ConvertFromString(attemptedValue);
else
    return Activator.CreateInstance(type);

2
我收到了Resharper的提示,即“converter!= null”始终为真,因此可以从代码中删除。 - ErikE
5
我不总是相信那些ReSharper的警告。通常它们无法看到运行时发生的事情。 - ProfK
1
@ProfK MSDN上并没有说它可以返回null https://msdn.microsoft.com/zh-cn/library/ewtxwhzx.aspx - danio
@danio,我只是在一般情况下分享了我的R#警告经验。我当然没有暗示在这种情况下是错误的。 - ProfK

14

如果你坚持使用TryParse,你可以使用反射并按照以下方式进行操作:

public static bool Is<T>(this string input)
{
    var type = typeof (T);
    var temp = default(T);
    var method = type.GetMethod(
        "TryParse",
        new[]
            {
                typeof (string),
                Type.GetType(string.Format("{0}&", type.FullName))
            });
    return (bool) method.Invoke(null, new object[] {input, temp});
}

这非常酷,而且它消除了我不喜欢的异常。但仍然有点复杂。 - Piers Myers
9
不错的解决方案,但任何包含反射的答案(尤其是在一个内部循环中容易被调用的实用方法中)都需要关于性能的免责声明。参见:https://dev59.com/WnVD5IYBdhLWcg3wTJvF - Patrick M
唉,所以选择是(1)使用异常来进行代码流控制,(2)使用反射及其速度成本。我同意 @PiersMyers 的观点 - 两种选择都不理想。好在它们都能用。 :) - Jesse Chisholm
1
我认为你可以用 type.MakeByRefType() 替换 Type.GetType(string.Format(...)) - Drew Noakes
4
这种方法只需要每种类型反射一次,而不是每次调用都反射。如果你将其做成一个通用类并添加静态成员变量,那么就可以重复使用第一次反射的输出结果。 - Andrew Hill
@DrewNoakes,你的评论对我修复了这个答案,因为最初的提议会导致运行时异常。 - Earl Sven

8

这样的东西怎么样?

http://madskristensen.net/post/Universal-data-type-checker.aspx (存档)

/// <summary> 
/// Checks the specified value to see if it can be 
/// converted into the specified type. 
/// <remarks> 
/// The method supports all the primitive types of the CLR 
/// such as int, boolean, double, guid etc. as well as other 
/// simple types like Color and Unit and custom enum types. 
/// </remarks> 
/// </summary> 
/// <param name="value">The value to check.</param> 
/// <param name="type">The type that the value will be checked against.</param> 
/// <returns>True if the value can convert to the given type, otherwise false. </returns> 
public static bool CanConvert(string value, Type type) 
{ 
    if (string.IsNullOrEmpty(value) || type == null) return false;
    System.ComponentModel.TypeConverter conv = System.ComponentModel.TypeDescriptor.GetConverter(type);
    if (conv.CanConvertFrom(typeof(string)))
    { 
        try 
        {
            conv.ConvertFrom(value); 
            return true;
        } 
        catch 
        {
        } 
     } 
     return false;
  }

这可以很容易地转换为一个通用方法。
 public static bool Is<T>(this string value)
 {
    if (string.IsNullOrEmpty(value)) return false;
    var conv = System.ComponentModel.TypeDescriptor.GetConverter(typeof(T));

    if (conv.CanConvertFrom(typeof(string)))
    { 
        try 
        {
            conv.ConvertFrom(value); 
            return true;
        } 
        catch 
        {
        } 
     } 
     return false;
}

从try块返回true还是从catch块返回false是否重要?我想并不重要,但我仍然认为以这种方式使用异常感觉不对... - Piers Myers
3
无论你从catch块中返回还是不返回,都没有区别。顺便说一下,通常使用通用的catch子句catch { }是不好的。但是,在这种情况下没有其他选择,因为.NET的BaseNumberConverter在转换错误时会抛出Exception基类。这非常不幸。实际上,仍然有很多地方抛出此基类型。希望Microsoft能在将来的框架版本中修复这些问题。 - Steven
谢谢Steven,你说得再好不过了。 - Bob
不要使用转换结果,代码是冗余的。 - BillW

7

这使用了每个泛型类型的静态构造函数,因此它只需要在第一次为给定类型调用它时执行昂贵的工作。它处理系统命名空间中具有TryParse方法的所有类型。它还适用于每个可为空的版本(是结构体)除枚举外的每个类型。

    public static bool TryParse<t>(this string Value, out t result)
    {
        return TryParser<t>.TryParse(Value.SafeTrim(), out result);
    }
    private delegate bool TryParseDelegate<t>(string value, out t result);
    private static class TryParser<T>
    {
        private static TryParseDelegate<T> parser;
        // Static constructor:
        static TryParser()
        {
            Type t = typeof(T);
            if (t.IsEnum)
                AssignClass<T>(GetEnumTryParse<T>());
            else if (t == typeof(bool) || t == typeof(bool?))
                AssignStruct<bool>(bool.TryParse);
            else if (t == typeof(byte) || t == typeof(byte?))
                AssignStruct<byte>(byte.TryParse);
            else if (t == typeof(short) || t == typeof(short?))
                AssignStruct<short>(short.TryParse);
            else if (t == typeof(char) || t == typeof(char?))
                AssignStruct<char>(char.TryParse);
            else if (t == typeof(int) || t == typeof(int?))
                AssignStruct<int>(int.TryParse);
            else if (t == typeof(long) || t == typeof(long?))
                AssignStruct<long>(long.TryParse);
            else if (t == typeof(sbyte) || t == typeof(sbyte?))
                AssignStruct<sbyte>(sbyte.TryParse);
            else if (t == typeof(ushort) || t == typeof(ushort?))
                AssignStruct<ushort>(ushort.TryParse);
            else if (t == typeof(uint) || t == typeof(uint?))
                AssignStruct<uint>(uint.TryParse);
            else if (t == typeof(ulong) || t == typeof(ulong?))
                AssignStruct<ulong>(ulong.TryParse);
            else if (t == typeof(decimal) || t == typeof(decimal?))
                AssignStruct<decimal>(decimal.TryParse);
            else if (t == typeof(float) || t == typeof(float?))
                AssignStruct<float>(float.TryParse);
            else if (t == typeof(double) || t == typeof(double?))
                AssignStruct<double>(double.TryParse);
            else if (t == typeof(DateTime) || t == typeof(DateTime?))
                AssignStruct<DateTime>(DateTime.TryParse);
            else if (t == typeof(TimeSpan) || t == typeof(TimeSpan?))
                AssignStruct<TimeSpan>(TimeSpan.TryParse);
            else if (t == typeof(Guid) || t == typeof(Guid?))
                AssignStruct<Guid>(Guid.TryParse);
            else if (t == typeof(Version))
                AssignClass<Version>(Version.TryParse);
        }
        private static void AssignStruct<t>(TryParseDelegate<t> del)
            where t: struct
        {
            TryParser<t>.parser = del;
            if (typeof(t).IsGenericType
                && typeof(t).GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return;
            }
            AssignClass<t?>(TryParseNullable<t>);
        }
        private static void AssignClass<t>(TryParseDelegate<t> del)
        {
            TryParser<t>.parser = del;
        }
        public static bool TryParse(string Value, out T Result)
        {
            if (parser == null)
            {
                Result = default(T);
                return false;
            }
            return parser(Value, out Result);
        }
    }

    private static bool TryParseEnum<t>(this string Value, out t result)
    {
        try
        {
            object temp = Enum.Parse(typeof(t), Value, true);
            if (temp is t)
            {
                result = (t)temp;
                return true;
            }
        }
        catch
        {
        }
        result = default(t);
        return false;
    }
    private static MethodInfo EnumTryParseMethod;
    private static TryParseDelegate<t> GetEnumTryParse<t>()
    {
        Type type = typeof(t);

        if (EnumTryParseMethod == null)
        {
            var methods = typeof(Enum).GetMethods(
                BindingFlags.Public | BindingFlags.Static);
            foreach (var method in methods)
                if (method.Name == "TryParse"
                    && method.IsGenericMethodDefinition
                    && method.GetParameters().Length == 2
                    && method.GetParameters()[0].ParameterType == typeof(string))
                {
                    EnumTryParseMethod = method;
                    break;
                }
        }
        var result = Delegate.CreateDelegate(
            typeof(TryParseDelegate<t>),
            EnumTryParseMethod.MakeGenericMethod(type), false)
            as TryParseDelegate<t>;
        if (result == null)
            return TryParseEnum<t>;
        else
            return result;
    }

    private static bool TryParseNullable<t>(string Value, out t? Result)
        where t: struct
    {
        t temp;
        if (TryParser<t>.TryParse(Value, out temp))
        {
            Result = temp;
            return true;
        }
        else
        {
            Result = null;
            return false;
        }
    }

5

可能已经有点晚了,但这是我想到的方案。没有例外,一次(每种类型)反射。

public static class Extensions {
    public static T? ParseAs<T>(this string str) where T : struct {
        T val;
        return GenericHelper<T>.TryParse(str, out val) ? val : default(T?);
    }
    public static T ParseAs<T>(this string str, T defaultVal) {
        T val;
        return GenericHelper<T>.TryParse(str, out val) ? val : defaultVal;
    }

    private static class GenericHelper<T> {
        public delegate bool TryParseFunc(string str, out T result);

        private static TryParseFunc tryParse;
        public static TryParseFunc TryParse {
            get {
                if (tryParse == null)
                    tryParse = Delegate.CreateDelegate(
                        typeof(TryParseFunc), typeof(T), "TryParse") as TryParseFunc;
                return tryParse;
            }
        }
    }
}

额外的类是必需的,因为在泛型类内部不允许使用扩展方法。这样可以使代码更简洁易懂,如下所示,并且只在第一次使用类型时命中反射。
"5643".ParseAs<int>()

4

您不能在通用类型上执行此操作。

您可以创建一个接口ITryParsable,并将其用于实现此接口的自定义类型。

我猜想您想要使用基本类型,例如intDateTime。您无法更改这些类型以实现新接口。


1
我想知道在.NET 4中使用动态关键字是否能够起作用? - Pierre-Alain Vigeant
@Pierre:在C#中,默认情况下使用dynamic关键字是行不通的,因为它不能用于静态类型。你可以创建自己的动态对象来处理这个问题,但这不是默认的。 - Steven

4

您可以使用INumber<T>接口来完成此操作:

public static bool Is<T>(string input)
    where T : INumber<T>
{
    return T.TryParse(input, CultureInfo.InvariantCulture, out _);
}

这个接口是在.NET 7中作为通用数学的一部分引入的,它还允许您对通用基元类型进行计算。
您也可以通过添加System.Runtime.Experimental NuGet包,并将<EnablePreviewFeatures>true</EnablePreviewFeatures>添加到项目的任何PropertyGroup中,在.NET 6中实验性地使用它。

1
哦,那看起来很有趣,希望它能够在.NET 7中实现。谢谢提供信息。 - Piers Myers

4

受Charlie Brown在此处发布的解决方案的启发,我使用反射创建了一个通用的TryParse函数,可选择输出已解析的值:

/// <summary>
/// Tries to convert the specified string representation of a logical value to
/// its type T equivalent. A return value indicates whether the conversion
/// succeeded or failed.
/// </summary>
/// <typeparam name="T">The type to try and convert to.</typeparam>
/// <param name="value">A string containing the value to try and convert.</param>
/// <param name="result">If the conversion was successful, the converted value of type T.</param>
/// <returns>If value was converted successfully, true; otherwise false.</returns>
public static bool TryParse<T>(string value, out T result) where T : struct {
    var tryParseMethod = typeof(T).GetMethod("TryParse", BindingFlags.Static | BindingFlags.Public, null, new [] { typeof(string), typeof(T).MakeByRefType() }, null);
    var parameters = new object[] { value, null };

    var retVal = (bool)tryParseMethod.Invoke(null, parameters);

    result = (T)parameters[1];
    return retVal;
}

/// <summary>
/// Tries to convert the specified string representation of a logical value to
/// its type T equivalent. A return value indicates whether the conversion
/// succeeded or failed.
/// </summary>
/// <typeparam name="T">The type to try and convert to.</typeparam>
/// <param name="value">A string containing the value to try and convert.</param>
/// <returns>If value was converted successfully, true; otherwise false.</returns>
public static bool TryParse<T>(string value) where T : struct {
    T throwaway;
    var retVal = TryParse(value, out throwaway);
    return retVal;
}

可以这样称呼它:
string input = "123";
decimal myDecimal;

bool myIntSuccess = TryParse<int>(input);
bool myDecimalSuccess = TryParse<decimal>(input, out myDecimal);

更新:
同时感谢YotaXP提供的解决方案,我创建了一个版本,不使用扩展方法但仍具有单例,最大程度地减少了需要进行反射的需求:

/// <summary>
/// Provides some extra parsing functionality for value types.
/// </summary>
/// <typeparam name="T">The value type T to operate on.</typeparam>
public static class TryParseHelper<T> where T : struct {
    private delegate bool TryParseFunc(string str, out T result);

    private static TryParseFunc tryParseFuncCached;

    private static TryParseFunc tryParseCached {
        get {
            return tryParseFuncCached ?? (tryParseFuncCached = Delegate.CreateDelegate(typeof(TryParseFunc), typeof(T), "TryParse") as TryParseFunc);
        }
    }

    /// <summary>
    /// Tries to convert the specified string representation of a logical value to
    /// its type T equivalent. A return value indicates whether the conversion
    /// succeeded or failed.
    /// </summary>
    /// <param name="value">A string containing the value to try and convert.</param>
    /// <param name="result">If the conversion was successful, the converted value of type T.</param>
    /// <returns>If value was converted successfully, true; otherwise false.</returns>
    public static bool TryParse(string value, out T result) {
        return tryParseCached(value, out result);
    }

    /// <summary>
    /// Tries to convert the specified string representation of a logical value to
    /// its type T equivalent. A return value indicates whether the conversion
    /// succeeded or failed.
    /// </summary>
    /// <param name="value">A string containing the value to try and convert.</param>
    /// <returns>If value was converted successfully, true; otherwise false.</returns>
    public static bool TryParse(string value) {
        T throwaway;
        return TryParse(value, out throwaway);
    }
}

这样调用:

string input = "987";
decimal myDecimal;

bool myIntSuccess = TryParseHelper<int>.TryParse(input);
bool myDecimalSuccess = TryParseHelper<decimal>.TryParse(input, out myDecimal);

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