2

我经常发现自己在编写这样的代码:

Dim oRenewalOrder = (From c in dc.Orders _
                     Where c.CustomerID = iCustomerID _
                     And c.Type = "RENEWAL").FirstOrDefault()

If oRenewalOrder Is Nothing Then
   Throw New Exception("Can't find renewal order for customer " & iCustomerID) ' or assert, if you prefer
End If

' Continue processing...

我更喜欢使用 First() 方法,因为当没有元素时它已经抛出异常。但是这个异常并没有告诉你是什么原因造成的,这使得调试变得更加困难。所以我想写一个 FirstOrException() 扩展方法,我可以这样使用:

Dim oRenewalOrder = (From c in dc.Orders _
                     Where c.CustomerID = iCustomerID _
                     And c.Type = "RENEWAL").FirstOrException("Can't find renewal order for customer " & iCustomerID)

' Continue processing...

问题是很难以通用方式编写这样的方法,同时使用 LINQ to Objects利用 LINQ to SQL 提供的查询优化。我能想到的最好的是:

    ''' <summary>
    ''' Returns the first element of a sequence.
    ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
    <Extension()> _
    Function FirstOrException(Of T)(ieThis As IEnumerable(Of T), sMessage As String) As T
        Try
            Return ieThis.First()
        Catch ex As InvalidOperationException
            Throw New InvalidOperationException(sMessage, ex)
        End Try
    End Function

我还为 IEnumerable(Of T) 编写了另一个等效的扩展方法,以涵盖 LINQ to Objects 的情况。这些工作得很好,但是必须捕获异常并重新抛出它似乎很糟糕。我尝试了一种不同的方法,使用 Take(1) 和 AsEnumerable(),但是当我分析它时,它运行了两个单独的 SELECT TOP 1 语句:

    <Extension()> _
    Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
        Dim aFirst = iqThis.Take(1).AsEnumerable()
        If aFirst.Count() <= 0 Then
            Throw New InvalidOperationException(sMessage)
        Else
            Return aFirst(0)
        End If
    End Function

所以我回到异常处理方法。我不喜欢它,因为当出现不同的问题时,集合底层的任何 LINQ 提供程序都有可能抛出 InvalidOperationException - 不是缺少结果,而是其他问题。这会导致我的代码错误地认为没有结果,而实际上这完全是一个不同的问题。

...

好吧,正如您输入详细问题时经常发生的那样,我想我找到了更好的解决方案,我将在下面的答案中发布。但如果有人发现更好的东西,我会留下这个问题:-)

4

2 回答 2

0

您已经在没有更改 Linq-to-sql 优化的情况下进行了扩展。您需要至少访问数据库一次以检查那里是否有东西。您不需要使用Take(1),只需调用FirstOrDefault()并检查它是否返回Nothing

Function GetFirstOrException(Of T)(sMessage As String, sFirst as T) as T
   if (T is Nothing) 
      Throw New InvalidOperationException(sMessage)
   Return T
End Function

 <Extension()> _
Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
    Return GetFirstOrException(sMessage, idThis.FirstOrDefault())
End Function

 <Extension()> _
Function FirstOrException(Of T)(iqThis As IEnumerable(Of T), sMessage As String) As T
    Return GetFirstOrException(sMessage, idThis.FirstOrDefault())
End Function
于 2012-08-11T20:35:35.533 回答
0

我发现这Take(1).ToArray()是最干净的解决方案。它只导致将单个查询发送到数据库。请注意,在下面的代码中,我正在为 IEnumerable 和 IQueryable 执行单独的扩展方法,以支持 LINQ to SQL 和 LINQ to Objects。

    ''' <summary>
    ''' Returns the first element of a sequence.
    ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
    <Extension()> _
    Function FirstOrException(Of T)(ieThis As IEnumerable(Of T), sMessage As String) As T
        Dim aFirst = ieThis.Take(1).ToArray()
        If aFirst.Length <= 0 Then
            Throw New InvalidOperationException(sMessage)
        Else
            Return aFirst(0)
        End If
    End Function

     ''' <summary>
     ''' Returns the first element of a queryable (e.g. LINQ to SQL) sequence.
     ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
    <Extension()> _
    Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
        Dim aFirst = iqThis.Take(1).AsEnumerable()
        If aFirst.Length <= 0 Then
            Throw New InvalidOperationException(sMessage)
        Else
            Return aFirst(0)
        End If
    End Function
于 2012-08-11T00:59:03.977 回答