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)
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.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.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.