如何在Scala中强制封装不可变类



我正在尝试用Dart编写不可变的代码。Dart并没有真正考虑到不变性,这就是为什么我需要写很多样板才能实现不变性。正因为如此,我对Scala这样一种基于不变性概念构建的语言如何解决这个问题产生了兴趣。

我目前在Dart中使用以下类:

class Profile{
List<String> _inSyncBikeIds = []; // private field
String profileName; // public field
Profile(this.profileName); // You should not be able to pass a value to _inSyncBikeIds
void synchronize(String bikeId){
_inSyncBikeIds.add(bikeId);
}
bool isInSync(String bikeId){
return _inSyncBikeIds.contains(bikeId);
}
void reset(){
_inSyncBikeIds = [];
}
}

不可变中的同一类:

class Profile{
final List<String> _inSyncBikeIds = []; // private final field
final String profileName; // public final field
factory Profile(String profileName) => Profile._(profileName); // You should not be able to pass a value to _inSyncBikeIds
Profile._(this._inSyncBikeIds, this.profileName); // private contructor
Profile synchronize(String bikeId){
return _copyWith(inSyncBikeIds: _inSyncBikeIds.add(bikeId);
}
bool isInSync(String bikeId) {
return _inSyncBikeIds.contains(bikeId);
}
Profile reset(){
return _copyWith(inSyncBikeIds: []);
}
Profile copyWith({
String profileName,
}) {
return _copyWith(profileName: profileName)
}
Profile _copyWith({
String profileName,
List<Id> inSyncBikeIds,
}) {
return Profile._(
profileName: profileName ?? this.profileName,
inSyncBikeIds: inSyncBikeIds ?? _inSyncBikeIds);
}
}

到目前为止,我从Scala了解到的是,每个类都会自动创建一个copy方法。为了能够使用copy方法更改字段,它需要成为构造函数的一部分。

我希望字段_inSyncBikeIdsfinal(Scala中的val(。为了更改字段_inSyncBikeIds的值,我需要创建对象的副本。但是,为了使用copy方法,要更改字段,它需要成为类的构造函数的一部分,就像class Profile(private val _inSyncBikeIds, val profileName)一样。但这会破坏封装,因为每个人都可以创建一个对象并初始化_inSyncBikeIds。在我的情况下,_inSyncBikeIds在初始化后应该始终是一个空列表。

三个问题:

  • 如何在Scala中解决此问题
  • 当我在类中使用copy方法时,我可以使用copy方法更改私有字段吗
  • Scala中的copy方法是否也复制了私有字段(即使它们不是构造函数的一部分,当然也不能更改该私有字段(

Scala的传统倾向于将不可变数据视为免费共享的许可证(因此默认为公共等(。封装的解释更多的是,对象外部的代码无法直接变异数据:无论可见性如何,不可变数据都满足这一点。

可以通过使abstract(几乎总是使用private构造函数的sealed abstract(来抑制case class的自动生成的copy方法。这通常用于使apply/copy方法返回不同的类型(例如,将验证失败编码为值而不抛出异常的东西(如require((,但它可以用于您的目的

sealed abstract case class Profile private(private val _inSyncBikeIds: List[String], profileName: String) {
def addBike(bikeId: String): Profile = Profile.unsafeApply(bikeId :: _inSyncBikeIds, profileName)
// Might consider using a Set...
def isInSync(bikeId: String): Boolean = _inSyncBikeIds.contains(bikeId)
def copy(profileName: String = profileName): Profile = Profile.unsafeApply(_inSyncBikeIds, profileName)
}
object Profile {
def apply(profileName: String): Profile = unsafeApply(Nil, profileName)
private[Profile] def apply(_inSyncBikeIds: List[String], profileName: String): Profile = new Profile(_inSyncBikeIds, profileName) {}
}

unsafeApply更常见于作为值用例的验证,但它的主要目的是将抽象Profile的具体实现限制为仅匿名实现;这种单态性对运行时性能有着有益的影响。

注意:case classes是Serializable,所以有一个Java序列化漏洞:在应用程序代码中,这是可以通过解决的,因为它被破坏了,所以永远不要使用Java序列化,但它通过完全邪恶来弥补被破坏的(即,如果你有一个使用Java序列化的Scala应用程序,你可能应该重新评估导致你出现这种情况的选择(。

在JVM字节码AFAIK中无法对sealedness进行编码(Scala使用注释IIRC,因此Scala会将Profile的扩展限制在该编译单元内,但例如Kotlin不会(,private[Profile]访问控制的编码方式也不是JVM语言强制执行的(unsafeApply方法实际上是字节码中的public(。同样,在应用程序代码中,显而易见的问题是";你为什么要从Java/Kotlin/Clojure/…中使用这个&";。在库中,你可能需要做一些棘手的事情,比如抛出一个异常,抓住它并检查堆栈的顶部框架,如果它不太好,就再次抛出。

我不知道在dart中是否可能,但在scala中,这将使用一个私有构造函数来完成:

class Profile private (val _foo: Seq[String], val bar: String) {
def this(bar: String) = this(Nil, bar)
}

这可以让您定义

private copy(foo: Seq[String], bar: String) = new Profile(foo, bar)

只要类是final,这就很好。如果你对它进行子类化,那么坏的情况就会随之而来:Child.copy()返回Parent的一个实例,除非你在每个子类中重写copy,但没有好的方法来强制执行它(scala3确实对此有一些改进(。

您提到的生成的copy方法仅适用于case类。但是,对case类进行子类化会得到一些更有趣的结果。

这是真的很少有用。例如,查看您的代码,如果我正确阅读了ask,您希望用户能够而不是CCD_ 35但CCD_ 36仍然是可能的,即使它产生完全相同的结果。这似乎没什么用。

最新更新