Converting Between Types
Widening happens on its own, narrowing needs a cast, and TryParse is the one to use when the text came from outside your program.
bool ok = int.TryParse(text, out int value);Values often arrive as one type when you need another. A count read from a web request is text. A measurement is a double and you want a whole number. C# has four separate mechanisms for this, and picking the wrong one is how data quietly goes missing.
Widening and casting
When every possible value of the source type fits in the destination type, the conversion is implicit: you write nothing and the compiler does it. Every int fits in a long and in a double, so those just work. When values might not fit, you must write an explicit cast, the destination type in brackets: (int)measured. The cast is you taking responsibility for the loss. Casting a double to a whole number cuts towards zero; it does not round, so (int)3.9 is 3.
Convert and parsing
Convert.ToInt32 rounds instead of cutting, and sends an exact half to the even number, so 2.5 becomes 2 and 3.5 becomes 4. For text there are two methods. int.Parse("123") returns the number and throws an exception if the text is not a number. int.TryParse returns a bool saying whether it worked, and hands the number back through an out parameter, which is a parameter the method writes to rather than reads. On failure it sets that parameter to 0.
The out variable can be declared inside the call. These two are the same thing: int n; int.TryParse(s, out n); and int.TryParse(s, out int n);
What trips people up
Using Parse on anything a user, a file or an HTTP request supplied. Bad input is normal there, not a bug, so a thrown exception is the wrong answer. Use TryParse.
Example
// Implicit: every int fits in a long and in a double, so no cast is needed.
int count = 42;
long wideCount = count;
double countAsDouble = count;
Console.WriteLine($"{wideCount} {countAsDouble}");
// Explicit: a double does not fit in an int, so you write the cast yourself.
double measured = 3.9;
int cut = (int)measured; // cuts towards zero, it does not round
Console.WriteLine(cut);
// Convert rounds instead, and sends an exact half to the even number.
Console.WriteLine(Convert.ToInt32(3.9));
Console.WriteLine(Convert.ToInt32(2.5));
// Parse turns text into a number, and throws when the text is not a number.
Console.WriteLine(int.Parse("123"));
// TryParse reports failure instead, and writes the number to its out parameter.
bool ok = int.TryParse("123", out int parsed);
Console.WriteLine($"{ok} {parsed}");
bool bad = int.TryParse("twelve", out parsed); // parsed is set to 0 on failure
Console.WriteLine($"{bad} {parsed}");
// Output:
// 42 42
// 3
// 4
// 2
// 123
// True 123
// False 0When to use it
- Read a page number or a quantity out of a query string without letting bad input throw.
- Turn a measured double into a whole count, choosing deliberately between cutting and rounding.
- Narrow a long identifier to an int for an older API, with checked so the loss is reported.
- Accept a number typed with a thousands separator by passing NumberStyles to TryParse.
More examples
Reading a number from untrusted text
The same bad input, handled two ways. TryParse gives you a false you can turn into a 400 response or a validation message. Parse gives you a FormatException, which in a web request means an unhandled error unless something above you catches it. Keep Parse for values your own code produced, such as a constant in a test, where bad input really would be a bug.
// Text from a user, a file or an HTTP request is untrusted by definition.
string fromUser = "42";
string alsoFromUser = "4 2";
// TryParse reports failure, so you decide what happens next.
bool firstOk = int.TryParse(fromUser, out int firstValue);
bool secondOk = int.TryParse(alsoFromUser, out int secondValue);
Console.WriteLine($"{firstOk} {firstValue}");
Console.WriteLine($"{secondOk} {secondValue}");
// Parse throws. try runs the code and, if it fails, runs the catch instead.
try
{
int broken = int.Parse(alsoFromUser);
Console.WriteLine(broken);
}
catch (FormatException)
{
Console.WriteLine("FormatException from Parse");
}
// Output:
// True 42
// False 0
// FormatException from ParseCast, Convert, Floor and Truncate
A negative number separates all four. Cutting towards zero and rounding down are the same for 3.7 and different for -3.7, which is how an off-by-one appears only for negative inputs. The last part is a trap worth remembering: Convert.ToInt32 turns a null string into 0 rather than failing, so a missing value becomes a real-looking zero in your data.
// Four things that all look like "turning 3.7 into a whole number".
double value = -3.7;
Console.WriteLine((int)value); // cast: cut towards zero
Console.WriteLine(Convert.ToInt32(value)); // Convert: round to nearest
Console.WriteLine(Math.Floor(value)); // Floor: always round down
Console.WriteLine(Math.Truncate(value)); // same as the cast, as a double
// Convert also reads text, and treats null as zero rather than an error.
string? nothing = null;
Console.WriteLine(Convert.ToInt32(nothing));
Console.WriteLine(Convert.ToInt32("17"));
// Output:
// -3
// -4
// -4
// -3
// 0
// 17Narrowing, and catching the loss
The chain upwards needs no syntax at all, because a byte value is always a valid int, long and decimal. Going down, (byte)300 gives 44 with no complaint, which is the kind of value that passes a test and corrupts a file. Wrapping the cast in checked turns it into an OverflowException you can see, and is worth doing on any narrowing conversion of a value you did not compute yourself.
// Widening is implicit because nothing can be lost.
byte small = 200;
int wider = small;
long widest = wider;
decimal asDecimal = widest;
Console.WriteLine($"{small} {wider} {widest} {asDecimal}");
// Narrowing needs a cast, and the cast is you accepting the risk.
int big = 300;
byte wrapped = (byte)big; // 300 does not fit in a byte
Console.WriteLine(wrapped);
try
{
byte safe = checked((byte)big);
Console.WriteLine(safe);
}
catch (OverflowException)
{
Console.WriteLine("300 does not fit in a byte");
}
// Output:
// 200 200 200 200
// 44
// 300 does not fit in a byte
Discussion