如何将同一父类的两个子类视为相同但不同的类型?

Posted

技术标签:

【中文标题】如何将同一父类的两个子类视为相同但不同的类型?【英文标题】:How do I treat two children classes of the same parent class, as the same, yet different Type? 【发布时间】:2021-05-08 22:01:49 【问题描述】:

我需要帮助来弄清楚如何在 typescript 中正确地断言类型。

我正在使用的代码让 rn 感到困惑(我认为我让它变得比它需要的更复杂,哈哈 :3),所以我将简要介绍一下我正在尝试完成的工作。

所以基本上我有一个实现 IAbstractFactory 接口的 ConcreteFactory 类。 ConcreteFactory 类可以创建/生成 3 个不同的具体类 BaseDocumentDocumentADocumentB

interface IAbstractFactory
    createBaseDocument(): Promise<IAbstractDocument>;
    createDocumentA(): Promise<IAbstractDocument>;
    createDocumentB(): Promise<IAbstractDocument>;


class ConcreteFactory implements IAbstractFactory
    public async createBaseDocument(): Promise<IAbstractDocument> 
        return new BaseDocument();
    

    public async createDocumentA(): Promise<IAbstractDocument> 
        return new DocumentA();
    

    public async createDocumentB(): Promise<IAbstractDocument> 
        return new DocumentB();
    

请注意,只有 Document 类实现了 IAbstractDocument 接口。 DocumentA 和 DocumentB 都扩展了 BaseDocument

interface IAbstractDocument
    methodBase();


class BaseDocument implements IAbstractDocument
    methodBase();


// uses all of BaseDocuments functions but has some extra functions that DocumentB does not have
class DocumentA extends BaseDocument
    methodBase(); 
    methodA(); // not implemented in IAbstractDocument, throws error


// uses all of BaseDocuments functions but has some extra functions that DocumentA does not have 
class DocumentB extends BaseDocument
    methodBase(); 
    methodB(); // not implemented in IAbstractDocument, throws error

我还有一个名为 Tree 的类,它将构建一棵树,每个节点的类型都是 IAbstractDocument

class Tree
    rootNode:IAbstractDocument; // Realistically of type DocumentA or DocumentB, IAbstractDocument is throwing error, explained below
    
    nodes: IAbstractDocument[]; // Realistically of type DocumentA or DocumentB, IAbstractDocument is throwing error, explained below

    // this function will create factory and the factory will create DocumentA or Document B
    createDocument()

    addDocument()


我的目标是创建一个文档树。规则是 DocumentA 可以包含子节点,类型为 DocumentADocumentB,但 DocumentB 不能有任何子节点(它们只能是叶节点)。

问题是:

虽然 DocumentADocumentB 都扩展了 BaseDocument。在 Tree 类中,它们在 IAbstractDocument 类型下定义(我这样做是因为我认为这就是 ConcreteFactory 返回的内容)。所以现在我得到一个错误,DocumentADocumentB(分别是methodA 和methodB)中的唯一函数没有在IAbstractDocument 中定义。所以我不能在 Tree 类中使用它们,我只能使用 methodBase()

我的问题是:

如何将 DocumentADocumentB 视为相同但不同的类型?它们都应该被视为 中的文档节点树,但它们具有与它们相关的不同功能。

任何帮助将不胜感激,谢谢!

【问题讨论】:

请考虑修改这里的代码以构成一个minimal reproducible example 适合放入像The TypeScript Playground 这样的独立IDE,以便其他人可以重现您的问题(您所说的任何错误都应该是明显的) 并且只有您的问题(应删除其他错误,如拼写错误或未声明的依赖项)。如果我可以立即开始解决问题而不必先解决问题,那么提供帮助会容易得多。 我不知道在这里对Tree 说什么; nodes 不应该是其他 Tree 实例吗?如果您对子/叶区分有如此不同的要求,您可能希望Tree 本身是可子类化的,并具有DocumentATreeDocumentBTree 我倾向于使用union types 并避免使用一般的基本/抽象版本。 this 是否为您工作或为您提供指导?如果是这样,我会写一个答案;如果不是,请详细说明您的问题。 对不起,如果我没有给出一个可重现的例子,我应该考虑一下。 Union 类型确实有意义,我认为这就是我需要的答案。谢谢!但是,让节点成为其他 Tree 实例是什么意思?将树拆分为多个子树对性能更好吗?我试图重新创建此树遍历算法bfs-dfs-example 的功能,但我想用DocumentA 和/或DocumentB 替换TreeNode 关于这两种类型的文档,我还想澄清一件事。对不起,如果我没有很好地解释它,但 DocumentA 充当其他“子集合”(DocA)或“子项”(DocB)的一种“集合”(DocA)。而 DocumentB 只是作为一个项目,它可以单独存在(根节点)或者是“集合/子集合”(DocA)的子节点。所以树结构的一个例子可以是 DocA -> [DocB, DocB] OR DocA -> [DocA, DocA] -> [[DocB, DocB], [DocB, DocB]] DocB 【参考方案1】:

我认为您可能希望使用union types 来表示“非此即彼”,而不是超类或其他基类型,这些类型无法轻松检查。一般来说,您可能应该根据需要使用特定的类型,以使编译器知道您在做什么。

接下来我会将名称 DocumentA 更改为 DocumentCollection 并将 DocumentB 更改为 DocumentSingle,因为我认为这就是您的意图。 DocumentCollection 将具有 children 属性,而 DocumentSingle 将没有。这里是ConcreteFactory

class ConcreteFactory 
    public async createBaseDocument(): Promise<BaseDocument> 
        return new BaseDocument();
    

    public async createDocumentCollection(): Promise<DocumentCollection> 
        return new DocumentCollection();
    

    public async createDocumentSingle(): Promise<DocumentSingle> 
        return new DocumentSingle();
    

查看它如何在其返回签名中返回更具体的类型。现在让我们看看Document 类的可能实现:

class BaseDocument 
    methodBase()  ;
    constructor(public docName: string = "")  


class DocumentCollection extends BaseDocument 
    methodBase()  ;
    methodCollection()  console.log("MethodCollection by " + this.docName) ;
    children: Array<DocumentCollection> | Array<DocumentSingle> = [];


class DocumentSingle extends BaseDocument 
    methodBase()  ;
    methodSingle()  console.log("MethodSingle by " + this.docName) ;

我已经给文档一个docName 以便我们稍后可以看到一些有用的输出。注意DocumentCollectionchildren 属性是Array&lt;DocumentCollection&gt; 还是Array&lt;DocumentSingle&gt;。这将防止它成为一个混合集合,如果我从你的问题中理解正确,你想禁止它。请注意,使用联合数组比使用联合数组更难,但它就是这样。

让我们为DocumentSingleDocumentCollection 的联合命名:

type Document = DocumentSingle | DocumentCollection;

如果您能提供帮助,您可能希望使用Document 而不是AbstractDocumentBaseDocument


现在让我们看看Tree

class Tree 

    nodes: Document[]
    constructor(public rootNode: Document) 
        this.nodes = [];
        const addNodes = (x: Document): void => 
            this.nodes.push(x)
            if ("children" in x) 
                x.children.forEach(addNodes)
            
        
        addNodes(this.rootNode);

    
    walk(
      processSingle: (x: DocumentSingle) => void, 
      processCollection: (x: DocumentCollection) => void
    ) 
        const walk = (x: Document): void => 
            if ("methodSingle" in x) 
                processSingle(x);
             else 
                processCollection(x);
                x.children.forEach(walk)
            
        
        walk(this.rootNode);
    

Tree 有一个 rootNode 类型为 Document。它还有一个nodes 类型为Document[] 的数组。当您使用其rootNode 构造Tree 时,构造函数还将遍历树以构建nodes 数组(如果重要)。还有一个walk() 方法,它为DocumentSingleDocumentCollection 获取处理器回调,并遍历调用适当处理器的树。

请注意,在addNodes()walk() 箭头函数的实现中,编译器使用DocumentSingleDocumentCollection 的特定属性之一的存在将Document 缩小为两名成员。这是因为the in operator can be used as a type guard。


所以让我们创建一个Tree

const leafOne = new DocumentSingle("leafOne");
const leafTwo = new DocumentSingle("leafTwo");
const leafThree = new DocumentSingle("leafThree");
const leafFour = new DocumentSingle("leafFour");

const branchOne = new DocumentCollection("branchOne");
branchOne.children = [leafOne, leafTwo];

const branchTwo = new DocumentCollection("branchTwo");
branchTwo.children = [leafThree, leafFour];

const root = new DocumentCollection("root");
// note how I'm not allowed to assign a mixed thing to root.children
root.children = [branchOne, leafOne];

root.children = [branchOne, branchTwo]; // okay

const tree = new Tree(root);

从上面可以看出,DocumentCollectionchildren 属性中不允许使用DocumentSingleDocumentCollection 的混合数组。您还应该注意,我重新分配了 children 属性与一个新的数组,而不是尝试将 push() 放到它上面。如果您尝试使用push(),您会发现它不会让您添加任何内容,因为它不知道数组是否包含DocumentSingles 或DocumentCollections。不确定这是否会破坏您的用例,但如果是这样,我们可以更改它。 (嗯,可以改变它;我不知道我可以承诺它⏳)

太好了,我们有一个tree...让我们walk()吧:

tree.walk(x => x.methodSingle(), y => y.methodCollection());
/* [LOG]: "MethodCollection by root" 
[LOG]: "MethodCollection by branchOne" 
[LOG]: "MethodSingle by leafOne" 
[LOG]: "MethodSingle by leafTwo" 
[LOG]: "MethodCollection by branchTwo" 
[LOG]: "MethodSingle by leafThree" 
[LOG]: "MethodSingle by leafFour" */

太棒了!或者,我们可以迭代它的 nodes 属性:

tree.nodes.forEach(x => "methodSingle" in x ? x.methodSingle() : x.methodCollection());
/* [LOG]: "MethodCollection by root" 
[LOG]: "MethodCollection by branchOne" 
[LOG]: "MethodSingle by leafOne" 
[LOG]: "MethodSingle by leafTwo" 
[LOG]: "MethodCollection by branchTwo" 
[LOG]: "MethodSingle by leafThree" 
[LOG]: "MethodSingle by leafFour" */

看起来不错。


Playground link to code

【讨论】:

以上是关于如何将同一父类的两个子类视为相同但不同的类型?的主要内容,如果未能解决你的问题,请参考以下文章

java基础---多态

多态学习总结

重载和重写的区别

Java中的重载和重写的区别

方法调用

重载与重写