VB Nullables and Nothings - vb.net

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

Related

VB.NET library breaks on .Net Standard 2.1 when migrated from 2.0 - Requested operation is not avalaible on TryParse

There is a legacy Visual Basic class library targetting .Net Standard 2.0 currently:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
</ItemGroup>
</Project>
There is also a class inside
Public Class Class1
Public Shared Function TryGetInteger(ByVal value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value, out) Then
Return out
End If
Return Integer.MinValue
End Function
Public Shared Function BooleanToInt(ByVal bValue As Object) As Integer
If (bValue = DBNull.Value) Then
Return 0
ElseIf bValue = True Then
Return 1
Else
Return 2
End If
End Function
End Class
At this point everything works fine.
But changing <TargetFramework>netstandard2.0</TargetFramework> to 2.1 breaks the build with the following error in the Class1.TryGetInteger function:
Requested operation is not available because the runtime library function 'Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType' is not defined
How can this be fixed?
EDIT: Please disregard below idea:
The first idea comes from this closed github issue, that suggests adding <VBRuntime>Default</VBRuntime> to the project file, but this results in error:
could not find library 'Microsoft.VisualBasic.dll'
But it is already linked as a package... Any ideas?
EDIT: As mentioned is the comments, my take on the linked github issue is a bit misleading, so I guess I'm back to the beginning with the Requested operation is not available error.
Integer.TryParse() expects a String, but the input argument type is Object.
You can fix the issue like this:
Public Shared Function TryGetInteger(value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value.ToString(), out) Then Return out
Return Nothing 'Same as Integer.MinValue in this context
End Function
The extra ToString() call is the same thing the old framework was doing for you implicitly. Now you're aware of it.
But really I'd do this:
Public Shared Function TryGetInteger(value As String) As Integer
Dim out As Integer
If Integer.TryParse(value, out) Then Return out
Return Nothing
End Function
Which will probably bring up a whole new set of compiler errors, but there will be value in going back and fixing them at those calling locations. Most of the errors can be fixed by adding .ToString() to the end of value when calling the function, and any that can't were almost certainly hiding bugs.
Similarly for the BooleanToInt() function, I'd use explicit types, and maybe overloading to get better results, like this:
Public Shared Function BooleanToInt(bValue As DBNull) As Integer
Return 0 'If this overload was selected, we know we want 0
End Function
Public Shared Function BooleanToInt(bValue As Boolean) As Integer
If bValue Then Return 1
Return 2
End Function
Public Shared Function BooleanToInt(bValue As Object) As Integer
Return 2
End Function
As a rule of thumb, anywhere you need to use the Object type directly is a magnet for bugs. It's a smell in the code and something to avoid.
The class methods are relying on the implicit type conversions implemented by the VB compiler when Option Strict is set to Off.
The safest way to rewrite the code would be to use a de-compiler and implement the conversion operations used by the compiler. That could get quite involved.
If there is some sanity in the code that calls the the class methods, then a simple rewrite like the following can be done to avoid the usage of compiler's helper conversion methods.
Public Shared Function TryGetInteger(ByVal value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value.ToString, out) Then
Return out
End If
Return Integer.MinValue
End Function
Public Shared Function BooleanToInt(ByVal bValue As Object) As Integer
If (bValue Is DBNull.Value) Then
Return 0
Else
If TypeOf bValue Is Boolean Then
Return If(DirectCast(bValue, Boolean), 1, 2)
Else
Throw New ArgumentException("type of bValue must be either DbNull or Boolean")
End If
End If
End Function

Odd interaction of "If" and Nullable(Of Integer)

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.

TryCast for string to integer in VB.NET

I do not understand at all how to use TryCast in my code, but it is something I need to use for validating user input. I have done various searches and looked at various questions on here, but no one seems to actually say how to use it, and the MSDN website doesn't seem to help at all.
Function ValidateInput(Var_In As String) As Integer
If TryCast(Var_In, Integer) = Nothing Then
Return vbNull
Else
Return Var_In
End If
End Function
The error says that
The operand must be of reference type but Integer is of value type
What is the explanation of what I have done wrong?
TryParse doesn't accept more than 10 digits so for example, an input of "12345678901" won't be accepted. How do I fix this?
Let's try to understand the differences between TryCast, Convert and TryParse.
TryCast
This function will attempt to convert one object into another type, as long as it is a reference type.
Dim MyNewObject = TryCast(MyObject, MyReferenceClass)
If IsNothing(MyNewObject) Then
MessageBox.Show("Impossible to cast")
End If
Since Integer is a value type, it will not work, so we have to figure something out...
Convert
Convert Class on MSDN
From MSDN:
Converts a base data type to another base data type.
So we can try:
Dim myInt = Convert.ToInt32(MyObject)
The problem is that it will generate an exception InvalidCastException if it's impossible to do the conversion.
TryParse
This function is trying to convert a String into something you want. And it will not generate an exception:
Dim myInt As Integer = 0
If Not Integer.TryParse(MyString, myInt) Then
MessageBox.show("This is not an integer")
End If
Limitation
Converting a String into a Integer can sometimes be tricky... If the String represents a number that is greater or lesser than Integer.MaxValue and Integer.MinValue, you will end up with no conversion...
So you can go with a Double:
Double.TryParse(MyString, MyDouble)
Or personally, if you know that it will be a number, use Decimal:
Decimal.TryParse(MyString, MyDecimal)
See Decimals on MSDN
Decimal still has a Max and Min value, according to MSDN:
The Decimal value type represents decimal numbers ranging from positive 79,228,162,514,264,337,593,543,950,335 to negative 79,228,162,514,264,337,593,543,950,335. The Decimal value type is appropriate for financial calculations that require large numbers of significant integral and fractional digits and no round-off errors.
Convert.ChangeType
This one is also interesting, but is a bit weird...
You are attempting to perform TryCast against an Integer, which is a value type. TryCast works only on reference types, such as (but not limited to) a Class, Object, or String type.
If you are trying to convert the input parameter to an Integer, you might try one of the methods in the Convert class, such as Convert.ToInt32() or Integer.TryParse.
Instead of TryCast, use TryParse:
Function ValidateInput(Var_In As String) As Integer
Dim iNum As Integer
If (Integer.TryParse(Var_In, iNum)) Then
Return iNum
Else
Return vbNull
End If
End Function
Much better is to use TryParse:
Function ValidateInput(Var_In As String) As Integer
Dim num as Integer
If Not Integer.TryParse(Var_In, num) Then
Return vbNull
Else
Return num
End If
End Function
I'm late to the discussion, but if anyone lands here (like I did) looking for a quick & dirty solution, here a function I'm using for simple cell validation in a DataGridView control.
Function TryTypeFit(theString As String, theType As Type, ShowError As Boolean) As Boolean
Dim TempReturn As Boolean = False
Dim TempObject As Object
Try
TempObject = Convert.ChangeType(theString, theType)
TempReturn = True
Catch ex As Exception
' didn't work
TempReturn = False
If ShowError Then
Dim eMessage As String = "Error: Value must be castable to a " & CStr(theType.Name)
MsgBox(eMessage)
End If
End Try
100:
Return TempReturn
End Function

Reflection - creating datatype from a string literal value

Please see the code below:
Public Function Test()
Dim o As Object = getVariable("Integer")
If TypeOf o Is Integer Then
'Do some processing on the integer
ElseIf TypeOf o Is Decimal Then
'Do some processing on the integer
End If
End Function
Public Function getVariable(ByVal strDataType As String)
If strDataType = "Integer" Then
Return New Integer
ElseIf strDataType = "Decimal" Then
Return New Decimal
ElseIf strDataType = "Double" Then
Return New Double
End If
End Function
I suspect there is an easier way (fewer lines of code) of doing this with Reflection?
You can use Type.GetType together with Activator.CreateInstance:
Public Function getVariable(ByVal strDataType As String)
Return Activator.CreateInstance(Type.GetType(strDataType))
End Function
For strDataType you need to be using System.Int32, System.Decimal and System.Double respectively. If you want to keep it as Integer etc., you need to incorporate string translation, for example, have a Dictionary(Of String, String), with entries like ("Integer", "System.Int32").
The real question is what ars you trying to solve? Activator will work. Are trying to create a factory or an IOC container. Could you provide more detail? You could also create a dictionary were the key is of type string and then item stores delegates used to actual create the type.

Get the Integer value of an enumeration which is a generic

Here is the basic situation.
Public Class MyEnumClass(of T)
Public MyValue as T
End Class
This is vast oversimplification of the actual class, but basically I know that T is an enumeration (if it is not then there will be many other problems, and is a logical error made by the programmer)
Basically I want to get the underlying integer value of MyValue.
Using Cint or Ctype, does not work.
I was going to use a cool piece of reflection code but just a simple Convert.ToInt32 works great... Forgive my VB I'm a C# guy
Public Function GetEnumInt(Of T)(enumVal As T) As Integer
Return Convert.ToInt32(enumVal)
End Function
I tried this and it worked:
String.Format("{0:d}", MyValue)
I know you can do the following to get all the underlying values (I hope my VB syntax is correct... I've been working in C# mostly of late):
Dim intVal As Integer
For Each intVal In [Enum].GetValues(GetType(T))
//intValue is now the enum integer value
Next
That might at least get you started in the right direction.
This also works :
Fix(enumval)
Another simple way in VB.NET is to add it to 0:
Dim intVal As Integer = 0 + myEnum
So, this should work:
Sub GetEnumInt(of T)(enumVal as T) as Int
return 0 + enumVal
End Sub
Thanks to 'Jon Skeet'. But his code does not work in my Excel-2016. Minwhile the next code works fine:
Public Enum TypOfProtectWs
pws_NotFound = 0
pws_AllowAll = 1
pws_AllowFormat = 2
pws_AllowNone = 3
End Enum
Private Function TypOfProtectWs2I(pws As TypOfProtectWs) As Integer
TypOfProtectWs2I = Format("0", pws)
End Function
Private Sub test_TypOfProtectWs2I()
Debug.Print TypOfProtectWs2I(pws_AllowAll)
End Sub