Skip to content

Commit a483a15

Browse files
committed
Extract asList to an extension function and return a custom immutable list
1 parent 6edb7ba commit a483a15

File tree

16 files changed

+290
-144
lines changed

16 files changed

+290
-144
lines changed

buildSrc/src/main/kotlin/com/danrusu/pods4k/immutableArrays/core/ImmutableArrayExtensionsGenerator.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import java.io.File
2121
internal object ImmutableArrayExtensionsGenerator {
2222
fun generate(destinationPath: String) {
2323
val fileSpec = createFile(ImmutableArrayConfig.packageName, "ImmutableArrays") {
24+
addAsList()
2425
addContains()
2526
addIndexOf()
2627
addLastIndexOf()
@@ -37,6 +38,53 @@ internal object ImmutableArrayExtensionsGenerator {
3738
}
3839
}
3940

41+
private fun FileSpec.Builder.addAsList() {
42+
val standardKdoc = "Returns an immutable list that wraps the same backing array without copying the elements."
43+
44+
for (baseType in BaseType.entries) {
45+
function(
46+
kdoc = when (baseType) {
47+
GENERIC -> standardKdoc
48+
else -> {
49+
"""
50+
$standardKdoc
51+
52+
Note that accessing values from the resulting list will auto-box them everytime they are accessed. This is because [${baseType.generatedClassName}] stores primitive values whereas [List] is defined as a generic type. If the number of accesses is expected to be multiple times larger than the size of this array, then you might want to consider using [toList] instead in order to copy all the elements into a standalone list and only auto-box each element once.
53+
""".trimIndent()
54+
}
55+
},
56+
receiver = baseType.getGeneratedTypeName(),
57+
name = "asList",
58+
returns = List::class.asTypeName().parameterizedBy(baseType.type),
59+
) {
60+
if (baseType == GENERIC) {
61+
addTypeVariable(baseType.type as TypeVariableName)
62+
}
63+
// IMPORTANT: Don't attempt to delegate to the backing array (eg. "return values.asList()") because that can allow an outsider to mutate the backing array via the list wrapper
64+
// See https://youtrack.jetbrains.com/issue/KT-70779/Array.asList-exposes-mutation-back-door
65+
addCode(
66+
"""
67+
return object : %T<%T>(), %T {
68+
override val size: Int get() = this@asList.size
69+
override fun isEmpty(): Boolean = this@asList.isEmpty()
70+
override fun contains(element: %T): Boolean = this@asList.contains(element)
71+
override fun get(index: Int): %T = this@asList[index]
72+
override fun indexOf(element: %T): Int = this@asList.indexOf(element)
73+
override fun lastIndexOf(element: %T): Int = this@asList.lastIndexOf(element)
74+
}
75+
""".trimIndent(),
76+
AbstractList::class.asTypeName(),
77+
baseType.type,
78+
RandomAccess::class.asTypeName(),
79+
baseType.type,
80+
baseType.type,
81+
baseType.type,
82+
baseType.type,
83+
)
84+
}
85+
}
86+
}
87+
4088
private fun FileSpec.Builder.addContains() {
4189
for (baseType in BaseType.entries) {
4290
function(
@@ -192,6 +240,7 @@ private fun FileSpec.Builder.addSortedDescending() {
192240
receiver = receiver,
193241
name = "sortedDescending",
194242
returns = receiver,
243+
forceFunctionBody = true,
195244
) {
196245
if (baseType == GENERIC) {
197246
addTypeVariable(

buildSrc/src/main/kotlin/com/danrusu/pods4k/immutableArrays/core/ImmutableArrayGenerator.kt

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,21 +220,6 @@ private fun generateImmutableArrayFile(baseType: BaseType): FileSpec {
220220
baseType = baseType,
221221
returns = Sequence::class.asTypeName().parameterizedBy(baseType.type),
222222
)
223-
"asList"(
224-
typeSpecBuilder = this,
225-
baseType = baseType,
226-
kdoc = when (baseType) {
227-
GENERIC -> "Wraps the backing array in a class that implements the read-only [List] interface by referencing the same backing array without copying the elements."
228-
else -> {
229-
"""
230-
Wraps the backing array in a class that implements the read-only [List] interface by referencing the same backing array without copying the elements.
231-
232-
Note that [${baseType.generatedClassName}] stores primitive values whereas [List] operates on generic types so this will auto-box the value that is accessed on every access. If the total number of accesses is expected to be multiple times larger than the total number of elements then you might want to consider converting it into a list instead as that will auto-box all the elements only once at the cost of allocating a separate backing array.
233-
""".trimIndent()
234-
}
235-
},
236-
returns = List::class.asTypeName().parameterizedBy(baseType.type),
237-
)
238223
"forEach"(
239224
typeSpecBuilder = this,
240225
baseType = baseType,

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ _Date TBD_
99

1010
**Breaking Changes:**
1111

12+
* Extracted the `asList` method to an extension function and replaced the previous implementation, which delegated to
13+
the `asList` function on the backing array, with a custom list implementation that's guaranteed to always be
14+
immutable. This was necessary because the Kotlin standard library `Array<T>.asList` extension function doesn't
15+
guarantee immutability via the wrapper list that it returns.
1216
* Renamed the `specializations` package to `multiplicativeSpecializations` to better reflect its contents. These
1317
specializations are generated by multiplying all combinations of base types and using the most efficient resulting
1418
type for each combination. There are hundreds of regular non-multiplicative specializations so this updated name stops

immutable-arrays/core/src/main/kotlin/com/danrusu/pods4k/immutableArrays/ImmutableArray.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import kotlin.Unit
1818
import kotlin.collections.IndexedValue
1919
import kotlin.collections.Iterable
2020
import kotlin.collections.Iterator
21-
import kotlin.collections.List
2221
import kotlin.jvm.JvmInline
2322
import kotlin.ranges.IntRange
2423
import kotlin.sequences.Sequence
@@ -225,12 +224,6 @@ public value class ImmutableArray<out T> @PublishedApi internal constructor(
225224
*/
226225
public inline fun asSequence(): Sequence<T> = values.asSequence()
227226

228-
/**
229-
* Wraps the backing array in a class that implements the read-only [List] interface by
230-
* referencing the same backing array without copying the elements.
231-
*/
232-
public inline fun asList(): List<T> = values.asList()
233-
234227
/**
235228
* See [Array.forEach]
236229
*/

immutable-arrays/core/src/main/kotlin/com/danrusu/pods4k/immutableArrays/ImmutableArrays.kt

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.danrusu.pods4k.immutableArrays
33

44
import java.lang.IllegalArgumentException
55
import java.util.Arrays
6+
import java.util.RandomAccess
67
import kotlin.Boolean
78
import kotlin.Byte
89
import kotlin.Char
@@ -13,6 +14,168 @@ import kotlin.Int
1314
import kotlin.Long
1415
import kotlin.Short
1516
import kotlin.Suppress
17+
import kotlin.collections.AbstractList
18+
import kotlin.collections.List
19+
20+
/**
21+
* Returns an immutable list that wraps the same backing array without copying the elements.
22+
*/
23+
public fun <T> ImmutableArray<T>.asList(): List<T> = object : AbstractList<T>(), RandomAccess {
24+
override val size: Int get() = this@asList.size
25+
override fun isEmpty(): Boolean = this@asList.isEmpty()
26+
override fun contains(element: T): Boolean = this@asList.contains(element)
27+
override fun get(index: Int): T = this@asList[index]
28+
override fun indexOf(element: T): Int = this@asList.indexOf(element)
29+
override fun lastIndexOf(element: T): Int = this@asList.lastIndexOf(element)
30+
}
31+
32+
/**
33+
* Returns an immutable list that wraps the same backing array without copying the elements.
34+
*
35+
* Note that accessing values from the resulting list will auto-box them everytime they are
36+
* accessed. This is because [ImmutableBooleanArray] stores primitive values whereas [List] is defined
37+
* as a generic type. If the number of accesses is expected to be multiple times larger than the size
38+
* of this array, then you might want to consider using [toList] instead in order to copy all the
39+
* elements into a standalone list and only auto-box each element once.
40+
*/
41+
public fun ImmutableBooleanArray.asList(): List<Boolean> = object :
42+
AbstractList<Boolean>(),
43+
RandomAccess {
44+
override val size: Int get() = this@asList.size
45+
override fun isEmpty(): Boolean = this@asList.isEmpty()
46+
override fun contains(element: Boolean): Boolean = this@asList.contains(element)
47+
override fun get(index: Int): Boolean = this@asList[index]
48+
override fun indexOf(element: Boolean): Int = this@asList.indexOf(element)
49+
override fun lastIndexOf(element: Boolean): Int = this@asList.lastIndexOf(element)
50+
}
51+
52+
/**
53+
* Returns an immutable list that wraps the same backing array without copying the elements.
54+
*
55+
* Note that accessing values from the resulting list will auto-box them everytime they are
56+
* accessed. This is because [ImmutableByteArray] stores primitive values whereas [List] is defined as
57+
* a generic type. If the number of accesses is expected to be multiple times larger than the size of
58+
* this array, then you might want to consider using [toList] instead in order to copy all the elements
59+
* into a standalone list and only auto-box each element once.
60+
*/
61+
public fun ImmutableByteArray.asList(): List<Byte> = object : AbstractList<Byte>(), RandomAccess {
62+
override val size: Int get() = this@asList.size
63+
override fun isEmpty(): Boolean = this@asList.isEmpty()
64+
override fun contains(element: Byte): Boolean = this@asList.contains(element)
65+
override fun get(index: Int): Byte = this@asList[index]
66+
override fun indexOf(element: Byte): Int = this@asList.indexOf(element)
67+
override fun lastIndexOf(element: Byte): Int = this@asList.lastIndexOf(element)
68+
}
69+
70+
/**
71+
* Returns an immutable list that wraps the same backing array without copying the elements.
72+
*
73+
* Note that accessing values from the resulting list will auto-box them everytime they are
74+
* accessed. This is because [ImmutableCharArray] stores primitive values whereas [List] is defined as
75+
* a generic type. If the number of accesses is expected to be multiple times larger than the size of
76+
* this array, then you might want to consider using [toList] instead in order to copy all the elements
77+
* into a standalone list and only auto-box each element once.
78+
*/
79+
public fun ImmutableCharArray.asList(): List<Char> = object : AbstractList<Char>(), RandomAccess {
80+
override val size: Int get() = this@asList.size
81+
override fun isEmpty(): Boolean = this@asList.isEmpty()
82+
override fun contains(element: Char): Boolean = this@asList.contains(element)
83+
override fun get(index: Int): Char = this@asList[index]
84+
override fun indexOf(element: Char): Int = this@asList.indexOf(element)
85+
override fun lastIndexOf(element: Char): Int = this@asList.lastIndexOf(element)
86+
}
87+
88+
/**
89+
* Returns an immutable list that wraps the same backing array without copying the elements.
90+
*
91+
* Note that accessing values from the resulting list will auto-box them everytime they are
92+
* accessed. This is because [ImmutableShortArray] stores primitive values whereas [List] is defined
93+
* as a generic type. If the number of accesses is expected to be multiple times larger than the size
94+
* of this array, then you might want to consider using [toList] instead in order to copy all the
95+
* elements into a standalone list and only auto-box each element once.
96+
*/
97+
public fun ImmutableShortArray.asList(): List<Short> = object : AbstractList<Short>(), RandomAccess {
98+
override val size: Int get() = this@asList.size
99+
override fun isEmpty(): Boolean = this@asList.isEmpty()
100+
override fun contains(element: Short): Boolean = this@asList.contains(element)
101+
override fun get(index: Int): Short = this@asList[index]
102+
override fun indexOf(element: Short): Int = this@asList.indexOf(element)
103+
override fun lastIndexOf(element: Short): Int = this@asList.lastIndexOf(element)
104+
}
105+
106+
/**
107+
* Returns an immutable list that wraps the same backing array without copying the elements.
108+
*
109+
* Note that accessing values from the resulting list will auto-box them everytime they are
110+
* accessed. This is because [ImmutableIntArray] stores primitive values whereas [List] is defined as
111+
* a generic type. If the number of accesses is expected to be multiple times larger than the size of
112+
* this array, then you might want to consider using [toList] instead in order to copy all the elements
113+
* into a standalone list and only auto-box each element once.
114+
*/
115+
public fun ImmutableIntArray.asList(): List<Int> = object : AbstractList<Int>(), RandomAccess {
116+
override val size: Int get() = this@asList.size
117+
override fun isEmpty(): Boolean = this@asList.isEmpty()
118+
override fun contains(element: Int): Boolean = this@asList.contains(element)
119+
override fun get(index: Int): Int = this@asList[index]
120+
override fun indexOf(element: Int): Int = this@asList.indexOf(element)
121+
override fun lastIndexOf(element: Int): Int = this@asList.lastIndexOf(element)
122+
}
123+
124+
/**
125+
* Returns an immutable list that wraps the same backing array without copying the elements.
126+
*
127+
* Note that accessing values from the resulting list will auto-box them everytime they are
128+
* accessed. This is because [ImmutableLongArray] stores primitive values whereas [List] is defined as
129+
* a generic type. If the number of accesses is expected to be multiple times larger than the size of
130+
* this array, then you might want to consider using [toList] instead in order to copy all the elements
131+
* into a standalone list and only auto-box each element once.
132+
*/
133+
public fun ImmutableLongArray.asList(): List<Long> = object : AbstractList<Long>(), RandomAccess {
134+
override val size: Int get() = this@asList.size
135+
override fun isEmpty(): Boolean = this@asList.isEmpty()
136+
override fun contains(element: Long): Boolean = this@asList.contains(element)
137+
override fun get(index: Int): Long = this@asList[index]
138+
override fun indexOf(element: Long): Int = this@asList.indexOf(element)
139+
override fun lastIndexOf(element: Long): Int = this@asList.lastIndexOf(element)
140+
}
141+
142+
/**
143+
* Returns an immutable list that wraps the same backing array without copying the elements.
144+
*
145+
* Note that accessing values from the resulting list will auto-box them everytime they are
146+
* accessed. This is because [ImmutableFloatArray] stores primitive values whereas [List] is defined
147+
* as a generic type. If the number of accesses is expected to be multiple times larger than the size
148+
* of this array, then you might want to consider using [toList] instead in order to copy all the
149+
* elements into a standalone list and only auto-box each element once.
150+
*/
151+
public fun ImmutableFloatArray.asList(): List<Float> = object : AbstractList<Float>(), RandomAccess {
152+
override val size: Int get() = this@asList.size
153+
override fun isEmpty(): Boolean = this@asList.isEmpty()
154+
override fun contains(element: Float): Boolean = this@asList.contains(element)
155+
override fun get(index: Int): Float = this@asList[index]
156+
override fun indexOf(element: Float): Int = this@asList.indexOf(element)
157+
override fun lastIndexOf(element: Float): Int = this@asList.lastIndexOf(element)
158+
}
159+
160+
/**
161+
* Returns an immutable list that wraps the same backing array without copying the elements.
162+
*
163+
* Note that accessing values from the resulting list will auto-box them everytime they are
164+
* accessed. This is because [ImmutableDoubleArray] stores primitive values whereas [List] is defined
165+
* as a generic type. If the number of accesses is expected to be multiple times larger than the size
166+
* of this array, then you might want to consider using [toList] instead in order to copy all the
167+
* elements into a standalone list and only auto-box each element once.
168+
*/
169+
public fun ImmutableDoubleArray.asList(): List<Double> = object :
170+
AbstractList<Double>(),
171+
RandomAccess {
172+
override val size: Int get() = this@asList.size
173+
override fun isEmpty(): Boolean = this@asList.isEmpty()
174+
override fun contains(element: Double): Boolean = this@asList.contains(element)
175+
override fun get(index: Int): Double = this@asList[index]
176+
override fun indexOf(element: Double): Int = this@asList.indexOf(element)
177+
override fun lastIndexOf(element: Double): Int = this@asList.lastIndexOf(element)
178+
}
16179

17180
/**
18181
* See [Array.contains]
@@ -425,8 +588,9 @@ public fun ImmutableDoubleArray.sorted(): ImmutableDoubleArray {
425588
*
426589
* The sort is _stable_ so equal elements preserve their order relative to each other after sorting.
427590
*/
428-
public fun <T : Comparable<T>> ImmutableArray<T>.sortedDescending(): ImmutableArray<T> =
429-
sortedWith(reverseOrder())
591+
public fun <T : Comparable<T>> ImmutableArray<T>.sortedDescending(): ImmutableArray<T> {
592+
return sortedWith(reverseOrder())
593+
}
430594

431595
/**
432596
* Leaves [this] immutable array as is and returns an [ImmutableByteArray] with all elements sorted

immutable-arrays/core/src/main/kotlin/com/danrusu/pods4k/immutableArrays/ImmutableBooleanArray.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import kotlin.Unit
1717
import kotlin.collections.IndexedValue
1818
import kotlin.collections.Iterable
1919
import kotlin.collections.Iterator
20-
import kotlin.collections.List
2120
import kotlin.jvm.JvmInline
2221
import kotlin.ranges.IntRange
2322
import kotlin.sequences.Sequence
@@ -224,18 +223,6 @@ public value class ImmutableBooleanArray @PublishedApi internal constructor(
224223
*/
225224
public inline fun asSequence(): Sequence<Boolean> = values.asSequence()
226225

227-
/**
228-
* Wraps the backing array in a class that implements the read-only [List] interface by
229-
* referencing the same backing array without copying the elements.
230-
*
231-
* Note that [ImmutableBooleanArray] stores primitive values whereas [List] operates on generic
232-
* types so this will auto-box the value that is accessed on every access. If the total number of
233-
* accesses is expected to be multiple times larger than the total number of elements then you
234-
* might want to consider converting it into a list instead as that will auto-box all the elements
235-
* only once at the cost of allocating a separate backing array.
236-
*/
237-
public inline fun asList(): List<Boolean> = values.asList()
238-
239226
/**
240227
* See [BooleanArray.forEach]
241228
*/

immutable-arrays/core/src/main/kotlin/com/danrusu/pods4k/immutableArrays/ImmutableByteArray.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import kotlin.Unit
1818
import kotlin.collections.IndexedValue
1919
import kotlin.collections.Iterable
2020
import kotlin.collections.Iterator
21-
import kotlin.collections.List
2221
import kotlin.jvm.JvmInline
2322
import kotlin.ranges.IntRange
2423
import kotlin.sequences.Sequence
@@ -225,18 +224,6 @@ public value class ImmutableByteArray @PublishedApi internal constructor(
225224
*/
226225
public inline fun asSequence(): Sequence<Byte> = values.asSequence()
227226

228-
/**
229-
* Wraps the backing array in a class that implements the read-only [List] interface by
230-
* referencing the same backing array without copying the elements.
231-
*
232-
* Note that [ImmutableByteArray] stores primitive values whereas [List] operates on generic
233-
* types so this will auto-box the value that is accessed on every access. If the total number of
234-
* accesses is expected to be multiple times larger than the total number of elements then you
235-
* might want to consider converting it into a list instead as that will auto-box all the elements
236-
* only once at the cost of allocating a separate backing array.
237-
*/
238-
public inline fun asList(): List<Byte> = values.asList()
239-
240227
/**
241228
* See [ByteArray.forEach]
242229
*/

0 commit comments

Comments
 (0)