如何在C#中制作一个泛型数字解析器
本文关键字:泛型 一个 数字 | 更新日期: 2023-09-27 17:57:48
要将字符串解析为int,可以调用Int32.Parse(string)
、表示double、Double.Parse(string)
、表示long、Int64.Parse(string)
等。
是否可以创建一个使其通用的方法,例如ParseString<T>(string)
?其中T
可以是Int32
、Double
等。我注意到类型的数量没有实现任何公共接口,Parse
方法也没有任何公共父级。
有什么方法可以实现这一点或类似的事情吗?
您基本上必须使用反射来找到相关的静态Parse
方法,调用它,并将返回值强制转换回T
。或者,您可以使用Convert.ChangeType
或获取相关的TypeDescriptor
和关联的TypeConverter
。
一种更有限但更有效(在某些方面也很简单)的方法是将字典从类型转换为解析委托-将委托转换为Func<string, T>
并调用它。这将允许您对不同的类型使用不同的方法,但您需要提前知道需要转换为的类型。
无论你做什么,你都无法指定一个通用约束,这将使它在编译时安全。你真的需要像我的静态接口这样的东西。编辑:如前所述,有IConvertible
接口,但这并不一定意味着您可以从string
转换。另一个类型可以实现IConvertible
,而不需要任何从字符串转换为该类型的方法。
实际上,标准数字类型do实现了一个通用接口:IConvertable。这是Convert.ChangeType
使用的。
不幸的是,没有等效的TryParse
,如果无法解析字符串,它将抛出异常。
顺便说一句,BCL团队似乎已经完全忘记了整个"转换"领域。自.NET Framework 1以来,这里没有什么新内容(除了TryParse方法)。
这很难理解,但它可以使用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
是的,可以从字符串中解析的类型很可能具有静态Parse
和TryParse
重载,您可以像Jon建议的那样通过反射找到这些重载。
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 });
}
性能方面-Invoke
方法将方法参数作为object[]
接受,这是不必要的分配,如果参数包括值类型,则会导致装箱。它还返回一个object
,导致生成的数字(假设它也是一个值类型)被装箱。
为了应对这种情况,您可以构建lambda表达式:
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();
}
调用编译后的lambda表达式实际上与调用原始解析方法本身一样快,但首先构建和编译它则不然。这就是为什么,再次像Jon建议的那样,我们应该缓存生成的委托。
我使用一个静态的泛型类来缓存ValueString中的解析器。
private static class Parser<T>
{
public static readonly Func<string, T> Parse = InitParser();
private static Func<string, T> InitParser()
{
// Our initialization logic above.
}
}
之后,你的解析方法可以这样写:
public static T Parse<T>(string s)
{
return Parser<T>.Parse(s);
}
我写了一些代码,使用反射在类型上查找Parse
/TryParse
方法,并从泛型函数访问这些方法:
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
但我还没有对它们进行太多测试,所以它们可能会有一些错误/不能正确地与每种类型一起工作。错误处理也有点欠缺。
并且它们没有用于文化不变解析的重载。所以你可能需要补充一下。