ОК, так вот мое предложение, прямо навеянное ответами axel22 , Rex Kerr и Debilski :
class SetOnce[T] {
private[this] var value: Option[T] = None
def isSet = value.isDefined
def ensureSet { if (value.isEmpty) throwISE("uninitialized value") }
def apply() = { ensureSet; value.get }
def :=(finalValue: T)(implicit credential: SetOnceCredential) {
value = Some(finalValue)
}
def allowAssignment = {
if (value.isDefined) throwISE("final value already set")
else new SetOnceCredential
}
private def throwISE(msg: String) = throw new IllegalStateException(msg)
@implicitNotFound(msg = "This value cannot be assigned without the proper credential token.")
class SetOnceCredential private[SetOnce]
}
object SetOnce {
implicit def unwrap[A](wrapped: SetOnce[A]): A = wrapped()
}
Мы получаем безопасность во время компиляции, что :=
не вызывается случайно, так как нам нужен объект SetOnceCredential
, который возвращается только один раз.Тем не менее, var может быть переназначен при условии, что у вызывающей стороны есть исходные учетные данные.Это работает с AnyVal
с и AnyRef
с.Неявное преобразование позволяет мне использовать имя переменной напрямую во многих обстоятельствах, и если это не сработает, я могу явно преобразовать его, добавив ()
.
Типичное использование будет следующим:
object AppProperties {
private val mgr = new SetOnce[FileManager]
private val mgr2 = new SetOnce[FileManager]
val init /*(config: Config)*/ = {
var inited = false
(config: Config) => {
if (inited)
throw new IllegalStateException("AppProperties already initialized")
implicit val mgrCredential = mgr.allowAssignment
mgr := makeFileManager(config)
mgr2 := makeFileManager(config) // does not compile
inited = true
}
}
def calledAfterInit {
mgr2 := makeFileManager(config) // does not compile
implicit val mgrCredential = mgr.allowAssignment // throws exception
mgr := makeFileManager(config) // never reached
}
Это не приводит к ошибке времени компиляции, если в какой-то другой точке того же файла я пытаюсь получить другие учетные данные и переназначить переменную (как в calledAfterInit
), но происходит сбой во время выполнения.