42

我正在开发一个 Scala API(顺便说一下,对于 Twilio),其中操作具有大量参数,其中许多具有合理的默认值。为了减少打字并提高可用性,我决定使用带有命名和默认参数的案例类。例如对于 TwiML Gather 动词:

case class Gather(finishOnKey: Char = '#', 
                  numDigits: Int = Integer.MAX_VALUE, // Infinite
                  callbackUrl: Option[String] = None, 
                  timeout: Int = 5
                  ) extends Verb

这里感兴趣的参数是callbackUrl。它是唯一一个真正可选的参数,如果没有提供值,则不会应用任何值(这是完全合法的)。

我已经将它声明为一个选项,以便在 API 的实现端使用它来执行 monadic map 例程,但这会给 API 用户带来一些额外的负担:

Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// Should have been
Gather(numDigits = 4, callbackUrl = "http://xxx")

// Without the optional url, both cases are similar
Gather(numDigits = 4)

据我所知,有两种选择(没有双关语)来解决这个问题。要么让 API 客户端将隐式转换导入范围:

implicit def string2Option(s: String) : Option[String] = Some(s)

或者我可以使用 null 默认值重新声明案例类并将其转换为实现端的选项:

case class Gather(finishOnKey: Char = '#', 
                  numDigits: Int = Integer.MAX_VALUE, 
                  callbackUrl: String = null, 
                  timeout: Int = 5
                  ) extends Verb

我的问题如下:

  1. 有没有更优雅的方法来解决我的特殊情况?
  2. 更一般地说:命名参数是一种新的语言特性(2.8)。难道选项和命名的默认参数就像油和水一样吗?:)
  3. 在这种情况下,使用 null 默认值可能是最佳选择吗?
4

7 回答 7

25

这是另一个解决方案,部分灵感来自Chris' answer。它还涉及到一个包装器,但包装器是透明的,您只需定义一次,API 的用户不需要导入任何转换:

class Opt[T] private (val option: Option[T])
object Opt {
   implicit def any2opt[T](t: T): Opt[T] = new Opt(Option(t)) // NOT Some(t)
   implicit def option2opt[T](o: Option[T]): Opt[T] = new Opt(o)
   implicit def opt2option[T](o: Opt[T]): Option[T] = o.option
}

case class Gather(finishOnKey: Char = '#', 
                  numDigits: Opt[Int] = None, // Infinite
                  callbackUrl: Opt[String] = None, 
                  timeout: Int = 5
                 ) extends Verb

// this works with no import
Gather(numDigits = 4, callbackUrl = "http://xxx")
// this works too
Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// you can even safely pass the return value of an unsafe Java method
Gather(callbackUrl = maybeNullString())

为了解决更大的设计问题,我认为选项和命名默认参数之间的交互并不像乍一看那样多油腻。可选字段和具有默认值的字段之间有明确的区别。可选字段(即 type 之一Option[T])可能永远不会有值。另一方面,具有默认值的字段根本不需要将其值作为参数提供给构造函数。因此,这两个概念是正交的,字段可能是可选的并且具有默认值也就不足为奇了。

也就是说,我认为可以为使用Opt而不是Option为此类字段提出合理的论据,而不仅仅是为客户节省一些打字。这样做会使 API 更加灵活,因为您可以用T参数替换Opt[T]参数(反之亦然),而不会破坏构造函数的调用者 [1]。

至于为null公共字段使用默认值,我认为这是一个坏主意。“您”可能知道您期望一个null,但访问该字段的客户端可能不知道。即使该字段是私有的,null当其他开发人员必须维护您的代码时,使用 a 也是自找麻烦。所有关于null值的常见论点都在这里发挥作用——我不认为这个用例有任何特殊的例外。

[1] 前提是您删除 option2opt 转换,以便调用者在需要T时必须传递 a Opt[T]

于 2010-11-17T01:38:24.637 回答
9

不要将任何内容自动转换为选项。在这里使用我的答案,我认为您可以以类型安全的方式很好地做到这一点。

sealed trait NumDigits { /* behaviour interface */ }
sealed trait FallbackUrl { /* behaviour interface */ }
case object NoNumDigits extends NumDigits { /* behaviour impl */ }
case object NofallbackUrl extends FallbackUrl { /* behaviour impl */ }

implicit def int2numd(i : Int) = new NumDigits { /* behaviour impl */ }
implicit def str2fallback(s : String) = new FallbackUrl { /* behaviour impl */ }

class Gather(finishOnKey: Char = '#', 
              numDigits: NumDigits = NoNumDigits, // Infinite
              fallbackUrl: FallbackUrl = NoFallbackUrl, 
              timeout: Int = 5

然后你可以随心所欲地调用它 - 显然将你的行为方法添加到FallbackUrl适当NumDigits的位置。这里的主要缺点是它是大量的样板

Gather(numDigits = 4, fallbackUrl = "http://wibble.org")
于 2010-11-16T22:13:56.743 回答
5

就个人而言,我认为在这里使用“null”作为默认值是完全可以的。当您想向您的客户传达可能未定义某些内容时,使用 Option 而不是 null。所以返回值可以声明为 Option[...],或者是抽象方法的方法参数。这使客户免于阅读文档,或者更有可能因为没有意识到某些内容可能为空而获得 NPE。

在您的情况下,您知道可能存在空值。如果你喜欢 Option 的方法,只需val optionalFallbackUrl = Option(fallbackUrl)在方法的开头做。

但是,这种方法仅适用于 AnyRef 类型。如果您想对任何类型的参数使用相同的技术(而不导致 Integer.MAX_VALUE 作为 null 的替换),那么我想您应该选择其他答案之一

于 2010-11-17T05:26:06.223 回答
4

我认为只要 Scala 中没有语言支持真正的 void(下面的解释)“类型”,Option从长远来看,使用可能是更清洁的解决方案。甚至可能适用于所有默认参数。

问题是,使用您的 API 的人知道您的某些参数是默认的,可能还不如将它们作为可选参数来处理。因此,他们将它们声明为

var url: Option[String] = None

这一切都很好,很干净,他们可以等着看他们是否得到任何信息来填写这个选项。

当最终使用默认参数调用您的 API 时,他们将面临一个问题。

// Your API
case class Gather(url: String) { def this() = { ... } ... }

// Their code
val gather = url match {
  case Some(u) => Gather(u)
  case _ => Gather()
}

我认为这样做会容易得多

val gather = Gather(url.openOrVoid)

在. *openOrVoid_ None但这是不可能的。

所以你真的应该考虑谁将使用你的 API 以及他们可能如何使用它。很可能您的用户已经使用Option来存储所有变量,因为他们知道它们最终是可选的……</p>

默认参数很好,但它们也使事情复杂化;特别是当周围已经有一种Option类型时。我认为你的第二个问题有些道理。

于 2010-11-16T23:59:55.633 回答
1

我可以只支持您现有的方法Some("callbackUrl")吗?API 用户需要再输入 6 个字符,向他们表明该参数是可选的,并且可能使您的实现更容易。

于 2010-11-16T22:29:54.863 回答
1

我认为你应该硬着头皮继续Option。我以前遇到过这个问题,通常在一些重构后就消失了。有时它没有,我忍受它。但事实是默认参数不是“可选”参数——它只是一个具有默认值的参数。

我非常赞成Debilski 的 回答

于 2010-11-17T01:37:49.193 回答
-1

我也对此感到惊讶。为什么不概括为:

implicit def any2Option[T](x: T): Option[T] = Some(x)

为什么这不能只是 Predef 的一部分?

于 2010-11-16T22:07:58.810 回答