Пользовательские типы безопасности - PullRequest
0 голосов
/ 27 июня 2018

Я хотел бы иметь два различных целочисленных типа, которые семантически различимы.

например. в этом коде тип «Meter» и тип «Pixel» int

typealias Meter = Int
typealias Pixel = Int

fun Meter.toPixel() = this * 100
fun Pixel.toMeter() = this / 100

fun calcSquareMeters(width: Meter, height: Meter) = width * height
fun calcSquarePixels(width: Pixel, height: Pixel) = width * height

fun main(args: Array<String>) {
    val pixelWidth: Pixel = 50
    val pixelHeight: Pixel = 50

    val meterWidth: Meter = 50
    val meterHeight: Meter = 50

    calcSquareMeters(pixelWidth, pixelHeight) // (a) this should not work

    pixelWidth.toPixel() // (b) this should not work
}

Проблема с этим решением

(а) я могу вызывать calcSquareMeters с моим типом «Пиксель», который я не хочу, и

(b) я могу вызывать функцию расширения toPixel (), которую я хочу иметь только для моего типа «Метр» для моего типа «Пиксель», что я не хочу, чтобы это было возможно.

Я предполагаю, что это предполагаемое поведение typealias, поэтому я предполагаю, что для достижения моей цели я должен использовать что-то отличное от typealias ...

Так как мне этого добиться?

Ответы [ 5 ]

0 голосов
/ 27 июня 2018

Я бы тоже пошел с решением от TheOperator . Но я бы хотел добавить ключевое слово inline в функции оператора. Тем самым вы можете избежать вызова виртуальной функции, когда бы вы ни использовали эти операторы.

inline operator fun <T : MetricType<T>> T.plus(rhs: T) = new(this.value + rhs.value)
inline operator fun <T : MetricType<T>> T.minus(rhs: T) = new(this.value + rhs.value)
inline operator fun <T : MetricType<T>> T.times(rhs: Int) = new(this.value * rhs)
0 голосов
/ 27 июня 2018

В дополнение к существующему ответу : если у вас много общих функций между двумя типами и вы не хотите дублировать их, вы можете работать с интерфейсом:

interface MetricType<T> {
    val value: Int

    fun new(value: Int): T
}

data class Meter(override val value: Int) : MetricType<Meter> {
    override fun new(value: Int) = Meter(value)
}

data class Pixel(override val value: Int) : MetricType<Pixel> {
    override fun new(value: Int) = Pixel(value)
}

Таким образом, вы можете легко определить операции на базовом интерфейсе, такие как сложение, вычитание и масштабирование:

operator fun <T : MetricType<T>> T.plus(rhs: T) = new(this.value + rhs.value)
operator fun <T : MetricType<T>> T.minus(rhs: T) = new(this.value + rhs.value)
operator fun <T : MetricType<T>> T.times(rhs: Int) = new(this.value * rhs)

Комбинация интерфейса и обобщений обеспечивает безопасность типов, поэтому вы не можете случайно смешивать типы:

fun test() {
    val m = Meter(3)
    val p = Pixel(7)

    val mm = m + m // OK
    val pp = p + p // OK
    val mp = m + p // does not compile
}

Имейте в виду, что это решение требует затрат времени выполнения благодаря виртуальным функциям (по сравнению с переписыванием операторов для каждого типа отдельно). Это в дополнение к накладным расходам на создание объекта.

0 голосов
/ 27 июня 2018

Действительно, typealiases не гарантирует такого рода безопасность типов. Вместо этого вам нужно будет создать классы-оболочки вокруг значения Int - это хорошая идея, чтобы эти классы данных работали так, чтобы с ними работало сравнение на равенство:

data class Meter(val value: Int)
data class Pixel(val value: Int)

Создание экземпляров этих классов можно решить с помощью свойств расширения:

val Int.px
    get() = Pixel(this)

val pixelWidth: Pixel = 50.px

Единственная проблема в том, что вы больше не можете напрямую выполнять арифметические операции над экземплярами Pixel и Meter, например, функции преобразования теперь будут выглядеть так:

fun Meter.toPixel() = this.value * 100

Или квадратные вычисления, подобные этому:

fun calcSquareMeters(width: Meter, height: Meter) = width.value * height.value

Если вам действительно нужно прямое использование оператора, вы все равно можете определить его, но это будет довольно утомительно:

class Meter(val value: Int) {
    operator fun times(that: Meter) = this.value * that.value
}

fun calcSquareMeters(width: Meter, height: Meter) = width * height
0 голосов
/ 27 июня 2018

Существует предложение (пока не гарантировано, что оно будет принято), чтобы добавить inline class es для этой цели. * Т.е. 1002 *

@InlineOnly inline class Meter(val value: Int)

действительно будет Int во время выполнения.

См. https://github.com/zarechenskiy/KEEP/blob/28f7fdbe9ca22db5cfc0faeb8c2647949c9fd61b/proposals/inline-classes.md и https://github.com/Kotlin/KEEP/issues/104.

0 голосов
/ 27 июня 2018

Из котлина Док :

Псевдонимы типов не вводят новые типы. Они эквивалентны соответствующим базовым типам. Когда вы добавляете typealias Predicate и используете Predicate в своем коде, компилятор Kotlin всегда расширяет его до (Int) -> Boolean. Таким образом, вы можете передавать переменную вашего типа всякий раз, когда требуется общий тип функции, и наоборот

Это означает, что невозможно проверить ваши typealias, и вы объявляете свои функции расширений как:

fun Int.toPixel() = this * 100
fun Int.toMeter() = this / 100

fun calcSquareMeters(width: Int, height: Int) = width * height
fun calcSquarePixels(width: Int, height: Int) = width * height

Боюсь, единственный способ добиться этого - реализовать дополнительный класс для каждого типа.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...