我对编程有些新意,我对C#中的类,继承和多态有疑问。在了解这些主题的同时,我偶尔会遇到类似这样的代码:
Animal fluffy = new Cat(); // where Animal is a superclass of Cat*
这让我感到困惑,因为我不明白为什么有人会创建一个类型为Animal的变量来存储Cat类型的对象。为什么一个人不会简单地写这个:
Cat fluffy = new Cat();
我确实理解为什么将子对象存储在父类型变量中是合法的,而不是为什么它有用。是否有充分理由将Cat
对象存储在Animal
变量与Cat
变量中?一个人可以举个例子吗?我确定它与多态和方法覆盖(和/或方法隐藏)有关,但我似乎无法绕过它。提前谢谢!
答案 0 :(得分:19)
我能给你的最简单的例子是你想要一份所有动物的清单
List<Animal> Animals = new List<Animal>();
Animals.Add(new Cat());
Animals.Add(new Dog());
如果您曾使用Winforms创建项目,那么您将使用类似的东西,因为所有控件都来自Control
。然后您会注意到一个Window有一个控件列表(this.Controls
),它允许您一次访问窗口上的所有子控件。 I.E隐藏所有控件。
foreach(var control in this.Controls)
control.Hide();
答案 1 :(得分:14)
但不是为什么它有用。
看一些更好的例子:
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);
和
void CareFor(Animal animal) { ... }
CareFor(myCat); // implicit conversion of Cat reference to Animal reference
CareFor(myDog);
模式Animal fluffy = new Cat();
在实际代码中远不常见(但确实会发生)。
考虑到显示 某些功能如何工作的非常简化的代码,总是很好地展示了该功能的原因。
答案 2 :(得分:6)
让我们看一个实际但极端的例子。
class Animal { }
class Bird : Animal { }
class Cat : Animal { }
class Dog : Animal { }
class Elephant : Animal { }
class Fennec : Animal { }
我们说我们有一个Person类。我们如何存储他唯一和独特的宠物的参考?
方法1:疯狂的方式
class Person
{
public Bird myBird;
public Cat myCat;
public Dog myDog;
public Elephant myElephant;
public Fennec myFennec;
}
在那个混乱中,我们如何找回宠物?
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;
}
我在这里很好,只有5种动物。让我们说我们有超过1000种动物。你是那个在Person类中编写所有变量的人,并添加所有那些&else;如果()&#39;在你申请的每个地方?
方法2:更好的方法
class Person
{
public Animal myPet;
}
这样,由于多态性,我们对这个人的宠物有了独一无二的独特参考,为了得到宠物,我们只需写下:
return myPet;
那么,做事的最佳方式是什么?方法1还是2?
答案 3 :(得分:3)
由于尚未回答,我会尽量给出一个好的答案。
看看以下程序:
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!");
}
}
输出:
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!
您可以看到我们创建了a
类型的变量Animal
。它指向Animal
类型的对象。它具有静态和运行时类型Animal
。
接下来,我们创建指向Cat
对象的Cat
变量。第三个目标是棘手的部分。我们创建了一个Animal
变量,它具有运行时类型Cat
,但是静态类型Animal
。为什么这很重要?因为在编译时您的编译器知道变量ac
实际上是Animal
类型。毫无疑问。因此,它将能够完成Animal
对象可以执行的所有操作。
但是,在运行时,变量内的对象已知为Cat
。
为了演示我创建了9个函数调用。
首先,我们将对象传递给Animal
的实例。该对象有一个采用Animal
个对象的方法。
这意味着在Noise()
内,我们可以使用Animal
类所具有的所有方法和字段。没有其他的。因此,如果Cat
有一个方法Miauw()
,我们将无法在不将动物强加给Cat
的情况下调用它。 (Typecasting很脏,尽量避免它)。因此,当我们执行这3个函数调用时,我们将打印Animal making noise!
三次。显然。那么我的静态类型又如何呢?
好吧,我们会在一瞬间到达那里。
接下来的三个函数调用是Cat
对象内的方法。 Cat
对象有两种方法Noise()
。一个需要Animal
,另一个需要Cat
。
首先我们传递一个普通的Animal
。运行时将查看所有方法,并看到它有一个方法Noise
,它采用Animal
。正是我们需要的!所以我们执行那个,我们打印Animal
制造噪音。
下一个调用传递一个包含Cat
对象的Cat
变量。再次,运行时将看一看。我们有一个采用Cat
的方法,因为这是我变量的类型。是的,是的,我们这样做。所以我们执行方法并打印"Cat making noise".
第三个调用,我们有变量ac
,类型为Animal
,但指向Cat
类型的对象。我们来看看是否能找到适合我们需求的方法。我们看一下静态类型(即变量的类型),我们看到类型为Animal
,因此我们将Animal
作为参数调用。
这是两者之间的细微差别。
接下来,pooping。
所有动物大便。但是,鞋子里有Cat
便便。因此,我们覆盖基类的方法并实现它,以便鞋中的Cat
大便。
您会注意到,当我们在Animal上调用Poop()
时,我们会得到预期的结果。 Cat c
也是如此。但是,当我们在Poop
上调用ac
方法时,我们发现它是一个Animal
大便,你的鞋子很干净。这是因为再一次,编译器说变量ac
的类型是Animal
,你这么说。因此,它将调用Animal
类型中的方法。
我希望这对你来说足够清楚了。
编辑:
我通过这种方式考虑这一点:Cat x;
是一个类型为Cat
的框。该框不包含cat,但它的类型为Cat
。这意味着该框具有类型,无论其内容如何。现在,当我在其中存储一只猫:x = new Cat();
时,我在其中放置了Cat
类型的对象。所以我把一只猫放进了猫咪盒子里。但是,当我创建一个方框Animal x;
时,我可以将动物存放在此框中。所以当我把Cat
放在这个盒子里面时,没关系,因为它是一种动物。所以x = new Cat()
将Cat存储在Animal框中,这没关系。
答案 4 :(得分:3)
包含初始化的声明,例如Animal joesPet = new Cat()
,可以有两个目的:
创建一个标识符,在整个范围内始终代表相同的东西。
创建一个最初会保留一件事的变量,但稍后可能会保留其他内容。
初始化父类型变量以引用子类型实例的声明通常用于第二个目的,在变量最初分配给特定子类型的实例但稍后可能需要保留的情况下引用不属于该子类型的东西。如果声明是Cat joesPet = new Cat();
或var joesPet = new Cat();
,那么(无论好坏)也不可能说joesPet = new Dog();
。如果代码不应该能够说出joesPet = new Dog();
,那么声明为Cat
或var
的事实会阻止这一点,这将是一件好事。另一方面,如果代码可能需要joesPet
不是Cat
,那么它应该以允许的方式声明变量。
答案 5 :(得分:1)
原因是多态性。
Animal A = new Cat();
Animal B = new Dog();
如果Func采用Animal
和Animal
实施MakeNoise()
:
Func(A);
Func(B);
...
void Func(Animal a)
{
a.MakeNoise();
}
答案 6 :(得分:1)
简单回答:如果您使用界面或基类动物,您可以编写可以采用所有类型动物而不是一种动物的通用方法。
请参阅Why use an interface when the class can directly implement the functions 。
答案 7 :(得分:1)
我也使用过那种模式,在一些更高级的环境中,但也许值得一提。在编写运行服务/存储库或实现接口的任何类的单元测试时,我经常使用接口而不是具体类型键入其变量:
IRepository repository = new Repository();
repository.Something();
Assert.AreEquals(......);
我认为这个特殊情况是将变量作为接口类型的更好选择,因为它有助于作为附加检查,接口实际上已正确实现。由于最有可能在实际代码中我不会直接使用具体类,我觉得最好有这个额外的验证。
答案 8 :(得分:0)
如果您正在编写一个模仿动物行为的程序,那么所有动物都有共同点。他们走路,吃饭,呼吸,消除等等。他们吃什么,走路怎么样,都是不同的。
所以你的程序知道所有动物都做了一些事情,所以你编写了一个名为Animal
的基类来完成所有这些事情。所有动物都做同样事情(呼吸,消除)你可以在基类中编程。然后,在子类中,您编写的代码处理他们所做的事情,但与其他动物的做法不同,例如他们吃什么以及他们如何走路。
但控制每只动物行为方式的逻辑并不关心它们如何做任何事情的细节。 &#34;大脑&#34;动物只知道它是吃饭,走路,呼吸或消除的时间。所以它调用在Animal类型的变量上执行这些操作的方法,最终调用正确的方法,具体取决于它所引用的对象的实际动物类型。