Which is the best practice in C# for type casting?

At least there are two possibilities for casting, one for type checking and a combination of both called pattern matching. Each has its own purpose and it depends on the situation:

Hard cast

var myObject = (MyType)source;

You normally do that if you are absolutely sure if the given object is of that type. A situation where you use it, if you subscribed to an event handler and you cast the sender object to the correct type to work on that.

private void OnButtonClick(object sender, EventArgs e)
{
    var button = (Button)sender;

    button.Text = "Disabled";
    button.Enabled = false;
}

Soft cast

var myObject = source as MyType;

if (myObject != null)
    // Do Something

This will normally be used if you can't know if you really got this kind of type. So simply try to cast it and if it is not possible, simply give a null back. A common example would be if you have to do something only if some interface is fullfilled:

var disposable = source as IDisposable;

if(disposable != null)
    disposable.Dispose();

Also the as operator can't be used on a struct. This is simply because the operator wants to return a null in case the cast fails and a struct can never be null.

Type check

var isMyType = source is MyType;

This is rarely correctly used. This type check is only useful if you only need to know if something is of a specific type, but you don't have to use that object.

if(source is MyType)
   DoSomething();
else
   DoSomethingElse();

Pattern matching

if (source is MyType myType)
    DoSomething(myType);

Pattern matching is the latest feature within the dotnet framework that is relevant to casts. But you can also handle more complicated cases by using the switch statement and the when clause:

switch (source)
{
    case SpecialType s when s.SpecialValue > 5
        DoSomething(s);
    case AnotherType a when a.Foo == "Hello"
        SomethingElse(a);
}

I think this is a good question, that deserves a serious and detailed answer. Type casts is C# are a lot of different things actually.

Unlike C#, languages like C++ are very strict about these, so I'll use the naming there as reference. I always think it's best to understand how things work, so I'll break it all down here for you with the details. Here goes:

Dynamic casts and static casts

C# has value types and reference types. Reference types always follow an inheritance chain, starting with Object.

Basically if you do (Foo)myObject, you're actually doing a dynamic cast, and if you're doing (object)myFoo (or simply object o = myFoo) you're doing a static cast.

A dynamic cast requires you to do a type check, that is, the runtime will check if the object you are casting to will be of the type. After all, you're casting down the inheritance tree, so you might as well cast to something else completely. If this is the case, you'll end up with an InvalidCastException. Because of this, dynamic casts require runtime type information (e.g. it requires the runtime to know what object has what type).

A static cast doesn't require a type check. In this case we're casting up in the inheritance tree, so we already know that the type cast will succeed. No exception will be thrown, ever.

Value type casts are a special type of cast that converts different value types (f.ex. from float to int). I'll get into that later.

As, is, cast

In IL, the only things that are supported are castclass (cast) and isinst (as). The is operator is implemented as a as with a null check, and is nothing more than a convenient shorthand notation for the combination of them both. In C#, you could write is as: (myObject as MyFoo) != null.

as simply checks if an object is of a specific type and returns null if it's not. For the static cast case, we can determine this compile-time, for the dynamic cast case we have to check this at runtime.

(...) casts again check if the type is correct, and throw an exception if it's not. It's basically the same as as, but with a throw instead of a null result. This might make you wonder why as is not implemented as an exception handler -- well, that's probably because exceptions are relatively slow.

Boxing

A special type of cast happens when you box a value type into an object. What basically happens is that the .NET runtime copies your value type on the heap (with some type information) and returns the address as a reference type. In other words: it converts a value type to a reference type.

This happens when you have code like this:

int n = 5;
object o = n; // boxes n
int m = (int)o; // unboxes o

Unboxing requires you to specify a type. During the unboxing operation, the type is checked (like the dynamic cast case, but it's much simpler because the inheritance chain of a value type is trivial) and if the type matches, the value is copied back on the stack.

You might expect value type casts to be implicit for boxing -- well, because of the above they're not. The only unboxing operation that's allowed, is the unboxing to the exact value type. In other words:

sbyte m2 = (sbyte)o; // throws an error

Value type casts

If you're casting a float to an int, you're basically converting the value. For the basic types (IntPtr, (u)int 8/16/32/64, float, double) these conversions are pre-defined in IL as conv_* instructions, which are the equivalent of bit casts (int8 -> int16), truncation (int16 -> int8), and conversion (float -> int32).

There are some funny things going on here by the ways. The runtime seems to work on multitudes of 32-bit values on the stack, so you need conversions even on places where you wouldn't expect them. For example, consider:

sbyte sum = (sbyte)(sbyte1 + sbyte2); // requires a cast. Return type is int32!
int sum = int1 + int2; // no cast required, return type is int32.

Sign extension might be tricky to wrap your head around. Computers store signed integer values as 1-complements. In hex notation, int8, this means that the value -1 is 0xFF. So what happens if we cast it to an int32? Again, the 1-complement value of -1 is 0xFFFFFFFF - so we need to propagate the most significant bit to the rest of 'added' bits. If we're doing unsigned extensions, we need to propagate zero's.

To illustrate this point, here's a simple test case:

byte b1 = 0xFF;
sbyte b2 = (sbyte)b1;
Console.WriteLine((int)b1);
Console.WriteLine((int)b2);
Console.ReadLine();

The first cast to int is here zero extended, the second cast to int is sign extended. You also might want to play with the "x8" format string to get the hex output.

For the exact difference between bit casts, truncation and conversion, I refer to the LLVM documentation that explains the differences. Look for sext/zext/bitcast/fptosi and all the variants.

Implicit type conversion

One other category remains, and that's the conversion operators. MSDN details how you can overload the conversion operators. Basically what you can do is implement your own conversion, by overloading an operator. If you want the user to explicitly specify that you intend to cast, you add the explicit keyword; if you want implicit conversions to happen automagically, you add implicit. Basically you'll get:

public static implicit operator byte(Digit d)  // implicit digit to byte conversion operator
{
    return d.value;  // implicit conversion
}

... after which you can do stuff like

Digit d = new Digit(123);
byte b = d;

Best practices

First off, understand the differences, which means implementing small test programs until you understand the distinction between all of the above. There's no surrogate for understanding How Stuff Works.

Then, I'd stick to these practices:

  • The shorthands are there for a reason. Use the notation that's the shortest, it's probably the best one.
  • Don't use casts for static casts; only use casts for dynamic casts.
  • Only use boxing if you need it. The details of this go well beyond this answer; basically what I'm saying is: use the correct type, don't wrap everything.
  • Notice compiler warnings about implicit conversions (f.ex. unsigned/signed) and always resolve them with explicit casts. You don't want to get surprises with strange values due to sign/zero extension.
  • In my opinion, unless you know exactly what you're doing, it's best to simply avoid the implicit/explicit conversion -- a simple method call is usually better. The reason for this is that you might end up with an exception on the loose, that you didn't see coming.

Tags:

C#

.Net