-1

I have two different data class, eg: Guitar & Piano.

I wanna create a list to store both data class, eg: instruments, so that I can add both dataclass into list by:

instruments.add(Guitar())
instruments.add(Piano())

I thinking about using:

val instruments = arrayListOf<Any>()

My question is it any better way to achieve this?

scr
  • 134
  • 2
  • 7
  • You lose type safety if you put data of different types, do these classes have a common interface, if yes you can use that? – Animesh Sahu Nov 24 '20 at 04:15
  • @Animesh Sahu, sorry, I can't get what u mean. Those data do have some common properties, but not much. But both belong to the same categories, such as what I stated in the question. – scr Nov 24 '20 at 05:38
  • Just create abstract class/interface and extendend/implement it. Say `abstract class MusicIntstrument()` and then `class Guitar : MusicIntstrument()`. And then initialize list as `arrayListOf()` – FirePapaya Nov 24 '20 at 07:11

1 Answers1

1

Kotlin does not support multi-typing. However, you can apply workarounds.

First, to help modeling your problem, you could create a super type (interface or abstract class) as suggested in comments, to extract common properties, or just have a "marker" interface. It allows to narrow accepted objects to a certain category, and improve control.

Anyhow, you can filter any list to get back only values of wanted type using filterIsInstance :

enum class InstrumentFamily {
    Strings, Keyboards, Winds, Percussions
}

abstract class Instrument(val family : InstrumentFamily)

data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)

data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)

fun main() {
    val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
    
    val guitars: List<Guitar> = mix.filterIsInstance<Guitar>()
    guitars.forEach { println(it) }
    
    val pianos : List<Piano> = mix.filterIsInstance<Piano>()
    pianos.forEach { println(it) }
}

However, beware that this operator will scan all list, so it can become slow if used with large lists or many times. So, don't rely on it too much.

Another workaround would be to create an index per type, and use sealed classes to ensure full control over possible types (but therefore, you'll lose extensibility capabilities).

Exemple :

import kotlin.reflect.KClass

enum class InstrumentFamily {
    Strings, Keyboards, Winds, Percussions
}

sealed class Instrument(val family : InstrumentFamily)

data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)

data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)

/** Custom mapping by value type */
class InstrumentContainer(private val valuesByType : MutableMap<KClass<out Instrument>, List<Instrument>> = mutableMapOf()) : Map<KClass<out Instrument>, List<Instrument>> by valuesByType {
    /** When receiving an instrument, store it in a sublist specialized for its type */
    fun add(instrument: Instrument) {
        valuesByType.merge(instrument::class, listOf(instrument)) { l1, l2 -> l1 + l2}
    }
    
    /** Retrieve all objects stored for a given subtype */
    inline fun <reified I :Instrument> get() = get(I::class) as List<out I>
}

fun main() {
    val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
    
    val container = InstrumentContainer()
    mix.forEach { if (it != null) container.add(it) }
    
    container.get<Guitar>().forEach { println(it) }
}
amanin
  • 3,436
  • 13
  • 17
  • maybe I should change the way to create two separate data list & merge together as suggest from here: https://stackoverflow.com/questions/55404428/how-to-combine-two-different-length-lists-in-kotlin thanks for ur answer =) – scr Nov 25 '20 at 01:32