Kotlin允许扩展现有类型,这是一个好的特性吗?

6

Kotlin 可以扩展现有类型。例如,我们可以这样做

fun String.replaceSpaces(): String {
    return this.replace(' ', '_')
}

val formatted = str.replaceSpaces()

然而在JavaScript中,这是一种反模式

Kotlin是否规避了这在JavaScript中引起的问题?

6个回答

9

这并不是一个反模式。在js中,它是一种反模式,因为js是动态的,因此更改原型会在运行时更改代码的工作方式,这使得它成为一个反模式。基于in操作符的工作方式以及您可以重写everything的事实,这也极其危险,因此更改原型可能会影响页面上某个地方的代码:

Number.prototype.toString = function(){
 return "bullshit";
};

alert(""+12);

在 Kotlin 中,情况并非如此,因为 Kotlin 是静态的,并且所有引用都是在编译时构建的。此外,您无法覆盖现有方法,因此完全不会存在危险性。

另外需要注意的是,扩展方法对JVM而言就像静态方法一样。编写的代码str.replaceStrings()在编译后变成了Foo.replaceStrings(str) - killjoy

3
您不能将像JS这样的原型语言与Kotlin进行比较。所有扩展都是静态解析的,并且不会修改扩展类型(“接收器”)。这一点非常重要,可以消除您的担忧。请查看文档以了解有关扩展在后台(编译器)中发生的事情。
在我看来,您需要谨慎处理扩展。不要允许每个Kotlin项目中的开发人员向随意类型添加新扩展。我认为项目必须定义处理在现有类型上定义新函数/属性的过程的某些规则,否则可能很难阅读外部代码。此外,应该安排固定的位置来放置这些扩展。

不要允许每个 Kotlin 项目中的开发者向随意类型添加新扩展。我真的不认为这是一个问题。扩展函数在特定范围内定义,必须导入才能使用。您甚至可以在类中创建本地私有扩展函数。创建扩展函数时,您不会污染全局命名空间。 - marstran
当然,仅拥有一个名为“Extensions.kt”的大文件中包含所有扩展函数是不好的。您必须适当地将其作用域限制。但这不仅适用于扩展函数,对于所有内容都需要进行适当的作用域限制。 - marstran
这不是我建议的;-) 但是,需要一些纪律。你说得对,这也适用于其他任何事情。 - s1m0nw1

2
你应该编译这个例子并查看生成的代码。这样一切都会变得清晰明了:
function replaceSpaces($receiver) {
  return replace($receiver, 32, 95);
}
function foo(str) {
  var formatted = replaceSpaces(str);
}

完全没有猴子补丁!扩展函数只是Kotlin中的一种语法糖。这只是将第一个参数传递给静态函数的另一种方式。


是的,如果你生成代码,你会看到发生了什么,这是真的。但我的观点是这是一个额外的步骤。我们不应该只为了理解扩展功能而必须在大量生成的代码中搜索。在生产环境中,我们没有那么多时间!如果我们花费太多时间潜伏在生成的代码中,我们的工作评价会受到影响。这是不值得的。 - Coder Roadie

2
JavaScript中扩展原型的主要反对理由有两个:
  • 您添加的方法可能与应用程序中使用的某个库添加的方法具有相同的名称,但具有不同的行为。
  • 未来版本的JavaScript/ECMAScript本身可能会包含一个与您添加的方法同名但具有不同行为的方法。
在JavaScript中,这两种情况都会导致在运行时调用错误版本的方法,从而意外地导致运行时崩溃或意外的运行时行为。
在Kotlin中,大多数类似于此的情况都会导致编译时错误或至少警告,因此在Kotlin中扩展类型时遇到的危险通常可以避免。但是,在极少数情况下仍然存在发现类似运行时错误的微小范围。
为了说明这一点,让我们编写一些代码示例,其中存在冲突的方法实现,并查看会发生什么。

情况1:两个库提供具有相同签名的扩展方法

假设我们分别在Main.ktLibrary1.kt中有以下代码:
import library1.*
import library2.*

fun main() {
    listOf(99, 101, 103, 104).printOddVals()
}

package library1

fun List<Int>.printOddVals() {
    for (x in this) {
        if (x % 2 != 0) {
            println(x)
        }
    }
}

因此,`library1` 包定义了一个 `printOddVals` 方法,用于打印列表中的奇数值,并且 `main()` 使用它。现在假设在 `library2` 中,我们引入了一个冲突的 `printOddVals` 方法,该方法打印具有奇数索引的值:
package library2

fun List<Int>.printOddVals() {
    for ((i, x) in this.withIndex()) {
        if (i % 2 != 0) {
            println(x)
        }
    }
}

在 JavaScript 中,相同的情况可能会导致运行时错误。而在 Kotlin 中,它只会导致编译时错误:
Main.kt:5:31: error: overload resolution ambiguity: 
public fun List<Int>.printOddVals(): Unit defined in library1 in file Library1.kt
public fun List<Int>.printOddVals(): Unit defined in library2 in file Library2.kt
    listOf(99, 101, 103, 104).printOddVals()
                              ^

IntelliJ IDEA 还会告诉你如何解决这个问题 - 通过引入一个导入别名:

Screenshot showing the IntelliJ suggestion

这样做,我们就得到了这段代码,并消除了对于我们想要调用哪个`printOddVals`函数的歧义:
import library1.*
import library2.*
import library2.printOddVals as printOddVals1

fun main() {
    listOf(99, 101, 103, 104).printOddVals1()
}

场景2:图书馆引入了一个成员函数,它掩盖了你已经编写的扩展方法
假设我们有以下文件:
package library1

class Cow {
    fun chewCud() {}
}

import library1.*

fun Cow.moo() {
    println("MOOOOOOO!")
}

fun main() {
    val cow = Cow()
    cow.moo()
}

所以Cow.moo最初是我们编写的扩展方法。但是然后我们更新了library1,新版本有一个moo成员函数:
package library1

class Cow {
    fun chewCud() {}
    fun moo() { println("moo") }
}

现在,由于在解析方法调用时更喜欢使用成员函数而不是扩展函数,因此当我们在main()中调用cow.moo()时,我们的扩展函数不会被使用,而是使用库的新成员函数。这是运行时行为的变化,如果库的新moo()实现不能充分替代我们之前编写的扩展函数,则可能存在错误。然而,好在这至少产生了一个编译器警告:
Main.kt:3:9: warning: extension is shadowed by a member: public final fun moo(): Unit
fun Cow.moo() {
        ^

场景3:图书馆引入了一个成员函数,它遮蔽了你已经编写的扩展方法,并且不会产生编译器警告。
一旦我们将继承(或接口实现)加入到混合中,我们就可以构造一个类似于上述情况的情况,在库更新后,我们先前使用的扩展函数被一个成员函数部分遮蔽,导致运行时行为发生变化,并且没有编译器警告。
这一次,假设我们有以下文件:
import library1.*

fun Animal.talk() {
    println("Hello there! I am a " + this::class.simpleName)
}

fun main() {
    val cow = Cow()
    val sheep = Sheep()
    cow.talk()
    sheep.talk()
}

package library1

interface Animal

class Cow : Animal
class Sheep : Animal

起初,当我们运行main()函数时,我们的扩展函数被用于cow.talk()sheep.talk()
Why hello there. I am a Cow.
Why hello there. I am a Sheep.

但是假设我们给Cow添加一个talk成员:

package library1

interface Animal

class Cow : Animal {
    fun talk() {
        println("moo")
    }
}
class Sheep : Animal

现在,如果我们运行程序,会优先调用`Cow`类的成员函数而不是扩展方法,这样就改变了程序的行为 - 而且没有编译器错误或警告:
moo
Why hello there. I am a Sheep.

因此,尽管 Kotlin 中的扩展函数在大多数情况下是安全的,但仍然存在一小部分可能会遇到与 JavaScript 相同陷阱的潜在风险。
这种理论上的危险足以意味着应该谨慎使用扩展函数,或者完全避免使用吗?根据官方编码规范的观点,不是这样的,他们认为扩展函数很棒,你应该大量使用它们:

扩展函数

自由地使用扩展函数。每当您有一个主要作用于对象的函数时,请考虑将其制作为接受该对象作为接收器的扩展函数。为了最小化 API 污染,请尽可能限制扩展函数的可见性。必要时,使用本地扩展函数、成员扩展函数或具有私有可见性的顶级扩展函数。

当然,你可以自由地形成自己的观点!

1

不,这并不好。Kotlin允许对现有类型进行扩展是非常糟糕的。

使用扩展,就没有任何理由创建类层次结构,甚至也不需要创建新的类定义。

Kotlin的创造者们可能还不如直接使用erlang而不费心地创建扩展。

扩展意味着您不能再依赖于类定义为常量。想象一下人们将花费多少时间来查找开发人员的“创意”扩展,更不用说调试它们了。


我必须表示同意。这是JS中的一个反模式,不仅因为语言的动态特性,还因为这里引用的挑战。读者应该对类定义有一些保证。如果将其与突变混合使用,就会面临更多挑战。 - Ian

1
几种语言已经实现了这一点。JavaScript 的问题在于其工作方式,正如链接中所述的答案所述。
JavaScript 有几种覆盖属性的方法。库可能已经定义了一个覆盖,在另一个库再次进行覆盖。来自两个库的函数都被调用,混乱不堪。
在我看来,这更像是一种类型系统和可见性问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接