你能用一个实际的例子帮我理解抽象类和接口的使用吗?

5
你能给我提供一个非常简单易懂的抽象类与继承使用的理解,并帮助我真正理解这个概念和如何实现吗?我正在尝试完成一个项目,但不知道如何实现。我一直在和我的教授聊天,但几乎总是被斥责,他说如果我无法弄明白,我可能还没有准备好学习这门课程。我已经涵盖了前置课程,但仍然很难理解这些概念。
为了澄清问题,我已经完成的项目如下所示。我尚未填写狗/猫等类的内容。你能给我一些指针吗?我不是在求别人给我“答案”,我只是迷失在这个领域中。我参加的是在线课程,他与我交流的方式让我感到困扰。我刚刚以全A的成绩通过了其他所有课程,所以我愿意付出努力,但我在理解这些概念和如何实际应用它们方面感到困惑。
请问有什么评论或帮助能让我在这个项目中取得进展吗?
我要实现的描述如下:
概述:
本次练习的目的是演示接口、继承、抽象类和多态的使用。您的任务是采取提供的程序框架,添加适当的类和相应的类成员/方法,以使该程序能够正常运行。您不能更改任何提供的代码,只能添加您编写的类。虽然有许多方法可以使程序正常工作,但您必须使用展示接口、继承、抽象类和多态使用的技术。再次强调,您可以在提供的代码中进行添加,但不能更改或删除其中任何内容。所提供的代码将与非常少量的附加代码一起工作,并满足练习的要求。
如果您成功完成任务,程序运行时应输出以下语句:
我的名字是斯波特,我是一只狗
我的名字是费利克斯,我是一只猫
要求:
1)您必须拥有一个名为“Animal”的抽象基类,从中派生出Dog和Cat类。
2)Animal基类必须从接口“IAnimal”派生,它是应该从IAnimal派生的唯一类。
3)由于所有动物都有一个名称,而且名称不是特定于狗或猫的属性,因此Animal基类应该是存储名称和实现WhatIsMyName get-property的位置。
4)您需要创建一个仅从Animal基类派生的Dog和Cat类。
5)Dog和Cat类应实现WhatAmI get-property,并返回适当的字符串值。
using System;

namespace IT274_U2
{
    public interface IAnimal
    {
        string WhatAmI { get; }
        string WhatIsMyName { get; }
    }

    public class TesterClass
    {
        public static void DescribeAnimal(IAnimal animal)
        {
            Console.WriteLine("My name is {0}, I am a {1}", animal.WhatIsMyName, animal.WhatAmI);
        }

        static void Main(string[] args)
        {
            Dog mydog = new Dog("Spot");
            Cat mycat = new Cat("Felix");
            DescribeAnimal(mydog);
            DescribeAnimal(mycat);
        }
    }
}

我已经编写的代码:

using System;


namespace IT274_U2
{
    public interface IAnimal
    {
        string WhatAmI { get; }
        string WhatIsMyName { get; }
    }


    public class Dog
    {
        public abstract string WhatAmI
        {
            get;
            set;
        }
    }//end public class Dog

    public class Cat
    {
    public abstract string WhatIsMyName  
    {
        get;
        set;
    }
    }//end public class Cat

    public abstract class Animal : IAnimal
    {
    // fields
    protected string Dog;
    protected string Cat;

                  // implement WhatIsMyName 

    //properties
    public abstract String Dog
    {
        get;  
        set;
    }
    public abstract String Cat
    {
        get;
        set;
    }
    public abstract string WhatIsMyName();

    } //end public abstract class Animal


    public class TesterClass
    {
        public static void DescribeAnimal(IAnimal animal)
        {
            Console.WriteLine("My name is {0}, I am a {1}", animal.WhatIsMyName, animal.WhatAmI);
        }

        static void Main(string[] args)
        {

            Dog mydog = new Dog("Spot");
            Cat mycat = new Cat("Felix");
            DescribeAnimal(mydog);
            DescribeAnimal(mycat);
        }
    }
}

您发布的代码示例是不允许修改的代码吗? - Mike Powell
@Mike - 我正要问这个问题 :) - Russ Cam
我已经更新了上面的问题,加入了教授给出的代码和我自己编写的代码。谢谢你提供的帮助,我甚至没有考虑到这一点。他确实告诉我大致的轮廓是正确的,但是他很快就结束了会话,只是简单地看了一下。 - sheldonhull
我强烈推荐《Head First设计模式》这本书,它是一个很好的了解抽象类和接口之间差异的方式 - http://www.amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/0596007124/ref=pd_bbs_3?ie=UTF8&s=books&qid=1236620677&sr=8-3 - Russ Cam
我订购了这本书以及 C# Head First 书籍... 感谢你们的推荐,我认为它们会有所帮助。 - sheldonhull
显示剩余3条评论
9个回答

6

编辑:

我已将每个类的代码主体提取出来 - 如果您想查看我的答案,请查看编辑修订版:)

首先,我们定义了接口

public interface IAnimal
{
    string WhatAmI { get; }
    string WhatIsMyName { get; }
}

任何实现此接口的类都必须实现这些属性。接口就像一份合同;实现接口的类同意提供接口方法、属性、事件或索引器的实现。
接下来,我们需要定义您的抽象Animal类。
public abstract class Animal : IAnimal
{
    //Removed for Training, See Edit for the code
}

这个类被声明为 抽象类,意味着该类只能作为其他类的基类。我们实现了接口的属性,并定义了一个私有字段来存储动物名称。此外,我们将 WhatAmI 属性访问器声明为抽象的,以便在每个派生类中实现自己特定的属性访问逻辑,并定义了一个构造函数,接受一个字符串参数并将其赋值给 _name 私有字段。

现在,让我们定义我们的 CatDog 类。

public class Dog : Animal
{
    //Removed for Training, See Edit for the code
}

public class Cat : Animal
{
    //Removed for Training, See Edit for the code
}

两个类都继承自Animal,每个类都有一个构造函数,定义了一个字符串参数,并将该参数作为参数传递给基础构造函数。此外,每个类实现了自己的属性访问器WhatAmI,分别返回它们的类型的字符串。

其余代码不变。

public class Program
{
    public static void DescribeAnimal(IAnimal animal)
    {
        Console.WriteLine("My name is {0}, I am a {1}", animal.WhatIsMyName, animal.WhatAmI);
    }

    static void Main(string[] args)
    {
        Dog mydog = new Dog("Spot");
        Cat mycat = new Cat("Felix");
        DescribeAnimal(mydog);
        DescribeAnimal(mycat);
        Console.ReadKey();
    }
}

静态方法DescribeAnimal接受一个IAnimal作为参数,并输出传入的IAnimal对象的WhatIsMyNameWhatAmI属性访问器返回的值。由于Animal实现了IAnimalDogCat都是从Animal继承而来,任何CatDog对象都可以作为参数传递给DescribeAnimal方法。
希望我已经清楚地解释了这个问题,如果有人觉得我的措辞需要精练一些,请评论并我很乐意修改我的回答。

1
很好的东西 :) 如果您希望我删除我的答案,或者在您得到解决方案后再回来,请告诉我。 - Russ Cam
顺便说一句,感谢您删除了所有多余的代码,这很有帮助。我会仔细阅读它,因为我已经花了整整一个小时还是感到沮丧,试图通过所有这些答案并理解而不是只是从代码中“获取”答案。 - sheldonhull
1
我强烈推荐《Head First设计模式》或《Head First C#》。这两本书都写得很好,将每个主题分解成易于消化的块,并提供难忘的示例。 - Russ Cam
《Head First设计模式》是针对Java编写的,但差异很小。 - Russ Cam
感谢大家提交的项目,Russ的回答为我提供了最有用的信息和分解来理解,我通常不会这么迷茫,这是我第一次遇到,但是你们的分解和解释帮助我完成了我的课程,非常感谢。 - sheldonhull
显示剩余2条评论

6
接口和抽象类的主要区别在于,在接口中,您仅定义实现此接口的对象的公共方法和属性。抽象类提供一些基本实现,但具有一些“空白” - 继承者需要实现的抽象方法。
我不会替你完成作业,但是提示:动物类不应包含任何特定于狗和猫的内容。

3
你已经很接近答案了,但是把问题复杂化了。我不想直接给你答案 ;) 但是这里有一些指针。
首先,你正在创建3个类和1个接口。然而,我认为你可能缺少的是需要3种不同类型的对象(从“最不明确”到“最明确”):
1)接口 这是IAnimal - 可以由任何可以像动物一样行事的东西实现
2)抽象基类 这是Animal类 - 任何IS an animal的东西都应该派生自Animal,但这些不能直接创建。如果你假装自己是上帝,你不会创造一个Animal,你会创造一只狗、猫、松鼠或者毛茸茸的兔子
3)动物的具体实现 这些是实际的类本身。这就是你要创建的。在你的情况下是狗或猫。
关键在于你只能创建具体类,但是你可以使用IAnimal或Animal(接口或抽象基类)来操作和处理任何动物(或者在接口的情况下,任何像动物一样的东西)。

非常感谢您提供的详尽答案。我需要指引,而不是答案。我只是有些困惑,这种帮助将大大加快我的进度,而不是放弃!午餐时间即将到来,我会阅读所有的答案,并努力完成我的项目。真的非常感激您的帮助。 - sheldonhull
请重新阅读我发布的内容,并查看您的Dog和Cat类 - 在单个属性上使用“abstract”将使该类成为抽象类,因此您正在强制Dog和Cat成为我的第二类别。请查看C#中的“override”关键字-它可能有助于让您更进一步。 - Reed Copsey

2

一般来说:

  • 接口描述了对象将响应的方法。它是对象承诺满足的合同。

  • 抽象类描述了基本功能,并让子类实现专门功能。

因此,当您希望不同性质的对象响应相同特定方法时,可以使用接口。

当您需要有某个类的专门版本时,可以使用抽象类。

假设您想创建一个系统,任何类型的对象都可以通过唯一的ID进行标识,而且您不关心它们所属的类别。

您可以拥有:

  • 动物

  • 交通工具

  • 计算机设备

  • 其他任何东西

由于它们是无关主题,您可以选择实现一个接口,比如:

public interface IIdentifiable 
{ 

      public long GetUniqueId();
}

所有想要满足这个合约的类都将实现该接口。

public class IPod: IIdentifiable 
{
      public long GetUniqueId() 
      {
           return this.serialNum + this.otherId;
      }
}

public class Cat: IIdentifiable 
{
      public long GetUniqueId()
      { 
           return this.....
      }
}

iPod和猫的本质截然不同,但它们都可以响应“GetUniqueId()”方法,该方法将用于目录系统。

然后可以像这样使用:

    ...

    IIdentifiable ipod = new IPod(); 
    IIdentifiable gardfield = new Cat();

    store( ipod );
    store( gardfield );


    ....
    public void store( IIdentifiable object )  
    {

         long uniqueId = object.GetUniqueId();
        // save it to db or whatever.
    }

另一方面,您可以拥有一个定义对象可能具有的所有常见行为的抽象类,并让子类定义专业版本。

  public abstract class Car 
  {
       // Common attributes between different cars
       private Tires[] tires; // 4 tires for most of them 
       private Wheel wheel; // 1 wheel for most of them.

        // this may be different depending on the car implementation.
       public abstract void move(); 
  }


  class ElectricCar: Car 
  {
      public void move()
      {
         startElectricEngine();
         connectBattery();
         deploySolarShields();
         trasnformEnertyToMovemetInWheels();
      }
  }

  class SteamCar: Car 
  {     
       public void move() 
       {
          fillWithWather();
          boilWater();
          waitForCorrectTemperature();
          keepWaiting();
          releasePreasure....
        }
   }

这里有两种汽车,它们以不同的方式实现“移动”方法,但仍然在基类中共享一些相同的东西。

为了使事情更有趣,这两辆汽车可能还实现了IIdentifiable接口,但这样做只是承诺响应GetUniqueId方法,而不是因为它们是汽车的本质。这就是为什么汽车本身可能不实现该接口。

当然,如果标识可以基于汽车可能具有的通用属性,则GetIdentifiableId可以由基类实现,子类将继承该方法。

// 情况1...每个子类都实现了该接口

   public class ElectricCar: Car, IIdentifiable 
   {
       public void move()
       {
         .....
       }
       public long GetUniqueId() 
       { 
         ....
       }
   }

   public class SteamCar: Car, IIdentifiable 
   {
       public void move()
       {
         .....
       }
       public long GetUniqueId() 
       { 
         ....
       }
  }

案例2,基类实现接口,子类从中受益。
   public abstract class Car: IIdentifiable 
   {
       // common attributes here
       ...
       ...
       ...



       public abstract void move();
       public long GetUniqueId()
       {
          // compute the tires, wheel, and any other attribute 
          // and generate an unique id here.
       }
   }

   public class ElectricCar: Car
   {
       public void move()
       {
         .....
       }
   }

   public class SteamCar: Car
   {
       public void move()
       {
         .....
       }
  }

我希望这可以帮到您。

1
  1. 接口是一份合同。这是您想要描述提供的功能的地方,没有任何实现细节。

  2. 抽象类是一个旨在在其子类之间共享实现细节的类。由于它仅用于代码共享/因素化目的,因此无法实例化。

  3. 您的实际类将从您的抽象类继承,并在需要时使用抽象类中共享的代码来实现其特定于类的功能。


1
说实话,无论这是一道作业题还是其他问题,行业中不知道这个的人数让我感到害怕。因此,我会回答这个问题。
接口抽象了实现,抽象类也是如此。没有“对比”之分,因为你也可以创建一个实现接口的抽象类。所以不要认为它们互相对立。
因此,当你不想让消费者了解太多实现细节时,两者都可以使用。接口在这方面做得更好,因为它没有实现,只声明了消费者可以按哪些按钮、获取哪些值并发送到哪里,而抽象类可能会声明更多(甚至更多!)。因此,如果你只考虑这一点,你只需要接口。因此,第二点:
当你想在两个不同的接口实现之间共享公共代码时,可以使用抽象类。在这种情况下,两个具体的实现都继承自实现接口的抽象类。
一个简单的例子是IDataStore。SavingToATextFile数据存储只是实现IDataStore的一个类。然而,MsSqlDataStore和MySqlDataStore将共享公共代码。它们都继承自实现IDataStore的抽象类SqlDataStore。

0
另一个建议 - (略微偏题,但仍然相关)
我建议为了学习目的,避免使用自动属性。如果你显式地实现它们,这将有助于你理解正在发生的事情。
例如,不要这样做:
class SomeClass
{
   public string MyProperty
   {
       get;
       set;
   }
}

试着自己实现一下:

class SomeClass
{
    public string MyProperty
    {
        get
        {
             return "MyValue"; // Probably a private field
        }
        set
        {
             // myField = value; or something like that
        }
}

我提到这个是因为它将有助于您在这种特定情况下。由于您正在使用自动属性,编译器正在为您“填空”,而在您的情况下,我认为它阻止了您获得一些非常有用的编译器错误。当试图理解这些概念的工作原理时,自己做通常会使事情变得更容易,而不是更难。

0

抽象类:

为派生类建立一个基础,它们为所有派生类提供了一个契约。它强制执行继承关系。

接口:

接口不是一个类,而是方法的定义。

一个类可以继承多个接口,但只能继承一个抽象类。

希望这有所帮助。


一个接口不是一个类,它是方法的定义。哦...我以为它是类的一种类型?你是说它不是吗?那真的帮了很多忙! - sheldonhull
“接口是一种特殊的C#类,支持多态替换和后期绑定而无需继承。”现代软件设计-理查德·韦纳。这似乎与“接口不是类”的说法相矛盾,有何评论? - sheldonhull
@Sheldon 我坚持认为接口不是一个类,而是一个契约。 - CodeMonkey

0
基本上,接口定义了所有实现者必须提供的“契约”(即一组操作/属性)。在这种情况下,IAnimal接口需要WhatAmI和WhatIsMyName属性。抽象类可以提供某些功能,但也会留下一些操作,必须由其子类实现。
在你的例子中,Animal基类能够提供WhatIsMyName功能,因为“名称”是所有动物的属性。然而,它不能提供“WhatAmI”属性,因为只有特定的子类知道它们是什么“类型”。
你发布的示例代码的问题在于,Animal类具有其子类的知识,而不应该有。

我不想点踩因为这是一个技术上正确的答案,但鉴于明显的帖子作者正在尝试理解这个作业问题,我发现你已经给了他大部分答案的事实令人失望。 - Mike Powell
好的观点...虽然我不会复制和粘贴滥用,所以这不应该是一个问题。但我感觉自己得到了太多的帮助。不过我还是会接受任何可以得到的帮助,因为我已经卡了将近两个星期了,一直不理解。 - sheldonhull

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接