在scala中,如何创建包含引用的不可变对象列表?
下面的示例构建了一组人员,他们之间有家庭链接在scala中,如何创建包含引用的不可变对象列表?,scala,Scala,下面的示例构建了一组人员,他们之间有家庭链接 case class Person(id: Int, name: String, father: Option[Int], mother: Option[Int], children: Set[Int]) val john = Person(0, "john", None, None, Set(2)) val maria = Person(1, "maria", None, None, Set(2)) val georges = P
case class Person(id: Int, name: String, father: Option[Int], mother: Option[Int], children: Set[Int])
val john = Person(0, "john", None, None, Set(2))
val maria = Person(1, "maria", None, None, Set(2))
val georges = Person(2, "georges", Some(0), Some(1), Set.empty)
val people = Set(john, maria, georges)
val peopleMap = people.map(p => (p.id, p)).toMap
val meanChildrenSize = people.map(p => p.children.map(peopleMap).size).sum.toDouble / people.size
这个例子工作正常,但我不喜欢这样,我需要构建这个额外的peopleMap
,并调用p.children.map(peopleMap)
,因为它使阅读变得困难。我更愿意将示例建模如下:
case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])
val john = Person(1, "john", None, None, Set.empty)
val maria = Person(2, "maria", None, None, Set.empty)
val georges = Person(3, "georges", Some(john), Some(maria), Set.empty)
val people = Set(john, maria, georges)
val meanChildrenSize = people.map(p => p.children.size).sum.toDouble / people.size
但是,现在的问题是john和maria无法初始化子集合,因为georges尚未创建。如何解决这个问题(最好使用不可变的case类)
更新: 有人提议使用lazy:
case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])
lazy val john: Person = Person(1, "john", None, None, Set(georges))
lazy val maria: Person = Person(2, "maria", None, None, Set(georges))
lazy val georges: Person = Person(3, "georges", Some(john), Some(maria), Set.empty)
StackOverflower错误导致此操作失败。完全不可变的案例类无法解决此问题,因为您需要
lazy val
s在不可变的数据结构中创建循环依赖项
以下是定义提供所需API的Person
类的一种方法:
class Person(val id: Int, val name: String, father0: => Option[Person], mother0: => Option[Person], children0: => Set[Person]) {
lazy val father = father0
lazy val mother = mother0
lazy val children = children0
override def equals(that: Any): Boolean = that match {
case Person(this.id, this.name, this.father, this.mother, this.children) => true
case _ => false
}
override def hashCode(): Int = { ... }
}
object Person {
def apply(id: Int, name: String, father0: => Option[Person], mother0: => Option[Person], children0: => Set[Person]): Person =
new Person(id, name, father0, mother0, children0)
def unapply(p: Person): Some[(Int, String, Option[Person], Option[Person], Set[Person])] =
Some(p.id, p.name, p.father, p.mother, p.children)
}
不幸的是,您不能直接使用case类这一事实意味着您有相当多的样板要编写。我认为@srjd的答案虽然不完整,也有问题,但是朝着正确方向迈出的一步 对于更完整的工作解决方案,您可以使用
class Person(id: Int, name: String, mother: => Option[Person], father: => Option[Person], children: => Set[Person]) {
def getId = id
def getName = name
def getMother = mother
def getFather = father
def getChildren = children
override def equals(that: Any): Boolean = that match {
case Person(tId, tName, tMother, tFather, tChildren) => {
id == tId && name == tName && mother == tMother && father == tFather && children == tChildren
}
case _ => false
}
// you will want a better hashcode method
override def hashCode(): Int = List(id, name).hashCode()
}
object Person {
def apply(id: Int, name: String, mother: => Option[Person], father: => Option[Person], children: => Set[Person]): Person =
new Person(id, name, mother, father, children)
def unapply(p: Person): Option[(Int, String, Option[Person], Option[Person], Set[Person])] =
Some((p.getId, p.getName, p.getFather, p.getMother, p.getChildren))
}
现在你可以像下面这样使用它
lazy val father: Person = Person(1, "father", None, None, Set(son))
lazy val mother: Person = Person(2, "mother", None, None, Set(son))
lazy val son: Person = Person(2, "son", Some(father), Some(mother), Set.empty[Person])
val people = Set[Person](father, mother, son)
val totalChildrenSize1 = people
.map({
case Person(id, name, mother, father, children) => children.size
})
.sum
.toDouble
// Or
val totalChildrenSize2 = people
.map(p => p.getChildren.size)
.sum
.toDouble
val meanChildrenSize1 = totalChildrenSize1 / people.size
// Or
val meanChildrenSize2 = totalChildrenSize2 / people.size
再懒一点会有帮助的。让我们从极端情况开始:
case class Person(id: Int, name: String, father: () => Option[Person], mother: () => Option[Person], children: () => Set[Person])
lazy val john = Person(1, "john", () => None, () => None, () => Set.empty)
lazy val maria = Person(2, "maria", () => None, () => None, () => Set.empty)
lazy val georges = Person(3, "georges", () => Some(john), () => Some(maria), () => Set.empty)
val people = Set(john, maria, georges)
实验:
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.0
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.3333333333333333
这将允许您避免关系中的循环,但可能会破坏“equals”契约(在数据中使用循环将破坏naiveequals
-方法并导致堆栈溢出,因此最好不要为function0()=>…
成员实现它)-请参见下面的示例。它还将导致重新评估问题(这可以通过@srjd方法的名称调用来解决,但是,如果def max:Person=Person(1,“aaa”,None,None,Set(max));max==max,则该方法可能会堆叠溢出等式)
因此,更实际的方法是:
case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])
lazy val john = Person(1, "john", None, None, Set.empty)
lazy val maria = Person(2, "maria", None, None, Set.empty)
lazy val georges = Person(3, "georges", Some(john), Some(maria), Set.empty)
val people = Set(john, maria, georges)
实验得到了同样的结果。但是,一些复杂的交叉依赖可能会导致问题,具体取决于人员
集中的初始化顺序(我应该提到,任何顺序都适用于您当前的示例和我对数据的随机非循环修改)
或者,正如@YuvalItzchakov所建议的,您可以先初始化georges
,但它的可伸缩性较差,因为您需要考虑初始化顺序
lazy val
方法不起作用时的示例:
//John is his own child now (almost like Fry from Futurama)
lazy val john: Person = Person(1, "john", None, None, Set(john))
val people = Set(georges,john, maria)
java.lang.StackOverflowError
因此,如果您希望数据中出现一些循环,最好使用上面介绍的“极端”方法:
...
//John is his own child now
lazy val john: Person = Person(1, "john", () => None, () => None, () => Set(john))
val people = Set(john, maria, georges)
实验:
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.0
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.3333333333333333
因此,结果是正确的,但如果手动重写等于account for father/mother/children成员,则会产生堆栈溢出。您的模型建议id相等,因此您可以使用这样的smthng:
case class Person(id: Int, name: String, father: () => Option[Person], mother: () => Option[Person], children: () => Set[Person]){
override def equals(that: Any): Boolean = that match {
case Person(id2, name2, _, _, _) => id == id2 && name == name2
case _ => false
}
//You might not need to override hashCode (if you're not gonna put data as a key into some big dictionaries) as `equals` is stronger
override def hashCode = (id, name).hashCode
}
例如:
val a = Person(1, "john", () => None, () => None, () => Set()) == Person(1, "john", () => None, () => None, () => Set())
val b = a
@ Map(a -> "a", b -> "b")
res55: Map[Person, String] = Map(Person(1, "john", <function0>, <function0>, <function0>) -> "b")
@ res55(a)
res56: String = "b"
@ res55(b)
res57: String = "b"
您可能会注意到,您的方法确实考虑了重复的儿童尺寸(!),因此您可能需要:
val meanChildrenSize = people.toList.map(_.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.6666666666666666
为什么不先创建georges
,然后从georges
中创建的引用绑定john
和maria
?这实际上有更多的问题mother
每次调用getMother
@Jasper-M时都会重新评估。如果您没有阅读两个答案,请现在阅读,并注意两种方法在这方面是相同的。与sjrd的答案相比,这个答案只是一个更完整的无问题改进。这会增加多少开销?作为母亲
只是=>实例
。请记住,正常的getter也是实例。为什么会被否决?有人吗?这不仅仅是开销的问题。还有副作用。嗯。。。副作用是Scala FP的祸根。Scala中几乎所有的FP构造都可以被认为是不安全的,因为可能会引入副作用@Jasper-M以及我对StackOverFlow的看法。。。当你发现一个重大问题的答案不可接受时,应该使用否决票。当你喜欢某件事的答案时,请举手表决。在其他情况下,答案应该被搁置。为什么这一点会被否决?我最近注意到这里有一个讨厌的习惯,就是对答案进行向下投票,不提供随时可以复制粘贴的代码。我看不出这是如何解决问题的。如何初始化这3个人?lazy val john:Person=…
,等等。我看不出这是如何解决问题的。john.children.size仍然是0,应该是1(乔治)。谢谢,回答被接受。下面是一个相关的问题: