Scala 访问控制修饰符

2019-05-02 12:33:15 | 编辑 | 添加

1.Scala与Java访问修饰符的差异

  • 如果不指定任何访问修饰符,那么Java 会默认为包内部可见,而Scala 则默认为公开。

  • Java 主张全有或全无,也就是说,对当前包的所有类可见或者对所有都不可见。而Scala 对可见性的控制是细粒度的。

  • Java 的protected 是宽泛的,其作用域包括在任意包中的派生类和当前包中的任意类,而Scala 的protected 与C++和C#的类似,只有派生类能够访问。然而,在Scala 中protected 还有相当自由和灵活的用法。

  • Java 的封装是类级别的。可以在一个类的实例方法中访问该类的任何实例的所有私有字段和方法,在Scala 中也一样,不过,在Scala 中也可以进行定制,让其只能在当前的实例方法中访问,这样就和Ruby 比较像了。



2.Scala定制访问修饰符

在不使用任何访问修饰符的情况下,Scala 默认认为类、字段和方法都是公开的。如果想将一个成员标记为private 或者protected,只要像下面这样用相应的关键字标记即可。

class Microwave {
def start(): Unit = println("started")
def stop(): Unit = println("stopped")
private def turnTable(): Unit = println("turning table")
}
val microwave = new Microwave
microwave.start() // 编译正确


这段代码中,start()和stop()方法默认都是公开的,我们可以在Microwave 类的任意实例中访问这两个方法。另外,我们将turnTable()方法显式定义为private,因此我们无法在这个类外面访问这个方法。如果我们像前面的例子那样尝试,就会得到如下错误:

Access.scala:9: error: method turnTable in class Microwave cannot be
accessed in this.Microwave
microwave.turnTable() // 编译错误
^
one error found

对于公开的字段和方法,可省略访问修饰符。而对于其他成员,要显式指定访问修饰符,以达到期望的访问控制效果。


3.Scala 的protected

在Scala 中,protected 让所修饰的成员仅对自己和派生类可见。对于其他类来说,即使正好和所定义这个类处于同一个包中,也无法访问这些成员。更进一步,派生类在访问protected 成员的时候,成员的类型也需要一致。让我们用下面的例子做检验。


class Vehicle {
protected def checkEngine() {}
}
class Car extends Vehicle {
def start() { checkEngine() /* 编译正确 */ }
def tow(car: Car) {
car.checkEngine() // 编译正确
}
def tow(vehicle: Vehicle) {
vehicle.checkEngine() // 编译错误
}
}
class GasStation {
def fillGas(vehicle: Vehicle) {
vehicle.checkEngine() // 编译错误
}
}


通过编译这段代码我们可以看到,在编译器的错误消息中,这些访问控制已经生效:

Protected.scala:12: error: method checkEngine in class Vehicle cannot be
accessed in automobiles.Vehicle
Access to protected method checkEngine not permitted because
prefix type automobiles.Vehicle does not conform to
class Car in package automobiles where the access take place
vehicle.checkEngine() // 编译错误
Protected.scala:17: error: method checkEngine in class Vehicle cannot be
accessed in automobiles.Vehicle
Access to protected method checkEngine not permitted because
enclosing class GasStation in package automobiles is not a subclass of
class Vehicle in package automobiles where target is defined
vehicle.checkEngine() // 编译错误
two errors found

在上面的代码中,Vehicle 的checkEngine()方法是protected 的,能够被Vehicle 的任何实例方法访问到。我们可以在一个实例方法中访问这个方法,如派生类Car的start()方法中,也可以在一个Car 的实例中访问这个方法,如Car 类的tow()方法,但我们不能在Car 的实例中通过Vehicle 的实例访问这个方法,其他任意类也都不行,如GasStation,尽管GasStation 和Vehicle 在同一个包中。这种行为模式和Java 中protected 是有区别的。Scala 对protected 成员的访问控制更加严格。


4.细粒度的访问控制

一方面,Scala 在protected 修饰符上的限制比Java 更多;另一方面,Scala 在设置访问可见性上面有很大的灵活度以及细粒度的控制。

可以为private 和protected 修饰符指定额外的参数。故而,除了简单地将一个成员标记为private,还可以标记为private[AccessQualifier],其中AccessQualifier可以是任何封闭类名、一个封闭的包名或者是this(即实例级别的可见性)。

访问修饰符上的限定词告诉 Scala,对于所有类该成员都是私有的,除了以下情况。


  • 如果没有指定AccessQualifier(在默认情况下),那么该成员只能在当前类或者其伴生对象中访问。

  • 如果AccessQualifier 是一个类名,那么该成员可以在当前类、伴生对象以及AccessQualifier 对应的封闭类和其伴生对象中可访问。

  • 如果AccessQualifier 是一个封闭的包名,那么该成员可以在当前类、伴生对象以及所提到的包下面的所有类中访问。

  • 如果AccessQualifier 是this,那么将会限制该成员只能在该实例中访问,对于同一个类的其他实例,也是不可见的,这是所有选项中限制最严格的。


组合之后情况有点儿多,比较费脑力。下面这个细粒度访问控制的例子可以把这些细节都讲清楚。


package society {
package professional {
class Executive {
private[professional] var workDetails = null
private[society] var friends = null
private[this] var secrets = null
def help(another: Executive): Unit = {
println(another.workDetails)
println(secrets)
println(another.secrets) // 编译错误
}
}
class Assistant {
def assist(anExec: Executive): Unit = {
println(anExec.workDetails)
println(anExec.friends)
}
}
}
package social {
class Acquaintance {
def socialize(person: professional.Executive) {
println(person.friends)
println(person.workDetails) // 编译错误
}
}
}
}


编译这段代码将会产生如下错误:

FineGrainedAccessControl.scala:12: error: value secrets is not a member of
society.professional.Executive
println(another.secrets) // 编译错误
FineGrainedAccessControl.scala:28: error: variable workDetails in class
Executive cannot be accessed in society.professional.Executive
println(person.workDetails) // 编译错误
two errors found


这个例子展示了不少 Scala 中的细微差别。在Scala 中,我们可以定义嵌套包,类似于C++和C#中的嵌套命名空间。在定义包名时,我们可以遵循Java 的风格—使用点,如society.professional,也可以使用C++或者C#的嵌套命名空间的风格。如果决定把从属于一个层次结构的包下面的多个比较小的类放在同一个文件中,那么遵循Java 的风格就没有后者方便。


在上面的代码中,我们让Executive 中的私有字段workDetails 在封闭的包professional 中可见。Scala 就会允许这个包中的Assistant 类中的方法访问这个字段。但是,在别的包里的Acquaintance 类,就不能访问这个字段。


对于私有字段 friends,我们让封闭包society 下面的所有类都能访问。这就使得在Acquaintance 类中可以访问字段friends,因为该类是在society 包所包含的子包之中。


private 默认的可见性是类级别的,在一个类的实例方法中,可以访问同一个类的任何实例中标记为private 的成员。然而,Scala 通过this 限定符可以对private 和protected 做细粒度的控制。例如,在前面的例子中,因为secrets 已经被标记为private[this],所以实例方法只能在隐式实例下访问这个字段,也就是说,在这个实例中,这个字段不能在其他实例中访问。这也是我们在实例方法help()中能够访问secrets但是不能访问another.secrets 的原因。同样,一个标记为protected[this]的字段可以在派生类的实例方法中访问,但是仅限于当前实例。