如何在Scala中模拟“只赋值一次”的变量?

6

这是对我的上一个初始化变量问题的跟进问题。

假设我们正在处理以下上下文:

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++了:-))。还有其他的想法吗?

8个回答

8

您可以使用隐式转换来实现,只需在允许重新分配的方法中提供该隐式即可。查看值不需要使用隐式,因此“变量”对其他方法是可见的:

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())                                                                                                                                                                                             
  }                                                                                                                                                                                                            

}

最终解决方案发布在这里:https://dev59.com/J1LTa4cB1Zd3GeqPbYx1#4407534 - Jean-Philippe Pellet

4

好的,这是我的建议,直接受到axel22Rex KerrDebilski答案的启发:

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,它只会返回一次,所以不会意外调用:=。但是,如果调用者拥有原始凭据,则可以重新分配变量,这适用于AnyValAnyRef。隐式转换允许我在许多情况下直接使用变量名,如果这不起作用,我可以通过附加()来显式转换它。
典型用法如下:
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),但在运行时失败。

1
但是就我现在看来,您可以随时在代码中检索SetOne.allowMe凭据(与sealed版本相比),只要它是第一次分配,分配就会起作用。因此,基本上,隐式凭据现在已经无用了。或者我错过了什么? - Debilski
好的,我现在已经修改了我的建议:现在SetOnceCredentialSetOnce的内部类。对于allowAssignment(之前的allowMe)的调用现在只能在mgr的范围内进行。 - Jean-Philippe Pellet
也许你应该举个例子,说明错误/缺失的凭证实际上会阻止分配,并且无法检索它。 :) - Debilski
我添加了一个例子,其中:=在范围内没有适当的凭证,则不会编译。仍然可以尝试获取凭据,但会抛出异常...正如您所写的,在这一点上,似乎很难修复。 - Jean-Philippe Pellet
وˆ‘هˆڑهˆڑن؛†è§£هˆ°@implicitNotFoundه¹¶ه·²ç»ڈو·»هٹ ن؛†ه®ƒ :-) - Jean-Philippe Pellet
显示剩余2条评论

2

我假设你不需要以原始数据类型高效地执行此操作,并且为了简单起见,你也不需要存储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的隐式赋值来使用。 - Jean-Philippe Pellet
你为什么默认选用 null: A 而不是 None: Option[A] 呢? - Debilski
我已经在这里发布了我的最终解决方案:https://dev59.com/J1LTa4cB1Zd3GeqPbYx1#4407534 - Jean-Philippe Pellet
@Debilski - null 对于外部世界是不可见的,而且比 Option 的计算开销要小。这是一些低级别的东西,可能会被大量使用,而且写起来更有效率。但如果想要存储 null,当然可以选择其他方法(尽管我会再次选择私有布尔值,因为它的开销更小)。 - Rex Kerr

2

这并不是最好的方式,也不完全符合您的要求,但它可以为您提供一些访问封装的方式:

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)
    }
  }
}

这个解决方案仅创建一个额外的对象,即使我有多个这样的“一次赋值”变量也是如此,这是一个加分项。重新分配_init.mgr仍然是可能的,但在客户端代码中看起来肯定是“足够错误”的。 - Jean-Philippe Pellet
由于“_init”仅在“AppProperties”内可见,而“def mgr”是不可更改的,因此无法在客户端代码中重新分配。 - Debilski
没错,但这只需要一个简单的private var就可以实现了。我想防止在 AppProperties 的其余实现中出现此问题。我猜我不应该写“客户端代码”。 - Jean-Philippe Pellet

2

看了JPP的帖子,我做了另一种变化:

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

这次,我们可以多次分配变量,但需要确保凭据在作用域内正确。凭据在第一次分配时创建并返回,这就是为什么我们需要立即保存它到implicit val credential = mgr := new FileManager(config)中。如果凭证不正确,它将无法工作。
(请注意,如果作用域中有更多的凭据,隐式凭据将无效,因为它们将具有相同的类型。可能可以解决这个问题,但我目前不确定。)

1
能否将SetOnceCredential类从SetOnce对象移动到SetOnce类中,以避免您提到的多凭据问题?似乎我们甚至可以摆脱key中缓存的凭据。我喜欢这个想法,但现在编译时保证被削弱了,因为任何赋值都会编译(使用默认的null值),但在运行时会失败。 - Jean-Philippe Pellet
你是正确的,将它放在类内部当然可以修复这个问题。 - Debilski
非常好!我不喜欢的最后一件事是,你的最后一行 mgr1 := new FileManager(new Config),虽然它会在运行时失败,但仍然编译通过,没有表明它实际上需要凭据。 - Jean-Philippe Pellet
哦,那个问题很棘手。我不确定我们能修复它。可能考虑为凭据设置一次读取变量,并使 := 的隐式强制执行。但仍然可以访问一次性读取方法以检索凭据,因此类型系统不会抱怨,即使返回的是 null 而不是凭据。 - Debilski

0
我在想,可以这样做:
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。 - Jean-Philippe Pellet

0

虽然并非完全相同,但在许多情况下,解决此“设置变量一次后继续使用它”的方法是使用特殊工厂方法进行简单的子类化。

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(...)
}

0

您可以将该值始终移动到另一个对象中,仅初始化一次并在需要时访问它。

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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接