C# & .NET (C# 14, .NET 10 LTS) Module 2 – Types, variables and operators
Conversions, casting and parsing
How C# converts between types: implicit widening, casts that cut or wrap, how Convert rounds, boxing, and parsing text with TryParse and a culture.
What you will learn
- Distinguish implicit, explicit and user-defined conversions, and say which ones can lose data
- Predict what a cast, Convert.ToInt32 and Math.Round return for the same double
- Parse amounts with group separators, signs and currency symbols using TryParse, NumberStyles and a culture
- Recognise when a value is boxed, and unbox it with its exact type
Before you start
What you’ll learn · Before you start
What you will learn
- Distinguish implicit, explicit and user-defined conversions, and say which ones can lose data
- Predict what a cast, Convert.ToInt32 and Math.Round return for the same double
- Parse amounts with group separators, signs and currency symbols using TryParse, NumberStyles and a culture
- Recognise when a value is boxed, and unbox it with its exact type
Before you start
On this page
(int)2.99 is 2 in C#, Convert.ToInt32(2.99) is 3, and casting an object that holds 72 to long throws
InvalidCastException, although 72 fits in a long with room to spare. Each of these conversions follows its own rule
for fractions, overflow and bad input, and the rule decides whether a fare, a count or an amount typed into a form
comes out right.
You will see which conversions C# makes by itself, what casting a double to an int drops, how Convert rounds a
half, and how parsing reads numbers from text with TryParse, NumberStyles and an explicit culture.
Widening happens by itself; narrowing needs a cast
A conversion turns a value of one type into a value of another. When every value of the source type fits in the target
type, C# converts on its own: an int becomes a long or a double in an assignment. These are
implicit conversion: A conversion that C# makes without being asked, because every value of the source type fits in the target type, such as int to long. A few of them, such as long to double, can still lose digits, never the size of the number.. When some values cannot fit, as with double to int, the
compiler asks for a cast, (int)value, an explicit conversion: A conversion written as a cast, such as (int)price, because it can lose information or fail. Unchecked, an out-of-range value is cut or clamped without an error; in a checked context it throws OverflowException. that says you
accept what may be lost. This program makes both kinds:
using System.Globalization;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// Widening: every int fits in a long and in a double, so no cast is needed.
int seats = 72;
long seatsAsLong = seats;
double seatsAsDouble = seats;
Console.WriteLine($"int {seats} -> long {seatsAsLong}, double {seatsAsDouble}");
// Also implicit, and still not exact: a double keeps about 15 to 17 digits.
long tickets = 9_007_199_254_740_993;
double ticketsAsDouble = tickets;
Console.WriteLine($"long {tickets} -> double {ticketsAsDouble:F0}");
// Narrowing needs a cast, and the cast decides what is lost.
double[] fares = [2.99, -2.99, 3e9, double.NaN];
foreach (double fare in fares)
{
Console.WriteLine($"(int){fare} = {(int)fare}");
}
long total = 3_000_000_000;
Console.WriteLine($"(int){total} = {(int)total}");
try
{
Console.WriteLine(checked((int)fares[2]));
}
catch (OverflowException)
{
Console.WriteLine($"checked((int){fares[2]}) threw OverflowException");
} Output
int 72 -> long 72, double 72 long 9007199254740993 -> double 9007199254740992 (int)2.99 = 2 (int)-2.99 = -2 (int)3000000000 = 2147483647 (int)NaN = 0 (int)3000000000 = -1294967296 checked((int)3000000000) threw OverflowException
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run widen.cs
The second line is the exception to “implicit means safe”: a long converts to a double by itself, yet 9,007,199,254,740,993 came back as 9,007,199,254,740,992, because a
double keeps about 15 to 17 significant digits. Microsoft’s reference lists this case, and int to float, as
conversions that can lose precision but never the order of magnitude. Step through what casting each double to int
did:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 2.99 arrives. The cast starts from the double 2.99, the first value of widen.cs. It is a number, so the NaN check lets it through.
- The fraction goes. A cast keeps the whole part and drops the rest, rounding toward zero. 2.99 becomes 2: nothing here rounds to the nearest whole number.
- It fits. 2 lies well inside the range of int, so it is the result. widen.cs printed (int)2.99 = 2.
- -2.99. Toward zero means up for a negative number: -2.99 becomes -2, not -3, as the second line printed.
- Three billion. 3e9 has no fraction to drop, but it is larger than int.MaxValue. Since .NET 9 the cast then gives the nearest end of the range, 2,147,483,647, on x64 and Arm64 alike.
- The same cast, checked. Inside checked(…) a value that does not fit stops the program with OverflowException instead, as the last line of widen.cs shows. Use it where a clamped number would be wrong.
- NaN. NaN, not a number, takes the first way out: the cast gives 0, without an error. A NaN that reaches a cast has to be caught before it, with double.IsNaN.
A cast to int drops the fraction, rounding toward zero, so 2.99 became 2 and -2.99 became -2. A double too large
for an int gave its largest value, 2,147,483,647, and NaN gave 0, the rule of .NET 9 and later; a long too large
kept only its low 32 bits and came out as -1,294,967,296. Inside checked(…) the out-of-range cast threw
OverflowException instead.
Checkpoint
Check: Implicit and explicit conversions
Convert rounds to the nearest whole number; a cast cuts
Cutting 2.99 to 2 is right for “how many whole boxes fit” and wrong for “the nearest whole rupee”. Four ways to turn a
double into a whole number agree on most values and differ on halves:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// The same four doubles turned into whole numbers in four ways.
double[] values = [2.5, 3.5, -2.5, 2.99];
Console.WriteLine($"{"value",6} {"(int)",6} {"Convert",8} {"Round",6} {"AwayFromZero",13}");
foreach (double v in values)
{
int cast = (int)v;
int converted = Convert.ToInt32(v);
double rounded = Math.Round(v);
double away = Math.Round(v, MidpointRounding.AwayFromZero);
Console.WriteLine($"{v,6} {cast,6} {converted,8} {rounded,6} {away,13}");
} Output
value (int) Convert Round AwayFromZero 2.5 2 2 2 3 3.5 3 4 4 4 -2.5 -2 -2 -2 -3 2.99 2 3 3 3
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run halves.cs
The cast cut every value toward zero, 2.99 included. Convert.ToInt32 rounds to the nearest int, and a value exactly
halfway goes to the even neighbour: 2.5 became 2 and 3.5 became 4, the rule that Microsoft’s reference for
Convert.ToInt32(Double) states. Math.Round without a mode does the same and returns a double; with
MidpointRounding.AwayFromZero halves go away from zero, 2.5 to 3 and -2.5 to -3, as in the lesson on decimal.
A cast where you meant rounding
(int)fare turns a fare of 2.99 into 2. When you need the nearest whole number, round first and cast the rounded
value: (int)Math.Round(fare, MidpointRounding.AwayFromZero) gives 3, and the mode says what happens to halves.
Checkpoint
Check: Casts, Convert and Math.Round
Parsing turns text into numbers, and the culture decides what a comma means
Most numbers arrive as text. Parsing reads them: int.Parse throws when the
text is not a number, int.TryParse returns false instead, and Convert.ToInt32 parses like Parse but turns
null into 0:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// Five inputs, three ways to turn text into an int.
string?[] inputs = ["42", " 42 ", "4.5", "", null];
Console.WriteLine($"{"text",6} {"int.Parse",-21} {"int.TryParse",-13} Convert.ToInt32");
foreach (string? text in inputs)
{
string shown = text is null ? "null" : $"\"{text}\"";
string parsed = Outcome(() => int.Parse(text!, CultureInfo.InvariantCulture));
bool ok = int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value);
string converted = Outcome(() => Convert.ToInt32(text, CultureInfo.InvariantCulture));
Console.WriteLine($"{shown,6} {parsed,-21} {$"{ok}, {value}",-13} {converted}");
}
// Amounts as text, read as decimals with the invariant culture and NumberStyles.Number.
Console.WriteLine();
string[] amounts = ["1,234.50", "1,23,456.50", "12,34", "1.234,50", "-250.00"];
foreach (string amount in amounts)
{
bool read = decimal.TryParse(amount, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal d);
Console.WriteLine($"{amount,-12} {(read ? d.ToString() : "not a number")}");
}
static string Outcome(Func<int> parse)
{
try
{
return parse().ToString();
}
catch (Exception e)
{
return e.GetType().Name;
}
} Output
text int.Parse int.TryParse Convert.ToInt32
"42" 42 True, 42 42
" 42 " 42 True, 42 42
"4.5" FormatException False, 0 FormatException
"" FormatException False, 0 FormatException
null ArgumentNullException False, 0 0
1,234.50 1234.50
1,23,456.50 123456.50
12,34 1234
1.234,50 not a number
-250.00 -250.00
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run parse.cs
Only TryParse never threw, and null separates the other two: int.Parse threw ArgumentNullException, while
Convert.ToInt32 returned 0, a number nobody typed. The lower half reads amounts as decimal with
NumberStyles.Number and the invariant culture: CultureInfo.InvariantCulture: a culture tied to no country that formats and parses the same way on every computer, with a dot for decimals and a comma between thousands. Use it for text that programs exchange. More in “Console input, output, arguments and exit codes”. It accepted 1,234.50 and also 1,23,456.50, because parsing accepts
a group separator anywhere between digits, and for the same reason it read 12,34, which a German writer means as
twelve point three four, as 1234 without a word. Pass the culture the text was written in, and check the shape of
text that matters.
Numbers stored as object, or as an interface they implement, are a fourth case. Storing them there is
boxing: Storing a value of a value type, such as an int, in a variable of type object or of an interface type. Getting the value back out is unboxing, and it must name exactly the type that went in., and getting them back is unboxing, which needs the exact type that went in.
Answer three questions about a value you need as an int, and the table rules out the conversions that would get it
wrong:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Conversion | A fraction, such as 2.5 | Out of range | Bad or missing input |
|---|---|---|---|
| (int)value Takes: a number | Mixed cut toward zero: 2.99 became 2 | Poor no error: 3e9 became 2,147,483,647 | Poor NaN became 0 without a word |
| Math.Round, then (int) Takes: a number | Good the mode you pass: AwayFromZero made 2.5 into 3 | Poor the cast after it decides, with no error | Poor NaN stays NaN, then the cast gives 0 |
| Convert.ToInt32 Takes: a number, text or null | Mixed nearest, halves to even: 2.5 became 2 | Good throws OverflowException | Mixed null became 0; bad text threw FormatException |
| int.Parse Takes: text | Poor "4.5" threw FormatException | Good throws OverflowException | Poor throws, and ArgumentNullException for null |
| int.TryParse Takes: text | Mixed "4.5" gave false: read it as a decimal first | Good returns false | Good returns false and never throws |
How the answers decide
- “Is the value you have a number or text?” A number, such as a double: rules out int.Parse and int.TryParse. Parse and TryParse read text. A double needs a cast, Math.Round or Convert.
- “Is the value you have a number or text?” Text, from a form, a file or an argument: rules out (int)value and Math.Round, then (int). A cast cannot turn a string into a number: text has to be parsed.
- “Should 2.5 become 3, the nearest whole number with halves going up?” Yes, the nearest whole number: rules out (int)value and Convert.ToInt32. A cast drops the fraction and Convert sends halves to the even neighbour: only Math.Round with AwayFromZero turned 2.5 into 3.
- “Is the value you have a number or text?” Text, from a form, a file or an argument and “Should 2.5 become 3, the nearest whole number with halves going up?” Yes, the nearest whole number: adds a caveat to int.Parse and int.TryParse. int.Parse and int.TryParse refuse "2.5". Read the text with decimal.TryParse first, then round it with Math.Round and the mode you need.
- “Should 2.5 become 3, the nearest whole number with halves going up?” No, I count whole units: adds a caveat to Math.Round, then (int) and Convert.ToInt32. For whole units, such as how many full boxes fit, cutting is the right answer, and rounding would count a box that is not there.
- “Can the value be wrong, missing or out of range?” Yes, it comes from outside the program: rules out int.Parse and adds a caveat to (int)value, Math.Round, then (int) and Convert.ToInt32. Input from outside can be bad text or too large. TryParse reports it without an exception; a cast reports nothing, and Convert turns null into 0.
- “Can the value be wrong, missing or out of range?” No, my own code produced it: adds a caveat to int.TryParse. Text your own code wrote in a known format can use Parse: an exception then points at a bug, not at bad input.
Key idea
Choose the conversion by what the value means: a cast for whole units, Math.Round with a mode for the nearest
number, and TryParse with a culture for text.
Checkpoint
Check: Parse, TryParse and Convert on text
Numeric casts at the edges, and conversions of your own types Pro: opens with a Pro, Premium or Ultimate pass
A numeric cast follows a rule that depends on the two types, the checked context and, for floating point, the version of .NET. The runs below reproduce the cases that catch people out, then give a type of its own conversions that keep the same promises.
In this part:
- Casts at the edges of their types, reproduced
- Conversions of your own types, implicit and explicit
2 runnable examples (C#) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Parsing real-world text: cultures, currency, dates and an import Pro: opens with a Pro, Premium or Ultimate pass
Text from people and from other programs carries the conventions of a country: separators, currency signs, the order of day and month. The runs below parse the same texts under three cultures, read money and dates, and import a file while rejecting every line it cannot trust.
In this part:
- The same text under three cultures
- Dates under three cultures, and with an exact format
- An import of grouped amounts that rejects bad lines
3 runnable examples (C#, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Boxing and unboxing, with the bytes counted Pro: opens with a Pro, Premium or Ultimate pass
Any value can be stored in a variable of type object, and getting it back out has rules of its own. The runs below
unbox values from a data row, then count the bytes that boxing allocates.
In this part:
- Unboxing needs the exact type that went in
- What boxing costs, measured
2 runnable examples (C#) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: round a reading the way you mean it
Round, then cast
Copy halves.cs to your computer and add a column that prints (int)Math.Round(v, MidpointRounding.AwayFromZero).
Predict the four numbers before you run it with dotnet run halves.cs, then add -3.5 to the values and predict
again.
Then try the questions on casts, Convert and parsing. The idea you have answered worst comes first, and each choice
explains what it gets right or wrong:
Practice that adapts to you
Practise this lesson
9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between a cast, Convert.ToInt32 and int.Parse?
A cast converts a value whose type the language already knows how to convert, such as a double to an int, and
drops the fraction: (int)2.99 is 2. Convert.ToInt32 takes many types: given a double it rounds to the nearest
whole number with halves to the even neighbour, so 2.99 gives 3 and 2.5 gives 2; given a string it parses, and it turns
null into 0. int.Parse takes only text and throws FormatException on text that is not a whole number, and
ArgumentNullException on null. For text from users or files, use int.TryParse with an explicit culture, which
returns false instead of throwing.
-
Mid to senior
A nightly job reads amounts from files sent by branches in several countries with decimal.Parse(text). One morning a branch's totals are a hundred times too large, and some of its files fail to load. What happened, and how do you fix the import and the totals already stored?
-
Mid to senior
An image filter computes a brightness as a double and stores it with (byte)value. After an upgrade of the .NET runtime, a test that checks the result for 300.7 fails, although no code changed. Explain how that can happen, and write the conversion so that it is right on every version.
-
Fresher to mid
A data layer returns each row as Dictionary<string, object>. long seats = (long)row["seats"] throws InvalidCastException although the value is 72, and a teammate wants to replace every such cast with Convert.ToInt64. Explain the exception and decide what the code should do.
Key takeaways
- Widening conversions such as
inttolonghappen by themselves, while narrowing ones such asdoubletointneed a cast, which says that something may be lost. - Casting a
doubleto anintdrops the fraction toward zero, whileConvert.ToInt32rounds to the nearest whole number with halves going to the even neighbour. int.Parsethrows on bad text,int.TryParsereturnsfalse, andConvert.ToInt32turnsnullinto 0.- Parsing accepts a group separator anywhere between digits, so pass the culture the text was written in and check the shape of text that matters.
Terms in this lesson
- boxing
- Storing a value of a value type, such as an int, in a variable of type object or of an interface type. Getting the value back out is unboxing, and it must name exactly the type that went in.
- explicit conversion
- A conversion written as a cast, such as (int)price, because it can lose information or fail. Unchecked, an out-of-range value is cut or clamped without an error; in a checked context it throws OverflowException.
- implicit conversion
- A conversion that C# makes without being asked, because every value of the source type fits in the target type, such as int to long. A few of them, such as long to double, can still lose digits, never the size of the number.
References
- Type conversions, casting, and boxing (Microsoft Learn)
- Built-in numeric conversions (Microsoft Learn)
- Floating point-to-integer conversions are saturating (.NET 9) (Microsoft Learn)
- Floating-point conversions to small integral types are saturating (.NET 11) (Microsoft Learn)
- Decimal and BigInteger floating-point conversions are correctly rounded (.NET 11) (Microsoft Learn)
- Convert.ToInt32 method (Microsoft Learn)
- User-defined explicit and implicit conversion operators (Microsoft Learn)
- Parsing numeric strings in .NET (Microsoft Learn)
- Decimal.Parse method (the NumberStyles.Number default) (Microsoft Learn)
- DateTime.ParseExact method (Microsoft Learn)
- INumberBase<TSelf>.CreateSaturating method (Microsoft Learn)
- Boxing and unboxing (Microsoft Learn)
- DefaultInterpolatedStringHandler struct (Microsoft Learn)
- What's new in the .NET 9 runtime (object stack allocation for boxes) (Microsoft Learn)
- GC.GetAllocatedBytesForCurrentThread method (Microsoft Learn)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress