创建类型为 <基类> 的变量以在 C# 中存储<派生类>对象
本文关键字:存储 派生 对象 变量 类型 基类 创建 | 更新日期: 2023-09-27 17:58:58
我对编程有点陌生,我对 C# 中的类、继承和多态性有疑问。在学习这些主题时,我偶尔会遇到如下所示的代码:
Animal fluffy = new Cat(); // where Animal is a superclass of Cat*
这让我感到困惑,因为我不明白为什么有人会创建一个 Animal 类型的变量来存储 Cat 类型的对象。 一个人为什么不简单地写这个:
Cat fluffy = new Cat();
我确实理解为什么将子对象存储在父类型变量中是合法的,但不理解为什么它很有用。是否有充分的理由将Cat
对象存储在Animal
变量中而不是Cat
变量中? 一个人可以给我举个例子吗? 我确信它与多态性和方法覆盖(和/或方法隐藏(有关,但我似乎无法理解它。 提前感谢!
我能给你的最短的例子是,如果你想要一个所有动物的列表
List<Animal> Animals = new List<Animal>();
Animals.Add(new Cat());
Animals.Add(new Dog());
如果您曾经使用 WinForms 创建过项目,那么您已经使用了类似的东西,因为所有控件都派生自 Control
.然后,您会注意到窗口有一个控件列表(this.Controls
(,允许您一次访问窗口上的所有子控件。即隐藏所有控件。
foreach(var control in this.Controls)
control.Hide();
但不是为什么它有用。
看看一些更好的例子:
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();
在实际代码中远不常见(但它确实发生了(。
考虑一下,显示某些功能如何工作的非常简化的代码并不总是擅长演示该功能的原因。
让我们看看一个实用但极端的例子。
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 if (("吗?
方法
2:更好的方法
class Person
{
public Animal myPet;
}
这样,由于多态性,我们对人的宠物有了我们唯一而独特的参考,为了得到宠物,我们简单地写:
return myPet;
那么,最好的做事方式是什么?方法 1 或 2 ?
初始化的声明(如 Animal joesPet = new Cat()
(可以有两个用途:
- 创建一个标识符,
该标识符将在其整个范围内始终表示相同的内容。
创建一个变量,该变量最初将保存一件事,但以后可能会保存其他东西。
声明通常用于第二个目的,在这种情况下,变量最初被分配给特定子类型的实例,但以后可能需要保存对不属于该子类型的事物的引用。 如果声明是Cat joesPet = new Cat();
或var joesPet = new Cat();
,那么(无论好坏(都不可能说joesPet = new Dog();
。 如果代码不能说joesPet = new Dog();
,那么声明为Cat
或var
会阻止这一事实将是一件好事。 另一方面,如果代码可能需要joesPet
不是Cat
,那么它应该以允许的方式声明变量。
由于还没有回答,我将尝试给出一个尽可能好的答案。
看看下面的程序:
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!
您可以看到我们创建了一个类型为 Animal
的变量a
。它指向类型 Animal
的对象。它具有静态和运行时类型Animal
。
接下来,我们创建指向Cat
对象的变量Cat
变量。第三个对象是棘手的部分。我们创建一个 Animal
变量,它的运行时类型 Cat
,但静态类型Animal
。为什么这很重要?因为在编译时,编译器知道变量ac
实际上是 Animal
的类型。毫无疑问。因此,它将能够执行Animal
对象可以执行的所有操作。
但是,在运行时,已知变量中的对象是Cat
。
为了演示,我创建了 9 个函数调用。
首先,我们将对象传递给 Animal
的实例。此对象具有一个获取Animal
对象的方法。
这意味着在Noise()
中,我们可以利用Animal
类具有的所有方法和字段。没有别的。因此,如果Cat
有一个方法Miauw()
,我们将无法在不将我们的动物投Cat
的情况下调用它。(类型转换很脏,尽量避免它(。因此,当我们执行这 3 个函数调用时,我们将打印 Animal making noise!
三次。清楚。那么我的静态类型有什么关系呢?
好吧,我们一会儿就到那里。
接下来的三个函数调用是 Cat
对象内的方法。Cat
对象有两个方法Noise()
。一个拿Animal
,另一个拿Cat
。
所以首先我们给它传递一个常规Animal
.运行时将查看所有方法,并看到它有一个方法Noise
需要Animal
。正是我们所需要的!所以我们执行那个,我们打印Animal
制造噪音。
下一个调用传递一个包含Cat
对象的Cat
变量。同样,运行时将查看。我们是否有一个接受Cat
的方法,因为这是我变量的类型。是的,是的,我们愿意。所以我们执行该方法并打印"Cat making noise".
第三次调用,我们有变量 ac
,它是 Animal
的类型,但指向一个 Cat
类型的对象。我们将看一看,看看我们是否能找到适合我们需求的方法。我们看一下静态类型(即变量的类型(,我们看到它是Animal
类型,所以我们调用了Animal
作为参数的方法。
这是两者之间的微妙区别。
接下来,大便。
所有动物都会拉屎。但是,Cat
在您的鞋子上大便。因此,我们重写基类的方法并实现它,以便Cat
在您的鞋子中拉屎。
您会注意到,当我们调用动物Poop()
时,我们得到了预期的结果。Cat c
也是如此.但是,当我们在ac
上调用Poop
方法时,我们看到这是一种Animal
便便,并且您的鞋子很干净。这是因为编译器再次说我们的变量ac
的类型是Animal
,你这么说。因此,它将调用类型 Animal
中的方法。
我希望这对你来说足够清楚。
编辑:
我通过这样思考来记住这一点:Cat x;
是一个具有 Cat
类型的盒子。盒子里没有猫,但是,它是 Cat
型的。这意味着盒子有一个类型,无论它的内容如何。现在,当我将一只猫存放在里面时:x = new Cat();
,我在里面放了一个类型为 Cat
的对象。所以我把一只猫放在猫箱里。但是,当我创建一个盒子Animal x;
我可以将动物存放在这个盒子里。所以当我把Cat
放进这个盒子里时,没关系,因为它是一种动物。所以x = new Cat()
把一只猫放在动物盒子里,这没关系。
原因是多态性。
Animal A = new Cat();
Animal B = new Dog();
如果 Func 采用Animal
并Animal
实现MakeNoise()
:
Func(A);
Func(B);
...
void Func(Animal a)
{
a.MakeNoise();
}
简单的答案:如果你使用接口或基类动物,你可以编写可以接受所有类型的动物而不是只接受一种动物的泛型方法。
请参阅 当类可以直接实现函数时,为什么要使用接口 。
更高级的上下文中使用了几次这种模式,但也许值得一提。在编写执行服务/存储库或任何实现接口的类的单元测试时,我经常使用接口而不是具体类型键入其变量:
IRepository repository = new Repository();
repository.Something();
Assert.AreEquals(......);
我认为这种特殊情况是将变量作为接口类型的更好选择,因为它有助于额外检查接口实际上是否正确实现。在实际代码中,我很可能不会直接使用具体类,因此我发现最好进行一点额外的验证。
如果你正在编写一个模仿动物行为的程序,那么所有动物都有共同点。 他们走路,他们吃饭,他们呼吸,他们消除,等等。 他们吃什么和走路的方式等是不同的。
所以你的程序知道所有动物都会做一些事情,所以你写了一个名为Animal
的基类来做所有这些事情。 所有动物都做同样的事情(呼吸,消除(,你可以在基类中编程。 然后,在子类中,您编写代码来处理它们都做的事情,但与其他动物不同,例如它们吃什么和如何走路。
但是控制每只动物行为的逻辑并不关心它们如何做任何事情的细节。 动物的"大脑"只知道是时候吃东西、走路、呼吸或消除。因此,它调用在 Animal 类型的变量上执行这些操作的方法,最终根据它所引用的对象的实际 Animal 类型调用正确的方法。