Deep C# - Casting the Escape from Strong Typing
Written by Mike James   
Monday, 04 December 2023
Article Index
Deep C# - Casting the Escape from Strong Typing
Type Conversion
Downcast
Casting and Overriding

Type Conversion

The simplest use of a cast is to convert from one numeric type to another and this is where active representation conversion first enters the picture. When the cast is safe from the point of view of representation then the compiler will do the job implicitly for you.

For example:

int MyInt;
long MyLong;
MyLong = MyInt;

is fine as the compiler can always store a 32-bit integer in a 64-bit integer.

This is an example of a “widening” cast in the sense that the conversion is from a smaller or narrower type to a bigger or wider type. Here “bigger” and “smaller” refer both to the amount of memory the data takes to store but also, and more abstractly, the range of data that they can store. A narrower type can, in principle, always be represented by a wider type but usually only a sub-range of a wider type can be represented by a narrower type. The terminology is commonly used but isn’t precise.

The key idea is that:

A wider data type can always be used in place of a narrower data type because it can represent everything the narrower type can.

As in this case the required conversion is from a narrower to a wider type the compiler assumes that you intended to write:

MyLong = (long)MyInt;

However, if the implied cast could be unsafe then the compiler insists that you write out your intentions explicitly. For example, for a wider to a narrower cast:

MyInt = MyLong;

the compiler will, quite rightly, generate a compile-time error because you can’t always convert a 64-bit integer into a 32-bit integer.

The fact that you can’t always do it doesn’t mean that it is always wrong. If you really mean it then you can write:

MyInt = (int)MyLong;

and the compiler will perform the type conversion by generating the necessary IL (intermediate language) to convert a 32-bit integer into a 64-bit integer. Notice that there is no natural way to do this conversion and in practice C# simply moves the low 32 bits of the long value into the int.

Notice that casting only works in this way for numeric data. A beginner might suspect that if:

MyInt = (int)MyLong;

works then so should:

MyString=(string)MyInt;

but of course it doesn’t. Why it doesn’t is something we could argue about for a long time. If you are going to use casting to perform a change of representation for numeric values, why not for strings and other data-based objects? Instead, for most active type conversions, you have to use a method:

string MyString=MyInt.ToString();

There are, of course, a number of versions of the ToString() method that allow you to control the format of the conversion and this makes it more flexible than the cast syntax.

Convert

So the next question is – are there other conversion methods? The answer is yes, but you have to use the Convert class. So while you can’t write:

MyInt = MyLong.ToInt32();

you can write:

MyInt = Convert.ToInt32(MyLong);

Unlike the simple cast, the method throws an exception if the long is too big to fit into the int.

If you want to convert a single or a double to an int then things are even more confusing as there are two ways which give different answers:

double MyDouble = 2.6;
MyInt = (int)MyDouble;
MyInt = Convert.ToInt32(MyDouble);

The cast truncates to give 2 but the ToInt32 rounds to give 3.

Of course, if you want to take control of type conversion you can always use the Math class as in:

Math.Round(MyDouble);
Math.Truncate(MyDouble);

and there are overloaded methods that will give you control of how the rounding is to be performed. Notice, however, that neither method actually performs a type conversion so you will still need a cast as in:

MyInt = (int) Math.Round(MyDouble);
MyInt = (int) Math.Truncate(MyDouble);

Of course you could also use Convert.

Upcasting and Downcasting

We have already encountered the ideas of upcasting and downcasting, but it is worth going over again briefly in the light of casting in general.

Originally casting may have been about changing data representation, but today it is mostly about changing the type label on a variable, not the active conversion of representations. You can view its involvement with data format conversion of value types as an accident due to the need for some consistency. Indeed, C# modeled its casting on C++ just at the point where C++ was attempting to put its house in order by adding some constructs that removed the need to use casts at all.

Casting acquired its wider meaning when we moved to strongly typed object oriented languages that supported inheritance. This gives rise to an inheritance hierarchy which it is natural to interpret as a type hierarchy. There is a sense in which a base class is smaller than a derived class in the same way that an int is “smaller” than a long. That is you can use a long anywhere that an int can be used simply because a long can represent every number than an int can.

This notion of “x can be used anywhere y can” generalizes to the wider type hierarchy only in this case x is “derived class” and y is “base class”.

That is, as we have seen in previous chapters:

class MyClassA;
class MyClassB : MyClassA;

then

MyClassA myA = new MyClassB();

is valid and myA can be treated as if it was referencing an object of type MyClassA even though it is actually referencing an object of type MyClassB.

As MyClassB has all of the methods and properties of MyClassA this is always safe to do and so the cast is automatic – this is like a widening cast for value types but the analogy is far from perfect. If you want to make it look more like a cast, you can write:

MyClassA myA = (MyClassA) new MyClassB();

If you try this, however, you will most likely find that there is a warning that a cast is unnecessary but it does make it clear that a MyClassB is being treated as a MyClassA object, an upcast, just as an int is treated as a long in the earlier example.

Upcast

Upcasting, moving up the inheritance hierarchy, is always safe



Last Updated ( Monday, 04 December 2023 )