如何在 Scala 中模拟“赋值一次”变量?
Posted
技术标签:
【中文标题】如何在 Scala 中模拟“赋值一次”变量?【英文标题】:How to simulate an “assign-once” var in Scala? 【发布时间】:2011-05-23 04:28:50 【问题描述】:这是my previous initialization variable question 的后续问题。
假设我们正在处理这个上下文:
object AppProperties
private var mgr: FileManager = _
def init(config: Config) =
mgr = makeFileManager(config)
这段代码的问题是AppProperties
中的任何其他方法都可能重新分配mgr
。有没有一种技术可以更好地封装mgr
,让其他方法感觉就像val
?我想过这样的事情(灵感来自this answer):
object AppProperties
private object mgr
private var isSet = false
private var mgr: FileManager = _
def apply() = if (!isSet) throw new IllegalStateException else mgr
def apply(m: FileManager)
if (isSet) throw new IllegalStateException
else isSet = true; mgr = m
def init(config: Config) =
mgr(makeFileManager(config))
...但这对我来说感觉相当沉重(初始化让我想起了太多的 C++ :-))。还有什么想法吗?
【问题讨论】:
【参考方案1】:您可以使用隐式来做到这一点,使隐式仅在应该能够重新分配的方法中可用。查看值不需要隐式,因此“变量”对其他方法可见:
sealed trait Access
trait Base
object mgr
private var i: Int = 0
def apply() = i
def :=(nv: Int)(implicit access: Access) = i = nv
val init =
implicit val access = new Access
() =>
mgr := 5
object Main extends Base
def main(args: Array[String])
println(mgr())
init()
println(mgr())
【讨论】:
最终解决方案发布在这里:***.com/questions/4404024/…【参考方案2】:好的,这是我的建议,直接受到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
s 和AnyRef
s。隐式转换允许我在很多情况下直接使用变量名,如果这不起作用,我可以通过附加 ()
来显式转换它。
典型用法如下:
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
),但在运行时失败,这不会产生编译时错误。
【讨论】:
但正如我现在所看到的,您始终可以在代码中的任何位置检索SetOne.allowMe
凭据(与sealed
版本相比),只要它是第一次分配,分配就可以工作。所以基本上,隐式凭证现在没用了。还是我错过了什么?
分配不能在我的代码中的任何地方工作,因为mgr
是私有的,但你是对的,这是一个小缺点。但这真的比 axel22 的版本差吗,我可以在同一个文件的任何位置创建另一个 Access
? (我可以将 Base
移动到另一个文件以获得 100% 的保证,但是这对于一个字段的初始化会不会过大?我宁愿将它保存在同一个文件中获得更好的概述)。其次,我更喜欢易于重用的SetOnce
,而不是更冗长的解决方案,它需要我为每个新的 set-once var 重新声明新的访问特征......
没有好坏之分,只是凭证在您的情况下是无用的,因为您可以从代码中的任何位置调用SetOnce.allowMe
,从而获取凭证变量。你实际上并没有从中获得任何好处。
好的,我现在改变了我的提议:现在SetOnceCredential
是SetOnce
的内部类。现在只能在mgr
的范围内调用allowAssignment
(以前的allowMe
)。
也许您应该展示一个示例,说明错误/丢失的凭据实际上会阻止分配并且无法检索它。 :)【参考方案3】:
我假设您不需要使用原语有效地执行此操作,并且为简单起见,您也不需要存储 null
(但如果这些假设不成立,您当然可以修改这个想法):
class SetOnce[A >: Null <: AnyRef]
private[this] var _a = null: A
def set(a: A) if (_a eq null) _a = a else throw new IllegalStateException
def get = if (_a eq null) throw new IllegalStateException else _a
只要在需要该功能的地方使用这个类。 (也许你更喜欢apply()
而不是get
?)
如果您真的希望它看起来就像一个变量(或方法)访问,没有额外的技巧,请将 SetOnce 设为私有,然后
private val unsetHolder = new SetOnce[String]
def unsetVar = unsetHolder.get
// Fill in unsetHolder somewhere private....
【讨论】:
不错的一个。我将结合使用您的答案和 axel22 的隐式赋值。 有什么理由默认null: A
而不是None: Option[A]
?
我已经在这里发布了我的最终解决方案:***.com/questions/4404024/…
@Debilski - null
对世界不可见,并且计算开销低于 Option
。这是一个足够低级的东西,它可以被大量使用,并且要更有效地编写它需要大约相同的努力。但是,如果想存储null
,当然可以做其他事情(尽管我会再次选择私有布尔值而不是Option
,因为开销较小)。【参考方案4】:
这不是最好的方式,也不是您所要求的,但它为您提供了一些访问权限:
object AppProperties
def mgr = _init.mgr
def init(config: Config) = _init.apply(config)
private object _init
var mgr: FileManager = _
def apply(config: Config) =
mgr = makeFileMaker(config)
【讨论】:
+1,因为即使我有几个这样的“一次性赋值”变量,这个解决方案也只会创建一个额外的对象。重新分配 _init.mgr 仍然是可能的,但在客户端代码中肯定看起来“足够错误”。 无法在客户端代码中重新分配,因为_init
仅在AppProperties
内可见,而def mgr
不可更改。
对,但为此,一个简单的private var
就可以了。我有兴趣在 AppProperties
的其余实现中阻止它。我想我不应该写“客户端代码”。【参考方案5】:
看着JPP’s post我又做了一个变种:
class SetOnce[T]
private[this] var value: Option[T] = None
private[this] var key: Option[SetOnceCredential] = None
def isSet = value.isDefined
def ensureSet if (value.isEmpty) throwISE("precondition violated: uninitialized value")
def apply() = value getOrElse throwISE("uninitialized value")
def :=(finalValue: T)(implicit credential: SetOnceCredential = null): SetOnceCredential =
if (key != Option(credential)) throwISE("Wrong credential")
else key = Some(new SetOnceCredential)
value = Some(finalValue)
key get
private def throwISE(msg: String) = throw new IllegalStateException(msg)
class SetOnceCredential private[SetOnce]
private val mgr1 = 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 credential1 = mgr1 := new FileManager(config)
mgr1 := new FileManager(config) // works
implicit val credential2 = mgr2 := new FileManager(config) // We get a new credential for this one
mgr2 := new FileManager(config) // works
inited = true
init(new Config)
mgr1 := new FileManager(new Config) // forbidden
这一次,我们完全可以多次分配 var,但我们需要在范围内拥有正确的凭据。凭据在第一次分配时创建并返回,这就是为什么我们需要立即将其保存到implicit val credential = mgr := new FileManager(config)
。如果凭据不正确,它将无法工作。
(请注意,如果范围内有更多凭据,则隐式凭据不起作用,因为它们将具有相同的类型。可能可以解决此问题,但我目前不确定。)
【讨论】:
难道不能将SetOnceCredential
类从SetOnce
对象移动到SetOnce
类以避免您提到的多凭据问题吗?看来我们甚至可以摆脱key
中的缓存凭证,那么。我喜欢这个想法,但现在编译时保证被削弱了,因为任何赋值都会编译(使用默认的 null 值)但在运行时会失败。
你是对的。将它移到类中当然可以解决这个问题。
非常好!我不喜欢的最后一件事是您的最后一行mgr1 := new FileManager(new Config)
,虽然它会在运行时失败,但仍然可以编译,并不表示它实际上需要凭据。
是的,这很难。我不确定我们能不能解决这个问题。有人可能会考虑为凭证设置一个只读变量,并将 :=
的隐含变量设为强制性。但是,仍然可以访问一次读取方法来检索凭证,因此类型系统不会抱怨,即使返回的是 null
而不是凭证。【参考方案6】:
我在想这样的事情:
object AppProperties
var p : Int => Unit = v : Int => p = _ => throw new IllegalStateException ; hiddenx = v
def x_=(v : Int) = p(v)
def x = hiddenx
private var hiddenx = 0
X 只能设置一次。
【讨论】:
谢谢,但并不比我最初的代码好,因为 AppProperties 中的其他方法可能仍会重新分配 hiddenx。【参考方案7】:这并不完全相同,但在许多情况下,“设置一次变量,然后继续使用它”的解决方案是简单的子类化,有或没有特殊的工厂方法。
abstract class AppPropertyBase
def mgr: FileManager
//.. somewhere else, early in the initialisation
// but of course the assigning scope is no different from the accessing scope
val AppProperties = new AppPropertyBase
def mgr = makeFileMaker(...)
【讨论】:
【参考方案8】:您总是可以将该值移动到另一个对象,只初始化一次并在需要时访问它。
object FileManager
private var fileManager : String = null
def makeManager(initialValue : String ) : String =
if( fileManager == null )
fileManager = initialValue;
return fileManager
def manager() : String = fileManager
object AppProperties
def init( config : String )
val y = FileManager.makeManager( config )
// do something with ...
def other()
FileManager.makeManager( "x" )
FileManager.makeManager( "y" )
val y = FileManager.manager()
// use initilized y
print( y )
// the manager can't be modified
object Main
def main( args : Array[String] )
AppProperties.init("Hello")
AppProperties.other
【讨论】:
以上是关于如何在 Scala 中模拟“赋值一次”变量?的主要内容,如果未能解决你的问题,请参考以下文章