The value of Long.MaxValue
is 9,223,372,036,854,775,807
. When cast to a single precision value, becomes 9.223372E18. However the cast back to a Long throws a System.OverflowException
. This despite the fact that the value in the Single is actually less than the possible maximum value of the long. Experimentation in the watch window shows that in fact the value
8.070450532247929344e18
is the largest double precision value that will safely ctype to a Long. Try this in the watch window:
CType(8.070450532247929344000001e18, Long)
further
8.070450807125836288e18
is the largest double precision value that after conversion to single precision, will safely convert to a Long, using this statement in the watch window:
CType(CType(8.0704508071258362880000001e18, Single), Long)
This seems like very strange behaviour.
Why?
Edit: Further Experimentation
The above experimentation is using the watch window and CTypes. Another experiment yields another answer:
Dim d As Long = 200000000000
Dim delta As Long = d / 2
Dim c As Long
Dim a As Single
Dim b As Long
While delta > 0
c = Long.MaxValue - d
a = c
Try
b = a
delta = delta \ 2
d = d - delta
Catch ex As Exception
d = d + delta
End Try
End While
The above code yields the largest Long that can be cast back and forth is:
9,223,371,761,976,868,860
or
Long.MaxValue - 274,877,906,947
When cast to a single precision value,
Long.MaxValue
becomes 9.223372E18, which is less than the possible maximum value of the Long.
Although 9.223372E18
is the default string representation of the result, this is misleading. The exact result is not 9.223372 × 1018 but rather 263 = 9,223,372,036,854,775,808
, which is the closest possible Single
to the original value of 263 − 1. (The difference of 1 is due to the inability of Single
to precisely represent the original value.)
Because 263 exceeds Long.MaxValue
, attempting to cast it back to a Long
throws OverflowException.
Long
that can be represented exactly by a Single
?We want to find the maximum possible value of L = 2e × s < 263, where the integer exponent e satisfies −126 ≤ e ≤ 127, and the significand s = (1.b22b21...b0)2 consists of the binary digit 1 followed by 23 fractional binary digits.
Thus L = 262 × (2 − 2−23) = 263 − 239 = 9,223,371,487,098,961,920.
Long
that can be converted to a Single
and back to a Long
without overflowing?The answer M is the midpoint of L (from above) and Long.MaxValue
:
M = (L + 263 − 1) \ 2 = 263 − 238 − 1 = 9,223,371,761,976,868,863.
When you convert any Long
between L and M to a Single
, you get L:
Dim a As Long = 9223371761976868863
Dim b As Single = a
Dim c As Long = CType(b, Long)
Console.WriteLine(c) ' Output: 9223371487098961920
When you convert any Long
between M + 1 and Long.MaxValue
to a Single
, you get 263, which exceeds Long.MaxValue
.
CType(8.07045053224793E+18, Long)
fail to compile?Good question. A Double
should be convertible to a Long
at compile time if its value is within the range of the Long
data type (approximately ±9.223E+18). 8.07045053224793E+18
is less than Long.MaxValue
, and in fact the conversion succeeds at run time:
' This code generates error BC30439: Constant expression not representable in type 'Long'.
Const a = 8.07045053224793E+18
Const b As Long = CType(a, Long)
' But the identical code with Dim instead of Const compiles successfully.
Dim a = 8.07045053224793E+18
Dim b As Long = CType(a, Long)
Console.WriteLine(b) ' Output: 8070450532247929856
Therefore, this is clearly a bug in VB.NET’s compile-time conversion logic. Indeed, I traced the bug to the following decade-old block of code within the compiler, which is supposed to check whether the sourceValue
is between &H7000000000000000L
and &H8000000000000000UL
, and if so, return False
to indicate "no overflow":
If sourceValue > &H7000000000000000L Then
Dim temporary As Double = (sourceValue - &H7000000000000000L)
If temporary < &H7000000000000000L AndAlso UncheckedCLng(temporary) > &H1000000000000000L Then
Return False
End If
Else
But the developer incorrectly used >
instead of <
in the comparison UncheckedCLng(temporary) > &H1000000000000000L
. I’ve submitted a pull request to get this corrected.
The usage of the somewhat arbitrary constant &H7000000000000000L
in this code explains the strange behavior you observed: converted to a Double
, this constant equals 8.0704505322479288E+18
.