我正在尝试用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
方法更改字段,它需要成为构造函数的一部分。
我希望字段_inSyncBikeIds
是final
(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 class
es是Serializable
,所以有一个Java序列化漏洞:在应用程序代码中,这是可以通过解决的,因为它被破坏了,所以永远不要使用Java序列化,但它通过完全邪恶来弥补被破坏的(即,如果你有一个使用Java序列化的Scala应用程序,你可能应该重新评估导致你出现这种情况的选择(。
在JVM字节码AFAIK中无法对sealed
ness进行编码(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仍然是可能的,即使它产生完全相同的结果。这似乎没什么用。