Problem overview
A shop ships parcels in three ways. Every way answers the same two questions: what is it called, and what does it cost for a parcel of a given weight? Only the answers differ. Standard is called "Standard" and costs 5 + weight. Express is called "Express" and costs 10 + 2 * weight. Economy is called "Economy" and costs 3 + weight / 2, where the division drops the remainder.
You write three classes: Shipping for the standard option, and Express and Economy, which inherit from it and give their own versions of Name and Cost. A parcel of 4 kilograms costs 9, 18 and 5 with the three options.
Approach: one parent, two overrides
Without inheritance you would write three unrelated classes and the code using them would need to know which one it holds. With inheritance, Express and Economy are kinds of Shipping. A variable of type Shipping can hold any of the three, and calling Name() or Cost(weight) on it runs the version that belongs to the object actually stored there.
In C# this needs two keywords that work as a pair:
virtualon the parent's method says "a child class may replace this";overrideon the child's method says "this is the replacement", with exactly the same name, parameters and return type.
A child class is declared with : Shipping after its name. It inherits everything that is accessible in the parent, and only has to write what is different.
Step by step
- Write
Shipping.Nameso that it returns"Standard". Its header ispublic virtual string Name(). - Write
Shipping.Costso that it returns5 + weight: a base price plus one unit per kilogram. - In
Express, return"Express"fromName. The header is the same as the parent's, withoverridein place ofvirtual. - In
Express.Cost, return10 + 2 * weight. - In
Economy.Name, return"Economy". - In
Economy.Cost, return3 + weight / 2. Both operands areint, so the division drops the remainder:7 / 2is 3.
The solution in C#
class Shipping {
public virtual string Name() {
return "Standard";
}
public virtual int Cost(int weight) {
return 5 + weight;
}
}
class Express : Shipping {
public override string Name() {
return "Express";
}
public override int Cost(int weight) {
return 10 + 2 * weight;
}
}
class Economy : Shipping {
public override string Name() {
return "Economy";
}
public override int Cost(int weight) {
return 3 + weight / 2;
}
}
The classes have no fields: the price depends only on the weight passed in, so an object needs no data of its own. That is why each class is just two small methods.
Using the classes
This code builds one object of each kind, stores them all in an array of the parent type and asks each one the same two questions for a 7 kilogram parcel:
Shipping[] options = { new Shipping(), new Express(), new Economy() };
foreach (Shipping s in options) {
Console.WriteLine(s.Name() + ": " + s.Cost(7));
}
Standard: 12
Express: 24
Economy: 6
The loop never checks which class it has. The same two calls give three different answers, because each object runs its own overridden version. With a weight of 0 only the base prices remain: 5, 10 and 3. With a weight of 1, Economy costs 3, since 1 / 2 is 0.
Adding a fourth option
Because the code that uses shipping options only knows the parent type, a new option is just one more class. This one, Overnight, reuses the parent's price with base.Cost(weight) and adds 20:
class Overnight : Shipping {
public override string Name() { return "Overnight"; }
public override int Cost(int weight) { return base.Cost(weight) + 20; }
}
Added to the same array as the other three, with a 4 kilogram parcel, it prints:
Standard: 9
Express: 18
Economy: 5
Overnight: 29
base means "the parent's version", which is handy when the child only wants to adjust the parent's answer. The loop that prints these lines did not change at all. That is the benefit of overriding: the code that uses the options stays closed to changes, while the list of options stays open.
Common mistakes
- Forgetting
virtualoroverride. If the child's method has the same name but nooverride, and the parent's is notvirtual, the child's method hides the parent's instead of replacing it. The compiler warns (CS0108), and a call through aShippingvariable still runs the parent's version. For example, aChildobject kept in aBasevariable answered"Standard", while the same object kept in aChildvariable answered"Express". - Changing the header. An override must match the parent's method exactly: a different return type or parameter list is a different method.
- Rounding Economy's weight the wrong way. The cost is
3 + weight / 2with integer division. Writing it withdoublewould produce fractions such as 6.5, which this lesson does not want. - Mixing up the prices. Each child replaces the whole formula: Express does not add its 10 on top of the parent's 5.
Key takeaways
- A child class declared with
: Parentinherits the parent's members. - Mark the parent's method
virtualand the child's versionoverrideso that a call through the parent type runs the child's code. - Code written against the parent type works unchanged for every child class, including ones added later.
Key takeaways
- Each class holds only what is different about it. Here that is two short formulas per class.
- The parent's own version still matters: it is the standard option, and any child that does not override a method inherits it unchanged.
- Overriding replaces behaviour, it does not add to it. Use
base.Cost(weight)when the child wants the parent's answer as a starting point. - When a result looks wrong for one option, check that child's formula first: the other classes cannot affect it.
