C# Polymorphism and Overriding: One Contract, Different Behavior
A base-class reference can invoke specialized behavior from its actual object. Learn how virtual and override connect that call, how hiding differs, and how one consumer can work with several implementations.
What you will understand
- Track compile-time reference type separately from runtime object type.
- Declare a virtual base operation and a matching derived override.
- Process several implementations through a shared array and parameter.
- Use base to extend an implementation without accidental recursion.
- Distinguish ordinary overload selection from virtual dispatch.
- Repair new member hiding when overriding is intended.
- Explain a sealed override without confusing it with a sealed class.
- Verify returned preview text through base references and changed messages.
One contract can have different runtime implementations
Polymorphism lets a caller use a shared contract while the actual object supplies specialized behavior. Our Notification contract exposes Format(string message), which returns preview text. EmailNotification and SmsNotification specialize that formatting without requiring the caller to know their concrete types.
There are two types to track: the compile-time type of the expression and the runtime type of its object. Notification notice = new EmailNotification() makes the distinction visible. Notification defines which operation the caller may request; the virtual call uses the EmailNotification override at runtime.
These examples deliberately format strings instead of sending messages. A prefix saying Email preview is not evidence of an email provider or delivery. Each editor contains a separate complete Program.cs. Predict the selected method body before comparing its actual output with the expected reference.
Program.cs · A base reference can call a derived override
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Lesson ready
The variable’s declared type is Notification; its referenced object is an EmailNotification. Format is virtual and the derived declaration uses override, so this call executes the email implementation. Formatting this text does not send an email.
Program.cs · Changing the reference view does not change the object
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Ready
Email preview: Ready
True
Both references identify the same object and both calls use its override. Upcasting does not erase runtime behavior, clone the object or replace it with a Notification instance. The accessible call contract and the selected virtual implementation are separate ideas.
Virtual opens the operation; override specializes it
The base method declares public virtual string Format(string message). virtual means derived classes may replace this implementation through an override. The base still has a usable default body, so a plain Notification instance can return the generic preview.
The derived declaration uses public override string Format(string message). It participates in that existing virtual operation rather than defining an unrelated member with the same spelling. In the ordinary examples here, keep the parameter list, return type and accessibility aligned with the base contract.
An ordinary method is not automatically virtual. A changed parameter list also does not override the string operation. The compiler failures below explain both mistakes. Advanced covariant return rules are unnecessary for this lesson; all examples return string so the dispatch behavior stays easy to follow.
Program.cs · Override requires an overridable base member
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: the inherited Format member is not virtual, abstract or override.Ordinary instance methods are not virtual by default. The compiler rejects override when the base method does not permit it. Do not replace override with new if runtime dispatch through Notification is the intended behavior.
Program.cs · A different parameter list does not override this method
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: no suitable Format(int) method was found to override.The base virtual method takes string, not int. An override must match the inherited operation’s signature and accessibility under the language rules. Another parameter type could define an overload without override, but it would not customize this string contract.
Write consumers against the shared contract
A Notification array can hold a base instance and different derived instances. A foreach loop can call Format on each element without an if statement for each subtype. The loop asks for the shared operation; each receiver determines its virtual implementation.
The same idea works in a method parameter. PrintPreview(Notification notice, string message) can accept either preview implementation and print its returned string. A new derived formatter can fit that contract without adding another subtype branch to this consumer.
The shared contract still needs consistent meaning. Every implementation here returns preview text for the supplied message and performs no delivery. An override that secretly sends a real message or ignores required inputs would surprise the caller even if its C# signature compiled. Test behavior through the base contract, not only direct derived calls.
Program.cs · One loop can use several implementations
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Notice: Lesson ready
Email preview: Lesson ready
SMS preview: Lesson ready
Every array element supports the Notification contract. The loop calls Format without checking subtype names. A base instance uses its default implementation; each specialized instance uses its override. Array order determines output order, while runtime receiver type determines each implementation.
Program.cs · A method can depend on the base contract
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Welcome
SMS preview: Welcome
PrintPreview accepts a Notification and calls its virtual operation. It does not need an email-specific parameter or a type switch. It assumes a non-null receiver and a supplied message. The only side effect here is Main’s helper printing a local preview.
Use base deliberately and preserve the operation’s promise
An override may replace the base result completely or extend it when the contract calls for that. UrgentNotification uses base.Format(message) to obtain the shared generic wording, then adds its urgency prefix. The call targets the base implementation on the current object.
Calling Format(message) from inside the same override without base would invoke itself again in this example. That is recursion rather than delegation to the base implementation. Do not demonstrate that mistake by running an unbounded self-call; understand the call target before editing it.
Do not call virtual operations from a constructor merely to get a specialized report. Runtime dispatch can reach derived code before the derived constructor body has finished setting its state. Complete construction first, then invoke the public operation. The earlier inheritance lesson’s constructor forwarding and this lesson’s virtual method dispatch are separate mechanisms.
Program.cs · An override can deliberately extend the base result
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
URGENT | Notice: Review now
base.Format selects the base implementation on this same object rather than dispatching back into this override. The derived method adds a prefix around that returned result. Calling Format(message) without base here would call the override again and recurse. A base call is useful when the contract requires extending its work, not mandatory in every override.
Overloading, overriding and hiding are different mechanisms
Overloading offers several parameter lists for a name. In the ordinary non-dynamic calls here, compile-time argument types select the overload. Overriding supplies a runtime implementation for an inherited virtual operation. Both can appear in one program, but they act at different stages.
The Choose example receives a variable typed Notification and therefore selects Choose(Notification). Calling that variable’s virtual Format still reaches EmailNotification. The runtime object does not retroactively make ordinary overload selection choose the EmailNotification parameter.
The new modifier on a method explicitly hides an inherited member. It does not override the inherited virtual slot. The guided repair shows that a derived-typed call and a base-typed call can then produce different results for the same object. Use override when the design requires specialization through the shared base contract.
Program.cs · Overload choice and virtual dispatch happen at different stages
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Notification parameter
Email preview: Ready
For these ordinary calls, the argument’s compile-time Notification type selects Choose(Notification), even though Choose(EmailNotification) also exists. The selected virtual Format call then dispatches using the receiver’s runtime type. Overloading and overriding answer different questions; this example does not use dynamic.
Virtual extension points should be intentional
Not every method needs to be virtual. An operation that must preserve a fixed rule may remain non-virtual, while a focused formatting operation may be an intentional customization point. Making everything overridable expands the behavior that a base consumer must be prepared to encounter.
A sealed override prevents a further derived class from overriding that operation again. It remains the virtual implementation used through earlier base references. A sealed class instead prevents further derivation from the class itself. The compiler example distinguishes these two boundaries.
Abstract methods and interfaces offer additional ways to require implementations, covered in the following lessons. This lesson keeps a concrete base default so you can compare inherited behavior, replacement, explicit base calls and member hiding before removing the default implementation.
Program.cs · A sealed override prevents another override farther down
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: the inherited Format member is sealed and cannot be overridden.sealed override closes this virtual extension point for further derived overrides. It does not prevent EmailNotification itself from being subclassed; a sealed class would prevent that. Existing base-reference virtual calls still use EmailNotification’s override.
Test through base references and check every supported receiver
A direct EmailNotification call alone cannot prove that overriding works. Assign the same instance to a Notification variable and call Format there as well. Both should select the email implementation when the override contract is correct. ReferenceEquals can confirm you are comparing two views of one object.
Test a mixed array, a base instance that retains the default, changed messages, and an empty message under this exercise’s simple interpolation contract. Every implementation should preserve the supplied text rather than hard-code the reference lesson message. An empty array should simply produce no lines.
The lab changes only the incomplete SMS formatter. The caller remains a loop over Notification references. Check the returned previews and output order, then inspect the modifiers so a passing direct call does not hide a new-versus-override mistake. None of these programs perform external communications or save data.
Guided repair: override instead of hiding
EmailNotification prints the email prefix directly but prints the generic prefix through a Notification reference. The derived method uses new even though runtime specialization is intended. Repair the declaration, not the caller.
Program.cs · New hides a member rather than overriding its slot
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Ready
Notice: Ready
The email method hides the inherited member. The derived-typed call finds that hidden method, while the base-typed virtual call still uses the base implementation because no override replaced it. Two references to one object therefore show different behavior. This program compiles, but violates the intended shared preview contract.
Repair checkpoints
- Confirm the base Format method is virtual.
- Replace new with override on the derived string Format method.
- Keep both references pointing at the same EmailNotification instance.
- Expect Email preview: Ready from both calls.
- Do not add a cast or subtype switch to conceal the missing override.
Reveal the repair and explanation
Program.cs · Use override for the intended runtime customization
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Ready
Email preview: Ready
Replacing new with override participates in the inherited virtual slot. Both call paths now use the specialized implementation. The base declaration was already virtual; both sides of that contract matter. No type checks or downcasts are necessary in the caller.
Independent lab: one preview pipeline, two implementations
Complete an SMS preview formatter in a shared Notification pipeline. Keep the caller independent of the concrete types and verify the implementation through base references.
Acceptance checklist
- Keep Notification.Format virtual with its generic default result.
- Keep EmailNotification.Format as an override returning the email preview prefix.
- Complete SmsNotification.Format as an override returning SMS preview: followed by the message.
- Leave the Notification[] loop unchanged and avoid casts or subtype conditions.
- Return text from Format; keep printing in the consumer.
- Preserve each supplied message instead of hard-coding the reference text.
- Treat the programs as local previews only; no email or SMS is sent.
Program.cs · Shared preview pipeline starter
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Your practice is ready
Notice: Your practice is ready
The loop already depends only on Notification, and both derived methods are overrides. SMS still delegates to the generic result, so the specialized preview is missing. Complete the SMS override while keeping the caller unchanged.
Show hints
- The SMS method currently delegates to the generic base result.
- Return an interpolated string using the method’s message parameter.
- Check the call through Notification, not just a SmsNotification variable.
Reveal the reference solution
Program.cs · Shared preview pipeline reference solution
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Your practice is ready
SMS preview: Your practice is ready
The SMS implementation now supplies its channel-specific prefix. The same loop works for both implementations through the base contract. These methods return local preview strings only: no recipient, network call, email delivery or SMS service is implemented.
Changed-input checks
- Changed message Welcome appears unchanged after both channel prefixes.
- An empty message still produces each prefix with its separating space.
- Reversing the array reverses output order without changing implementations.
- A plain Notification instance returns the generic Notice prefix.
- An empty Notification array prints no preview lines.
- The repaired hiding example returns the email implementation through both reference types.
Recap: select the operation, dispatch the implementation
Track the expression’s compile-time contract and the receiver’s runtime implementation separately.
virtual- Declare a base operation that derived classes may override.
override- Supply an implementation for an inherited overridable operation.
base.Format(message)- Select the base implementation on the current object.
base reference- Expose the base contract while retaining the object’s runtime identity.
overloading- Choose among parameter lists using compile-time types in these ordinary calls.
new member hiding- Introduce a hiding member rather than replacing the inherited virtual slot.
sealed override- Prevent further overriding while preserving virtual dispatch to this implementation.
shared consumer- Use the base operation without branching on every subtype.
Frequently asked questions
Does a base reference turn a derived object into a base object?
No. It identifies the same instance through the base contract. Virtual dispatch still uses its runtime override.
Is a same-named method automatically an override?
No. The base must allow overriding and the derived declaration must use override with a matching contract. new explicitly hides instead.
Must every override call base?
No. Call it when extending its behavior is part of the contract. A complete replacement may be appropriate, but it must preserve the operation’s promised meaning.
Does runtime type choose an ordinary overload?
Not in the non-dynamic calls taught here. Compile-time types choose the overload; a selected virtual operation then uses runtime dispatch.
Does sealed override stop all inheritance?
No. It closes that overriding extension point. A sealed class prevents further class derivation.
Do these notification examples deliver messages?
No. They return and print local preview strings. There are no recipients, provider integrations or network delivery calls.
Can you predict the selected implementation?
Trace virtual dispatch, base calls, hiding and ordinary overload selection. Read the explanation after checking your answers.
Next step and references
Virtual and override connect a shared operation to a runtime implementation. Preserve that contract, distinguish hiding from overriding and keep ordinary overload selection separate from dispatch.
Next is C# Abstract Classes. You will define shared state and partial behavior while requiring concrete types to implement missing operations.
Official references
Original SovranCode explanations, examples and exercises. Technical details were checked against these references.
Last updated: .