使用LINQ时VB TryCast和C#“as”运算符之间的差异

我有一个LINQ查询来检索整数列的最大值。 该列在数据库中定义为NOT NULL。 但是,在SQL中使用MAX聚合函数时,如果查询没有返回任何行,则会得到NULL结果。

这是我在Northwind数据库中使用的LINQ查询示例,用于演示我正在做什么。

var maxValue = (from p in nw.Products where p.ProductID < 0 select p.ProductID as int?).Max(); 

C#正确解析此查询,而maxValue的类型为int?。 此外,生成的SQL是完美的:

 SELECT MAX([t0].[ProductID]) AS [value] FROM [Products] AS [t0] WHERE [t0].[ProductID] < @p0 

问题是,如何使用VB.NET对其进行编码并获得相同的结果? 如果我直接翻译:

 dim maxValue = (from p in Products where p.ProductID < 0 select TryCast(p.ProductID, integer?)).Max() 

我收到编译错误。 TryCast仅适用于引用类型,而不适用于值类型。 TryCast和“as”在这方面略有不同。 C#为装箱处理值类型做了一些额外的工作。 所以,我的下一个解决方案是使用CType而不是TryCast:

 dim maxValue = (from p in Products where p.ProductID > 0 select CType(p.ProductID, integer?)).Max() 

这有效,但它生成以下SQL:

 SELECT MAX([t1].[value]) AS [value] FROM ( SELECT [t0].[ProductID] AS [value], [t0].[ProductID] FROM [Products] AS [t0] ) AS [t1] WHERE [t1].[ProductID] > @p0 

虽然这是正确的,但它不是很干净。 当然,在这种特殊情况下,SQL Server可能会将查询优化为与C#版本相同,我可以设想这种情况可能并非如此。 有趣的是,在C#版本中,如果我使用普通的强制转换(即(int?)p.ProductID)而不是使用“as”运算符,我会得到与VB版本相同的SQL。

有没有人知道是否有办法在VB中为这种类型的查询生成最佳SQL?

简短的回答:你可以。

然后答案很长:

我能看到你能做到这一点的唯一方法是显式创建包含TypeAs转换的lambda。 您可以使用以下扩展方法来帮助您:

  _ Public Module TypeAsExtensions  _ Public Function SelectAs(Of TElement, TOriginalType, TTargetType)( _ ByVal source As IQueryable(Of TElement), _ ByVal selector As Expression(Of Func(Of TElement, TOriginalType))) _ As IQueryable(Of TTargetType) Return Queryable.Select(source, _ Expression.Lambda(Of Func(Of TElement, TTargetType))( _ Expression.TypeAs(selector.Body, GetType(TTargetType)), _ selector.Parameters(0))) End Function  _ Public Function SelectAsNullable(Of TElement, TType As Structure)( _ ByVal source As IQueryable(Of TElement), _ ByVal selector As Expression(Of Func(Of TElement, TType))) _ As IQueryable(Of TType?) Return SelectAs(Of TElement, TType, TType?)(source, selector) End Function End Module 

SelectAs会导致任何TTryCast(value, T) ,包括Integer?

你会说,要使用它

 Dim maxValue = Products _ .Where(Function(p) p.ProductID < 0) _ .SelectAsNullable(Function(p) p.ProductID) _ .Max() 

它不漂亮,但它的工作原理。 (这会生成与C#相同的查询。)只要您不在子查询中调用SelectAsNullable,就可以了。

另一个选择可能是使用

 Dim maxValue = (From p In Products _ Where p.ProductID < 0 Select p.ProductID) _ .SelectAsNullable(Function(id) id) _ .Max() 

这个问题是你得到一个双重选择,即,

 from p in Products where p.ProductID < 0 select p.ProductID select p.ProductID as int? 

用C#表示。 引用LINQ to SQL仍然可以为此生成子查询。

无论如何,为此你可以创建一个额外的扩展方法

  _ Public Function SelectAsNullable(Of TType As Structure)( _ ByVal source As IQueryable(Of TType)) _ As IQueryable(Of TType?) Return SelectAs(Of TType, TType, TType?)(source, Function(x) x) End Function 

进一步简化LINQ查询

 Dim maxValue = (From p In Products _ Where p.ProductID < 0 Select p.ProductID) _ .SelectAsNullable() _ .Max() 

但正如我所说,这取决于LINQ提供商。

  Dim maxValue = ctype((From p In db.Products _ Where p.ProductID > 0 _ Select p.ProductID).Max(),Integer?) 

HOLY废话是什么PITA

  Dim maxValue = (From p In db.Products _ Where p.ProductID > 300 _ Select new With {.id=CType(p.ProductID, Integer?)}).Max(Function(p) p.id) 

有一个更好的方法,对吧?

这有所需的查询计划,没有空值的错误,但有人可以看到它并清理它吗?

C#

 var maxValue = nw.Products .Where(p => p.ProductID < 0) .Select(p => p.ProductID) .DefaultIfEmpty(int.MinValue) .Max(); 

VB

 Dim maxValue = nw.Products _ .Where(Function(p) p.ProductID < 0) _ .Select(Function(p) p.ProductID) _ .DefaultIfEmpty(Integer.MinValue) _ .Max() 

返回Nullable的函数怎么样? (对不起,如果语法不太正确。)

 Function GetNullable(Of T)(val as Object) If (val Is Nothing) Then Return new Nullable(Of T)() Else Return new Nullable(Of T)(DirectCast(val, T)) End If End Function dim maxValue = (from p in Products where p.ProductID < 0 select GetNullable(Of Integer)(p.ProductID)).Max() 

为什么不在查询中构建等效的isnull检查?

 dim maxValue = (from p in Products where IIf(p.ProductID=Null, 0, p.ProductID) < 0 select p.ProductID)).Max() 

对不起,如果这不起作用 - 我实际上并没有在这方面测试它,只是在墙上扔意大利面!