可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 5 years ago.
I'm somewhat new to programming and I have a question about classes, inheritance, and polymorphism in C#. While learning about these topics, occasionally I'll come across code that looks something like this:
Animal fluffy = new Cat(); // where Animal is a superclass of Cat*
This confuses me, because I don't understand why someone would create a variable of type Animal to store an object of type Cat. Why wouldn't a person simply write this:
Cat fluffy = new Cat();
I do understand why it's legal to store a child object in a parent type variable, but not why it's useful. Is there ever a good reason to store a Cat
object in an Animal
variable vs. a Cat
variable? Can a person give me an example? I'm sure it has something to do with polymorphism and method overriding (and/or method hiding) but I can't seem to wrap my head around it. Thanks in advance!
回答1:
The shortest example I can give you is if you want a list of all animals
List<Animal> Animals = new List<Animal>();
Animals.Add(new Cat());
Animals.Add(new Dog());
If you have ever created a project using Winforms, you will have already used something similar since all controls derive from Control
. You will then notice that a Window has a list of controls (this.Controls
), that allows you to access all child controls on a window at once. I.E to hide all controls.
foreach(var control in this.Controls)
control.Hide();
回答2:
but not why it's useful.
Take a look at some better examples:
Cat myCat = new Cat();
Dog myDog = new Dog();
List<Animal> zoo = ...; // A list of Animal references
zoo.Add(myCat); // implicit conversion of Cat reference to Animal reference
zoo.Add(myDog);
and
void CareFor(Animal animal) { ... }
CareFor(myCat); // implicit conversion of Cat reference to Animal reference
CareFor(myDog);
The pattern Animal fluffy = new Cat();
is far less common in real code (but it does occur).
Consider that very simplified code that shows how some feature works is anot always good at demonstrating the why of that feature.
回答3:
Let's see a practical but extreme exemple.
class Animal { }
class Bird : Animal { }
class Cat : Animal { }
class Dog : Animal { }
class Elephant : Animal { }
class Fennec : Animal { }
Let's say we have a Person class. How do we store a reference to his lone and unique pet ?
Method 1 : the insane way
class Person
{
public Bird myBird;
public Cat myCat;
public Dog myDog;
public Elephant myElephant;
public Fennec myFennec;
}
In that mess, how do we retrieve the pet ?
if (myBird != null)
{
return myBird;
}
else if (myCat != null)
{
return myCat;
}
else if (myDog != null)
{
return myDog;
}
else if (myElephant != null)
{
return myElephant;
}
else if (myFennec != null)
{
return myFennec;
}
else
{
return null;
}
And I'm being nice here, with only 5 types of Animal. Let's say we have over 1000 types of Animal. Will you be the one to write all those variables in the Person class, and add all those 'else if ()' in every place they are, in your application ?
Method 2 : a better approach
class Person
{
public Animal myPet;
}
That way, thanks to polymorphism, we have our lone and unique reference to the person's pet, and in order to get the pet, we simply write :
return myPet;
So, what is the best way of doings things ? Method 1 or 2 ?
回答4:
A declaration which includes initialization, such as Animal joesPet = new Cat()
, can have two purposes:
Create an identifier which will throughout its scope will always represent the same thing.
Create a variable which will initially hold one thing, but may later hold something else.
Declarations where a parent-type variable is initialized to refer to a sub-type instance are commonly used for the second purpose, in situations where the variable is initially assigned to an instance of a particular subtype, but may later need to hold references to things that are not of that subtype. If the declaration had been Cat joesPet = new Cat();
or var joesPet = new Cat();
, then it would (for better or worse) not be possible to say joesPet = new Dog();
. If code shouldn't be able to say joesPet = new Dog();
, then the fact that declaring as Cat
or var
would prevent that would be a good thing. On the other hand, if code might need to have joesPet
be something other than a Cat
, then it should declare the variable in such a way as to allow that.
回答5:
As it is not answered yet, I will try to give a good answer as possible.
Take a look at the following program:
class Program
{
static void Main(string[] args)
{
Animal a = new Animal();
Cat c = new Cat();
Animal ac = new Cat();
a.Noise(a);
a.Noise(c);
a.Noise(ac);
c.Noise(a);
c.Noise(c);
c.Noise(ac);
a.Poop();
c.Poop();
ac.Poop();
Console.Read();
}
}
public class Animal
{
public void Noise(Animal a)
{
Console.WriteLine("Animal making noise!");
}
public void Poop()
{
Console.WriteLine("Animal pooping!");
}
}
public class Cat : Animal
{
public void Noise(Cat c)
{
Console.WriteLine("Cat making noise!");
}
public void Noise(Animal c)
{
Console.WriteLine("Animal making noise!");
}
public void Poop()
{
Console.WriteLine("Cat pooping in your shoe!");
}
}
Output:
Animal making noise!
Animal making noise!
Animal making noise!
Animal making noise!
Cat making noise!
Animal making noise!
Animal pooping!
Cat pooping in your shoe!
Animal pooping!
You can see that we create a variable a
of type Animal
. It points to an object of type Animal
. It has static and runtime type Animal
.
Next we create Cat
variable which points to a Cat
object. The third object is the tricky part. We create an Animal
variable, which has runtime type Cat
, but static type Animal
. Why is this important? Because at compiletime your compiler knows that the variable ac
is in fact of type Animal
. No doubt about it. So it will be able to do all the stuff that an Animal
object can do.
However, at runtime the object inside the variable is known to be a Cat
.
To demonstrate I created 9 function calls.
First, we pass the objects to an instance of Animal
. This object has a method that takes Animal
objects.
This means that inside Noise()
we can make use of all the methods and fields that an Animal
class has. Nothing else. So if Cat
would have a method Miauw()
, we wouldn't be able to call it without casting our animal a Cat
. (Typecasting is dirty, try to avoid it). So when we execute these 3 function calls we will print Animal making noise!
three times. Clearly. So what does my static type matter then?
Well, we'll get there in a second.
The next three function calls are methods inside the Cat
object. The Cat
object has two methods Noise()
. One takes an Animal
, and the other one takes a Cat
.
So first we pass it a regular Animal
. The runtime will have a look at all the methods and see it has a method Noise
that takes an Animal
. Exactly what we need! So we execute that one and we print Animal
making noise.
The next call passes a Cat
variable which contains a Cat
object. Again, the runtime will have a look. Do we have a method that takes a Cat
, because that is the type of my variable. Yes, yes we do. So we execute the method and we print "Cat making noise".
The third call, we have our variable ac
, which is of type Animal
, but points to an object of type Cat
. We will have a look and see if we can find a method that suits our needs. We take a look at the static type (i.e., type of the variable) and we see that is of type Animal
, so we call the method which has Animal
as parameter.
That is a subtle difference between the two.
Next, the pooping.
All animals poop. However, a Cat
poops in your shoe. So we override the method of our base class and implement it so that the Cat
poops in your shoe.
You will notice that when we call Poop()
on our Animal we get the expected result. The same goes for the Cat c
. However, when we call the Poop
method on ac
, we see that it's an Animal
pooping and your shoe is clean. This is because again, the compiler said the type of our variable ac
is Animal
, you said so. So therefore, it will call the method in the type Animal
.
I hope this is clear enough for you.
Edit:
I keep this in mind by thinking about it this way: Cat x;
is a box which has type Cat
. The box doesn't contain a cat, however, it is of type Cat
. This means that the box has a type, regardless of it's contents. Now when I store a cat inside it: x = new Cat();
, I have put an object inside it of type Cat
. So I put a Cat in a Cat-box. However, When I create a box Animal x;
I can store animals in this box. So when I put a Cat
inside this box, that's okay, because it's an animal. So x = new Cat()
stores a Cat inside an Animal box, which is okay.
回答6:
The reason is Polymorphism.
Animal A = new Cat();
Animal B = new Dog();
If Func takes an Animal
and Animal
implements MakeNoise()
:
Func(A);
Func(B);
...
void Func(Animal a)
{
a.MakeNoise();
}
回答7:
Easy answer: If you use the interface or base class animal you can write generic methods that that can take all types of animals instead of only one.
See Why use an interface when the class can directly implement the functions .
回答8:
I use that pattern a few times too, in a somewhat more advanced context but maybe it's worth mentioning. When writing unit tests exercising a service/repository or any class that implements an interface, I often type its variable with the interface instead of the concrete type:
IRepository repository = new Repository();
repository.Something();
Assert.AreEquals(......);
I see this particular case a better option to have the variable as the interface type because it helps as an additional check that the interface is in fact properly implemented. As most likely in real code I won't be using the concrete class directly, I find better to have this little extra verification.
回答9:
If you're writing a program that emulates the behavior of animals, all animals have things in common. They walk, they eat, they breath, they eliminate, etc. What they eat and how they walk, among other things, are different.
So your program knows that all animals do some things, so you write a base class called Animal
that does all those things. The things that all animals do the same (breath, eliminate) you can program in the base class. Then, in the subclasses, you write the code that handles the things they all do but do differently from other animals, like what they eat and how they walk.
But the logic that controls how each animal behaves doesn't care about the details of how they do anything. The "brain" of the animal just knows that it's time to eat, walk, breath, or eliminate. So it calls the method that does those things on a variable of type Animal, which ends up calling the correct method depending on what the actual type of Animal the object it's referring to is.