【问题标题】:Idiomatic approach to Scala Value ClassesScala 值类的惯用方法
【发布时间】:2015-02-07 10:48:00
【问题描述】:

有谁知道是否有一种惯用的方式来命名 Scala 值类的内部值?假设我有一个产品 ID 的值类,是否更好地定义为:

case class ProductId(productId:String) extends AnyVal

case class ProductId(underlying:String) extends AnyVal

case class ProductId(value:String) extends AnyVal

?

这只是偏好问题还是有惯用的指导方针?

【问题讨论】:

  • IMO,underlyingvalue 更好
  • 我不知道任何标准。我倾向于使用inner,因为它简短明了。
  • 我不认为有一些惯用的方法可以做到这一点,因为值类对编译器来说只是一个不错的技巧。但就像@om-nom-nom 所说的那样,underlying 在您需要创建某种包装器的不同情况中很常见。
  • “基础”也是 SIP 中使用的术语,IIRC。
  • 什么是 IIRC? SIP15 在示例中同时使用了底层,但在定义中使用了class C(val u: U) extends AnyVal,所以我猜它也可能是case class ProductId(string: String) extends AnyVal

标签: scala idioms value-class


【解决方案1】:

我一般喜欢用下面的模式-

final class ProductId(val underlying: String) extends AnyVal

这有以下好处 -

  • 您可以随时访问underlying
  • not 使用case class 防止构造函数上的模式匹配,这有助于避免实际构造对象,因此运行时值保持为String(或任何基础值)
  • 您可以使用伴随对象创建智能构造函数来验证其输入并在构造值时提供更简洁的接口,这样用户就不必使用new

下面的智能构造函数示例 -

final class ProductId(val underlying: String) extends AnyVal

object ProductId {
  def apply(s: String): Result = {
    if (s.isEmpty) {
      new Failure("ProductId cannot be empty!")
    } else {
      new Success(new ProductId(s))
    }
  }

  sealed trait Result
  final case class Success(productId: ProductId) extends Result
  final case class Failure(message: String) extends Result
}

如果您想确保用户必须使用智能构造函数,请将值类的构造函数标记为私有 -

final class ProductId private (val underlying: String) extends AnyVal

如果您想确保不会意外分配 ProductId 的实例,您可以检查字节码 -

scala> :paste
class Test {
  def testProductId = new ProductId("foo")

  def testSmartCtor = ProductId("bar") match {
    case ProductId.Success(productId) => productId
    case ProductId.Failure(message) => throw new AssertionError(message)
  }
}

// Ctrl+D

:javap -c ProductId$

// Skipping to the apply() method

public ProductId$Result apply(java.lang.String);
  Code:
     0: aload_1
     1: invokevirtual #20                 // Method java/lang/String.isEmpty:()Z
     4: ifeq          19
     7: new           #22                 // class ProductId$Failure
    10: dup
    11: ldc           #24                 // String ProductId cannot be empty!
    13: invokespecial #27                 // Method ProductId$Failure."<init>":(Ljava/lang/String;)V
    16: goto          27
    19: new           #29                 // class ProductId$Success
    22: dup
    23: aload_1
    24: invokespecial #30                 // Method ProductId$Success."<init>":(Ljava/lang/String;)V
    27: areturn

字节码中没有new ProductId 引用,因此在运行时您的ProductId 将表示为String

请注意,如果您尝试将值类包装在使用泛型(例如 Option、Either)的类中,那么您的值将被装箱。您可以通过创建一个专门用于您的值类的简单案例类来避免这种情况。虽然案例类将被实例化(因为您不能用另一个值类包装一个值类),但底层的ProductId 在运行时仍将表示为String

【讨论】:

    【解决方案2】:

    惯用方法是将其设为private val,现在您可以了。

    就标准而言,标准库更喜欢self

    implicit final class ArrowAssoc[A](private val self: A) extends AnyVal
    

    还有repr,回忆起来自AnyVal 的scaladoc 和集合的repr 的短语“底层运行时表示”。

    class StringOps(override val repr: String) extends AnyVal with StringLike[String]
    

    还有少量的in

    就我个人而言,我使用特殊标识符YMMV

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-10-12
      • 2020-03-04
      • 1970-01-01
      • 2017-02-21
      • 1970-01-01
      • 2017-10-25
      • 2015-09-06
      相关资源
      最近更新 更多