Я бы хотел иметь два разных целочисленных типа, которые семантически различимы.
Например. в этом коде тип 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 ...
Итак, как я могу этого добиться?





Из котлина док:
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
}
Имейте в виду, что это решение требует затрат времени выполнения из-за виртуальных функций (по сравнению с переписыванием операторов для каждого типа отдельно). Это в дополнение к накладным расходам на создание объекта.
Я бы тоже пошел с решением от Оператор. Но я бы хотел добавить ключевое слово 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)
Я пытался определить эти операторы обычным образом ... Расширения для дженериков - это решение, которое я как-то упустил. Хорошая работа с ними!