有没有办法用类型变量引用当前类型?

2023-12-01

假设我正在尝试编写一个函数来返回当前类型的实例。有没有办法制作T指的是确切的子类型(所以T应该参考B在班上B)?

class A {
    <T extends A> foo();
}

class B extends A {
    @Override
    T foo();
}

建立在脱衣战士的回答,我认为以下模式是必要的(这是分层流畅构建器 API 的秘诀)。

SOLUTION

首先,一个基本抽象类(或接口),它制定了返回扩展该类的实例的运行时类型的契约:

/**
 * @param <SELF> The runtime type of the implementor.
 */
abstract class SelfTyped<SELF extends SelfTyped<SELF>> {

   /**
    * @return This instance.
    */
   abstract SELF self();
}

所有中间扩展类必须是abstract并维护递归类型参数SELF:

public abstract class MyBaseClass<SELF extends MyBaseClass<SELF>>
extends SelfTyped<SELF> {

    MyBaseClass() { }

    public SELF baseMethod() {

        //logic

        return self();
    }
}

进一步的派生类可以按照相同的方式进行。但是,如果不借助原始类型或通配符(这违背了该模式的目的),这些类都不能直接用作变量类型。例如(如果MyClass wasn't abstract):

//wrong: raw type warning
MyBaseClass mbc = new MyBaseClass().baseMethod();

//wrong: type argument is not within the bounds of SELF
MyBaseClass<MyBaseClass> mbc2 = new MyBaseClass<MyBaseClass>().baseMethod();

//wrong: no way to correctly declare the type, as its parameter is recursive!
MyBaseClass<MyBaseClass<MyBaseClass>> mbc3 =
        new MyBaseClass<MyBaseClass<MyBaseClass>>().baseMethod();

这就是我将这些类称为“中级”的原因,也是它们都应该被标记的原因abstract。为了关闭循环并利用该模式,“叶”类是必要的,它解析继承的类型参数SELF有自己的类型和实现self()。它们也应该被标记final为避免违反合同:

public final class MyLeafClass extends MyBaseClass<MyLeafClass> {

    @Override
    MyLeafClass self() {
        return this;
    }

    public MyLeafClass leafMethod() {

        //logic

        return self(); //could also just return this
    }
}

这些类使该模式可用:

MyLeafClass mlc = new MyLeafClass().baseMethod().leafMethod();
AnotherLeafClass alc = new AnotherLeafClass().baseMethod().anotherLeafMethod();

这里的值是方法调用可以在类层次结构中上下链接,同时保持相同的特定返回类型。


免责声明

上面是一个实现奇怪的重复模板模式在爪哇。这个图案是本质上并不安全并且应该仅保留用于内部 API 的内部运作。原因是无法保证参数的类型SELF上面的例子实际上会被解析为正确的运行时类型。例如:

public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {

    @Override
    AnotherLeafClass self() {
        return getSomeOtherInstanceFromWhoKnowsWhere();
    }
}

这个例子暴露了模式中的两个漏洞:

  1. EvilLeafClass可以“说谎”并替换任何其他类型的扩展MyBaseClass for SELF.
  2. 除此之外,没有任何保证self()实际上会返回this,这可能是也可能不是问题,具体取决于基本逻辑中状态的使用。

由于这些原因,这种模式很可能被误用或滥用。为了防止这种情况,允许none涉及公开扩展的类 - 请注意我在中使用的包私有构造函数MyBaseClass,它取代了隐式公共构造函数:

MyBaseClass() { }

如果可能的话,保留self()也是包私有的,因此它不会给公共 API 增加噪音和混乱。不幸的是,这只有在以下情况下才有可能:SelfTyped是一个抽象类,因为接口方法是隐式公共的。

正如于忠杰指出在评论中,绑定SELF可能会被简单地删除,因为它最终无法确保“自我类型”:

abstract class SelfTyped<SELF> {

   abstract SELF self();
}

Yu 建议仅依赖合约,避免因不直观的递归约束而产生任何混乱或错误的安全感。就我个人而言,我更喜欢离开界限,因为SELF extends SelfTyped<SELF>代表closestJava 中 self 类型的可能表达。但余的观点绝对符合Comparable.


结论

这是一个有价值的模式,允许对构建器 API 进行流畅且富有表现力的调用。我在认真的工作中使用过它几次,最值得注意的是编写一个自定义查询构建器框架,它允许像这样的调用站点:

List<Foo> foos = QueryBuilder.make(context, Foo.class)
    .where()
        .equals(DBPaths.from_Foo().to_FooParent().endAt_FooParentId(), parentId)
        .or()
            .lessThanOrEqual(DBPaths.from_Foo().endAt_StartDate(), now)
            .isNull(DBPaths.from_Foo().endAt_PublishedDate())
            .or()
                .greaterThan(DBPaths.from_Foo().endAt_EndDate(), now)
            .endOr()
            .or()
                .isNull(DBPaths.from_Foo().endAt_EndDate())
            .endOr()
        .endOr()
        .or()
            .lessThanOrEqual(DBPaths.from_Foo().endAt_EndDate(), now)
            .isNull(DBPaths.from_Foo().endAt_ExpiredDate())
        .endOr()
    .endWhere()
    .havingEvery()
        .equals(DBPaths.from_Foo().to_FooChild().endAt_FooChildId(), childId)
    .endHaving()
    .orderBy(DBPaths.from_Foo().endAt_ExpiredDate(), true)
    .limit(50)
    .offset(5)
    .getResults();

关键是QueryBuilder不仅仅是一个平面实现,而是从复杂的构建器类层次结构延伸出来的“叶子”。相同的模式也用于像这样的助手Where, Having, Or等等,所有这些都需要共享重要的代码。

然而,你不应该忽视这样一个事实:所有这些最终都只是语法糖。一些经验丰富的程序员坚决反对CRT模式, 或者至少对其好处与增加的复杂性的权衡持怀疑态度。他们的担忧是合理的。

最重要的是,在实施之前仔细考虑一下是否真的有必要——如果有的话,不要让它公开可扩展。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

有没有办法用类型变量引用当前类型? 的相关文章

随机推荐