Odd interaction of "If" and Nullable(Of Integer) - vb.net

I'm using Nullable(Of Integer) and have just been stung by Nothing being cast to 0. That's excatly what I don't want when using Nullable(Of Integer).
func1 below doesn't behave as I'd expect. I can get it to do my will by ammending it (see func2). But I don't see why this should be necessary (and I think I might find it hard to remember to do it).
Why isn't func1 doing what I want? I think I've encountered this before, and I'd rather not see it again.
Function func1(parameter As Integer) As Nullable(Of Integer)
Return If(parameter > 10, parameter, Nothing)
End Function
Function func2(parameter As Integer) As Nullable(Of Integer)
Return If(parameter > 10, parameter, DirectCast(Nothing, Integer?))
End Function
Sub Main
' Should be True
System.Console.WriteLine(func1(11).HasValue)
System.Console.WriteLine(func2(11).HasValue)
System.Console.WriteLine()
' Should be False
System.Console.WriteLine(func1(9).HasValue)
System.Console.WriteLine(func2(9).HasValue)
End Sub
The results I get (running this in LINQPad) are:
True
True
True
False

Facts important in your case:
Inline If method expected that both "true" and "false" expressions
must return same type.
Nothing is default value of type.
For Integer it is 0.
For reference type it is null
In first method inline If method expects that "False" expression must return an Integer, because compiler cannot decide return type based on Nothing, it will use type produced by "True" expression. So Nothing will produce default value of Integer type which is 0.
In second method both parameters have explicitly declared returned types, where Integer can be implicitly converted to the Nullable, so compiler will return Nullable as result of If method.
The key role in your problem is inline If method. Which uses Nothing as default value of Integer.
If you rewrite it as normal If .. Else then everything works without DirectCast
Private Function GetNullableInteger(parameter As Integer) As Integer?
If parameter > 10 Then
Return parameter
Else
Return Nothing
End If
End Function

To explain what is happening here, I'll start by removing the shorthand part of your code which may help with my explanation.
Integers in VB.net cannot be assigned using Null or DBNull. IT can be assigned using Nullable-of-T as you have. However, as soon as you make the object Nullable-ish, it can be evaluated to be 0.
Consider the following
dim x as Integer = nothing 'evaluates to x=0
So when your function runs, you use DirectCast() to return a nullable-ish Integer, which then evaluates to be not nullable by func2
Function func1(parameter As Integer) As Nullable(Of Integer)
Return If(parameter > 10, parameter, Nothing)
End Function
Function func2(parameter As Integer) As Nullable(Of Integer)
Return If(parameter > 10, parameter, DirectCast(Nothing, Nullable(of Integer)))
End Function
Sub Main()
' Should be True
System.Console.WriteLine(func1(11).HasValue)
System.Console.WriteLine(func2(11).HasValue)
System.Console.WriteLine()
' Should be False
System.Console.WriteLine(func1(9).HasValue)
System.Console.WriteLine(func2(9).HasValue)
Console.ReadLine()
End Sub

Here is func1 rewritten. Note that there is no need for casting.
Function func1(parameter As Integer) As Nullable(Of Integer)
Dim rv As New Nullable(Of Integer)
If parameter > 10 Then
rv = parameter
End If
Return rv
End Function
The If operator, If(foo,foo=true,foo=false), should be used sparingly because it is slower than the standard If construct.
edit: The statement about the If operator is incorrect.
Thanks to Chris.

Related

HasValue giving value 0 instead of Nothing

Question is simple, when I pass CustomClass which is Nothing into Run method at the end in Query method second.HasValue is showing 0. Shouldn't be Nothing?
Public Function Run() As Boolean
Return Query(if(CustomClass IsNot Nothing, CustomClass.Id, Nothing))
End Function
Public Function Query(second As Integer?) As Boolean
...
If second.HasValue Then
'value = 0 !
Else
'some query
End If
...
End Function
That's a VB.NET oddity. Nothing doesn't only mean null(C#) but also default(C#). So it will return the default value of a given type. You can even assign Nothing to an Integer variable(or any other reference- or value-type) for that reason.
In this case the compiler decided that Nothing means the default value of Integer which is 0. Why? Because he needs to find an implicit conversion to the Id-property which is Int32.
If you want a Nullable(Of Int32) use:
Return Query(if(CustomClass IsNot Nothing, CustomClass.Id, New Int32?()))
Because i mentioned C#, if you try the same there you will get a compiler error that there is no implicit conversion between null and int. In VB.NET there is one, the default value 0.
The reason is the inline If-statement.
It will return an Integer instead of an Integer? because CustomClass.Id apparently is of type Integer.
So you can either define CustomClass.Id as Integer? or use CType to convert it to Integer? in the inline If.
Public Function Run() As Boolean
Return Query(if(CustomClass IsNot Nothing, CType(CustomClass.Id, Integer?), Nothing))
End Function

VB Nullables and Nothings

I researched C#'s default keyword equivalence in VB.NET and came across this question.
Then I got curious. Some background - I'm working with parsing an excel spreadsheet, where many columns can be null, and there is certainly a difference for me between an integer column being 0 and being null.
I wrote a little parse method:
Function Test(ByVal i As String) As Nullable(Of Integer)
Dim j As Integer
If Integer.TryParse(i, j) Then
Return j
Else
Return Nothing
End If
End Function
this seems to work correctly. But here, I can return an Integer if i want:
Function Test(ByVal i As String) As Nullable(Of Integer)
Return 2 'or Return Nothing
End Function
which I can in C# as well:
static void Main(string[] args)
{
int? j = Test("1");
}
public static int? Test(string i)
{
return 2; //or return null
}
In the code above, if I change j to an int, I'll get a compile-time conversion error, which makes total sense.
Now my question - in VB, if i attempt a similar approach:
Sub Main()
Dim j As Integer = Test("hello")
End Sub
Function Test(ByVal i As String) As Nullable(Of Integer)
Dim j As Integer
Return If(Integer.TryParse(i, j), j, Nothing)
End Function
or, in my test case where i is not an Integer it can be rewritten as:
Function Test(ByVal i As String) As Nullable(Of Integer)
Return DirectCast(Nothing, Integer)
End Function
because of the way Nothing works in VB.NET, this code compiles and runs without error -- j is set to Integer's default 0.
This feels so dirty to me. In this scenario you can somewhat alter the method's intentions without any warnings or errors. I'm just curious I suppose, is this an unintentional by-product of the way Nothing works in VB, or is this the intended purpose?
Your VB.Net code compiles because you're using late binding, which allows changing the type of a variable at runtime.
If you compile your code with OPTION STRICT ON, you'll get a compiler error like:
Option Strict On disallows implicit conversions from 'Integer?' to 'Integer'.
You can't assign NULL to a value type in VB.Net in which it instantiates that type with its default value. In your case you are not creating a NULL Integer, but an integer that holds the default value of 0.
Another good note: turn Option Strict On

Why does Assert.AreEqual fail only in one direction with my custom type that has a widening conversion?

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.

SQL Data Reader - How to handle Null column values elegantly

I'm using an SQLDataReader to retrieve values from a database which may be null. I've worked out how to handle Null string values but can't get the same trick to work with integers or booleans:
Using cmd As DbCommand = store.GetStoredProcCommand("RetrievePOCO")
store.AddInParameter(cmd, "ID", DbType.Int32, ID)
Using reader As IDataReader = store.ExecuteReader(cmd)
If reader.Read() = True Then
Dim newPOCO As New POCO()
With newPOCO
'If the source column is null TryCast will return nothing without throwing an error
.StatusXML = TryCast(reader.GetString(reader.GetOrdinal("StatusXML")), String)
'How can a null integer or boolean be set elegantly?
.AppType = TryCast(reader.GetInt32(reader.GetOrdinal("AppType")), System.Nullable(Of Integer))
.Archived = TryCast(reader.GetBoolean(reader.GetOrdinal("Archived")), Boolean)
So how can a null integer or boolean be set elegantly? I've seen suggestions in C# but they don't translate correctly to VB giving a 'TryCast operand must be reference type, but integer? is a value type' compiler errors.
I use the following function in this scenario:
Public Shared Function NoNull(ByVal checkValue As Object, ByVal returnIfNull As Object) As Object
If checkValue Is DBNull.Value Then
Return returnIfNull
Else
Return checkValue
End If
End Function
Your code would look something like this:
With newPOCO
.StatusXML = NoNull(reader("StatusXML"), "")
.AppType = NoNull(reader("AppType"), -1)
.Archived = NoNull(reader("Archived"), False)
End With
Note that this function requires you to pass the value which should be used if the value is DbNUll as the second Parameter.
You can take advantage of the IsDBNull method of the SqlDataReader and use the VB.NET ternary operator to assign a default value to your poco object
.StatusXML = If(reader.IsDBNull(reader.GetOrdinal("StatusXML")), _
"",reader.GetString(reader.GetOrdinal("StatusXML")))
It is just one line, not very elegant because you need to call two times the GetOrdinal method.
Public Function NotNull(Of T)(ByVal Value As T, ByVal DefaultValue As T) As T
If Value Is Nothing OrElse IsDBNull(Value) Then
Return DefaultValue
Else
Return Value
End If
End Function

List(of Int32).Contains([int64 value]) doesn't work because of disimilar types

I have a list of integers that have been deserialized by WCF from json. This list deserializes as a list(of int32). I don't have any control over how it deserializes.
I need to be able to see if this list contains a value that happens to be an Int64. It obviousy doesn't work. Here is a sample of the function:
Private Shared Function IsIn(aPropertyValue As Int64, aList As IList) As Boolean
Return aList.Contains(aPropertyValue)
End Function
The reason I'm passing IList is because I don't want to have to create this function 12 times, once for each numeric type, byte through uint64. If I did create it 12 times, acutally 12 x 12 times for every possible option, I guess I could use List(of T).Exists().
The documentation for Contains() says it uses IEquatable(Of T).Equals to perform the comparison. I have to think that an Int32 and an Int64 with the same value can be compared and be found to be equal.
I must be missing something obvious.
Try this:
Private Function IsIn(ByVal aPropertyValue As Long, ByVal list As IList) As Boolean
Dim genericListType As Type = CType(list, Object).GetType().GetGenericArguments()(0)
Dim convertedPropertyValue As Object = Nothing
Try
convertedPropertyValue = Convert.ChangeType(aPropertyValue, genericListType)
Catch
End Try
If convertedPropertyValue IsNot Nothing Then
Return list.Contains(convertedPropertyValue)
Else
Return False
End If
End Function