Skip to main content
ANVISoftware Solutions
Lesson 11 of 62Beginner15 min

Type Conversion

By the end of this lesson

Convert between types safely using casts, Convert and TryParse.

Values keep arriving in the wrong type. A web form hands you text, a database hands you a long, an average comes back as a double when the report needs a decimal.

C# will not convert for you when something could be lost. Beyond that you have four tools, and they differ in exactly one important way: what happens when the conversion cannot work.

The four tools, and what each does on failure:

Implicit conversion
Happens on its own when no information can be lost, such as int to long or int to decimal. You write nothing, and it cannot fail.
Cast
(int)someDouble. You are telling the compiler you accept the loss. Fractions are cut off, and an out-of-range whole number wraps without complaint.
The Convert class
Convert.ToInt32(value). Rounds rather than truncating, and accepts many source types. Throws when the input makes no sense.
Parse and TryParse
For turning text into a number, date or bool. Parse throws on bad input; TryParse reports failure as false and never throws.
Implicit conversions and casts
C#
int unitsShipped = 1450;

long totalUnits = unitsShipped;       // implicit — a long holds every int
decimal asDecimal = unitsShipped;     // implicit
double asDouble = unitsShipped;       // implicit

double measured = 12.87;
int truncated = (int)measured;        // 12 — the fraction is cut off
int negative = (int)-12.87;           // -12 — cut off towards zero, not down

long fromDatabase = 3_000_000_000;
int wrapped = (int)fromDatabase;      // -1294967296, with no warning

Console.WriteLine($"{truncated} {negative} {wrapped}");
  • Widening conversions are implicit because the target type can hold every possible source value, so there is nothing to decide.
  • A cast to int truncates towards zero. It does not round: 12.87 becomes 12, and -12.87 becomes -12 rather than -13.
  • A cast between whole-number types that does not fit wraps, exactly like overflow. Wrap checked(...) around it if you would rather be told.
  • The underscores in 3_000_000_000 are digit separators. They are ignored by the compiler and exist only so a person can read the number.
Casting truncates; Convert rounds
C#
Console.WriteLine((int)2.5);               // 2  — truncated
Console.WriteLine((int)3.9);               // 3  — truncated
Console.WriteLine(Convert.ToInt32(2.5));   // 2  — rounded to the nearest even
Console.WriteLine(Convert.ToInt32(3.5));   // 4  — rounded to the nearest even

Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));   // 3
  • A cast discards the fraction. Convert rounds to the nearest whole number, which is a different answer for anything above .5.
  • On an exact half, Convert rounds to the nearest even number, so 2.5 gives 2 and 3.5 gives 4. That is deliberate: always rounding halves upwards biases a long column of figures upwards.
  • It is also not what most people expect. When the business rule is "a half rounds up", write it out with Math.Round and MidpointRounding.AwayFromZero so the intent is on the page.

Text into numbers — the conversion you will write most

C#
Console.Write("Quantity ordered: ");
string? entered = Console.ReadLine();

if (int.TryParse(entered, out int quantity) && quantity > 0)
{
    Console.WriteLine($"Ordering {quantity} units.");
}
else
{
    Console.WriteLine("Enter a whole number greater than zero.");
}
  • TryParse returns true or false instead of throwing, and fills in the out variable when it succeeds.
  • out int quantity declares the variable inside the same statement, so there is no line above it holding a placeholder value.
  • When parsing fails, quantity is 0. Do not read it in the failure branch — a zero there means "no answer", not "zero units".
  • && stops at the first false, so quantity > 0 is only reached when parsing worked.

Parse and TryParse differ only in how they report failure:

 int.Parseint.TryParse
Text that is not a numberThrows FormatExceptionReturns false
null inputThrows ArgumentNullExceptionReturns false
Number too large for the typeThrows OverflowExceptionReturns false
Where the result comes fromThe return valueThe out parameter, meaningful only when it returned true
Reach for it whenThe value is already validated, so failure means a bugThe value came from a person, a file, or another system
Parsing a value that crossed a system boundary
C#
using System.Globalization;

string fromApi = "1240.50";

// Explicit about the format the sender used.
bool parsed = decimal.TryParse(
    fromApi,
    NumberStyles.Number,
    CultureInfo.InvariantCulture,
    out decimal amount);

Console.WriteLine($"{parsed}: {amount}");   // True: 1240.50

// Uses whatever culture the machine is configured for.
decimal.TryParse(fromApi, out decimal ambient);
Console.WriteLine(ambient);
  • The second call reads the machine's regional settings. On a server configured for Germany the dot is a thousands separator, so "1240.50" parses as 124050 — a value a thousand times too large, with no exception to tell you.
  • InvariantCulture pins the format so the same text gives the same number on every machine.
  • The rule worth keeping: pass an explicit culture for anything machine-to-machine, and leave culture-aware parsing for text a person typed in their own locale.
Converting between reference types
C#
object stored = new Employee("Priya Sharma");

if (stored is Employee employee)
{
    Console.WriteLine(employee.Name);
}

Employee? maybe = stored as Employee;   // null instead of throwing
if (maybe is not null)
{
    Console.WriteLine(maybe.Name);
}

// Invoice invoice = (Invoice)stored;   // InvalidCastException at run time

public class Employee
{
    public Employee(string name)
    {
        Name = name;
    }

    public string Name { get; }
}
  • is with a variable name tests the type and gives you a typed variable in one step. This is the form to reach for first.
  • as returns null when the conversion is not possible, which suits code that is going to check for null anyway.
  • A plain cast throws InvalidCastException. Use it when the wrong type would be a bug you want reported loudly rather than handled.

Summary

  • Implicit conversions happen only where nothing can be lost
  • A cast truncates towards zero and can wrap silently between whole-number types
  • Convert rounds, and sends an exact half to the nearest even number
  • TryParse turns bad text into false rather than an exception — use it for anything external
  • Pass an explicit culture when parsing numbers or dates that crossed a system boundary

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Ask for a quantity and a unit price as text, convert both, and print the line total. Give a different message for an invalid quantity and an invalid price, so the person knows which field to fix.

Treat the price as though it arrived from another system, formatted with a dot as the decimal separator.

Show solution

Two separate TryParse checks, because one shared "invalid input" message forces the user to guess which field was wrong.

The price uses InvariantCulture so the dot is read as a decimal point regardless of the machine's regional settings. The quantity does not need it — a whole number has no separator to misread.

The multiplication mixes int and decimal, which is fine: int converts to decimal implicitly because nothing can be lost.

C#
using System.Globalization;

Console.Write("Quantity: ");
string? quantityText = Console.ReadLine();

Console.Write("Unit price: ");
string? priceText = Console.ReadLine();

if (!int.TryParse(quantityText, out int quantity) || quantity <= 0)
{
    Console.WriteLine("Quantity must be a whole number above zero.");
}
else if (!decimal.TryParse(
    priceText, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal unitPrice))
{
    Console.WriteLine("Unit price must be a number, for example 12.50.");
}
else
{
    decimal lineTotal = quantity * unitPrice;
    Console.WriteLine($"Line total: {lineTotal:N2}");
}

Think about it

Think about it

Convert.ToInt32(2.5) gives 2, and Convert.ToInt32(3.5) gives 4. Both inputs are exactly half way.

Why would a numeric library choose that rule rather than always rounding a half upwards?

Show solution

Always rounding halves upwards adds a small bias in one direction. Over a column of thousands of figures the total drifts high, and in accounting that drift is a real discrepancy somebody has to explain.

Rounding to the nearest even number sends halves up and down in roughly equal measure, so the errors cancel out across a large set.

The trade is predictability for individual values. When a rule says a half rounds up — which many invoicing rules do — state it with Math.Round and MidpointRounding.AwayFromZero rather than relying on the default.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

Given double measured = 4.7;, what do (int)measured and Convert.ToInt32(measured) give?

Saved in this browser only.