【问题标题】:Override base class property to a derived type and access via base class将基类属性重写为派生类型并通过基类访问
【发布时间】:2017-04-05 20:39:48
【问题描述】:

想象以下类:

public abstract class State {}

public abstract class View : MonoBehaviour {
    public State state;
}

现在我想从这两个类中派生出StateAItemA

public class StateA { // some properties }

public class ViewA : View {
    public StateA state;
}

ViewA 应该只接受 StateA 作为其状态。设置一个假设的StateB 应该是不可能的。当我试图设置ViewA的状态时

View view = someGameObject.GetComponent<View>;
view.state = new StateA ();

只会设置基类的状态。在我的情况下,转换为 ViewA 是不可能的,因为它必须动态转换。

现在我想出了两个可能的解决方案。

解决方案 1 - 类型检查和强制转换

所有状态类保持不变。基类正在检查状态的正确类型。

public abstract class View : MonoBehaviour {
    public State state;
    public Type stateType;

    public void SetState (State state) {
        if (state.GetType () == this.stateType) {
            this.state = state;
        }
    }
}

要访问ViewA 中的状态,我这样做:

(StateA)this.state

解决方案 2 - 统一方式

State 将更改为从 MonoBehaviour 派生。

public abstract class State : MonoBehaviour {}

要为项目添加状态,我只需向游戏对象添加一个新的StateA 组件。然后该项目使用 GetComponent 加载该状态以访问它。

viewA.gameObject.AddComponent<StateA> (); // set state with fixed type
viewA.gameObject.AddComponent (type);     // set state dynamically

this.gameObject.GetComponent<StateA> (); // get state in item class

结论

在我看来,这两种解决方案都不理想。你们知道做这种事情的更好方法吗?我希望你明白我想要达到的目标。期待看到您的想法。

编辑

似乎我的问题过于简单化了。为了弄清楚我为什么这样做,可以在 UI 中将Item 视为ViewStateA 没有任何功能。相反,它只是让视图知道它应该做什么的一些属性。例如,对于表格视图,状态将包含要显示的内容的集合。

public class StateA : State {
    SomeObject[] tableViewData; // which data to load into the table view
}

或者换个角度看。

public class StateB : State {
    bool hideButtonXY; //should I hide that button?
}

在另一堂课中,我保留了上次显示视图及其各自状态的历史记录。通过这种方式,我正在从普通的网络浏览器实现一个来回功能。 我希望这能说明我的意图。

【问题讨论】:

  • Item 中使用State 实例时,是否需要知道具体的子类?
  • (如...是什么阻止您让ItemA 存储State 类型的变量,而不是专门存储StateA?)
  • 将特定实现耦合在一起通常不是一个好主意,您能否在示例中说明为什么需要这样做?
  • 我更新了问题以明确我想要实现的目标。

标签: c# inheritance unity3d properties polymorphism


【解决方案1】:

这是一个相当常见的问题,由在 Unity 中需要使用 MonoBehaviours 的奇怪方式和常见的继承错误应用共同造成。这不是您可以解决的问题,而是您为避免而设计的。

这完全取决于您为什么需要 State 的子类,但我假设它类似于:基本 State 有一些基本逻辑,而派生 StateA 有一些新的/修改的属性/逻辑。假设这是真的:

  • 保留所有使用对 State 的相同引用的 Item 子类。因为它是一个子类,所以您可以将它保存到基类型的属性中。
  • 制作一组标准的访问器/字段/方法,它们是 Item 子类将始终访问 State 子类的方式。示例:DoThing()、ProcessState() 等。
  • 使这些访问器/字段/方法具有在密封函数中始终运行的公共代码。
  • 使上述方法调用一个internal/protected 方法,该方法定义了您需要运行的任何自定义逻辑。如果您想提供它们的标准实现,它们应该是抽象的或虚拟的。
  • 让所有子类只覆盖抽象/虚拟方法,然后它们就可以访问任何自定义代码。
  • 当您调用 stateStoredAsBaseClass.DoThing() 时,它会自动调用 stateStoredAsBaseClass.DoThingInternal(),它在子类中被覆盖,因此其中包含您的自定义 StateA 代码。

用代码写出的例子:

abstract class State {
    public int sharedField;

    public sealed void DoThing() {
        sharedField = 1;

        DoThingInternal();
    }

    protected abstract void DoThingInternal();
}

abstract class Item {
    State state;

    public abstract void TestMethod();
}

class StateA : State {
    public int customField = 10;

    protected override void DoThingInternal() {
        sharedField = 10;
    }
}

class ItemA : Item {

    public ItemA() {
        //read from somewhere else, pass it in, etc.
        state = new StateA();
    }

    public override void TestMethod() {
        state.DoThing();
        Console.WriteLine(state.sharedField); //will print 10
    }
}

这是一种非常常见的模式,尤其是在 Unity 中,当您想像这样使用继承时。这种模式在 Unity 之外也能很好地工作,并且避免了泛型,因此总体上很有用。

注意: public 方法上的 seal 属性的要点是防止其他人/你自己以后试图隐藏它(public new void DoThing() 或只是 public void DoThing())然后在它不起作用时感到困惑。

如果你要重写它,那么因为你是通过 base 类调用它,它会调用 base 版本的函数而不是你的新方法。将其定义为抽象/虚拟并由基类本身调用可以解决这个问题。这确实意味着您已经有效地定义了:

interface IState {
    public int sharedField;

    public void DoThing();
}

所以你大概可以看到,通过一个接口并注意实现,也可以达到同样的效果。

已编辑以使示例符合问题:

abstract class State {
    public sealed Button[] GetHiddenButtons() {
        GetHiddenButtonsInternal();
    }

    public sealed object[] GetObjects() {
        GetObjectsInternal();
    }

    protected abstract Button[] GetHiddenButtonsInternal();
    protected abstract object[] GetObjects();
}

abstract class Item {
    State state;

    public abstract void Render();
}

class StateA : State {
    public Button[] _hiddenButtons;
    public object[] _objects;

    public StateA()
    {
        //get hidden buttons from somewhere/pass them in/create them.
        _hiddenButtons = new Button[0];

        //get objects from somewhere/pass them in/create them.
        _objects = new object[0];
    }

    protected override Button[] GetHiddenButtons() {
        return _hiddenButtons;
    }

    protected override object[] GetObjects() {
        return _objects;
    }
}

class ItemA : Item {

    public ItemA() {
        //read from somewhere else, pass it in, etc.
        state = new StateA();
    }

    public override void Render() {
        //get objects
        var objects = state.GetObjects(); //returns array from StateA

        //get hidden buttons to hide
        var hiddenButtons = state.GetHiddenButtons();
    }
}

基本上,您所做的是创建一个足够通用的接口,以服务于您需要状态执行的任何操作。该模式没有规定您需要如何返回它们,因此您的 GetHiddenButtons() 方法可以:返回 ID 列表、返回对象、联系 GameObject 以获取列表、返回硬编码列表等。

这种模式允许你做你想做的事,你只需要确保基本状态的设计足够通用,可以让你做。

【讨论】:

  • 扩展答案以解释与接口的相似性,并警告修改覆盖中的“密封”方法(如果不使用密封)。
  • 感谢您的回答。这是一个我不知道的有趣模式。自从我更新了我的问题以来,这并不是我想要做的。不过还是谢谢。
  • 我的回答符合您更新的问题。基类将有一个名为 GetObjects 或其他的方法,并且没有共享字段。自定义类将覆盖 GetObjectsInternal 并具有一些自定义字段,这些字段会以您想要的任何形式序列化并返回。
  • 我以前从未使用过序列化。在我的情况下我会怎么做?例如,我认为这用于将对象存储到保存文件中。
  • 序列化只是意味着“将一个类/一组字段转换为一种数据形式,您可以根据需要存储/显示/发送”。例如,如果您试图显示存储在 State 子类中的任何内容,那么您可以只拥有一个公共字符串 GetText() 或一个公共对象 [] GetObjects() 或任何东西,然后实现 GetTextInternal() 和 GetObjectsInterval( ) 在子类中,使其读取您的字段并将它们转换为您想要的任何类型的数据。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-05
  • 1970-01-01
相关资源
最近更新 更多