Kotlin: Jak pracować z rzutami List: Unchecked Cast: kotlin.collections.List <Kotlin.Any?> To kotlin.colletions.List <Waypoint>

108

Chcę napisać funkcję, która zwraca każdą pozycję w pozycji, Listktóra nie jest pierwszą ani ostatnią pozycją (punkt pośredni). Funkcja pobiera dane ogólne List<*>jako dane wejściowe. Wynik powinien zostać zwrócony tylko wtedy, gdy elementy listy są typu Waypoint:

fun getViaPoints(list: List<*>): List<Waypoint>? {

    list.forEach { if(it !is Waypoint ) return null }

    val waypointList = list as? List<Waypoint> ?: return null

    return waypointList.filter{ waypointList.indexOf(it) != 0 && waypointList.indexOf(it) != waypointList.lastIndex}
}

Podczas przesyłania List<*>do List<Waypoint>, otrzymuję ostrzeżenie:

Niezaznaczone Cast: kotlin.collections.List to kotlin.colletions.List

Nie mogę znaleźć innego sposobu, aby to zaimplementować. Jaki jest właściwy sposób implementacji tej funkcji bez tego ostrzeżenia?

Lukas Lechner
źródło

Odpowiedzi:

191

W Kotlinie nie ma możliwości sprawdzenia parametrów ogólnych w czasie wykonywania w przypadku ogólnym (jak po prostu sprawdzanie elementów a List<T>, co jest tylko przypadkiem specjalnym), więc rzutowanie typu ogólnego na inny z innymi parametrami ogólnymi spowoduje ostrzeżenie, chyba że rzut mieści się w granicach wariancji .

Istnieją jednak różne rozwiązania:

  • Sprawdziłeś typ i masz pewność, że obsada jest bezpieczna. Biorąc to pod uwagę, możesz wyłączyć ostrzeżenie za pomocą @Suppress("UNCHECKED_CAST").

    @Suppress("UNCHECKED_CAST")
    val waypointList = list as? List<Waypoint> ?: return null
    
  • Użyj .filterIsInstance<T>()funkcji, która sprawdza typy elementów i zwraca listę z pozycjami o przekazanym typie:

    val waypointList: List<Waypoint> = list.filterIsInstance<Waypoint>()
    
    if (waypointList.size != list.size)
        return null
    

    lub to samo w jednym oświadczeniu:

    val waypointList = list.filterIsInstance<Waypoint>()
        .apply { if (size != list.size) return null }
    

    Spowoduje to utworzenie nowej listy żądanego typu (unikając w ten sposób niesprawdzonego rzutowania w środku), wprowadzając trochę narzut, ale jednocześnie oszczędza ci iteracji listi sprawdzania typów (w list.foreach { ... }linii), więc nie będzie zauważalny.

  • Napisz funkcję narzędzia, która sprawdza typ i zwraca tę samą listę, jeśli typ jest poprawny, tym samym hermetyzując rzutowanie (nadal niezaznaczone z punktu widzenia kompilatora) w nim:

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T : Any> List<*>.checkItemsAre() =
            if (all { it is T })
                this as List<T>
            else null
    

    Przy użyciu:

    val waypointList = list.checkItemsAre<Waypoint>() ?: return null
Klawisz skrótu
źródło
6
Świetna odpowiedź! Wybieram rozwiązanie list.filterIsInstance <Waypoint> (), ponieważ uważam, że jest to najczystsze rozwiązanie.
Lukas Lechner
4
Zwróć uwagę, że jeśli używasz, filterIsInstancea oryginalna lista zawiera elementy innego typu, kod po cichu je odfiltruje. Czasami tego chcesz, ale czasami wolisz IllegalStateExceptionrzucić coś podobnego. Jeśli tak jest później, możesz utworzyć własną metodę sprawdzenia, a następnie inline fun <reified R> Iterable<*>.mapAsInstance() = map { it.apply { check(this is R) } as R }
przesyłać
3
Zwróć uwagę, że .applynie zwraca wartości zwracanej przez lambdę, ale zwraca obiekt odbierania. Prawdopodobnie chcesz użyć, .takeIfjeśli chcesz, aby opcja zwracała wartość null.
bj0
10

Aby poprawić odpowiedź @ hotkey, oto moje rozwiązanie:

val waypointList = list.filterIsInstance<Waypoint>().takeIf { it.size == list.size }

To daje ci, List<Waypoint>czy wszystkie przedmioty mogą być rzucone, w przeciwnym razie null.

Adam Kis
źródło
3

W przypadku klas generycznych rzutowania nie można sprawdzić, ponieważ informacje o typie są usuwane w czasie wykonywania. Ale sprawdzasz, czy wszystkie obiekty na liście to Waypoints, więc możesz po prostu wyłączyć ostrzeżenie za pomocą @Suppress("UNCHECKED_CAST").

Aby uniknąć takich ostrzeżeń, musisz przekazać Listobiekt, na który można zamienić Waypoint. Kiedy używasz, *ale próbujesz uzyskać dostęp do tej listy jako listy wpisanej, zawsze będziesz potrzebować obsady, a ta obsada będzie odznaczona.

Michael
źródło
1

Zrobiłem małą różnicę w odpowiedzi @hotkey, gdy używam jej do sprawdzania obiektów Serializable to List:

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T : Any> Serializable.checkSerializableIsListOf() =
        if (this is List<*> && this.all { it is T })
          this as List<T>
        else null
Samiami Jankis
źródło
Szukałem takiego rozwiązania, ale te błędy:Cannot access 'Serializable': it is internal in 'kotlin.io'
daviscodesbugs
0

Zamiast

myGenericList.filter { it is AbstractRobotTurn } as List<AbstractRobotTurn>

lubię robić

myGenericList.filter { it is AbstractRobotTurn }.map { it as AbstractRobotTurn }

Nie jestem pewien, jak wydajne jest to, ale przynajmniej bez ostrzeżeń.

Ludvig Linse
źródło