【问题标题】:How to use interface as a method parameter but still access non-interface methods?如何使用接口作为方法参数但仍然访问非接口方法?
【发布时间】:2015-10-03 12:47:30
【问题描述】:

我花了几个小时试图找到一种方法来实现这一点,但到目前为止我还没有找到一个好的解决方案,所以我希望有人可以帮助我指出正确的方向。

我目前有一个 C# Winforms 项目,该项目有一个带有多个子类的抽象基类。大多数方法是相同的,但每个子类都有一些特定的附加方法。

我希望能够声明一次使用的类的类型并将该类型传递给我的所有其他方法,而不必通过“如果 Class Type=A Do This Else If Class Type=”来手动检查任何地方的类型B 这样做,等等”。

问题是我需要传递基类或接口来完成这个。但是,这样做意味着我无法再访问特定于子类的属性或方法,并且我不知道如何修复/解决此问题。

这是我正在尝试做的一个简化示例:

接口和类结构

public interface IAnimal
    {
        string NameOfAnimal { get; set; }

        void Eat();
    }

public abstract class Animal : IAnimal
    {
        public abstract string NameOfAnimal { get; set; }

        public abstract void Eat();
    }

public class Bird : Animal
{
    public Bird()
    {
        NameOfAnimal = "Bob the Bird";
        NumberOfFeathers = 100;
    }

    // Interface members
    public override string NameOfAnimal { get; set; }

    public override void Eat()
    {
        System.Windows.Forms.MessageBox.Show("Eating like a Bird");
    }

    // Bird specific properties and methods
    public int NumberOfFeathers { get; protected set; }
    public string SomeOtherBirdSpecificProperty { get; protected set; }

    public void Fly()
    {
        // Fly like a bird
    }
}

public class Fish : Animal
{
    public Fish()
    {
        NameOfAnimal = "Jill the Fish";
        DoesFishHaveSharpTeeth = true;
    }

    // Interface members
    public override string NameOfAnimal { get; set; }

    public override void Eat()
    {
        System.Windows.Forms.MessageBox.Show("Eating like a Fish");
    }

    // Fish specific properties
    public bool DoesFishHaveSharpTeeth { get; protected set; }
    public string SomeOtherFishSpecificProperty { get; protected set; }
}

主窗体代码

    private void button1_Click(object sender, EventArgs e)
    {
        IAnimal myCustomAnimal = null;
        string animalTheUserSelected = "Bird";

        // Only want to manually specify this once
        switch (animalTheUserSelected)
        {
            case "Bird":
                myCustomAnimal = new Bird();
                break;

            case "Fish":
                myCustomAnimal = new Fish();
                break;

            default:
                break;
        }

        DoSomethingWithACustomAnimal(myCustomAnimal);
    }

    private void DoSomethingWithACustomAnimal(IAnimal myAnimal)
    {
        // This works fine
        MessageBox.Show(myAnimal.NameOfAnimal);
        myAnimal.Eat();

        // This doesn't work
        MessageBox.Show(myAnimal.NumberOfFeathers);
        myAnimal.Fly();
    }

我明白为什么我在主表单代码中遇到这个问题...编译器还不知道传递给它的是哪种类型的动物,所以它不知道要显示什么。但是,我不知道该怎么做才能解决这个问题。

我试过了:

  • 将所有动物特有的属性放在界面中。这可行,但违反了几个 OOP 原则。鱼没有羽毛等,因此这些特定属性不属于那里。

  • 通过类似“If Type=Fish do abc Else If Type=Bird do def”之类的方式手动检查各处的类型。这也有效,但违反了 DRY 原则,因为我到处都在重复自己。还有很多使用动物的方法,这将是未来维护的噩梦。

  • 将 IAnimal 显式转换为特定动物,例如 ((Bird)myCustomAnimal).NumberOfFeathers。这也有效,但我不知道在编译时使用什么强制转换。在用户在运行时选择动物之前,我们不会知道这一点。

所以我只是想知道如何解决这个问题?

更具体地说,我想知道如何重新设计上述代码,以便两者兼得:

A) 只显式声明一种动物类型并在任何地方传递该类型(无需在每个方法中进行大量手动检查以查看它是什么类型)

还有

B) 不知何故,我仍然可以在需要时手动访问特定于动物的属性,例如 someBird.NumberOfFeathers。

有什么想法吗?

【问题讨论】:

  • 您确实需要考虑您的结构实际上应该是什么。当您遇到此类问题时,不妨坐下来思考一下基本属性是什么,以及如何将它们组合到一个或多个界面中。

标签: c# oop


【解决方案1】:

这实际上取决于您正在尝试做什么以及您希望实现什么。到目前为止,您尝试的所有方法都是可能的方法,但是如果没有更多上下文,很难给出一般性答案。这取决于您尝试执行的操作的正确抽象级别。

要记住的一点是,您可以让一个类实现您需要的任意数量的接口。所以你可以这样做:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Eat();
}

public interface IFly
{
    void Fly();
}

public interface IHaveFeathers
{
    int NumberOfFeathers { get; set; }
}

那你Bird类可以:

public Bird : Animal, IFly, IHaveFeathers
{
    // implementation
}

现在您可以在一个方法中执行以下操作:

private void DoSomethingWithACustomAnimal(IAnimal myAnimal)
{
    // This works fine
    MessageBox.Show(myAnimal.NameOfAnimal);
    myAnimal.Eat();

    var feathered = myAnimal as IHaveFeathers;
    if (feathered != null)
    {
        MessageBox.Show(feathered.NumberOfFeathers);
    }

    var flier = myAnimal as IFly;
    if (flier != null)
    {
        flier.Fly();
    }
}

要考虑的另一件事是如何将您需要的内容抽象到更高的层次。所以你需要Fly,但为什么呢?不能飞的Animal 会发生什么? Fly 你真的只是想Move 吗?那么也许你可以这样做:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Eat();
    void Move();
}

在您的Bird 中,您可以这样做:

public Bird : Animal, IFly, IHaveFeathers
{
    public override void Move()
    {
        Fly();
    }

    public void Fly()
    {
        // your flying implementation
    }
    // rest of the implementation...
}

【讨论】:

  • 这最终为我解决了。我不能在这里发布我更新的代码。但关键是你的其他 cmets 对结构的思考。我最终重构了我的设计几次,现在我在那个动物类中拥有了大部分动物特定的东西。我还做了 IAnimalSettings 之类的接口,然后是 IBirdSettings 之类的特定接口:IAnimalSettings 等。更进一步,我按照您所说的做了,因此 IAnimal 类具有诸如 TakeAction()、Move() 等公共方法,然后每个动物都会覆盖它们.所以 Bird Flys()、Fish Swims()、Bee TakeAction() 变成了 MakeHoney() 等等。谢谢。
【解决方案2】:

一种解决方案是使用visitor pattern 来定义您要对动物实例执行的操作。

首先,您将定义一个访问者界面,该界面为您在层次结构中拥有的每种动物提供一种方法。

public interface IAnimalVisitor
{
    void VisitBird(Bird bird);
    void VisitFish(Fish fish);
}

然后你需要修改你的动物类和接口以包含一个接受访问者的方法,如下所示:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Accept(IAnimalVisitor visitor);
}

您的实际动物类现在看起来像这样:

public class Bird : IAnimal
{
    public Bird()
    {
        NameOfAnimal = "Bob the Bird";
        NumberOfFeathers = 100;
    }

    public string NameOfAnimal { get; set; }
    public int NumberOfFeathers { get; protected set; }

    public void Accept (IAnimalVisitor visitor)
    {
        visitor.VisitBird(this);
    }
}

public class Fish : IAnimal
{
    public Fish()
    {
        NameOfAnimal = "Jill the Fish";
        DoesFishHaveSharpTeeth = true;
    }

    public string NameOfAnimal { get; set; }
    public bool DoesFishHaveSharpTeeth { get; protected set; }

    public void Accept (IAnimalVisitor visitor)
    {
        visitor.VisitFish(this);
    }
}

现在,对于您想要对每只动物执行的任何操作,您都需要定义IAnimalVisitor 接口的实现。在您的示例中,您显示了显示有关动物信息的消息框,因此执行该操作的 IAnimalVisitor 接口的实现可能如下所示:

public class AnimalMessageBoxes : IAnimalVisitor
{
    private void VisitAnimal(IAnimal animal)
    {
        MessageBox.Show(animal.NameOfAnimal);
    }

    public void VisitBird(Bird bird)
    {
        visitAnimal(bird);
        MessageBox.Show(bird.NumberOfFeathers);
    }

    public void VisitFish(Fish fish)
    {
        visitAnimal(fish);
        MessageBox.Show(fish.DoesFishHaveSharpTeeth);
    }
}

现在您只需将访问者传递给您的动物,就会显示正确的信息。您的事件处理代码现在看起来像这样:

string animalTheUserSelected = "Bird";
IAnimal myCustomAnimal = null;

switch (animalTheUserSelected)
{
    case "Bird":
        myCustomAnimal = new Bird();
            break;

    case "Fish":
        myCustomAnimal = new Fish();
            break;

     default:
        break;
}

AnimalMessageBoxes msgBoxes = new AnimalMessageBoxes();
myCustomAnimal.Accept(msgBoxes);

如果您想对您的动物实例执行其他操作(将它们保存到文件、生成 UI、播放声音...),您只需要定义一个新的 IAnimalVisitor 实现来提供您想要的行为。

为了平衡,我会说这可能不是一个合适的设计,因为它增加了一些额外的复杂性;每个“操作”都需要您实现一个访问者,而将另一种动物添加到您的层次结构中需要您更新访问者界面及其所有实现以适应新情况。

根据您的观点,这可能是好是坏。有些人认为上述几点是不好的,并且是避免访问者模式并使用已经建议的其他方法的原因。其他人(比如我)认为以上几点是一件好事;现在,只有当您为层次结构中的每个动物提供实现并且您的操作被分成小的专用类时,您的代码才会编译。

我的建议是尝试我提供的 SSCCE 并进一步研究访问者模式以确定此解决方案是否可以满足您的要求。

【讨论】:

  • 我喜欢你的榜样。我确实玩过它,它似乎是一个非常强大的模式。然而,它也增加了复杂性。就我而言,通过采纳其他一些建议并适当地重构,我能够消除进行大量转换的需要,因此额外的复杂性似乎不值得。我已将其保存以供将来参考,因此我可以在更复杂的项目中使用它:)
【解决方案3】:

我明白,你想要达到什么目的,但你的方法是错误的。

这样想:你的“主要”类/表单真的需要知道是鸟还是狗吗?答案是不”。您通过接口公开了一些常见属性(我建议在这里使用基类!)。其他一切都特定于给定的动物。最简单的方法是使用 DoAnimalSpecificStuff() 方法扩展您的界面 - 这将执行特定的操作。

说到演示,你应该看看 MVP 和 MVVM 模式。

附:使用工厂模式创建动物!

【讨论】:

  • 您的建议适用于改变动物状态的方法,但如果您想做一些事情,例如将动物信息保存到文件、在消息框中显示或发送电子邮件,则可能违反最佳实践.恕我直言,Animal 类具有 SaveDisplaySendEmail 方法是不合适的。如果您使用演示(或其他)模式来包装 Animal 类以提供这些功能,您最终会遇到同样的问题,即必须使用 if 语句和强制转换来对任意动物对象执行操作。
  • 这是错误的。显示:查看 - 特定于动物(您不能显示一只猫并假装它是一只狗)。保存:基于 IAnimal - 序列化(用于保存)是特定的(ISerializable)。邮件和其他东西也是如此。另外:永远不要使用“if”语句,在涉及类型时使用 AOP 或泛型
  • Animal 类型的变量可以引用CatDog。如果要执行特定于所引用动物的实际运行时类型的操作,您可以使用多态性并在 Animal 基类中提供一个方法来执行操作(如您所建议的那样),或者您可以提供一个单独的类来执行操作。如果该方法做了一些动物类不应该知道的事情,例如生成 UI 或发送电子邮件,那么该方法不属于 Animal 类,应该放在单独的类中。
  • 续:如果该方法被放置在一个单独的类中并且必须对 any 动物类型进行操作,那么您将不得不求助于使用强制转换和 if 语句来访问子类特定的方法和属性。您可以提供接受Animal 子类作为参数的重载方法,但您只是将确定动物类型的问题转移到调用该方法的代码中。 在这些情况下消除强制转换的唯一方法是使用我建议的访问者模式。正如我在帖子中所指出的,尽管这种模式确实有它自己的问题。
  • 一个类不必简单明了。类并不意味着是纯粹的容器。它们可能包含方面甚至行为。这些可以放在基类中或通过接口公开。利用服务(基于这些模式)将具体用例变为现实。
【解决方案4】:

如果您想访问特定的属性/方法,您必须将 IAnimal 对象显式转换为特定类型。

您可以使用as 运算符,然后检查转换是否成功,例如:

Bird b = myAnimal as Bird;
if(b != null)
{
     MessageBox.Show(b.NumberOfFeathers);
}

【讨论】:

  • 哦,好主意。我明白你的意思,但我唯一担心的是这将是很多重复的编码。如果我有很多动物和很多使用它们的方法,我将不得不到处检查它们,看看我能对每个动物对象做什么,不能做什么。你知道我可以设置我的代码的任何其他方法,所以我不会遇到这个问题吗?我不确定是否有任何设计模式或我可以阅读的任何内容可能会给我提供更好的设计方式?
  • 问题是您正在组合相似的属性,并且您仍然想访问独特的属性。没有其他方法,要使用特定属性,您必须使用特定类型而不是基本类型。
  • @Calcolat If I have lots of animals and lots of methods that use them,,这就是你的问题。为什么你有很多动物和很多其他类中存在的方法?你能把需要访问Bird(而不是Animal)属性和方法的部分放在insideBird吗?不需要暴露的就不要暴露。
猜你喜欢
  • 2013-01-24
  • 2021-02-20
  • 1970-01-01
  • 2017-03-05
  • 2020-07-12
  • 1970-01-01
  • 2011-02-04
  • 1970-01-01
  • 2012-08-18
相关资源
最近更新 更多