As a statically typed language, C# imposes some limitations on variable types. After we declare a variable of a given type, we can only assign values of that type to it. For example, if we declare a variable of a textual type, we cannot assign values of a numerical type to it. There are, however, situations that this is exactly what we need. Fortunately, conversions from one type to another are possible. In this article, we will learn how to perform different types of conversions in C#.

To download the source code for this article, you can visit our GitHub repository.

So, let’s get started.

Types of Conversions in C#

In some cases, conversions even occur unnoticed. Such conversions are called implicit conversions. They occur when there is no data loss. This is the case, for example, when we convert from smaller to larger integral types or from derived classes to base classes. Implicit conversions occur automatically and they don’t require any special syntax.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Sometimes, however, we want to convert from larger integral types to smaller ones. Or, we want to convert to numeric types with less precision. Sometimes we need to convert doubles to ints, strings to dates, or booleans to strings. And we could go on and on like that. What all these conversions have in common, is that there may be some data loss or the types may be so incompatible that the conversions just fail. Fortunately, such conversions do not occur automatically and we must use special syntax. We call them explicit conversions.

Explicit conversions can be implemented in a couple of different ways. We can use a cast operator or special helper classes. The special classes include System.BitConverter and System.Convert. With numeric types, such as int or double, we can also use the Parse() method. 

If you want to learn more about type conversions, you should check out our article Type Conversions in C#.

In this article, we’ll focus on the Convert class defined in the System namespace.

The Convert Class

The Convert class is a static class with methods to convert every base data type to every other base data type in .NET. It doesn’t mean all conversions are successful, which we are going to see later in the article. The base types supported by the class are Boolean, Char, SByte, Byte, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double, Decimal, DateTime, and String.

Besides base types, the class includes methods to perform other conversions. In particular, we can convert custom objects to base types, which we are also going to discuss in the article. The Convert class also supports base64 encoding. 

Let’s have a look at some examples to get the feel of the syntax.

Syntax

Each method in the Convert class is overloaded. The ToDouble() method, for example, has 18 overloaded versions. The ToString() method has 36. Each version takes one or two parameters. The first parameter is always of the type we’re converting from. Some examples of the overloaded ToDouble() method include:

Convert.ToDouble(bool value);
Convert.ToDouble(char value);
Convert.ToDouble(DateTime value);
Convert.ToDouble(int value);

It works the same for the other methods. Later in the article, we’ll see overloads that take two parameters. For now, however, let’s stick to the most basic form. 

We just saw the signatures of one of the methods. Now, let’s exercise a few of them:

public static string ConvertIntToOtherTypes(int value)
{
    var intToDouble = Convert.ToDouble(value);
    var intToBool = Convert.ToBoolean(value);
    var intToString = Convert.ToString(value);

    var intToDoubleText = $"{value} ({value.GetType()}) -> {intToDouble} ({intToDouble.GetType()})";
    var intToBoolText = $"{value} ({value.GetType()}) -> {intToBool} ({intToBool.GetType()})";
    var intToStringText = $"{value} ({value.GetType()}) -> {intToString} ({intToString.GetType()})";

    var output = $"{intToDoubleText}\n{intToBoolText}\n{intToStringText}";

    return output;        
}

Here we’re using three methods, ToDouble(), ToBoolean() and ToString() with one parameter of type int. If we call the method with the parameter 4, it returns the values and types before and after the conversion:

4 (System.Int32) -> 4 (System.Double)
4 (System.Int32) -> True (System.Boolean)
4 (System.Int32) -> 4 (System.String)

These three conversions turned out successful, but this isn’t always the case.

Possible Conversion Outcomes

Types in C# are related to one another to a varying extent. For example, int and short are more related to each other than DateTime and bool. Because of this, not all conversions are equally feasible. Some conversions are pretty straightforward. Some are completely impossible. Yet other conversions are possible, but cause loss of information, so we must be careful with them. 

In this article, we’re going to explore quite a few methods, so let’s create a convenient method to make our lives easier:

public static string MakeConversion<T1, T2>(Func<T1, T2> convert, T1 value)
{
    try
    {
        T2 result = convert(value);

        var output = $"{value} ({value?.GetType()}) -> {result} ({result?.GetType()})";

        if (value?.GetType() == result?.GetType())
        {
            output += "\nNo conversion, same types";
        }

        return output;
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

This is a generic method that takes two parameters. The first parameter is a delegate that will point to the method from the Convert class that we want to test. The second argument is of the type we want to convert from. If the conversion is successful, the MakeConversion method will return a message similar to the ones above. Otherwise, it will return an error message. 

We’re going to use this method to get a clear and meaningful outcome:

Utilities.MakeConversion(Convert.ToString, true);

This calls the Convert.ToString(bool) method and returns a message with the result of the conversion.

Let’s now explore all the possible conversion outcomes.

Successful Conversion

Conversions that work without any loss of data are always successful. As an example, let’s convert an int to a long. The resulting type has a wider range than the original one, so this is a widening conversion:

Utilities.MakeConversion(Convert.ToInt64, 1500);

Widening conversions always succeed:

1500 (System.Int32) -> 1500 (System.Int64)

In the opposite direction, though, the conversion will be a narrowing one, because int has a narrower range than long:

Utilities.MakeConversion(Convert.ToInt32, 1500L);

This conversion is only successful because we’re using a long type that is less than the maximum value of int:

1500 (System.Int64) -> 1500 (System.Int32)

But what happens if we try to convert a long that is beyond the range of the int type? Let’s find out.

Overflow Exception

An OverflowException is thrown if we try to convert a type with a wider range to a type with a narrower range. A conversion from a large long to an int is a good example:

Utilities.MakeConversion(Convert.ToInt32, 10_000_000_000L);

Such conversions are impossible because they would result in a loss of data:

Value was either too large or too small for an Int32.

The two types, long and int, differ in range, but they are both numeric types. Sometimes, however, the types we’re trying to convert between are completely incompatible. Let’s see what happens then.

Invalid Cast Exception

Some conversions are just not supported. For example, we can’t convert from char to bool, float, double, decimal or DateTime. We can’t convert in the opposite direction, either. Let’s see what happens if we try to convert from char to bool:

Utilities.MakeConversion(Convert.ToBoolean, 'a');

An InvalidCastException is thrown:

Invalid cast from 'Char' to 'Boolean'.

All conversions from and to DateTime result in an InvalidCastException, unless the other type is a string.

Speaking of strings, we can convert any type to string. But does it always work the other way around? Let’s check it out.

Format Exception

It turns out that we can only convert strings to other types if they are formatted correctly. For example, we can’t convert string to char if the former consists of more than a single character:

Utilities.MakeConversion(Convert.ToChar, "hey");

This results in a FormatException:

String must be exactly one character long.

Also, we can’t convert string to a numeric type if it contains invalid characters:

Utilities.MakeConversion(Convert.ToInt32, "125s");

We get a clear message:

The input string '125s' was not in a correct format.

We also get a FormatException if we try to convert string to bool. This conversion is only successful if the string equals Boolean.TrueString or Boolean.FalseString using StringComparison.OrdinalIgnoreCase.

Also, just like with numeric types, we can’t convert string to DateTime if its format is incorrect.

In the examples above, we either successfully converted from one type to another or the conversion failed. But sometimes there is no conversion at all.

No Conversion

Let’s try to convert a type to itself:

Utilities.MakeConversion(Convert.ToInt32, 10);

Here, we’re trying to convert int to int. In such cases, there is no conversion, and the original type is returned:

10 (System.Int32) -> 10 (System.Int32)
No conversion, same types

We’ve exhausted all the possible conversion outcomes. We saw that not all conversions are possible. Sometimes there would be a loss of data. This is the case with numeric types. Let’s discuss this aspect in more detail. 

Loss of Data and Loss of Precision

We know that conversions with a loss of data throw an OverflowException. This is what happens, for example, if we try to convert a very large double to a float. This isn’t the case, however, if there is only a loss of precision:

Utilities.MakeConversion(Convert.ToSingle, 246.123456789);

As double type supports more digits of precision than float, some of the least significant digits are lost:

246.123456789 (System.Double) -> 246.12346 (System.Single)

The last digit of the resulting float is rounded.

We also lose precision when we convert from a floating-point type to an integral type. Let’s try to convert float to int:

MakeConversion(Convert.ToInt32, 56.91f);

We lose all digits after the decimal point and the number is rounded:

56.91 (System.Single) -> 57 (System.Int32)

Here we saw some examples of conversions between numbers. There are many numeric types and the Convert class supports a great number of conversions between them. Let’s have a closer look at some examples.

Conversions Between Numeric Types

The Convert class supports conversions between any two numeric types, including signed and unsigned types:

Utilities.MakeConversion(Convert.ToByte, 125);
Utilities.MakeConversion(Convert.ToDecimal, 125); 
Utilities.MakeConversion(Convert.ToByte, 125u);
Utilities.MakeConversion(Convert.ToInt16, 125);
Utilities.MakeConversion(Convert.ToInt32, 125u);
Utilities.MakeConversion(Convert.ToSByte, 125L);
Utilities.MakeConversion(Convert.ToUInt32, 125);

All the conversions are successful. This is because the number 125 is small enough to fit in the range of all the types used in the example:

125 (System.Int32) -> 125 (System.Byte)
125 (System.Int32) -> 125 (System.Decimal)
125 (System.UInt32) -> 125 (System.Byte)
125 (System.Int32) -> 125 (System.Int16)
125 (System.UInt32) -> 125 (System.Int32)
125 (System.Int64) -> 125 (System.SByte)
125 (System.Int32) -> 125 (System.UInt32)

We must remember, though, that with larger numbers the OverflowException may be thrown.

Conversions between numeric types are very common. No less common are conversions to strings.

Conversions to Strings 

Let’s convert some common types to a string:

Utilities.MakeConversion(Convert.ToString, true);
Utilities.MakeConversion(Convert.ToString, 4.56f); 
Utilities.MakeConversion(Convert.ToString, 'c');
Utilities.MakeConversion(Convert.ToString, new DateTime());
Utilities.MakeConversion(Convert.ToString, new Random());

All these conversions are successful:

True (System.Boolean) -> True (System.String)
4.56 (System.Single) -> 4.56 (System.String)
c (System.Char) -> c (System.String)
1/1/0001 12:00:00 AM (System.DateTime) -> 1/1/0001 12:00:00 AM (System.String)
System.Random (System.Random) -> System.Random (System.String)

In the last example, we’re converting an object of type Random to string. The result of this conversion is the type information as a string.

We can convert any object to string, even an object we define ourselves. We can also convert binary data to string. Let’s have a look at how this works. 

Base64 Encoding

We can use the Convert class for base64 encoding. There are methods to convert an array of bytes to and from string. We can also convert to and from an array of base-64 Unicode characters. As an example let’s take the ToBase64String() method. It converts an array of 8-bit unsigned integers to its string representation encoded as base-64 digits:

byte[] bytes = [5, 10, 15, 20, 25, 30];
Utilities.MakeConversion(Convert.ToBase64String, bytes);

This example outputs a base-64 string:

System.Byte[] (System.Byte[]) -> BQoPFBke (System.String)

We’re not going to discuss this topic in detail here. If you want to read more, check out our article Base64 Encode and Decode in C#.

We’ve seen examples of conversions between strings and numbers. But these don’t have to be decimal numbers.

Conversions With Non-decimal Numbers

In the examples above we’ve been using only decimal integral numbers. What about binary, octal, and hexadecimal numbers? Well, some methods enable us to convert integral values to their non-decimal string representations. There are also methods to convert non-decimal string representations to integral values. Let’s start with the first group of methods.

Conversions to Non-decimal String Representations

The methods converting integral values to non-decimal string representations take two parameters. The first parameter is the integral number we want to convert. It may be of any signed integral type (byte, short, int, long). The second parameter is the base. The base is always of type int and its value is 2, 8, 10, or 16. For example, to convert the number 17 to its binary string representation, we pass the integer and base 2 as arguments:

Convert.ToString(17, 2);

The method returns the binary string representation of the number. To make it clear, let’s create a new method:

public static string ConvertToStringWithBase(int number, int baseValue)
{
    try
    {
        var numberRepresentation = Convert.ToString(number, baseValue);
        return $"{number} (in base 10) -> {numberRepresentation} (in base {baseValue})";
    }
    catch 
    {
        return "Wrong base (must be 2, 8, 10 or 16)";
    }
}

This method just calls the ToString(number, baseValue) method and returns a meaningful message. Let’s try it out with the four allowed bases and an incorrect one:

Utilities.ConvertToStringWithBase(100, 2);
Utilities.ConvertToStringWithBase(100, 8);
Utilities.ConvertToStringWithBase(100, 10);
Utilities.ConvertToStringWithBase(100, 16);
Utilities.ConvertToStringWithBase(100, 3);

We get the string representations of the number 100 in base 2, base 8, base 10, and base 16. The last conversion is impossible due to an incorrect base:

100 (in base 10) -> 1100100 (in base 2)
100 (in base 10) -> 144 (in base 8)
100 (in base 10) -> 100 (in base 10)
100 (in base 10) -> 64 (in base 16)
Wrong base (must be 2, 8, 10 or 16)

Conversions from Non-decimal String Representations

We can also convert in the opposite direction, so from non-decimal string representations to numbers. To do that, we use methods like ToByte(), ToInt16(), ToInt32() or ToInt64(). Signed bytes and unsigned integers are also supported. For the latter, we should use methods like ToUInt16() and so on.

These methods take two parameters, too. The first parameter is the string representation of the number. The second parameter is the base. Again, let’s create a convenient method to present the outcome in a readable way:

public static string ConvertFromStringWithBase(string numberRepresentation, int baseValue)
{
    try
    {
        var number = Convert.ToInt32(numberRepresentation, baseValue);
        return $"{numberRepresentation} (in base {baseValue}) -> {number} (in base 10)";
    }
    catch
    {
        return "Wrong base (must be 2, 8, 10 or 16)";
    }
}

And let’s see some examples:

Utilities.ConvertFromStringWithBase("1010", 2);
Utilities.ConvertFromStringWithBase("34", 8);
Utilities.ConvertFromStringWithBase("51", 10);
Utilities.ConvertFromStringWithBase("3A", 16);
Utilities.ConvertFromStringWithBase("65", 5);

We get the integral values of the string representations. Naturally, there is an incorrect base in the last example:

1010 (in base 2) -> 10 (in base 10)
34 (in base 8) -> 28 (in base 10)
51 (in base 10) -> 51 (in base 10)
3A (in base 16) -> 58 (in base 10)
Wrong base (must be 2, 8, 10 or 16)

We’ve seen quite a few conversions between numbers and strings. But there are lots of other conversions, like conversions to booleans. Let’s have a look at them next.

Conversions to Booleans

The Convert class supports many conversions to booleans, although not all are supported. The exceptions are char and DateTime. Conversions between these two types always result in an InvalidCastException. 

Conversions from numeric types to booleans work fine. If the value is zero, it’s converted to false. Otherwise, it’s converted to true:

Utilities.MakeConversion(Convert.ToBoolean, 5);
Utilities.MakeConversion(Convert.ToBoolean, 0);
Utilities.MakeConversion(Convert.ToBoolean, -19.875);
Utilities.MakeConversion(Convert.ToBoolean, 4.65f);
Utilities.MakeConversion(Convert.ToBoolean, 170.54m);

All numbers in the examples above are converted to true except the second one:

5 (System.Int32) -> True (System.Boolean)
0 (System.Int32) -> False (System.Boolean)
-19.875 (System.Double) -> True (System.Boolean)
4.65 (System.Single) -> True (System.Boolean)
170.54 (System.Decimal) -> True (System.Boolean)

Apart from numeric types, we can convert objects that implement the IConvertible interface to bool. We’re going to discuss such objects in a moment.

We can also convert strings that contain the value of TrueString or FalseString:

Utilities.MakeConversion(Convert.ToBoolean, "True");
Utilities.MakeConversion(Convert.ToBoolean, "false");

The string representations of the logical values can start with an uppercase or lowercase letter:

True (System.String) -> True (System.Boolean)
false (System.String) -> False (System.Boolean)

We know how to convert numeric types to booleans. But we can also convert in the opposite direction. And there are lots of other interesting conversions between numeric and non-numeric types.

Conversions Between Numeric and Non-numeric Types

Conversions between numeric and non-numeric types are pretty common. We’ve already seen examples of such conversions. Let’s have a look at some more. In particular, let’s explore conversions between numeric types on one side and booleans, strings and chars on the other.

Conversions from Booleans to Numeric Types

So, we can convert booleans to integers:

Utilities.MakeConversion(Convert.ToInt32, true);
Utilities.MakeConversion(Convert.ToInt32, false);

false is converted to 0, true to 1:

True (System.Boolean) -> 1 (System.Int32)
False (System.Boolean) -> 0 (System.Int32)

This conversion works the same for any other integral or floating-point numeric type.

Conversions from Strings to Numeric Types

If a string is formatted correctly, we can convert it to a numeric type. Let’s check out a conversion to int and another to double:

Utilities.MakeConversion(Convert.ToInt32, "128");
Utilities.MakeConversion(Convert.ToDouble, "1.83e3");

They both work as expected:

128 (System.String) -> 128 (System.Int32)
1.83e3 (System.String) -> 1830 (System.Double)

It’s also possible to convert strings to other numeric types, provided they’re correctly formatted. 

Conversions between Numeric Types and Unicode Characters

We can convert chars to integral types and integral types to chars. It’s not possible to convert between chars and non-integral types. 

We use the ToChar() method to convert an integer to its corresponding Unicode character:

Utilities.MakeConversion(Convert.ToChar, 65);
Utilities.MakeConversion(Convert.ToChar, 41);

In these two examples, we pass an int to the method, so we’re converting a 32-bit signed integer to its corresponding Unicode character:

65 (System.Int32) -> A (System.Char)
41 (System.Int32) -> ) (System.Char)

We can also pass other integral types, like long, byte, or ushort, to mention just a few.

To convert a Unicode character to its corresponding integer, we use methods like ToInt32(), ToInt64() or ToUInt16() and so on:

Utilities.MakeConversion(Convert.ToInt32, '&');

This particular Unicode character corresponds to the value 38:

& (System.Char) -> 38 (System.Int32)

Using the ToInt32() method, we get a 32-bit signed integer. Using the other methods, we would get other integral types.

The examples above demonstrate that conversions between numeric and non-numeric types are pretty common. Earlier examples show that conversions to strings and booleans are not uncommon. But there are types with a very limited number of possible conversions. An example is DateTime. Let’s have a look at possible conversions with this type.

DateTime Conversions

Using the Conversion class we can convert the DateTime type only to a string, and that’s it. Similarly, we can only convert to DateTime from a string. All other conversions result in an InvalidCastException. 

Conversion from DateTime to string is pretty straightforward:

Utilities.MakeConversion(Convert.ToString, new DateTime(2023, 11, 27, 9, 45, 28));

What we get is the string representation of the date and time:

11/27/2023 9:45:28 AM (System.DateTime) -> 11/27/2023 9:45:28 AM (System.String)

For a conversion in the opposite direction to work, the string must be in the correct format:

Utilities.MakeConversion(Convert.ToDateTime, "8/2/2003 12:00:00 AM");
Utilities.MakeConversion(Convert.ToDateTime, "02 June 1985 3:20:17 AM");

We can see two possible string formats. The conversion to DateTime works just fine:

8/2/2003 12:00:00 AM (System.String) -> 8/2/2003 12:00:00 AM (System.DateTime)
02 June 1985 3:20:17 AM (System.String) -> 6/2/1985 3:20:17 AM (System.DateTime)

The way we write dates and times varies from country to country. Actually, not just dates and times, but even numbers have different formatting. To address this issue, we can add culture-specific formatting information to the methods defined in the Convert class. Let’s have a look at how to do that.

Culture-Specific Formatting Information

Sometimes it’s necessary to use culture-specific formatting. This is when the IFormatProvider interface comes in handy. All base-type conversion methods have overloaded versions that take two parameters. The first parameter is of the type we’re converting from. The second parameter is of type IFormatProvider:

Convert.ToDateTime(String, IFormatProvider)

However, in most cases, the second parameter is ignored. It’s useful only when we convert to DateTime or numeric types.

Converting to DateTime with IFormatProvider

For the next two examples, we first need to set the default thread culture to en-USto ensure the current culture for our application is set to  US. If you are already running under a US culture, you can skip this step:

var culture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

With that step behind us, we are ready to jump into the examples.

Let’s start by converting a string representation of a date defined under the Austrian (de-AT) culture to a DateTime:

CultureInfo cultureDE = new("de-AT");
string deDate = "15/2/2021";

Console.WriteLine(
    $"Austrian culture: {deDate} --- US culture: {Convert.ToDateTime(deDate, cultureDE).ToShortDateString()}");

The difference between the US and Austrian cultures is that the month and the day of the date are in a different order:

Austrian culture: 15/2/2021 --- US culture: 2/15/2021

Converting to Numeric Types with IFormatProvider

Numeric formats across cultures differ too. For example, we use different decimal separators in the USA and Austria:

string deNumber = "280,99";

Console.WriteLine(
    $"Austrian culture: {deNumber} --- US culture: {Convert.ToDecimal(deNumber, cultureDE)}");

The American culture uses a point as the decimal separator, whereas the Austrian culture uses a comma:

Austrian culture: 280,99 --- US culture: 280.99

Although there are so many conversion methods, they don’t exhaust all the possibilities. What if we want to convert a custom object to a base type? Well, this is also possible.

Conversions from Custom Objects to Base Types

The Convert class contains methods to convert between base types. But we can also convert from any custom type to any base type. This is possible if the custom type implements the IConvertible interface. This interface defines methods that convert the implementing type to other types, such as ToByte(), ToChar(), ToInt32(), etc. If a particular conversion is not supported, it should throw an InvalidCastException. 

To see that in action, let’s define a custom type that implements the interface. The Note class can be used to represent a musical note. In our example, we can convert a Note object to any base type except DateTime:

public class Note : IConvertible
{
    private readonly char _tone;
    private static readonly char[] _tones = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];

    public Note(char tone) => _tone = tone;

    public bool ToBoolean(IFormatProvider? provider) => _tones.Contains(_tone);

    public char ToChar(IFormatProvider? provider) => _tone;

    public DateTime ToDateTime(IFormatProvider? provider)
    {
        throw new InvalidCastException();
    }

    public long ToInt64(IFormatProvider? provider) => Convert.ToInt64(Array.FindIndex(_tones, t => t == _tone) + 1);
  
    public string ToString(IFormatProvider? provider) => _tone.ToString();
    ...
}

This is not the complete implementation of the interface. But it will do for our purposes.

We can now convert Note to string, bool, int, long, double, and so on:

Note note = new('C');
Utilities.MakeConversion(Convert.ToBoolean, note);
Utilities.MakeConversion(Convert.ToInt64, note);
Utilities.MakeConversion(Convert.ToChar, note);

Here we’re defining a Note object that represents the C tone. Let’s examine the conversion:

ConversionsInCSharp.Note (ConversionsInCSharp.Note) -> True (System.Boolean)
ConversionsInCSharp.Note (ConversionsInCSharp.Note) -> 3 (System.Int64)
ConversionsInCSharp.Note (ConversionsInCSharp.Note) -> C (System.Char)

If we look at the implementation of the methods in the Note class, we see that the conversions work as expected.

Conclusion

In this article, we’ve covered different types of conversions. We took a look at the different options for conversion that exist within C#. We learned about both implicit and explicit conversions. From there we moved on to exploring the Convert class, which enables us to convert any base type to any other base type. It also supports converting from custom types to base types. 

The relations between types vary. Some are closely related, like the numeric types, and thus relatively easy to convert. Others, not so closely related, are a bit more challenging and may be impossible to convert. The main thing we have to remember when converting is that conversions sometimes result in a loss of data or precision and may be affected by culture-specific information.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!