【问题标题】:Is there a way to "enrich" a Scala class without wrapping the code into another object?有没有办法在不将代码包装到另一个对象中的情况下“丰富”一个 Scala 类?
【发布时间】:2013-02-10 20:58:42
【问题描述】:

在 Scala 2.9 中,要向库类添加自定义方法(丰富或“pimp”它),我必须编写如下内容:

object StringPimper {
  implicit def pimpString(s: String) = new {
    def greet: String = "Hello " + s
  }
}

随着 Scala 2.10 的发布,我读到它引入了隐式类定义,从理论上讲,它旨在通过消除返回匿名类对象的隐式方法的需要来简化上述任务。我以为它可以让我只写

implicit class PimpedString(s: String) {
  def greet: String = "Hello " + s
}

这对我来说看起来更漂亮。但是,这样的定义会导致编译错误:

`implicit' modifier cannot be used for top-level objects

通过将代码再次包装在一个对象中来解决:

object StringPimper {
  implicit class PimpedString(s: String) {
    def greet: String = "Hello " + s
  }
}

不用说,这几乎抵消了改进的感觉。

那么,有没有办法把它写得更短?摆脱包装对象?

我实际上有一个 MyApp.pimps 包,所有皮条客都去那里(我没有太多,如果有的话,我会使用一些单独的包)而且我厌倦了导入 MyApp.pimps.StringPimper._ 而不是 MyApp.pimps.PimpedStringMyApp.pimps._。当然,我可以将所有隐式类放在一个包装器对象中,但这意味着将它们全部放在一个文件中,这会很长 - 非常丑陋的解决方案。

【问题讨论】:

    标签: scala enrich-my-library


    【解决方案1】:

    允许扩充类驻留在不同文件中的解决方法是将其包装在特征而不是对象中:

    // file: package.scala
    package object mypackage extends StringImplicits {
      def test { println("Scala".greet) }
    }
    
    // file: StringImplicits.scala
    package mypackage
    
    trait StringImplicits {
      implicit class RichString(s: String) {
        def greet: String = "Hello " + s
      }
    }
    

    正如 Rex 指出的,你不能在其他类中使用值类。因此,如果您想从 Scala 2.10 的值类中受益,则不能使用嵌套的隐式类。 天啊!

    【讨论】:

    • 烦人,不是吗?我想知道是否有人为此提交了增强请求(希望是 2.11?)。放宽规则以使其在不破坏价值类的情况下正常工作,这并不是一个非常困难的改变。
    【解决方案2】:

    另一个放置隐式声明的好地方是伴生对象。

    对于从Type1Type2 的隐式转换,Scala 会查找这两种类型的所有祖先的伴随对象。因此,在不需要显式导入的地方放置隐式转换通常很容易。

    case class Curie(curie:String)
    
    object Curie {
      implicit def toCurie(s:String) = Curie(curie)
    }
    

    无论您在哪里调用需要 CurieString 的方法,都会调用转换方法,而无需任何额外的导入。

    【讨论】:

      【解决方案3】:

      现在的标准术语是丰富。我不确定为什么以前没有,因为使用它的库方法被命名为 RichString 而不是 PimpedString

      无论如何,你不能把隐式类放在任何东西上,但是为了使用隐式你需要方法,你不能把方法放在任何东西上。但是您可以作弊以获取pimps 中的所有内容。

      // This can go in one file
      trait ImplicitsStartingWithB {
        implicit class MyBoolean(val bool: Boolean) { def foo = !bool }
        implicit class MyByte(val byte: Byte) { def bar = byte*2 }
      }
      
      // This can go in another file
      trait ImplicitsStartingWithS {
        implicit class MyShort(val short: Short) { def baz = -short }
        implicit class MyString(val st: String) { def bippy = st.reverse }
      }
      
      // This is a third file, if you want!
      object AllImplicits extends ImplicitsStartingWithB with ImplicitsStartingWithS {}
      
      scala> import AllImplicits._
      import AllImplicits._
      
      scala> true.foo
      res0: Boolean = false
      

      您也可以使用 pimps 包对象执行此操作 - 通常您会将名为 package.scala 的文件放在名为 pimps 的目录中,然后

      package object pimps extends /* blah blah ... */ {
        /* more stuff here, if you need it */
      }
      

      需要注意的是,作为一项新功能,值类有很多限制。有些是不必要的,但如果您想避免分配新的MyBoolean 类(JVM 通常可以对其进行极大优化,但通常不会达到裸方法调用的程度),您将不得不解决这些限制。在这种情况下,

      // In some file, doesn't really matter where
      class MyBoolean(val bool: Boolean) extends AnyVal { def foo = !bool }
      
      package object pimps {
        def implicit EnrichWithMyBoolean(b: Boolean) = new MyBoolean(b)
      }
      

      这并不能节省您的工作量(但运行速度更快)。

      【讨论】:

      • “鉴于使用它的库方法被命名为 RichString 而不是 PimpedString”——这正是我使用“PimpedString”而不是“RichString”来丰富我自己的字符串的原因——以避免名称冲突。我过去曾经使用过“Rich...”,但已经放弃了这种做法。但是感谢您指出标准术语。
      • @Ivan - 为此我使用“Richer”。不过,要点。 (但有很多同义词。Enriched、Enhanced、Bling、Better、Awesome 等。)
      • @Mef - 我想你忘了使用 2.10。
      • 特质方法对我来说效果很好。它还指出了隐含从何处进入范围。
      【解决方案4】:

      好吧,编译器错误说明了一切。您只是不能在顶层定义隐式类。这意味着您要么使用包装器对象(也可以是包对象),要么直接在范围内定义要使用的类(在极少数情况下,它不必是可重用的)。

      这样做的原因是如果没有包装器,您将无法导入隐式转换。您不能按自己的名称导入隐式类(准确地说是转换)。

      除此之外,我建议使用稍微不同的签名(请参阅Value Classes):

      implicit class PimpedString(val self: String) extends AnyVal
      

      其效果是对您的扩展方法的调用可以(并且将会)内联。

      【讨论】:

      • 我实际上有一个MyApp.pimps 包,所有皮条客都去那里,我厌倦了导入MyApp.pimps.StringPimper._ 而不是MyApp.pimps.PimpedStringMyApp.pimps._。当然,我可以将所有隐式类放在一个包装器对象中,但这意味着将它们全部放在一个文件中,这将是一个非常长的文件 - 非常丑陋的解决方案。
      • @Ivan 将它们全部放入包对象中,是的,它会很长,但是您不必再写import MyApp.pimps.* -- 它们将在 MyApp 中可用,而无需每个 import 语句。
      • @om-nom-nom - 问题是,目前在 Scala 中存在不匹配。您只能在一个文件中定义一次包对象,然后您必须在一个文件中完全定义密封特征。我之前为对非顶级范围的限制进行了辩护,但这最近让我很痛苦。就像:你想要一个密封的特征还是想要一个包对象,或者你想把 10k loc 放在一个文件中?我最终使用了旧的隐式转换方法,这确实破坏了 IMO 隐式类的想法。
      • 我真的非常讨厌长代码文件,@om-nom-nom。理想情况下,单个代码文件应该少于一个屏幕页面,并且每个在逻辑上独立的单元都应该放在一个单独的文件中,恕我直言 - 这有助于可读性和版本控制。我也喜欢 C# 的“部分类”特性——它甚至允许将单个类的不同逻辑上区分的成员组放在单独的文件中。
      • @mef,你为什么认为我应该考虑在这里使用值类?我不只是拉皮条像 String 或 Int 这样的简单类型。我还拉皮条 Joda-Time、JDBC、我自己的类和案例类,甚至元组。
      猜你喜欢
      • 1970-01-01
      • 2022-11-16
      • 2014-01-11
      • 1970-01-01
      • 1970-01-01
      • 2019-10-05
      • 2021-02-07
      • 1970-01-01
      • 2019-03-18
      相关资源
      最近更新 更多