如何在 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 。这适用于AnyVals 和AnyRefs。隐式转换允许我在很多情况下直接使用变量名,如果这不起作用,我可以通过附加 () 来显式转换它。

典型用法如下:

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,从而获取凭证变量。你实际上并没有从中获得任何好处。 好的,我现在改变了我的提议:现在SetOnceCredentialSetOnce 的内部类。现在只能在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 中模拟“赋值一次”变量?的主要内容,如果未能解决你的问题,请参考以下文章

scala中var和val的区别

VC++ 如何向Edit框中动态赋值

在模拟类scala中初始化变量

Java中final关键字如何使用?

Java中final关键字如何使用?

如何在 Scala 中读取环境变量