Casting and Type Checking — is, as, and Pattern Matching

Polymorphism lets you work with objects through their base type. But sometimes you need to access derived-type-specific members — you know an object is a Circle and you need Radius, which only exists on Circle. C# provides several tools for this: the is operator checks whether an object is compatible with a type, the as operator attempts a cast and returns null on failure, an explicit cast (Type)obj succeeds or throws, and C# 7+ pattern matching checks and casts in a single readable expression. Knowing which tool to use in each situation is an important skill.

Type Checking and Casting

Shape shape = new Circle(5.0);  // stored as base type

// ── is — checks type, returns bool ────────────────────────────────────────
if (shape is Circle)
    Console.WriteLine("It is a Circle");  // true

// ── as — attempts cast, returns null on failure (no exception) ────────────
Circle? circle = shape as Circle;   // null if not a Circle, otherwise the cast
if (circle != null)
    Console.WriteLine($"Radius: {circle.Radius}");

Rectangle? rect = shape as Rectangle;   // null — shape is not a Rectangle
Console.WriteLine(rect is null);         // true — safe, no exception

// ── Explicit cast — succeeds or throws InvalidCastException ──────────────
Circle  c1 = (Circle)shape;             // ✓ shape IS a Circle — succeeds
// Rectangle r1 = (Rectangle)shape;     // ✗ throws InvalidCastException!

// Rule of thumb:
//   Use as + null check: when the type might or might not match (defensive)
//   Use explicit cast: when you are CERTAIN of the type (assertion)
Note: Use as only with reference types and nullable value types — you cannot use as with non-nullable value types like int or bool because they cannot be null. For value types, use the is pattern matching syntax instead: if (obj is int n) { ... }. The as operator is equivalent to obj is T ? (T)obj : null but expressed more concisely and with only one type check.
Tip: Prefer C# 7+ pattern matching (is T variable) over as + null check. Pattern matching is shorter, more readable, and declares the variable exactly where it is needed. Compare: var c = shape as Circle; if (c != null) Use(c.Radius); versus if (shape is Circle c) Use(c.Radius);. Both are safe, but the pattern matching version is one line and the variable scope is confined to the if block, which is cleaner. Pattern matching is the modern C# idiom.
Warning: Excessive type-checking and casting in a codebase is a sign of a design problem — it usually means polymorphism should be used instead. If you find yourself writing many if (x is TypeA) ... else if (x is TypeB) ... blocks, consider whether a virtual or abstract method on the base type would be cleaner. The goal of polymorphism is precisely to eliminate these type-dispatch chains. Use pattern matching for the genuine cases where you need to access type-specific members, not as a replacement for polymorphic design.

C# 7+ Pattern Matching

// ── Declaration pattern — test and bind in one expression ─────────────────
object obj = "Hello, World!";

if (obj is string s)
{
    Console.WriteLine(s.Length);   // s is available and is of type string
}

// ── Pattern matching in switch expression ─────────────────────────────────
Shape shape2 = new Rectangle(4, 6);

string description = shape2 switch
{
    Circle c    when c.Radius > 10  => $"Large circle, radius {c.Radius}",
    Circle c                        => $"Circle, radius {c.Radius}",
    Rectangle r when r.Width == r.Height => $"Square, side {r.Width}",
    Rectangle r                     => $"Rectangle {r.Width}×{r.Height}",
    _                               => "Unknown shape"
};
Console.WriteLine(description);   // "Rectangle 4×6"

// ── Pattern matching with and/or/not ──────────────────────────────────────
object value = 42;
if (value is int n and > 0 and < 100)
    Console.WriteLine($"Small positive int: {n}");

// ── is not null — modern null check ──────────────────────────────────────
string? input = GetInput();
if (input is not null)
    Console.WriteLine(input.Trim());

// ── Negative pattern ──────────────────────────────────────────────────────
if (shape2 is not Circle)
    Console.WriteLine("Not a circle");

Choosing the Right Casting Approach

Approach On Success On Failure Use When
is T Returns true Returns false Just checking the type
is T var true + bound variable false, no variable Check and use — preferred
as T Returns the cast value Returns null When failure is expected; must null-check
(T)obj Returns the cast value InvalidCastException When you are certain; fast-fail on wrong type

Common Mistakes

Mistake 1 — Using as without checking for null

❌ Wrong — NullReferenceException if the cast fails:

var circle = shape as Circle;
Console.WriteLine(circle.Radius);   // NullReferenceException if not a Circle!

✅ Correct — always null-check after as, or use pattern matching:

if (shape is Circle c) Console.WriteLine(c.Radius);   // ✓ safe pattern matching

Mistake 2 — Excessive type-switching (switch on type) instead of polymorphism

❌ Wrong — switch on type breaks OCP and requires modification for every new type.

✅ Correct — use a virtual/abstract method in the base type; let polymorphism dispatch.

🧠 Test Yourself

What is the difference between shape as Circle and (Circle)shape when shape is actually a Rectangle?