在Scala中,是否可以从投影类型中恢复一个封闭示例?

mcdcgff0  于 8个月前  发布在  Scala
关注(0)|答案(1)|浏览(70)

假设我们有…

class Outer(val name : String):
  class Inner:
    def thump = s"${name}'s heartbeat"

我们想写一个函数,它接受任何可能未知的外部变量的内部变量。轻松peasy与投影类型!

def stethoscope( inner : Outer#Inner ) = inner.thump

这可以正常工作,而且显然依赖于inner保留对其封闭示例的引用。
但是有没有可能写一个函数,

def enclosingInstance( inner Outer#Inner ) : Outer = ???

假设类的定义超出了我们的控制范围。
(So例如,我们不能将方法添加到内部引用到Outer的自引用。

wgeznvg7

wgeznvg71#

当我们编译时:

class Outer(val name : String):
  class Inner:
    def thump = s"${name}'s heartbeat"

内部类的定义可能看起来像这样:

> scala-cli compile --scala "3.3.0" .
Compiling project (Scala 3.3.0, JVM)
Compiled project (Scala 3.3.0, JVM)
> javap -p .scala-build/test2_23be26abb2/classes/main/Outer\$Inner.class
Compiled from "test.scala"
public class Outer$Inner {
  private final Outer $outer;
  public Outer$Inner(Outer);
  public java.lang.String thump();
  public final Outer Outer$Inner$$$outer();
}

正如我们看到的,对$outer: Outer的引用是private,所以我们不能以“法律的”方式访问它。Outer$Inner$$$outer(): Outerpublic...但是如果我们试图调用它,编译器不知道这样的符号,

value Outer$Inner$$$outer() is not a member of outer.Inner

这是有意义的,因为它是在稍后发出字节码时生成的,所以例如。typechecker阶段可能不知道它(或者假装不知道)。Scala的内部可以使用它来访问它,当一些代码必须合法访问它时(例如,在某些上下文中,Outer.this是法律的),但是用户可能没有法律的方式来调用它。
我们可以通过运行时反射来“破解”这个限制:

class Outer(val name : String):
  class Inner:
    def thump = s"${name}'s heartbeat"

def enclosingInstance( inner: Outer#Inner ) : Outer =
  val field = inner.getClass.getDeclaredField("$outer")
  field.setAccessible(true)
  field.get(inner).asInstanceOf[Outer]

val outer = new Outer("test")
val inner = new outer.Inner
 
println(outer == enclosingInstance(inner)) // true

See scastie.
我不建议依赖这样的技巧-因为名称是自动生成的,你必须例如。列出所有声明的字段,并检查其中哪些是:

  • private
  • final
  • 匹配Class[Outer]

不要依赖这个生成的名称在将来不会改变(可能不会,但我不会拿我的薪水打赌)。我也会检查这是否真的是我可以获得Outer示例的唯一方法(也许将它们与[O <: Outer](o: Outer, i: o.Inner)沿着传递是可能的?).
但如果你真的必须这么做,你就做你该做的。
也许(我没有检查)-在Scala 3的编译时反射中可见的声明定义列表将能够看到Outer$Inner$$$outer(),所以也许enclosingInstance也可以作为宏实现(类似于你不能手动调用方法来定义某些方法的默认值,但在宏中你可以访问这些方法-例如。def methodname(a: Int = 1)生成methodname$default$1,你不能在普通代码中调用它,但你可以在宏中调用它)。

相关问题