Пользовательские типы int

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

Например. в этом коде тип Meter и тип Int Pixel

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 с моим типом Pixel, что я не хочу, и

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

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

Итак, как я могу этого добиться?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
66
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

Type aliases do not introduce new types. They are equivalent to the corresponding underlying types. When you add typealias Predicate and use Predicate in your code, the Kotlin compiler always expand it to (Int) -> Boolean. Thus you can pass a variable of your type whenever a general function type is required and vice versa

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

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

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

В самом деле, 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

Есть предложение (которое еще не гарантировано) добавить для этой цели inline classes. Т.е.

@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.

Ответ принят как подходящий

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

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
}

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

Я пытался определить эти операторы обычным образом ... Расширения для дженериков - это решение, которое я как-то упустил. Хорошая работа с ними!

zsmb13 27.06.2018 11:47

Я бы тоже пошел с решением от Оператор. Но я бы хотел добавить ключевое слово 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)

Другие вопросы по теме