【问题标题】:Kotlin type-safe typealiasesKotlin 类型安全的类型别名
【发布时间】:2026-02-17 11:45:01
【问题描述】:

我在 Kotlin 代码中经常使用类型别名,但我想知道是否可以对它们强制执行类型安全。

typealias Latitude = Double
typealias Longitude = Double

fun someFun(lat: Latitude, lon: Longitude) {...}

val lat: Latitude = 12.34
val lon: Longitude = 56.78
someFun(lon, lat) // parameters are in a wrong order, but the code compiles fine

如果我能以某种方式防止类型别名之间的隐式转换,那就太好了,有助于避免此类问题。

当然,存在一个问题,即对基本类型的操作不适用于类型别名,但可以通过扩展函数(或强制转换)来解决。

我不想使用包含单个字段的数据类,因为这似乎有点矫枉过正,尤其是对于原始类型(或者我错了,它们会被优化掉?)

所以问题是:我可以以某种方式强制类型别名的类型安全吗?

【问题讨论】:

  • 目前唯一的方法是创建显式类型。 class Latitude : Double。或者我在这里错过了什么?然后编译器将强制输入,但在大多数情况下您仍然可以将其视为 Double。
  • @Mikezx6r 是的,你错过了几件事:1)你不能从原始类型继承,2)你不能从最终类继承 3)这种方法有运行时开销
  • 这在没有 Kotlin 1.5 的实验性功能的情况下是可能的,添加了一个答案。

标签: kotlin type-alias


【解决方案1】:

Kotlin 1.3 更新

从 Kotlin 1.3 开始,内联类已经可用,目前被标记为实验性的。 See the docs

原答案

很遗憾,您目前无法避免这种情况。有一个功能正在进行中 - inline classes (#9 in this document),它将解决运行时开销问题,同时强制编译时类型安全。它看起来与Scala's value classes 非常相似,如果您有大量数据,这很方便,而普通案例类将是一个开销。

【讨论】:

  • 一份不错的文件。 #4 将是一个杀手级功能:) 不过,我不明白为什么内联类应该提供运行时开销。此外,我的问题也可以通过类似 strict typealias Longitude = Double 的方式来解决,其中严格意味着该类型只允许显式转换。
  • 内联类没有任何运行时开销,因为这些值永远不会被装箱/拆箱,在编译器完成对 AST 的类型检查后,这些值将保留在底层类型中,所以在您的情况下,当 VM 执行代码时,这两个值仍然是 Long。我所说的运行时开销是 Scala 中的案例类,当用作单个值的类型安全包装器时(这是 Scala 的 SIP-15 的主要原因)。
【解决方案2】:

不幸的是,这对于 typealiases 是不可能的。 kotlin 参考说:

类型别名不会引入新类型。它们相当于 相应的基础类型。当您添加typealias Predicate<T> 并在你的代码中使用Predicate<Int>,Kotlin 编译器总是展开 发给(Int) -> Boolean。因此,您可以传递您的类型的变量 每当需要通用函数类型时,反之亦然:

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main(args: Array<String>) {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

请参阅 kotlin type aliases

tl;dr 你必须使用(数据)类

正如名称 typealias 所暗示的那样,typealias 只是一个别名,而不是一个新类型。在您的示例中,LatitudeLongitudeInts,与它们的名称无关。为了使它们类型安全,您必须声明一个类型。理论上你可以从Int 继承新类型。由于Int 是最终类,因此这是不可能的。所以它需要创建一个新类。

【讨论】:

  • 我已经读过不止一次了。问题是为什么要这样做,以及如何绕过这个限制。
  • 我添加了一个简短的解释,为什么这是不可能的。希望对你有帮助:)
  • Int 不是最终类,它是一个原始类。诠释?是最后一课。
  • 在 kotlin Int 是最后一课。它扩展了Number 并实现了Comparable&lt;Int&gt;。它在kotlin.Primitives.kt 中声明
  • 这也是一个可能的解决方案。您可以创建扩展 Number 的类。但我现在不知道这些类的互操作性和兼容性如何。
【解决方案3】:

您可以为此使用 inline classes(从 Kotlin 1.5 开始)。内联类在复杂化期间被删除,因此在运行时 latlon 只是 doubles,但您会从编译时检查中受益。

@JvmInline
value class Latitude(private val value: Double)

@JvmInline
value class Longitude(private val value: Double)

fun someFun(lat: Latitude, lon: Longitude) {
    println("($lat, $lon)")
}

fun main() {
    val lat = Latitude(12.34)
    val lon = Longitude(56.78)
    someFun(lon, lat) // Type mismatch: inferred type is Longitude but Latitude was expected
    someFun(lat, lon) // OK
}

【讨论】:

    【解决方案4】:

    这里是typealiasinline classes在避免params错误顺序的情况下的区别:

    类型别名:

    typealias LatitudeType = String
    typealias LongitudeType = String
    
    fun testTypeAlias() {
        val lat: LatitudeType = "lat"
        val long: LongitudeType = "long"
    
        testTypeAliasOrder(lat, long) // ok
        testTypeAliasOrder(long, lat) // ok :-(
    }
    
    fun testTypeAliasOrder(lat: LatitudeType, long: LongitudeType) {}
    

    内联类:

    @JvmInline
    value class Latitude(val lat: String)
    
    @JvmInline
    value class Longitude(val long: String)
    
    fun testInlineClasses() {
        val lat = Latitude("lat")
        val long = Longitude("long")
    
        testInlineClassesOrder(lat, long) // ok
        testInlineClassesOrder(long, lat) // Compilation error :-)
    }
    
    fun testInlineClassesOrder(lat: Latitude, long: Longitude) {}
    

    【讨论】:

      【解决方案5】:

      通过将LatitudeLongitude 定义为Double 的别名,可以将其视为传递别名,即您将Latitude 定义为Longitude 的别名,反之亦然。现在,所有三种类型名称都可以互换使用:

      val d: Double = 5.0
      val x: Latitude = d
      val y: Longitude = x
      

      作为替代方案,您可以简单地使用参数名称来明确传递的内容:

      fun someFun(latitude: Double, longitude: Double) {
      }
      
      fun main(args: Array<String>) {
          val lat = 12.34
          val lon = 56.78
          someFun(latitude = lon, longitude = lat)
      }
      

      【讨论】:

        【解决方案6】:

        我最近在处理类似的情况。内联类不是解决方案,因为它迫使我使用属性包装器。

        希望对我来说,我已经设法通过继承委托解决了我的问题。

        class ListWrapper(list: List<Double>): List<Double> by list
        
        

        这种方法允许我们直接在 ListWrapper 上操作,就像在常规 List 上一样。类型是严格标识的,因此它可以通过 Koin 依赖注入机制传递。

        我们可以更深入:

        class ListListWrapper(list: ListWrapper): ListWrapper by list
        

        但这需要我们用反射“打开”父类,因为`@Suppress("FINAL_SUPERTYPE") 不起作用。

        不幸的是,原语还有其他问题,因为它们以某种方式只提供空的私有构造函数,并使用一些未记录的魔法进行初始化。

        【讨论】:

        • 别怪我跑题了,把重点放在把我的问题标记为与这个问题重复的“聪明”人身上