我有以下代码对任何类型的对象执行空或空检查:
public static void IfNullOrEmpty(Expression<Func<string>> parameter)
{
Throw.IfNull(parameter);
if (parameter.GetValue().ToString().Length == 0)
{
throw new ArgumentException("Cannot be empty", parameter.GetName());
}
}
它调用下面的GetValue扩展方法:
public static T GetValue<T>(this Expression<Func<T>> parameter)
{
MemberExpression member;
Expression expression;
member = (MemberExpression)parameter.Body;
expression = member.Expression;
return (T)parameter.Compile()();
}
我在这个方法中传入一个包含字符串的表达式进行测试。这个方法在我的机器上平均需要2毫秒(在我测试的另一台机器上甚至更慢),如果在整个应用程序中多次调用它,它会增加。看来这种方法太慢了。执行此类空检查的最快方法是什么?
答案 0 :(得分:1)
编译表达式自然需要一些工作。如果这段代码经常运行,我通常会做的是我只编译表达式一次并保存已编译的委托以供进一步使用。
可以保持&#34;正常&#34;缓存,但为了使缓存高效,你需要一个好的哈希函数,我不知道如何在这里做到这一点。您需要对代码进行一些重构,以便您使用GetValue的每个位置都可以正确访问已编译的委托。如果没有看到更多代码,我就无法给出任何有关该代码的提示。
为什么您看到以下呼叫更快,原因可能有很多。由于难以哈希,我不期待那个。您更有可能看到现代CPU的工作,它需要大量猜测才能快速运行代码。如果你只是运行相同的表达式,那么CPU可以猜测下一次调用的更多信息,并且可以更快地运行。总有GC也要考虑。
测试猜测思想的一种方法是创建一个包含几个不同表达式的大型数组。做一个测试按表达式排序,另一个测试是随机排序。如果我的怀疑成立,第一个应该更快。
答案 1 :(得分:0)
如果我正确地阅读您的代码,那么您需要Expression
的唯一原因是,如果检查失败,您将能够提取参数的名称并将其传递给异常你会扔,对吧?如果是这样,这是一个非常陡峭的代价,以支付稍微更方便的错误消息(希望)只发生在极少数情况下。
2ms的开销略高于我的预期,但这里很难或无法避免大量的开销。您实际上是强制运行时遍历表达式树,将其转换为MSIL,然后通过JIT将该MSIL再次转换并优化为可执行代码,只需执行!= null
检查,除非开发人员具有此检查,否则该检查将始终成功某处犯了一个错误。
你可能想出某种缓存机制;实体框架通过遍历表达式树并构建它的散列来缓存表达式,并将其用作字典的键,其中编译的表达式存储为委托。它比通过JIT在每次调用时发送它要便宜得多,但与更简单的!= null
检查相比,它还需要几个数量级,这需要几纳秒或更短的时间(特别是考虑到现代分支时) - 预测CPU)。
所以在我看来,这种方法是一个不错的主意,但是当你考虑成本和替代方案非常轻松时(特别是对于新的nameof
运算符),这种方法根本不值得。它也相当脆弱,因为如果我是开发人员,他认为他可以这样做:
Throw.IfNullOrEmpty(() => clientId + "something")
你的演员MemberExpression
将失败。
同样,有人认为因为你传入一个表达式并且表达式只是代码作为数据可以安全地执行此操作是合理的:
Throw.IfNullOrEmpty(() => parent.Child.MightBeNull.MightAlsoBeNull.ClientID)
如果您部分遍历表达式树,则完全可以安全地评估该表达式,但在您的示例中,整个表达式会立即编译并执行,并且可能会失败并显示NullReferenceException
。
我想这归结为Expression<Func<T>>
类型的参数对于使用空检查而言不够严格。你可以在其中做各种奇怪的事情,编译器会非常高兴,但你会在运行时得到意想不到的结果。