Вопрос

I'm creating a wrapped type similar to Nullable(Of T) and I'm writing some unit test to test equality. Like Nullable(Of T) I have implicit conversion between MyWrapperType(Of T) and T (both directions). Therefore, I would have expected all of the following tests in NUnit to pass:

Dim x = New MyWrapperType(Of DateTime)(Date.MaxValue)
Assert.True(Date.MaxValue = x)
Assert.True(x = Date.MaxValue)
Assert.True(Date.MaxValue.Equals(x))
Assert.True(x.Equals(Date.MaxValue))
Assert.AreEqual(x, Date.MaxValue)
Assert.AreEqual(Date.MaxValue, x)

They all do, except the last one. It tells me that:

Failed: Expected: 9999-12-31 23:59:59.999 But was: <12/31/9999 11:59:59 PM>

Here are some functions from my type that may be relevant. Note: my type has a Value property similiar to Nullable(Of T):

Public Shared Widening Operator CType(value As T) As MyWrapperType(Of T)
  Return New MyWrapperType(Of T)(value)
End Operator

Public Shared Widening Operator CType(value As MyWrapperType(Of T)) As T
  Return value.Value
End Operator

Public Overrides Function Equals(other As Object) As Boolean
  If Me.Value Is Nothing Then Return other Is Nothing
  If other Is Nothing Then Return False
  Return Me.Value.Equals(other)
End Function

Public Overrides Function GetHashCode() As Integer
  If Me.Value Is Nothing Then Return 0
  Return Me.Value.GetHashCode()
End Function

When setting breakpoints on these methods methods for the test that fails, none of them get hit except ToString which gets called when they're formatting the error to display.

Why does this call to Assert.AreEqual only fail in one direction? Is this something wrong within nunit.framework.dll (using version 2.6.1.12217)? Or am I missing a bug in my code?

Это было полезно?

Решение

If T is a Date and you do

Return Me.Value.Equals(other)

the other is passed as Object to the Date.Equals method which looks like this:

Public Overrides Function Equals(ByVal value As Object) As Boolean
    If TypeOf value Is DateTime Then
        Dim time As DateTime = CDate(value)
        Return (Me.InternalTicks = time.InternalTicks)
    End If
    Return False
End Function

And as you can see the first condition will return False.

Dim isdate As Boolean = (TypeOf CObj(New MyWrapperType(Of Date)(Date.MaxValue)) Is Date)

To ensure correct casting you can do something like this:

Public Overrides Function Equals(other As Object) As Boolean
    If (TypeOf other Is MyWrapperType(Of T)) Then
        Dim obj As MyWrapperType(Of T) = DirectCast(other, MyWrapperType(Of T))
        '...Me.Value.Equals(obj.Value)
    ElseIf (TypeOf other Is T) Then
        Dim obj As T = DirectCast(other, T)
        '...Me.Value.Equals(obj)
    End If
    Return False
End Function

Edit

If we disassemble the Assert.AreEqual method it looks like this:

Call 1 : Assert

Public Shared Sub AreEqual(ByVal expected As Object, ByVal actual As Object)
    Assert.AreEqual(expected, actual, String.Empty, Nothing)
End Sub

Call 2 : Assert

Public Shared Sub AreEqual(ByVal expected As Object, ByVal actual As Object, ByVal message As String, ByVal ParamArray parameters As Object())
    Assert.AreEqual(Of Object)(expected, actual, message, parameters)
End Sub

Call 3 : Assert

Public Shared Sub AreEqual(Of T)(ByVal expected As T, ByVal actual As T, ByVal message As String, ByVal ParamArray parameters As Object())
    If Not Object.Equals(expected, actual) Then
        Dim str As String
        If (((Not actual Is Nothing) AndAlso (Not expected Is Nothing)) AndAlso Not actual.GetType.Equals(expected.GetType)) Then
            str = CStr(FrameworkMessages.AreEqualDifferentTypesFailMsg(IIf((message Is Nothing), String.Empty, Assert.ReplaceNulls(message)), Assert.ReplaceNulls(expected), expected.GetType.FullName, Assert.ReplaceNulls(actual), actual.GetType.FullName))
        Else
            str = CStr(FrameworkMessages.AreEqualFailMsg(IIf((message Is Nothing), String.Empty, Assert.ReplaceNulls(message)), Assert.ReplaceNulls(expected), Assert.ReplaceNulls(actual)))
        End If
        Assert.HandleFail("Assert.AreEqual", str, parameters)
    End If
End Sub

Call 4 : Object

Public Shared Function Equals(ByVal objA As Object, ByVal objB As Object) As Boolean
    Return ((objA Is objB) OrElse (((Not objA Is Nothing) AndAlso (Not objB Is Nothing)) AndAlso objA.Equals(objB)))
End Function

Assert.AreEqual(x, Date.MaxValue) = True

This would end up in this:

New MyWrapperType(Of DateTime)(Date.MaxValue).Equals(Date.MaxValue)

which finally ends up calling your Equals method:

Public Overrides Function Equals(other As Object) As Boolean
    If Me.Value Is Nothing Then Return other Is Nothing <- Pass, Value is Date.MaxValue, not null
    If other Is Nothing Then Return False <- Pass, other is Date.MaxValue, not null
    Return Me.Value.Equals(other) <- Pass, Value (Date.MaxValue) = other (Date.MaxValue)
End Function

Assert.AreEqual(Date.MaxValue, x) = False

This would end up in this:

Date.MaxValue.Equals(New MyWrapperType(Of DateTime)(Date.MaxValue))

which finally ends up calling your Date.Equals(obj As Object) method:

Public Overrides Function Equals(ByVal value As Object) As Boolean
    If TypeOf value Is DateTime Then '< Fail, value is not a DateTime, it's a MyWrapperType(Of T)
        Dim time As DateTime = CDate(value)
        Return (Me.InternalTicks = time.InternalTicks)
    End If
    Return False
End Function

Другие советы

Here's the cause to best of my understanding based off of the answer by Bjørn-Roger Kringsjå:

When I call Assert.True(Date.MaxValue.Equals(x)) the Date.Equals(other As Date) override gets called, as well as my widening operator on my type. It appears the compiler is choosing the most specific Equals override here (the one for Date) using my implicit type conversion.

When I call Assert.AreEqual(Date.MaxValue, x), the NUnit method calls Object.Equal(a, b) which then delegates it to Date.Equals(other As Object) method. This method returns false if other is not a Date. Therefore the assert fails.

If Assert.AreEqual had an override that takes dates (or maybe even two parameters of a generic type T?), it probably would have been fine, but since the only override that matches was for objects, my type conversions weren't able to come to save the day.

There are two ways your structure type could be converted to satisfy some overload of DateTime.Equals(): the compiler can use your implicit conversion operator to yield a DateTime, or it can do a boxing conversion to Object. There are many situations where, when multiple overloads are possible, the compiler should assume they're equivalent and just pick one without squawking. Unfortunately, the overloads of Equals are not equivalent (IMHO, the methods should have different names). The first four tests work because they choose an overload which causes the argument to be converted to the type of the thing doing the comparing.

The fifth assertion should fail because it's using the other overload which does not perform type coercion. The reason it succeeds is that your Equals method fails to abide by the Equals contract. No object of any type may legitimately report itself as equal to any object which will not reciprocate. Since no DateTime will consider itself equivalent to a non-converted instance of your type, your type must not consider itself equal to a non-converted DateTime.

If you dislike having inconsistent behavior with someWrapper.Equals(someDateTime) and someWrapper.Equals((Object)someDateTime), I would suggest that the best solution may be to declare overloads of == and != for (DateTime, WrapperType) and (WrapperType, DateTime) and mark them with an [Obsolete] attribute. That would cause the compiler to squawk at any effort to directly compare instances of your wrapper type to DateTime without first converting types so they match.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top