Kotlin供给了两个本机功用来完成托付形式。第一个是接口托付(例如策略形式)。另一种是特点托付,它专注于类成员/特点(例如推迟加载、observable等)。它们一起供给了一组丰厚而简洁的功用。经过本博客,您将了解在什么情况下运用此形式。这些示例向您展现优点,但也会展现已知的问题。
1、组合因为承继
众所周知,在面向目标语言中,承继是要害特性之一。这意味着您能够将一些功用和笼统放入基类中。其他类能够从基类承继因而取得承继的功用。这称为“Is-a”联系。作为教科书的示例,咱们能够幻想Shape类(基类)和rectangle、circle等为派生类。
过去,这种建模办法被滥用,导致规划不佳。例如,长承继链用于目标增加增量功用。作为一个过于夸大的示例,请考虑一下类设置。咱们想要创立“Animal”并完成它们的移动和进食办法。一种简单的办法是如下图所示完成它。很明显,这不会扩展。
另一方面,下图更倾向于组合而不是承继。该类Animal完成接口IMoving
和IEating
。但是它没有实践的完成代码,它将调用托付给Walking
和详细完成MeatEating
。正如咱们所看到的,这种规划愈加灵敏,因为能够完成Moving
和Eating
而不用修改Animal类。
委派承继是完成SOLID规划形式中的敞开-封闭原则的一种办法。
2、接口托付
怎么在Kotlin中创立托付?
Kotlin供给了原生功用来完成托付形式,而无需编写任何样板代码。这仅适用于接口(即笼统类)。接口完成IWalking
如下代码所示。
// 规范托付
interface IMoving {
fun move()
}
class Walking: IMoving {
override fun move() {
println("Walking")
}
}
class Animal(private val movable: IMoving): IMoving {
override fun move() {
movable.move()
}
}
fim main() {
var movable = Walking()
var animal = Animal(movable)
animal.move()
}
// 内置托付
interface IMoving {
fun move()
}
class Walking: IMoving {
override fun move() {
println("Walking")
}
}
class Animal(movable: IMoving): IMoving by movable {
}
fun main() {
var walking = Walking()
var animal = Animal(walking)
animal.move()
}
正如咱们所看到的,第一部分代码需求完成接口办法。在正确的版别中,编译器会为您执行此操作。当界面具有多种功用时,作用会愈加明显。在派生类类型的超类型的列表中运用by
-子句表明可移动的信息将在IMoving
目标的内部存储,编译器将生成一切将转发到IMoving
的接口办法。
2.1、掩盖接口成员
假如需求重写接口的特定成员函数,只需在派生类中编写该函数并增加要害字override
即可。编译器将运用重写办法的新规范。在下面的实例中,咱们创立了该办法的新版别move
。
// By Delegate的重写功用
class Animal(movable: IMoving): IMoving by movable {
override fun move() {
println("Something else")
}
}
fun main() {
var walking = Walking()
var animal = Animal(walking)
animal.move()
}
2.2、多个接口/承继
为了完成上面的例子,咱们有必要完成一切接口并将一切函数调用托付给成员变量。咱们能够经过将与上一节相同的办法应用于一切承继的接口来做到这一点。
// 介个接口托付
interface IMoving {
fun move()
}
class Walking: IMoving {
override fun move() {
println("Walking")
}
}
interface IEating {
fun eat()
}
class MeatEater: IEating {
override fun eat() {
println("Eat meat")
}
}
class Animal(movable: IMoving, eating: IEating): IMoving by movable, IEading by eating {
}
fun main() {
var walking = Walking()
var eating = MeatEater()
var animal = Animal(walking, eating)
animal.move()
animal.eat()
}
2.2.1、相同的函数签名
假如您需求完成多个声明了相同办法的接口,则会出现特殊情况。在这种情况下,托付是不明确的,编译器会给出一个过错,例如“Class 'xxx' must override public open fun doSomething(): Unit defined in Animal because it inherits many implementation of it
”。您需求显现完成(或重写)此函数并手动托付它。
2.3、在运转时替换托付
一般需求在运转时更改托付。这一般用在策略或状态形式中。(现在)Kotlin不支持在运用by
要害字进行注入时更改托付。
以下代码具有误导性,因为它打印以下内容:
Walking
Walking
Running@xxx
//在运转时更改托付
interface IMoving {
fun move()
}
class Running: IMoving {
override fun move() {
println("Running")
}
}
class Walking: IMoving {
override fun move() {
println("Walking")
}
}
class Animal(var movable: IMoving): IMoving by movable {
}
fun main() {
var walking = Walking()
var animal = Animal(walking)
animal.move()
var running = Running()
animal.movable = running
animal.move()
println(animal.movable)
}
3、特点托付
什么是特点托付? Kotlin供给了一些很好的功用来说完成托付特点。它向类特点增加了一些常见功用。您能够在库中创立一些特点(例如Lazy)并运用此功用包装您的类成员。
要了解其作业原理,您需求了解每个成员变量在暗地供给getValue
和setValue
。特点托付不需求完成接口,但需求为每种类型供给这些函数。正如您所看到的,特点托付是经过目标/函数。
在下面的示例,咱们供给了一个托付,该托付在每次拜访时都会打印消息。
3.1、只读托付
任何特点托付有必要至少为所托付的值类型完成以下函数。这type T
取决于被包装的值类型。
// Read requited delegate function
operator fun getValue(example: Any, property: KProperty<<*>): T {
return // value of T
}
作为示例,咱们将运用与Kotlin参考页面略有不同的代码。咱们的托付只能用于字符串类型,并且仅回来空字符串。这段代码毫无用处,但它显现了只读特点的用法。
// Example Read only delegate
class Example {
val p: String by Delegate()
}
class Delegate {
operator fun getValue(example: Any, property: KProperty<*>): String {
return ""
}
}
3.2、读/写托付
为了运用上述托付进行读写完成,咱们还有必要完成以下函数。请注意,咱们有一个字符串值类型。因而咱们写了s: String
。您需求将其调整为您想要运用的类型。
// Write required delegate function
operator fun setValue(example: Any, property: KProperty<*>, s: String) {
}
咱们上面的例子现在能够完成所需的功用了。咱们将引进一个类变量cached
来保存实践值。
// Example Read/Write delegate
class Example {
var p: String by Delegate()
}
class Delegate {
var cached = ""
operator fun getValue(example: Any, property: KProperty<*>): String {
return cached
}
operator fun setValue(example: Any, property: KProperty<*>, s: String) {
cached = s
}
}
3.3、完成接口作为扩展
另一种办法是将getValue()
和setValue()
函数完成为类的扩展函数Delegate
。假如该类不在您的源代码办理中,这很有用。
// Delegated property as extension function
class Example {
val string: String by Delegate()
}
class Delegate {
}
operator fun Delegate.getValue(example: Any, propety: KProperty<*>): String {
return ""
}
3.4、通用托付界说
明显的缺点是此代码仅适用于字符串类型的目标。假如咱们想将它用于其他类型,咱们有必要运用泛型类。咱们将向您展现怎么更改上述示例以使其兼容一切类型。
// Generic class for Delegate
class Example {
var string: String by Delegate("hello")
var int: Int by Delegate(3)
}
class Delegate<T>(var cached: T) {
operator fun getValue(example: Any, property: KProperty<*>): T {
return cached
}
operator fun setValue(example: Any, property: KProperty<*>, s: T) {
cached = s
}
}
3.5、匿名目标托付
Kotlin还供给了一种创立匿名目标托付而无需创立新类的办法。这是因为接口ReadOnlyProperty
和ReadWriteProperty
规范库而起作用的。这些接口将为ReadOnlyProperty
供给getValue
函数。ReadWriteProperty
经过增加setValue()
函数,扩展了ReadOnlyProperty
。
在上面的示例,托付特点是经过函数调用创立为匿名目标的。
// Anonymous objects delegates
class Example {
var string: String by delegate()
}
fun delegate(): ReadWriteProperty<Any?, String> = object: ReadWriteProperty<any?, String> {
var curValue = ""
override fun getValue<thisRef: Any?, property: KProperty<*>): String = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
curValue = value
}
}
3.6、托付给另一个特点
从一个特点托付给另一个特点可能是供给向后兼容性的一个十分有用的技巧。幻想一下,在类的1.0版别中,您有一些公共成员函数。但在他们的一生中,姓名发生了改动。为了不破坏客户端,咱们能够将旧的成员函数标记为deprecated
(经过注解)并将其调用转发到新的完成。
// Delegate to other member
class Example {
var newName: String = ""
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: String by this::newName
}
fun main() {
val example = Example()
example.oldName = "hello"
println(example.newName)
}
咱们的经验,在某些情况下,此代码无法编译,并出现过错代码:
Type 'KMutableProperty0' has no method 'getValue(MyClass, KProperty<*>)' and thus it cannot serve as a delegate.
知道有一个敞开的过错(LINK),咱们期望它能赶快修正!
4、托付示例和用例
Kotlin为常见问题供给了一些预先完成的解决方案。请拜访KotlinLang页面检查完好列表。关于当前的完成,咱们将向您展现几个示例。
4.1、经过observable
能够经过运用可观察托付来取得基本功用。当值改动时它供给回调。这样做得好处是您能够拜访新的和曾经的值。要运用可观察托付,您有必要指定初始值。
// by observable
class Book {
var content: Strign by obserable("") { property, oldValue, newValue ->
println("Book changed")
}
}
fun main() {
val book = Book()
book.content = "New content"
}
4.2、经过vetoable
vetoable有点类似观察者托付。但是,回调函数可用于撤销修改。请注意,回调有必要回来布尔值。假如成功,则为true;假如失败,则为false。在咱们的书本示例中,咱们检查书本的信内容是否为null或为空。在这种情况下,它被认为是过错的,咱们想要vetoable该更改。
// vetoable
class Book {
var content: String by vetoable("") { property, oldValue, newValue ->
!newValue.isNullOrEmpty()
}
}
fun main() {
val book = Book()
book.content = "New content"
println(book.content)
book.content = ""
println(book.content)
}
4.3、经过Lazy
Kotlin供给了变量推迟计算的现有完成。一个常见的用例是需求花费大量时刻来初始化但在开始时不会直接运用的成员变脸。例如,考虑一个Book类,它获取一个表明整个文本的字符串。书本类具有另一个对书本进行昂贵的后处理的类。开发人员决定让这个示例变得Lazy,因为它并不是总被调用。
Lazy特点附加到analyzer
类。运转代码时,它首要会打印出书本已创立,然后analyzer
成员变量已实例化。
// by lazy
class Book(private val rawText: String) {
private val analyser: Analyser by lazy { Analyser() }
fun analyse() {
analyser.doSomething()
}
}
class Analyser {
init {
println("Init analyser class")
}
fun doSomething() {
print("DoSomething")
}
}
fun main() {
val rawText = "MyBook"
val book = Book(rawText)
println("Book is created")
book.analyse()
}
4.4、经过Not Null
回来具有非null值的读/写特点的特点托付,该特点不是在目标构造期间而是在稍后的时刻初始化。因为它是在稍后时刻点创立的,因而与推迟初始化有关。但是,一个很大的缺点是没有本地办法能够知道该值是否已初始化。它需求一些样板代码才干保存。咱们的十本里将类似于以下代码。请注意,Lazy
要好得多。
// By Delegates.NotNull
class Analyser {
init {
println("Init analyser class")
}
fun doSomething() {
println("DoSomething")
}
}
class Book(private val rawText: String) {
var analyser: Analyser by Delegates.notNull()
fun analyse() {
analyser = Analyser()
analyser.doSomething()
}
}
fun main() {
val rawText = "MyBook"
val book = Book(rawText)
println("Book is created")
book.analyse()
}
4.5、Logging
您能够运用托付对应用程序日志记录库的拜访。在带有惰性的函数中,这是为每个需求的类供给对记录器的拜访的最惯用的办法。
// Idiomatic logging access
fun <R: Any> R.logger(): Lazy<Logger> {
return lazy {
Logger.getLogger(unwrapCompainionClass(this.javaClass).name)
}
}
class Something {
val LOG by logger()
fun foo() {
LOG.info("Hello from Something")
}
}