21.3. A Tale of Two Cats¶
Let’s revisit our Cat and HouseCat friends.
Check out this repository for the code for the lesson.
In Visual Studio, open the solution and examine the two classes inside the Cats project.
21.3.1. Inheriting Fields and Properties¶
Notice that Cat has a property called Family, representing
the biological family of all cats.
10 | public string Family { get; } = "Felidae";
|
The family field is not directly accessible by HouseCat and can only be accessed by the get accessor of the Family property.
However, there is no set accessor for Family, so the value of the family field may only be changed within Cat.
It makes sense that a subclass should not be able to change the biological family of a cat, since this property should rarely, if ever, change.
Methods of the base class Cat may be called on instances of the
subclass HouseCat as if they were defined as part of the
HouseCat.
Try it out. In your project, open up Program.cs. Within the Main method,
create an instance of HouseCat and call some of the methods it inherits from Cat.
1 2 3 | HouseCat garfield = new HouseCat("Garfield", 12.0);
garfield.Eat();
Console.WriteLine(garfield.Tired); // prints true
|
The Eat method was defined in Cat, but may be called on all
HouseCat instances as well. We say: “HouseCat inherits the
method Eat() from Cat.” We know we have successfully called Eat() on
garfield because the printed statement indicates the cat is now tired.
21.3.2. base¶
We mention above that a subclass inherits all non-constructor
methods from its base class. Indeed, when extending a class, we will not
be able to create new instances of our subclass HouseCat using any
constructors provided by Cat.
The base class Cat has a constructor that takes a single parameter
of type double.
12 13 14 15 | public Cat(double weight)
{
Weight = weight;
}
|
But because HouseCat does not have such a constructor, the following code
does not compile:
HouseCat mittens = new HouseCat(8.4);
Cat constructors are not inherited by HouseCat. If we want
to use a Cat constructor in this subclass, we must explicitly
provide it.
To do so, look at the constructor included in HouseCat:
11 12 13 14 | public HouseCat(string name, double weight) : base(weight)
{
Name = name;
}
|
Here, we use the : syntax again with the base keyword on the constructor signature.
This combination allows the constructor to extend the base class constructor that takes a weight parameter.
In this case, the subclass constructor also sets the value of the name field.
The call to the base class constructor must be on the subclass’s constructor signature.
If a base class constructor takes no arguments, then the no-argument constructor is implicitly called for you in the subclass. A no-argument, or no-arg constructor, is just as the name implies, a constructor that takes no arguments.
For example, we can add an additional constructor in Cat:
17 18 19 20 | public Cat()
{
Weight = 13.0;
}
|
Then in HouseCat, we can simply define another constructor as this:
16 17 18 19 | public HouseCat(string name)
{
Name = name;
}
|
Even though we don’t explicitly specify that we want to call a
constructor from Cat, the no-argument constructor will be called. Now, we can
initialize a new HouseCat with only a name field and the Cat no-argument
constructor will still be applied. Back in Program.cs, you can confirm that the base
class constructor has been called:
1 2 | HouseCat spike = new HouseCat("Spike");
Console.WriteLine(spike.Weight); // prints 13
|
As a consequence of this constructor syntax, we can easily expose any
constructor from the base class by providing a subclass constructor that has the same signature, no method body, and calls the base class constructor with : base.
1 2 3 4 | public HouseCat(double weight) : base(weight)
{
// This is all there is to this constructor!
}
|
Warning
This constructor is a bad one, and is included merely to introduce
syntax and usage. We would not want to have a constructor for
HouseCat that didn’t initialize an essential field such as name.
21.3.3. override¶
Sometimes when extending a class, we’ll want to modify behavior provided by the base class. This can be done by replacing the implementation of an inherited method by a completely new method implementation. For a given method, we can do this via method overriding.
In our example, the Noise method of HouseCat overrides the
method of the same name in Cat. When we override it, we should use override
in the signature of the method in the subclass and virtual in the signature of the base class.
Here are the methods in question.
In Cat:
38 39 40 41 | public virtual string Noise()
{
return "Meow!";
}
|
In HouseCat:
26 27 28 29 | public override string Noise()
{
return "Hello, my name is " + Name + "!";
}
|
If we have a HouseCat object and call its Noise() method, we will be
using the method defined in HouseCat.
1 2 3 4 5 | Cat plainCat = new Cat(8.6);
HouseCat cheshireCat = new HouseCat("Cheshire Cat", 26.0);
Console.WriteLine(plainCat.Noise()); // prints "Meow!"
Console.WriteLine(cheshireCat.Noise()); // prints "Hello, my name is Cheshire Cat!"
|
Warning
When overriding a method from a base class, the method name, access level, type and number of parameters, and return type must be exactly the same.
In this example, the parts of our method that we have to match are:
public string Noise();
When overriding a method, we may call the method from the base class
that we are overriding by using base. Modify your HouseCat.Noise()
method as follows:
38 39 40 41 42 43 44 45 46 47 48 | public override string Noise()
{
if (IsSatisfied())
{
return "Hello, my name is " + Name + "!";
}
else
{
return base.Noise(); // prints "Meow!"
}
}
|
This calls the overridden method in the base class via
base.Noise(), carrying out the original behavior if the given
conditional branch is reached.
21.3.4. Object Class¶
In a previous lesson, we introduced the special methods: Equals and
ToString. All classes contain default implementations of these methods that
can be overridden.
In fact, these default methods are part of a class called Object.
All classes we create in C# have access to the methods and members of the Object class, because it is the base class in all .NET class hierarchies.
In the case of Cat and HouseCat, Cat implicitly extends Object.
Since Cat is a subclass of the Object class and HouseCat is a subclass of the Cat class, HouseCat and Cat can both access different methods and members of the Object class.
So the default implementations of Equals and ToString (along with a few other
methods)
are made available to us via inheritance.
Note that we should use the override keyword when we provide new
implementations of these methods as well.
21.3.5. Check Your Understanding¶
Question
For this question, refer to the code block below.
1 2 3 4 5 6 7 8 9 10 11 | public class Message
{
public bool Friendly { get; } = true;
public string Language { get; }
public string Text { get; }
public Message(string language, string text) {
Language = language;
Text = text;
}
}
|
A class called Greeting extends Message. Greeting and
Message are both defined within a package called Speech.
Select all of the fields, properties, and methods that are inherited by
Greeting.
FriendlyLanguageTextMessagefriendlylanguagetext
Question
For this question, refer to the code block below.
1 2 3 4 5 6 7 8 9 10 11 | public class Message
{
public bool Friendly { get; } = true;
public string Language { get; }
public string Text { get; }
public Message(string language, string text) {
Language = language;
Text = text;
}
}
|
A class called Greeting extends Message. What would a constructor for
Greeting need to be to call the Message constructor?
public Greeting(string language, string text, bool friendly) { super(language, text); Friendly = friendly; }
public Greeting(string language, string text) : base(language, text) { }
public Greeting() : base(language, text) { }
public Greeting(string language, string text) { Language = language; Text = text; }
