Summary#
Convert.* converts between types explicitly. Osy# does not convert silently where information could be lost — a
decimal does not quietly become an int, dropping the pence — so where a conversion is what you want, you write it.
Signature#
Convert.ToInt(<value>) // → int (32-bit; truncates toward zero)
Convert.ToInt64(<value>) // → long (64-bit; truncates toward zero)
Convert.ToDecimal(<value>) // → decimal
Convert.ToDouble(<value>) // → double (float64)
Convert.ToString(<value>) // → string (numbers, Guids, bools …)
Convert.ToBool(<value>) // → bool
// C#'s own spellings work too — `Convert.ToInt32` IS `Convert.ToInt`, and `Convert.ToBoolean` IS
// `Convert.ToBool`. The same arms, so they behave identically; write whichever you reach for.
Convert.ToInt32(<value>) // → int
Convert.ToBoolean(<value>) // → bool
Convert.ToInt32(<string> s, <int> fromBase) // → int — base 2, 8, 10 or 16; THROWS on text it cannot readDescription#
Convert.* or a cast?#
Both narrow, and they are not interchangeable. Between two NUMBERS, reach for the cast — (int)x — which is the
C# spelling and reads at a glance; see (int)x — casts. Reach for Convert.* when the input might not be a number
at all, or when the output is text:
(int)amount | a numeric conversion. Truncates toward zero, and fails if the value does not fit |
Convert.ToInt(s) | a parse. Reads an integer out of text, and answers 0 for text it cannot read |
Convert.ToString(v) | renders a value as text |
The difference in the failure is the reason they are separate: a cast that cannot produce a value stops, while a
parse of user-supplied text answers 0 and carries on — which is right for a form field and wrong for arithmetic.
⚠ This is where Osy# and C# differ, and it is the difference most likely to cost you. C#'s
Convert.ToInt32("abc")throwsFormatException. This answers 0 — no error, no null, a number that is wrong. Soosy lintrefusesConvert.To*over text at MUST tier (correctness-parse-that-answers-zero): say which you mean, and say it where the reader can see it.if (decimal.TryParse(entered, out var cap)) { … } else { … } // the C# spelling, and it compiles hereA numeric conversion —
Convert.ToInt(Math.Floor(x))— is untouched and behaves exactly as C# does. The lenient parse is still here and still useful; it just has to be asked for out loud.
Narrowing is explicit#
Widening happens on its own — an int is usable where a decimal is expected, because nothing is lost. Going the
other way loses the fraction, so you must say so:
int WholeUnits(decimal amount) {
return Convert.ToInt(Math.Floor(amount)); // decide the rounding, THEN narrow
}Note the Math.Floor first. Convert.ToInt truncates toward zero, which is a rounding decision — and a rounding
decision you make by accident is a rounding bug. Choose it deliberately: Math.Floor, Math.Ceiling, or
Math.Round.
Convert.ToInt64 is the same narrowing to a 64-bit long instead of a 32-bit int — reach for it when the
value can exceed ±2.1 billion (an id, a byte count, a running total of small amounts). It truncates toward zero
exactly as Convert.ToInt does, so the same "round on purpose first" rule applies. See long for what a
long is and why its range matters.
long TotalCents(decimal amount) {
return Convert.ToInt64(Math.Round(amount * 100m)); // money as whole cents, 64-bit headroom
}How do I get a double for non-money maths?#
Convert.ToDouble is the float64 front door. Reach for it when the value in hand is a decimal and the arithmetic
downstream is not money — geometry, physics, a per-frame camera. Math.Round, Math.Floor, Math.Ceiling and
Math.Truncate all answer a decimal when given one, so this is the conversion back:
double SnappedTo(decimal value, decimal step) {
return Convert.ToDouble(Math.Round(value / step) * step);
}It coerces like its siblings: text is parsed (0 for text it cannot read), null is 0, true is 1. Two
things are true of a double that are not true of an int or a decimal, and both show up here:
Convert.ToDouble("NaN") · ("Infinity") · ("-Infinity") | those are values a double HAS, so the text reads (any casing, optional sign) |
Convert.ToDouble("1e400") | +Infinity — an out-of-range magnitude is not a parse failure for this type (and "1e-400" is 0) |
⚠ A double is not money. It cannot hold 0.1 exactly, so converting a price to one and back loses the pence
silently — see Numeric types & literal suffixes for which type a value should have been in the first place.
What counts as a number#
Every Convert.To* reads text the same way, so a string that parses for one parses for all of them. The accepted
shape is:
[white space] [+ or -] digits[,digits…][.digits] [e or E [+ or -] digits] [white space]"1,000" · "12,34" · "100," | group separators are accepted, and their PLACEMENT is not checked |
" 42 " · "\t42\n" | a space, tab, newline, vertical tab, form feed or carriage return may pad it |
"1e3" · "1.5e+2" · ".5" · "1." | an exponent, a bare fraction and an empty fraction are all fine |
",100" · "1.0,5" | ✗ a leading separator, or one in the fraction |
"(5)" · "5-" · "¤5" | ✗ accounting parentheses, a trailing sign, a currency symbol |
" 42" (non-breaking space) | ✗ a non-breaking space is not white space to a number |
"0x1F" · "nope" · "" | ✗ — and the answer is 0, never an error and never NaN |
The exact same grammar runs in the browser and inside a query the database executes, so a conversion answers the same
thing wherever the expression happens to run. Convert.ToDouble reads three more forms, because a double has values
the other types do not: "NaN", "Infinity" and "-Infinity", in any casing.
An integer conversion additionally refuses a non-zero fraction — Convert.ToInt("42.9") is 0, while
Convert.ToInt("42.0") is 42. If you want the number rounded, round it: Convert.ToInt(Math.Floor(x)).
Reading hex, binary or octal#
Convert.ToInt32(s, 16) is C#'s base overload, and it behaves exactly as C#'s does: bases 2, 8, 10 and 16 only,
an optional 0x in base 16, and a value above int.MaxValue read as two's complement in bases 2/8/16 — so
"FFFFFFFF" is -1. Unlike the one-argument parse above it throws on text it cannot read (a digit outside the
base, a sign outside base 10, a space), as C# does: a hex colour or checksum with a bad digit is a fault, not a 0.
A literal base outside the four is a compile error. It runs in the browser too, with the same answers.
int Channel(string hex, int at) => Convert.ToInt32(hex.Substring(at, 2), 16);[Test]
void Base_parse_answers() {
Assert.Equal(255, Channel("ff8000", 0));
Assert.Equal(128, Channel("ff8000", 2));
Assert.Equal(-1, Convert.ToInt32("FFFFFFFF", 16));
Assert.Equal(5, Convert.ToInt32("101", 2));
Assert.Throws(() => Convert.ToInt32("fg", 16));
}A second argument that is TEXT is a different overload — the culture the number is written in,
Convert.ToInt32(s, "sv-SE") — see Culture formatting — ToString(format, culture).
How do I turn a number or a Guid into text?#
string Describe(int count, Guid id) {
return "count=" + Convert.ToString(count) + " id=" + Convert.ToString(id);
}For building a sentence out of several values, string interpolation reads better —
$"count={count}" converts for you.
Can out stand inside an expression?#
Yes — int.TryParse(s, out var n) ? n : 0 works wherever an expression does: a local's initializer, a return, an
argument, an object initializer's value, an interpolation hole. d.TryGetValue(k, out v) fills a variable that
already exists (with default(V) on a miss, as C# does), and out _ discards the value. The one position that keeps
the refusal is a LAMBDA body, whose parse runs once per element.
Definite assignment works through || and && as in C#: after
if (!int.TryParse(a, out var y) || !int.TryParse(b, out var m)) { return null; } both y and m hold numbers, and
inside if (int.TryParse(a, out var y) && int.TryParse(b, out var m)) { … } so do both.
string Quantity(string text) => $"{(int.TryParse(text, out var n) ? n : 0)} items";
int Stock(Dictionary<string, int> d, string sku) { int qty = -1; return d.TryGetValue(sku, out qty) ? qty : 0; }
bool IsNumber(string text) => int.TryParse(text, out _);Money stays decimal#
Resist converting money to int or back. If you find yourself doing it to make arithmetic work, the accumulator is
probably an int that should have been seeded 0m — see var.
In a query#
A conversion answers the same inside a query the database runs — over the app's own tables, and on a data source that takes the query — as it does in a function:
Convert.ToInt(o.Total) where the total is null | 0 — a null converts as it does in memory; Convert.ToString of one is "", Convert.ToBool false |
Convert.ToInt(o.Ratio) holding NaN, ±Infinity or 1e20 | the read fails, as C#'s Convert.ToInt32 does: a double with no whole-number value has no int |
Convert.ToBool(o.Ratio) holding NaN | true — NaN is not zero |
Convert.ToString(o.Ratio) | a double as .NET writes it: 1E+20, 1000000000000000, 1E-05 |
A data source runs a conversion only where it can mean exactly that. A double written as text, and text read as a
double, are refused on a data source's entity — convert them after .ToList(). A date written as text is sent in the
culture's own pattern; see Culture formatting — ToString(format, culture).
See also#
- (int)x — casts —
(int)x/(double)x, the cast: converting between the NUMERIC types, and failing rather than answering 0 - Numeric types & literal suffixes — the literal suffixes, and which type a bare
0is - String interpolation & format specifiers —
$"…{value}…", which converts for you - Typed locals — pinning a local's type at its declaration