In the previous lesson you got Book and Magazine to share, through LibraryItem, the
properties and methods common to any library material. But for now you're still treating them
separately: if you wanted to show the description of every item in the catalog, you'd need two
separate loops — one for books and another for magazines — or a series of manual type checks.
Polymorphism (from the Greek for "many forms") is the ability to treat objects of
different classes that inherit from the same base uniformly, letting each one behave
according to its own real type at runtime. In this lesson you'll learn to walk through
BiblioTech's entire catalog — books and magazines mixed together — with a single loop, and
each item will "know" how to describe itself correctly.
Contents
- Two kinds of polymorphism: a recap of what you've already seen
- Runtime polymorphism: the
Describe()method - Dynamic binding: who decides which version runs
- A mixed catalog:
LibraryItem[]traversed polymorphically - The
isoperator: checking an object's real type - The
asoperator: casting safely - Application: filtering the catalog by specific type
- Two kinds of polymorphism: a recap of what you've already seen
In C# there are two forms of polymorphism:
| Kind of polymorphism | When it's decided which code runs | Where you saw it |
|---|---|---|
| Compile-time (static) | The compiler, based on the number/type of arguments | Method overloading, "Methods" lesson |
| Runtime (dynamic) | The program, while running, based on the object's real type | virtual/override, this lesson's topic |
You already used the first one without calling it by name: when you defined ShowDetails()
and ShowDetails(bool includeIsbn) in the Methods lesson, the compiler decided, at compile
time, which of the two versions to invoke, based on how many arguments you passed it. This
lesson focuses on the second kind, much more characteristic of object-oriented programming.
- Runtime polymorphism: the
Describe() method
Describe() methodIn the previous lesson, ShowDetails() was already virtual in LibraryItem and was
overridden in Book and Magazine. Let's add a second virtual method, Describe(), designed
specifically to illustrate polymorphism in action: instead of printing directly to the
console, it returns a string with a short description of the item, different depending
on its specific type.
class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
public bool Available { get; set; } = true;
public LibraryItem(string title, string author)
{
Title = title;
Author = author;
}
public virtual string Describe()
{
return $"Item: {Title}, by {Author}";
}
}
class Book : LibraryItem
{
public string Isbn { get; set; }
public Book(string title, string author, string isbn) : base(title, author)
{
Isbn = isbn;
}
public override string Describe()
{
return $"Book: {Title}, by {Author} (ISBN {Isbn})";
}
}
class Magazine : LibraryItem
{
public int IssueNumber { get; set; }
public Magazine(string title, string author, int issueNumber) : base(title, author)
{
IssueNumber = issueNumber;
}
public override string Describe()
{
return $"Magazine: {Title}, issue number {IssueNumber}";
}
}Each class offers its own version of Describe(), tailored to what makes sense to show for
that specific type of item.
- Dynamic binding: who decides which version runs
Now comes the key part of polymorphism. Look at this code:
LibraryItem item = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(item.Describe());
// Book: Hopscotch, by Julio Cortazar (ISBN 978-84-376-0495-4)The item variable is declared as type LibraryItem, but the object it points to is actually
a Book. When calling item.Describe(), C# doesn't run the LibraryItem version (which
would be expected if only the variable's type were considered), but rather the version
overridden in Book, because that's the object's real type in memory. This mechanism is
called dynamic binding (or late binding): which code runs is decided at runtime, based
on the object's real type, not on the declared type of the variable referencing it.
This only happens because Describe() is virtual in the base class and override in the
inheriting one; if Describe() weren't virtual, item.Describe() would always run the
LibraryItem version, regardless of which specific object the variable actually held.
- A mixed catalog:
LibraryItem[] traversed polymorphically
LibraryItem[] traversed polymorphicallyThis is where polymorphism shows its true usefulness: an array (or any collection) declared with the base type can contain objects of any of its inheriting classes, mixed together, and be traversed with a single loop:
LibraryItem[] catalog = new LibraryItem[]
{
new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
new Magazine("National Geographic", "Various authors", 302),
new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1")
};
foreach (LibraryItem item in catalog)
{
Console.WriteLine(item.Describe());
}Output:
Book: Hopscotch, by Julio Cortazar (ISBN 978-84-376-0495-4) Magazine: National Geographic, issue number 302 Book: Ficciones, by Jorge Luis Borges (ISBN 978-84-376-0496-1)
A single foreach loop, written just once over the base type LibraryItem, automatically
produces the correct description for each element, whether Book or Magazine, without the
loop needing to know anything about those specific types or containing any explicit type
check. Adding a third inheriting class tomorrow (for example, a future Dvd : LibraryItem)
wouldn't require touching this loop at all: it would be enough for the new class to override
Describe() in its own way. This is the essence of polymorphism: the code that traverses the
catalog stays stable even as the catalog grows in types.
- The
is operator: checking an object's real type
is operator: checking an object's real typeSometimes, however, you need to do something specific to a particular type, not just what the
base class offers. The is operator checks whether an object is (or derives from) a given
type, and optionally assigns it directly to a variable of that more specific type:
foreach (LibraryItem item in catalog)
{
if (item is Book book)
{
Console.WriteLine($"It's a book; its ISBN is {book.Isbn}");
}
else if (item is Magazine magazine)
{
Console.WriteLine($"It's a magazine; its issue number is {magazine.IssueNumber}");
}
}item is Book book reads as "if item is (at runtime) a Book, then assign that same
reference, now typed as Book, to a new variable called book." Inside the if block,
book already gives access to Isbn, a property that doesn't exist in LibraryItem and
that, therefore, wouldn't be directly accessible through the item variable. This pattern is
called pattern matching with is, and it will be expanded considerably in Module 4
("Pattern Matching and Modern Features").
- The
as operator: casting safely
as operator: casting safelyThe as operator attempts to convert (cast) an object to a more specific type, and returns
null if the conversion isn't possible, instead of throwing an exception:
LibraryItem firstElement = catalog[0];
Book? castBook = firstElement as Book;
if (castBook != null)
{
Console.WriteLine($"Cast succeeded: {castBook.Isbn}");
}
else
{
Console.WriteLine("The element wasn't a Book.");
}If firstElement were actually a Magazine, firstElement as Book wouldn't throw any error:
castBook would simply be null, and the code can check for that safely. This contrasts with
a direct cast in parentheses, like (Book) firstElement, which, if the object isn't
really a Book, does throw an exception (InvalidCastException) at runtime. In modern C#
practice, is with a pattern (previous section) is usually preferred over as for this kind
of check, for being more compact and readable, but it's worth knowing both.
| Operator/technique | If the type doesn't match | When to use it |
|---|---|---|
(Book) item (direct cast) |
Throws InvalidCastException |
When you're sure of the type and an error would be a genuine sign of failure |
item as Book |
Returns null |
When the conversion might reasonably fail and you'd rather check for null |
item is Book book |
The if condition is false (doesn't enter the block) |
The most readable form in modern C#; combines checking and assignment in one step |
- Application: filtering the catalog by specific type
We'll close the lesson by combining polymorphic traversal with is, to count how many
elements of the catalog are books and how many are magazines — a common operation when
generating statistics for BiblioTech's catalog:
LibraryItem[] catalog = new LibraryItem[]
{
new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
new Magazine("National Geographic", "Various authors", 302),
new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"),
new Magazine("Muy Interesante", "Various authors", 487)
};
int totalBooks = 0;
int totalMagazines = 0;
foreach (LibraryItem item in catalog)
{
Console.WriteLine(item.Describe()); // polymorphism: each one describes itself its own way
if (item is Book)
{
totalBooks++;
}
else if (item is Magazine)
{
totalMagazines++;
}
}
Console.WriteLine($"Total books: {totalBooks}");
Console.WriteLine($"Total magazines: {totalMagazines}");Note that in if (item is Book), without declaring an additional variable, it's enough to ask
"is this object a Book?" without needing to access any of its specific properties — useful
when you only care about counting or classifying, not operating on the specific type's own
data.
Common Mistakes and Tips
- Forgetting
virtualin the base class: ifDescribe()isn'tvirtual, writingoverrideinBookorMagazinecauses a compilation error; and if you instead use thenewkeyword to "hide" the method (rather than overriding it), you'll lose polymorphism:item.Describe()would go back to always running theLibraryItemversion, even if the real object were aBook. - Casting directly without checking the type:
(Book) itemwithout checking first withiscan throwInvalidCastExceptionif the real object isn't aBook; preferiswith a pattern oraswhen you're not sure of the real type. - Writing a separate loop for each inheriting type: if you catch yourself writing a
foreachfor books and another nearly identical one for magazines, that's a sign you should be traversing the base type (LibraryItem[]) with a single polymorphic loop, as in section 4. - Confusing the variable's type with the object's real type: a variable declared as
LibraryItemcan point to aBookor aMagazine; what determines which version of avirtualmethod runs is always the object's real type, never the type the variable was declared with. - Tip: before resorting to
is/asto distinguish types inside a loop, ask yourself whether that different behavior couldn't simply be solved by overriding avirtualmethod in each inheriting class; it's usually a cleaner, more extensible design.
Exercises
-
Using the
LibraryItem,BookandMagazineclasses defined in this lesson (withDescribe()as avirtual/overridemethod), create aLibraryItem[]array with two books and one magazine, and traverse it withforeach, printing the result ofDescribe()for each one. -
On the same array from the previous exercise, use the
isoperator with a pattern to show, only for the elements that areBook, a message with its ISBN (for example:"ISBN: 978-84-376-0495-4"). -
Write an expression using the
asoperator that tries to cast the array's first element toMagazine, and shows"It's a magazine with issue number <number>"if the cast succeeds, or"It's not a magazine"if the result isnull.
Solutions
LibraryItem[] catalog = new LibraryItem[]
{
new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"),
new Magazine("National Geographic", "Various authors", 302)
};
foreach (LibraryItem item in catalog)
{
Console.WriteLine(item.Describe());
}
foreach (LibraryItem item in catalog)
{
if (item is Book book)
{
Console.WriteLine($"ISBN: {book.Isbn}");
}
}
Magazine? castMagazine = catalog[0] as Magazine;
if (castMagazine != null)
{
Console.WriteLine($"It's a magazine with issue number {castMagazine.IssueNumber}");
}
else
{
Console.WriteLine("It's not a magazine");
}
Since catalog[0] is actually a Book (according to exercise 1), the cast with as
returns null, and the message shown is "It's not a magazine".
Conclusion
In this lesson you've seen runtime polymorphism in action: a virtual method like
Describe(), overridden differently in Book and Magazine, lets you traverse a mixed
catalog (LibraryItem[]) with a single loop and get the correct behavior for each object
according to its real type — thanks to dynamic binding. You've also learned to use is and
as to reach a specific type's particular features when you truly need to, without giving up
polymorphism as your main strategy.
Until now, however, Available can still be freely modified from outside the class
(item.Available = true;, without going through Lend() or Return()), which could leave
BiblioTech's catalog in an inconsistent state by mistake or oversight. In the next lesson,
Encapsulation, you'll learn to protect your classes' internal state, allowing it to be
changed only through the methods designed for that purpose.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
