我使用 Scala 隐式类来扩展我经常使用的对象。例如,我有一个类似于SparkDataFrame
上定义的方法:
implicit class DataFrameExtensions(df: DataFrame) {
def deduplicate: Boolean =
df.groupBy(df.columns.map(col): _*).count
}
但是,如果类已经定义了相同的方法,则不会调用隐式 defs。如果我稍后升级到定义DataFrame#deduplicate
方法的新版本 Spark,会发生什么情况?客户端代码将以静默方式切换到新的实现,这可能会导致细微的错误(或明显的错误,问题较少(。
使用反射,如果DataFrame
在我的隐式定义之前已经定义了deduplicate
,我可以抛出运行时错误。从理论上讲,如果我的隐式方法与现有方法冲突,我可以检测到它并重命名我的隐式版本。但是,一旦我升级了 Spark、运行应用程序并检测到问题,就使用 IDE 重命名旧方法为时已晚,因为对df.deduplicate
的任何引用现在都引用本机 Spark 版本。我必须还原我的 Spark 版本,通过 IDE 重命名该方法,然后再次升级。不是世界末日,但不是伟大的工作流程。
有没有更好的方法来处理这种情况?如何安全地使用"皮条客我的图书馆"模式?
您可以添加一个测试,以确保某些代码片段不会编译到DataFrameExtension
测试套件中。也许是这样的:
"(???: DataFrame).deduplicate" shouldNot compile
如果它在没有隐式转换的情况下进行编译,则意味着 Spark 库引入了deduplicate
方法。在这种情况下,测试将失败,并且您知道必须更新隐式。
如果导入启用了扩展方法,请使用-Xlint
来显示不再使用导入:
//class C
class C { def x = 17 }
trait T {
import Extras._
def f = new C().x
}
object Extras {
implicit class X(val c: C) {
def x = 42
}
}
另一种观点,其中证据必须使用-Xlint -Xfatal-warnings
:
//class C[A]
class C[A] { def x = 17 }
trait T {
import Mine.ev
val c = new C[Mine]
def f = c.x
}
trait Mine
object Mine {
implicit class X[A](val c: C[A]) {
def x(implicit @deprecated("unused","") ev: Mine) = 42
}
implicit val ev: Mine = null
}
object Test {
def main(args: Array[String]): Unit = println {
val t = new T {}
t.f
}
}
安全执行此操作的解决方案是显式请求扩展数据帧,以最大程度地减少影响,您可以使用隐式语法来获得很好的转换语法(如toJava/toScala等(:
implicit class DataFrameExtSyntax(df: DataFrame) {
def toExtended: DataFrameExtensions = DataFrameExtensions(df)
}
然后您的调用将看起来:
myDf.asExtended
.deduplicate
.someOtherExtensionMethod
.andMore
这样,您就可以在没有运行时检查/linting/单元测试技巧的情况下使您的扩展方法面向未来 (你甚至可以使用它myDf.ext
myDf.toExtended
太长:)