In this section I've included various problems that can bite you but that don't fall under any clear categories of their own.
It can sometimes be difficult to be sure that what you code is what the compiler sees. The code in Listing 2-13 demonstrates a nasty bug that arises from a classic confusion between developer and compiler: The intent in this code is to verify that a specific path is actually a folder, rather than a file. Once the file's attributes have been retrieved, they are compared with attributes that a folder should have, thus returning true or false . There is only one problem with this line of code: It doesn't work. If you specify any file that exists, this line will return true regardless of whether it is actually a folder.
Option Strict Off If GetAttr(Path + FolderName) And _ FileAttribute.Directory = FileAttribute.Directory Then
The problem here is operator precedence. When the compiler sees two or more operators and the order of execution has not been explicitly stated by the programmer, it uses precedence rules to determine the order of execution. In this case, because the = operator has a higher precedence than the And operator, the compiler understands that the line of code actually means the code shown in Listing 2-14.
If GetAttr(Path + FolderName) _ And (FileAttribute.Directory = FileAttribute.Directory) Then
This order of operand execution means that the test will always return true for any file or folder. What the developer really intended is shown in Listing 2-15.
If (GetAttr(Path + FolderName) _ And FileAttribute.Directory) = FileAttribute.Directory Then
So what should the developer have written instead? The first point is that the original code in Listing 2-13 would give a compiler build error if Option Strict On had been specified. Because the compiler is being asked to compare an Integer ” the result of GetAttr(Path + FolderName) ”with a Boolean ”the result of (FileAttribute.Directory = FileAttribute.Directory) ” Option Strict grumbles because this involves an implicit conversion that it dislikes. The compiler warning is perfect in this case and is just one of many reasons why you should always use Option Strict in your programs.
With Option Strict now turned on, you need to compare like with like. So your first try at fixing this line might be to try the solution shown in Listing 2-16.
Option Strict On If GetAttr(Path + FolderName) And FileAttribute.Directory Then
However, the compiler will still complain, this time about the implicit conversion from VisualBasic.FileAttribute into Boolean . Because there is an implicit " = True " at the end of this line, the compiler doesn't want to perform this conversion implicitly. In fact, if you add the " = True " to the end of the line, you can see the squiggly underline showing the location of the error move from under the whole line to under the True keyword. So to really get this right, you need to perform the conversion explicitly, as shown in Listing 2-17.
Option Strict On If CBool(GetAttr(Path + FolderName) And FileAttribute.Directory) = True Then
Now what would be really nice is if the VB .NET IDE automatically inserted any missing brackets where necessary to show what will actually happen when a line of code is run. In other words, the IDE could show clearly what the compiler will do in terms of operator precedence and associativity rather than just showing what the developer wrote. For those people worried about using more disk space due to the extra brackets, I would suggest using a smaller font.
The code in Listing 2-18 shows interesting confusion between the developer and compiler, and between the VB .NET and C# compilers. Unlike the previous example, both of the operators here have identical precedence ”obviously, because they're both the same operator. So the question is, which assignment will happen first, and therefore will the variable A contain 1 or 2?
Option Strict Off Dim A As Integer = 0, B As Integer = 1, C As Integer = 2 A = B = C
Because the precedence rules don't help here, the compiler instead uses associativity. The associativity protocol explains the real precedence among all operators that have the same precedence level. Its only use is to avoid confusion when an expression contains two or more equal-precedence operators.
The protocol for all assignment operators is that they have right associativity. This means that the rightmost operation in the expression is evaluated first, and evaluation proceeds from right to left. Given this, you might assume that C is first assigned to B , and then B is assigned to A , thereby leaving A with the value of 2. In fact, you are probably thinking that it works in the manner shown in Listing 2-19.
Option Strict Off Dim A As Integer = 0, B As Integer = 1, C As Integer = 2 B = C A = B
In fact, A will actually have the value of 0! A clue to this behavior becomes clear when you set Option Strict On . The compiler now complains because it doesn't like the implicit conversion from Boolean to Integer . It is interpreting the second = operator as a conditional test resulting in a true or false value, and the first = operator as an assignment operator that converts the true or false to an integer. The integer equivalent of false is, of course, 0. To verify this, you can set the value of C to 1 and A will then contain the value of “1, the integer equivalent of true (because B now equals C ).
This is not always a surprise to VB.Classic developers because that language behaves in exactly the same way. In both languages this kind of statement involves an ambiguity; does = mean assign or test? The = operator does double duty ”it can be said to be overloaded. It can mean either assignment ( B takes the value of C ) or equivalence ( B is the same value as C ).
So what the VB .NET compiler is seeing is the code in Listing 2-20.
Option Strict Off Dim A As Integer = 0, B As Integer = 1, C As Integer = 2 A = CInt(CBool(B = C))
This becomes more interesting when you translate the code in Listing 2-18 directly into C#. Listing 2-21 shows this translation.
int A = 0; int B = 1; int C = 2; A = B = C;
Assuming that the C# developer understands operator precedence and associativity, he or she will expect the code in Listing 2-21 to work like the code shown in Listing 2-22. Sure enough, the C# compiler sees exactly what the developer sees, and A will actually contain the value of 2.
int A = 0; int B = 1; int C = 2; B = C; A = B;
There is a fairly obvious reason behind the fact that the C# compiler sees this code differently from the VB .NET compiler. Unlike in VB .NET, in C# there are separate operators for assignment and for conditional test. Assignment is represented by the = operator, whereas equivalence is represented by the = = operator. This means that there is never an ambiguity in C# between assignment and equivalence.
There are two types of type in VB .NET and the CLR. Examples of value types include any type defined as boolean, int32 , or struct . When you assign a value-type variable to another value-type variable, the value of the first variable is copied to the second variable. So in the following code, TestVar1 has the value of 2 after the assignment:
Dim TestVar1 As Int32 = 1, TestVar2 As Int32 = 2 TestVar1 = TestVar2
Examples of reference types include any type defined as object or as string or as a class. When you assign a reference-type variable to another reference-type variable, you're assigning the reference to the variable, not the value of the variable. So in the following code, both variables will have their Name property set to "Martin" after the second assignment because they both reference the same instance after the first assignment:
Dim Person1 As New Person("Mark"), Person2 As New Person("Tim") Person1 = Person2 Person2.Name = "Martin"
Given the preceding example, you might be wondering what happens if you assign a value-type variable to a reference-type variable. When you do this, the CLR performs an automatic type conversion calling boxing. A boxing conversion implies making a copy of the value being boxed. Listing 2-23 shows what happens in a typical boxing operation and compares it with a similar operation that doesn't involve boxing.
First a boolean and an object variable are declared, and both are set to the value of false . The two variables are then passed in turn as a ByRef argument to the SwitchBoolean method. The boolean variable is automatically boxed to a reference value type because the method argument is of type object . The argument value is then changed inside the method from false to true and passed back out of the method via the ByRef argument. Then the two results are printed to the console. What would you expect to see shown as the results?
Option Strict On Module BoxingTest Sub Main() Dim MyBoolean As Boolean = False Dim MyObject As Object = False SwitchBoolean(CType(MyBoolean, Boolean) Console.WriteLine("MyBoolean = " & MyBoolean.ToString) SwitchBoolean(MyObject) Console.WriteLine("MyObject = " & MyObject.ToString) Console.ReadLine() End Sub Private Sub SwitchBoolean(ByRef TestBoolean As Object) TestBoolean = True End Sub End Module
The MyBoolean result shows as false , but the MyObject result shows as true . What's happened is that the automatic boxing that took place when MyBoolean was converted to the argument of type object meant that a new reference-type variable was created. Changing the reference-type variable inside the method had no effect on the ByRef argument because the boxing that was performed meant that a copy of the original variable was passed to the method rather than the original variable. Changing the copy of the method argument obviously had no effect on the original argument.
This is not intuitive, but the good news is that you probably won't encounter this type of situation very often. As long as you understand which types are value types and which are reference types, and then try not to mix the two, you should rarely have the sort of problem shown here.
VB.Classic is fairly strict about dividing by zero. Anywhere that you try it, it's going to result in an error. VB .NET does something rather different. The following code is divided into five separate tests: The first two tests perform integer divisions, and the final three tests perform floating-point divisions. The challenge is to predict which of the five tests laid out in Listing 2-24 will throw an exception and what exceptions will actually be thrown.
Option Strict On Module Test Sub Main() Dim intTest As Integer, dblTest As Double Dim intZero As Integer = 0, blnExceptionThrown As Boolean = False 'First test Console.WriteLine("Integer division by zero assigned to integer:") blnExceptionThrown = False Try intTest = 5 \ intZero Catch objException As Exception Console.WriteLine(objException.Message) blnExceptionThrown = True Finally If blnExceptionThrown = True Then Console.WriteLine("Result: not available") Else Console.WriteLine("No exception was thrown") Console.WriteLine("Result: " + intTest.ToString) End If Console.WriteLine() End Try 'Second test Console.WriteLine("Integer division by zero assigned to double:") blnExceptionThrown = False Try dblTest = 5 \ intZero Catch objException As Exception Console.WriteLine(objException.Message) blnExceptionThrown = True Finally If blnExceptionThrown = True Then Console.WriteLine("Result: not available") Else Console.WriteLine("No exception was thrown") Console.WriteLine("Result: " + dblTest.ToString) End If Console.WriteLine() End Try 'Third test Console.WriteLine("Float division by zero assigned to integer:") blnExceptionThrown = False Try intTest = CInt(5 / intZero) Catch objException As Exception Console.WriteLine(objException.Message) blnExceptionThrown = True Finally If blnExceptionThrown = True Then Console.WriteLine("Result: not available") Else Console.WriteLine("No exception was thrown") Console.WriteLine("Result: " + intTest.ToString) End If Console.WriteLine() End Try 'Fourth test Console.WriteLine("Float division by zero assigned to double:") blnExceptionThrown = False Try dblTest = 5 / intZero Catch objException As Exception Console.WriteLine(objException.Message) blnExceptionThrown = True Finally If blnExceptionThrown = True Then Console.WriteLine("Result: not available") Else Console.WriteLine("No exception was thrown") Console.WriteLine("Result: " + dblTest.ToString) End If Console.WriteLine() End Try 'Fifth test Console.WriteLine("Float division of zero by zero assigned to double:") blnExceptionThrown = False Try dblTest = intZero / intZero Catch objException As Exception Console.WriteLine(objException.Message) blnExceptionThrown = True Finally If blnExceptionThrown = True Then Console.WriteLine("Result: not available") Else Console.WriteLine("No exception was thrown") Console.WriteLine("Result: " + dblTest.ToString) End If Console.WriteLine() End Try Console.ReadLine() End Sub End Module
Developers accustomed to using VB.Classic might predict the following results:
Integer division by zero assigned to integer: Attempted to divide by zero Result: not available Integer division by zero assigned to double: Attempted to divide by zero Result: not available Float division by zero assigned to integer: Attempted to divide by zero Result: not available Float division by zero assigned to double: Attempted to divide by zero Result: not available Float division of zero by zero assigned to double: Attempted to divide by zero Result: not available
What actually happens is that VB .NET integer division by zero behaves in the same way as any zero division does in VB.Classic: An exception is thrown. Floating-point division by zero, however, results instead in no exception and a result of either Infinity or NaN (the acronym for "Not a Number"). These results can be held and expressed in a variable of type single or double, but not in an integer. So the code in Listing 2-24 produces the following results:
Integer division by zero assigned to integer: Attempted to divide by zero Result: not available Integer division by zero assigned to double: Attempted to divide by zero Result: not available Float division by zero assigned to integer: Arithmetic operation resulted in an overflow. Result: not available Float division by zero assigned to double: No exception was thrown Result: Infinity Float division of zero by zero assigned to double: No exception was thrown Result: NaN
The results from the first two tests are understandable: integer division by zero is not allowed. The result from the third test is interesting: The floating-point division causes a special floating-point value called Infinity to be generated, and since an integer cannot hold this value, an overflow exception is generated. The fourth test also produces Infinity , but no exception is thrown because this value can be held in a variable of type Double . The final test, where zero divides zero, produces another special floating-point value called NaN ; once again no exception is thrown. One final surprise: If you check, Infinity is not equivalent to NaN .
You can check for these floating-point values using the shared boolean members System.Double.IsInfinity(x) and System.Double.IsNaN(x) , along with their Single counterparts.
Why is this happening? It's because the VB compiler is finally exposing you to the real nature of the underlying processor. The X86 floating-point instruction set has always generated the nonfinite floating-point values Infinity and NaN in the case of division by zero. These values are part of the Institute of Electrical and Electronics Engineers (IEEE) standard for floating-point operations. To simplify matters, VB.Classic added extra instructions that caused these nonfinite values to throw the same error as thrown by an integer division by zero. Although you were able to turn off the generation of these instructions by manipulating the compiler optimization flags, few developers ever bothered. But now that VB has to interact with other .NET languages and with the underlying .NET base classes, the decision was made to admit that these floating-point values do actually exist and so developers are required to deal with them.
NaN and Infinity have some other peculiar properties. Listing 2-25 shows a console application performing some tests on these two values whose results might surprise you.
Option Strict On Module Test Sub Main() Console.WriteLine(Single.NaN = Single.NaN) Console.WriteLine((Single.NaN - Single.NaN) = 0) Console.WriteLine(Single.PositiveInfinity = Single.PositiveInfinity) Console.WriteLine((Single.PositiveInfinity - Single.PositiveInfinity) = 0) Console.WriteLine(1.0 < Single.NaN) Console.WriteLine(1.0 >= Single.NaN) Console.ReadLine() End Sub End Module
The six equality tests in this code return the following results:
False False True False False False
The first result shows that NaN doesn't equal NaN ! Once again, this is part of the IEEE floating-point standard, if somewhat nonintuitive. Given the first result, the second result then makes some sense. Although subtracting a number from itself normally gives a result of zero, you already know that NaN doesn't equal NaN .
The third result shows that, unlike NaN , PositiveInfinity does equal Positive-Infinity . But when you try to subtract PositiveInfinity from itself, the result isn't zero! In fact, the result is actually NaN .
The final two results look fine as long as they're taken one at a time, but they're rather peculiar if you look at them together. Combining these two tests shows that 1.0 is neither less than, equal to, or greater than NaN . As Single.NaN can be used wherever a normal Single value is expected, you therefore need to be careful whenever you perform comparisons on Single (or Double ) values. Listing 2-26 shows two different ways of coding a function that takes an argument of type Single . Only one of these two functions works as you might expect when passed Single.NaN instead of a normal number.
Option Strict On Module Test Sub Main() Try Console.WriteLine(CalcTotalOne(Single.NaN)) Console.WriteLine(CalcTotalTwo(Single.NaN)) Catch ex As Exception Console.WriteLine(ex.Message) Finally Console.ReadLine() End Try End Sub Function CalcTotalOne(ByVal PurchaseAmount As Single) As Single If PurchaseAmount < 0.0 Then Throw New ArgumentException("PurchaseAmount must be >= zero") End If Return PurchaseAmount * 1.08F End Function Function CalcTotalTwo(ByVal PurchaseAmount As Single) As Single If PurchaseAmount >= 0.0 Then Return PurchaseAmount * 1.08F Else Throw New ArgumentException("PurchaseAmount must be >= zero") End If End Function End Module
The CalcTotalOne function returns NaN and doesn't throw the exception that you might expect from reading the code. This is because comparing a number (in this case, 0.0) with NaN will always return false . The CalcTotalTwo function, on the other hand, throws the exception that you would expect. When you're using floating-point calculations, you should always be aware that you might be dealing with NaN or Infinity .
There are some other interesting side effects resulting from floating-point issues. One problem results from the rounding that VB .NET performs when using floating-point Double variables. The code in Listing 2-27 assigns a very large value to a variable of type Double and then assigns this to a second variable of type Double and adds 1. Of course, any developer can see that these two variables are therefore not equal.
Option Strict On Module Test Sub Main() Dim X As Double, Y as Double X = 10 ^ 18 Y = X + 1 Console.WriteLine("X = Y? " + (X = Y).ToString) Console.ReadLine() End Sub End Module
This code returns the following result:
X = Y? True
This behavior occurs because both 10 18 and (10 18) + 1 are represented with the same 64-bit floating-point Double value. 10 18 is in fact the largest value that can be expressed in 64 bits, and adding 1 to it will make no difference.
Converting the same code to VB.Classic normally returns the correct value of false . Specifically, this happens if you're running in interpreted mode or as compiled p-code . If, however, you compile to native code, you will also see true . The solution to this issue when using VB.Classic with native code is to use the Advanced Optimization compiler dialog box to turn on the "Allow Un-rounded Floating Point Operations" option. This option usually allows VB.Classic to reuse the 80-bit values already on the math coprocessor stack, instead of using the 64-bit values stored in variables (memory locations), and once again you will see the correct value of false returned. Unfortunately there does not appear to be any equivalent compiler optimization when using VB .NET.
In this type of situation, you should really be using the Decimal datatype. As explained in the next section, this datatype is not subject to floating-point issues.
There is another common floating-point issue missed by many developers, especially those doing financial calculations. This issue is also found in VB.Classic, but I'm discussing it here anyway because it has claimed so many victims. The problem occurs when you work with floating-point numbers that can only be represented approximately in binary. In the code shown in Listing 2-28, it seems obvious that both variables should have the same value at the end of the calculation.
Option Strict On Module Test Sub Main() Dim X As Double = 2.45, Y as Double = 245 X *= 100 Console.WriteLine("X = Y? " + (X = Y).ToString) Console.ReadLine() End Sub End Module
This code returns an interesting result:
X = Y? False
This behavior occurs because just as 1/3 cannot be represented exactly as a decimal (0.3333 ), 2.45 and many other numbers cannot be represented exactly in binary, the notation used by the CLR for floating-point numbers. If you use the Ildasm utility to examine the Common Intermediate Language (CIL) produced by the assignment of 2.45 to the variable X, you will see that the value actually assigned is 2.4500000000000002.
Financial calculations, the most common type of calculations affected by this issue, should always be done with Decimal variables rather than floating-point numbers. The Decimal datatype uses scaled integer arithmetic and isn't affected by this representation problem. If you have to compare Single or Double variables for equality, you should check that the absolute value of the difference between the two variables is less than Single.Epsilon or Double.Epsilon .