创建类型的变量以在C#中存储对象

我对编程有些新意,我对C#中的类,inheritance和多态有疑问。 在了解这些主题的同时,我偶尔会遇到类似这样的代码:

Animal fluffy = new Cat(); // where Animal is a superclass of Cat* 

这让我感到困惑,因为我不明白为什么有人会创建一个类型为Animal的变量来存储Cat类型的对象。 为什么一个人不会简单地写这个:

 Cat fluffy = new Cat(); 

我确实理解为什么将子对象存储在父类型变量中是合法的,但不是为什么它有用。 是否有充分的理由将Cat对象存储在Animal变量与Cat变量中? 一个人可以举个例子吗? 我确定它与多态和方法覆盖(和/或方法隐藏)有关,但我似乎无法绕过它。 提前致谢!

我能给你的最简单的例子是你想要一份所有动物的清单

  List Animals = new List(); Animals.Add(new Cat()); Animals.Add(new Dog()); 

如果您曾使用Winforms创建项目,那么您将使用类似的东西,因为所有控件都来自Control 。 然后您会注意到一个Window有一个控件列表( this.Controls ),它允许您一次访问窗口上的所有子控件。 IE隐藏所有控件。

  foreach(var control in this.Controls) control.Hide(); 

但不是为什么它有用。

看看一些更好的例子:

 Cat myCat = new Cat(); Dog myDog = new Dog(); List 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(); 在实际代码中远不常见(但确实会发生)。

考虑一下非常简化的代码,它显示某些function的工作原理,并且总能很好地展示该function的原因

让我们看一个实际但极端的例子。

 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类中编写所有变量的人,并在你的应用程序的每个地方添加所有那些’if 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(); 那么宣称为Catvar的事实会阻止这一点,这将是一件好事。 另一方面,如果代码可能需要让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! 

您可以看到我们创建了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 。 运行时将查看所有方法,并看到它有一个接收Animal的方法Noise 。 正是我们需要的! 所以我们执行那个,我们打印Animal制造噪音。

下一个调用传递一个包含Cat对象的Cat变量。 再次,运行时将看一看。 我们有一个接受Cat的方法,因为这是我变量的类型。 是的,是的,我们这样做。 所以我们执行方法并打印"Cat making noise".

第三个调用,我们有变量ac ,它是Animal类型,但是指向Cat类型的对象。 我们来看看是否能找到适合我们需求的方法。 我们看一下静态类型(即变量的类型),我们看到它是Animal类型,所以我们调用具有Animal作为参数的方法。

这是两者之间的微妙差异。

接下来,大便。

所有动物大便。 然而,一只Cat在你的鞋子里屎。 因此,我们覆盖基类的方法并实现它,以便让Cat在你的鞋子里大便。

您会注意到,当我们在Animal上调用Poop() ,我们会得到预期的结果。 Cat c 。 然而,当我们在ac上调用Poop方法时,我们发现它是一个Animal大便,你的鞋很干净。 这是因为再一次,编译器说我们的变量ac的类型是Animal ,你这么说。 因此,它将调用Animal类型中的方法。

我希望这对你来说已经足够清楚了。

编辑:

我通过这种方式考虑这一点,牢记这一点: Cat x; 是一个类型Cat的盒子。 该盒子不包含猫,但它是Cat型。 这意味着该框具有类型,无论其内容如何。 现在当我在里面存放一只猫时: x = new Cat(); ,我在它里面放了一个类型为Cat的物体。 所以我把一只猫放进了猫咪盒子里。 但是,当我创建一个盒子Animal x; 我可以在这个盒子里存放动物。 所以,当我把一只Cat放在这个盒子里时,没关系,因为它是一种动物。 所以x = new Cat()将Cat存储在Animal框中,这没关系。

原因是多态性。

 Animal A = new Cat(); Animal B = new Dog(); 

如果Func采用AnimalAnimal实现MakeNoise()

 Func(A); Func(B); ... void Func(Animal a) { a.MakeNoise(); } 

简单回答:如果您使用界面或基类动物,您可以编写通用方法,可以采用所有类型的动物而不是只有一种。

请参阅为什么在类可以直接实现函数时使用接口 。

我也使用过这种模式,在一些更高级的环境中,但也许值得一提。 在编写运行服务/存储库或实现接口的任何类的unit testing时,我经常使用接口而不是具体类型键入其变量:

 IRepository repository = new Repository(); repository.Something(); Assert.AreEquals(......); 

我认为这种特殊情况是将变量作为接口类型的更好选择,因为它有助于作为附加检查,接口实际上已正确实现。 最有可能在实际代码中我不会直接使用具体类,我觉得最好有这个额外的validation。

如果您正在编写一个模仿动物行为的程序,那么所有动物都有共同点。 他们走路,吃饭,呼吸,消除等等。他们吃什么,走路怎么样,都是不同的。

所以你的程序知道所有动物都做了一些事情,所以你写了一个名为Animal的基类来完成所有这些事情。 所有动物都做同样事情(呼吸,消除)你可以在基类中编程。 然后,在子类中,您编写的代码处理他们所做的事情,但与其他动物的做法不同,例如他们吃什么以及他们如何走路。

但控制每只动物行为方式的逻辑并不关心它们如何做任何事情的细节。 动物的“大脑”只知道吃饭,走路,呼吸或消除的时间。 因此,它调用在Animal类型的变量上执行这些操作的方法,最终根据它所引用的对象的动物实际类型调用正确的方法。