Scala中的内部类与不变性

Scala中的内部类与不变性,scala,immutability,inner-classes,Scala,Immutability,Inner Classes,请看以下玩具示例: case class Person(name: String, address: Person#Address = null) { case class Address(street: String, city: String, state: String) { def prettyFormat = s"To $name of $city" // note I use name here } def setAddress(street: String,

请看以下玩具示例:

case class Person(name: String, address: Person#Address = null) {
  case class Address(street: String, city: String, state: String) {
    def prettyFormat = s"To $name of $city" // note I use name here
  }

  def setAddress(street: String, city: String, state: String): Person =
    copy(address=Address(street,city,state))

  def setName(n: String): Person = copy(name=n)
}
你看到那里有虫子吗?是的,以下代码将在两种情况下打印相同的消息(John):

val p1 = Person("John").setAddress("Main", "Johntown", "NY")
println(p1.address.prettyFormat) // prints To John of Johntown
val p2 = p1.setName("Jane")
println(p2.address.prettyFormat) // prints To John of Johntown
当然,这是因为set方法中保留了Address中的$outer引用,所以p2内部对象仍然引用John。这个问题可以通过以下方式或通过重新创建Address对象来解决(如果我们在case类中预先编写了副本构造函数,那不是很好吗?)

然而,当有几个像这样的内部对象和几十个像setName这样的方法时,问题就变得更加恼人了。所以我的结论是,不变性和类内部类是互不兼容的

问:对于嵌套不可变对象的结构,内部对象需要访问外部对象才能完成其工作,是否有一种设计模式或有用的习惯用法


到目前为止,我已经考虑将person传递给prettyFormat,或者将内部方法包装到Reader monad中,因此当前person将应用于prettyFormat返回的monad。还有什么好主意吗?

这并不是说不变性和类内部类是互不兼容的,而是当您创建内部地址类时,它会绑定到那个Person实例(否则您将使用静态内部类,即在伴生对象中定义地址)

您的问题更多的是针对case类提供的拷贝>代码>方法的语义,不考虑内部类。因此,要么放弃不变性,要么在修改时创建一个真正的新人:

def setName(n: String): Person = Person(n, street, city, state)
请注意,我不应该将直接的
地址
实例传递给
人员()
,您的定义是,每个地址类型都是单个人员的一部分,只对该人员有意义,因此它不能存在于该人员之外,也不能从外部传递给正在创建的新人员。同样,如果不是这样,那么您需要用不同的语义重新思考您的结构

就我个人而言,我认为以下内容对该领域的描述更加清晰/直观:

case class Address(street: String, city: String, state: String)
case class Person(name: String, address: Address) {
   def prettyFormat = s"To $name of ${address.city}"
} 
然后,您可以创建地址/人的副本,而无需担心,并且完全不可变

问:对于嵌套不可变对象的结构,内部对象需要访问外部对象才能完成其工作,是否有一种设计模式或有用的习惯用法

我不这么认为,不

“外部”对象必然会引用“内部”对象

通过使用嵌套类,您已经从“内部”对象创建了对“外部”对象的(隐藏)引用。这混淆了Scala中的“案例类”机制,正如您在示例中所看到的


因此,您的两个类是1-1的,在设计上是相互依存的。如果它们真的是1-1,那么将它们都放在一个类中会更简单、更清晰。

使用镜头访问嵌套的不可变对象

透镜寻址的样板文件已设置为
setAddress

scala> case class Parent(name: String, child: Parent#Child) { case class Child(name: String) { def parent = Parent.this.name } }
defined class Parent
准父母

scala> val p1 = Parent("Bob", null)
p1: Parent = Parent(Bob,null)

scala> val p2 = Parent("Bob", new p1.Child("Billy"))
p2: Parent = Parent(Bob,Child(Billy))
鲍勃经历了变化,但孩子不知道她的名字

scala> val p3 = p2.copy(name = "Mary")
p3: Parent = Parent(Mary,Child(Billy))

scala> p3.child.parent
res0: String = Bob

scala> import scalaz._
import scalaz._

scala> val parentName = Lens.lensu[Parent,String]((a,v)=>a.copy(name=v),_.name)
parentName: scalaz.Lens[Parent,String] = scalaz.LensFunctions$$anon$5@39bd45b4
示例名称转换

scala> parentName =>= (_ + " Jo")
res1: Parent => Parent = <function1>

scala> res1(p1)
res3: Parent = Parent(Bob Jo,null)

scala> val parentChild = Lens.lensu[Parent, Parent#Child]((a,v)=>a.copy(child=a.Child(v.name)), _.child)
parentChild: scalaz.Lens[Parent,Parent#Child] = scalaz.LensFunctions$$anon$5@3cdeef1e

scala> val adopt = parentChild =>= identity
adopt: Parent => Parent = <function1>
scala>parentName=>=(uu+“Jo”)
res1:Parent=>Parent=
scala>res1(p1)
res3:Parent=Parent(Bob Jo,null)
scala>val parentChild=Lens.lensu[Parent,Parent#Child]((a,v)=>a.copy(Child=a.Child(v.name)),u.Child)
parentChild:scalaz.Lens[Parent,Parent#Child]=scalaz.LensFunctions$$anon$5@3cdeef1e
scala>val-adopt=parentChild=>=identity
采用:父项=>父项=
如果父项名称更改,子项必须采用它

scala> val rename = res1 andThen adopt
rename: Parent => Parent = <function1>

scala> val p4 = rename(p3)
p4: Parent = Parent(Mary Jo,Child(Billy))

scala> p4.child.parent
res4: String = Mary Jo
scala>val rename=res1,然后采用
重命名:父项=>父项=
scala>val p4=重命名(p3)
p4:父母=父母(玛丽·乔,孩子(比利))
scala>p4.child.parent
res4:String=Mary Jo

既然内部类和外部类都是不可变的,那么问题到底是什么?如果更新John的地址,您将构建一个新的地址实例,该实例将保存在John的更新/复制的person实例中。这不会对Jane的地址产生任何影响(如果您更新Jane的地址,则反之亦然)。我在那里做的测试很好地说明了这个问题:将p1名称更改为Jane根本不起作用,第二次打印会打印John!不,它打印的是简的地址,从结构上看,正好是约翰的地址。如果您不希望Jane拥有相同的地址,请在
p1.setName
的结果上调用setAddress。啊,我明白了-您的
prettyFormat
内部定义
address
正在引用外部定义的变量-是的,这样做会导致问题。我建议将该方法移到
Person
类中。@JacobEckel确切地说,没有。撇开Scala不谈,还有一个对象建模问题。您对域模型中的循环引用有何看法?嵌套case类已经隐式地创建了一个循环引用,将嵌套的case类移出会使循环引用显式,更重要的是,它对所有人都是显而易见的。这是否是一个好主意取决于您对首先允许循环引用是否好的看法,但除此之外,我建议将其显式化有助于使这种行为对所有人都显而易见(从而有助于避免一些错误)。实际上,解决方案之一可能是覆盖复制,使其成为深度复制和“内部对象“娱乐。这就是为什么我希望为case类提供预煮拷贝构造函数。至于你的第二个建议,首先,地址的漂亮打印不属于Person类,其次,按照这个逻辑,我必须将复杂系统的所有方法都推到外部类,这正是我想要避免的。我不相信。这不仅仅是“漂亮的地址打印”。它是此人的地址,包括此人的姓名。对我来说,认为“一个人有一个地址”比“一个地址有一个人”(或双边关系)更直观
scala> val rename = res1 andThen adopt
rename: Parent => Parent = <function1>

scala> val p4 = rename(p3)
p4: Parent = Parent(Mary Jo,Child(Billy))

scala> p4.child.parent
res4: String = Mary Jo