관리에게 조기 수정을 요청하십시오 : Kotlin의 마법 같은 BUG (모바일 개발 참고 사항)

누구도 완벽하지 않고, 금도 완벽하지 않으며, 공식적인 경우에도 피할 수없는 버그가 있습니다. 지난번에 Kotlin이 가져올 수있는 깊은 구덩이 에 대한 기사를 공유했습니다 (마지막 알림).

오늘 여러분과 공유하고 싶은 것은 Kotlin의 마법 버그의
원래 주소입니다. https://blog.csdn.net/m0_46962786/article/details/114027622

머리말

이 기사에서는 특정 비즈니스 시나리오를 통해 Kotlin의 버그를 더 얕은 것부터 더 깊은 것까지 소개하고 버그의 마법을 알려준 다음 버그의 원인을 찾고 마지막으로 버그를 피할 수 있도록 안내합니다.

버그 재발

실제 개발에서는 종종 Json 문자열을 객체로 역 직렬화하는 문제가 있습니다. 여기서는 Gson을 사용하여 다음과 같이 역 직렬화 코드를 작성합니다.

fun <T> fromJson(json: String, clazz: Class<T>): T? {
    return try {                                            
        Gson().fromJson(json, clazz)                  
    } catch (ignore: Exception) {                           
        null                                                
    }                                                       
} 

위 코드는 제네릭이없는 클래스에만 적용 할 수 있습니다. List와 같은 제네릭이있는 클래스의 경우 다음과 같이 다시 변환해야합니다.

fun <T> fromJson(json: String, type: Type): T? {
    return try {                                
        return Gson().fromJson(json, type)      
    } catch (e: Exception) {                    
        null                                    
    }                                           
} 

이 시점에서 Gson의 TypeToken 클래스를 사용하여 다음과 같이 모든 유형의 역 직렬화를 수행 할 수 있습니다.

//1、反序列化User对象
val user: User? = fromJson("{...}}", User::class.java)

//2、反序列化List<User>对象,其它带有泛型的类,皆可用此方法序列化
val type = object : TypeToken<List<User>>() {}.type
val users: List<User>? = fromJson("[{..},{...}]", type)

위의 글은 자바의 문법에서 번역 된 것으로, 단점이있다. 즉, 제네릭의 전송은 다른 클래스를 통해 구현되어야한다는 것이다.

위의 TypeToken 클래스를 사용했습니다. 많은 사람들이 받아들이지 않는 것 같습니다. 그래서 Kotlin에서는 새로운 키워드 수정이 나타났습니다 (여기에서는 소개하지 않았으며 이해가 안된다면 직접 관련 정보를 확인할 수 있습니다). .

Kotlin 인라인 함수의 특성을 결합하여 메서드 내에서 특정 제네릭 유형을 직접 얻을 수 있습니다. 위의 메서드를 다음과 같이 다시 변환 해 보겠습니다.

inline fun <reified T> fromJson(json: String): T? {
    return try {
        return Gson().fromJson(json, T::class.java)
    } catch (e: Exception) {
        null
    }
}

보시다시피 메서드 앞에 inline 키워드를 추가하여 이것이 인라인 함수임을 나타냅니다. 그런 다음 일반 T 앞에 reified 키워드를 추가하고 메서드에서 불필요한 Type 매개 변수를 제거했습니다. 마지막으로 T ::를 전달했습니다. class.java는 다음과 같이 사용되는 특정 일반 유형을 전달합니다.

val user = fromJson<User>("{...}}")
val users = fromJson<List<User>>("[{..},{...}]")

위 코드를 확실하게 테스트했을 때 다음과 같이 문제가 발생하고 목록 역 직렬화가 실패했습니다.

[외부 링크 이미지 전송에 실패했습니다. 소스 사이트에 안티 리치 링크 메커니즘이있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것이 좋습니다 (img-CbQt9QuJ-1614156224032) (https://upload-images.jianshu.io/ upload_images / 14735202-a9662c9073ad78ea? imageMogr2 / auto-orient / strip % 7CimageView2 / 2 / w / 1240)]

목록에있는 개체는 User가 아니라 LinkedTreeMap입니다. 문제가 무엇인가요? 제목에 언급 된 Kotlin 버그인가요? 당연히 아니지!

fromJson 메소드로 돌아가서 내부 전송이 T :: class.java 객체, 즉 클래스 객체임을 확인합니다. 클래스 객체에 제네릭 유형이 있으면 런타임 중에 제네릭 유형이 지워집니다. 이 기간 동안 실행되는 List 객체는 List.class 객체가되며 Gson은 수신하는 제네릭 유형이 명확하지 않은 경우 자동으로 json 객체를 LinkedTreeMap 객체로 역 직렬화합니다.

어떻게 처리할까요? 다루기 쉽고 TypeToken 클래스를 사용하여 제네릭을 전달할 수 있으며 이번에는 다음과 같이 메서드 내부에 한 번만 작성하면됩니다.

inline fun <reified T> fromJson(json: String): T? {
    return try {
        //借助TypeToken类获取具体的泛型类型
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

이 시점에서 다음과 같이 위 코드를 다시 테스트 해 보겠습니다.

[외부 링크 이미지 전송에 실패했습니다. 소스 사이트에 안티 리치 링크 메커니즘이있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것이 좋습니다. (img-Eg7vmm2h-1614156224038) (https://upload-images.jianshu.io/ upload_images / 14735202-b436d72383738f70? imageMogr2 / auto-orient / strip % 7CimageView2 / 2 / w / 1240)]

보시다시피 User 및 List 개체는 이번에 성공적으로 역 직렬화됩니다.

이 시점에서 어떤 사람들은 질문을 할 것입니다. 많은 이야기를 한 후 Kotlin의 버그는 어떻습니까? 걱정하지 마세요. 계속해서 아래를 살펴보세요. 버그가 곧 나타날 것입니다.

어느 날 갑자기 리더가 와서이 fromJson 메소드를 최적화 할 수 있는지 알려주었습니다. 이제 List 컬렉션을 역 직렬화 할 때마다 fromJson 뒤에 <List <>>를 작성해야합니다. 이러한 시나리오가 많이 있습니다. 약간 번거 롭습니다. 쓰기.

이때 1 만개의 물건이 뛰어 오르지 만 침착하고 생각 해보세요. 리더가 말한 것은 무리가 아닙니다. 다단계의 일반적인 상황이 발생하면 다음과 같이 작성하는 것이 더 번거로울 것입니다. fromJson < BaseResponse <목록 >>,

따라서 일반적으로 사용되는 제네릭 클래스를 분리하여 최적화의 길을 열고 마지막으로 다음 코드를 작성했습니다.

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

시험 중 이죠? 놀란 친숙한 질문은 다음과 같습니다.

[외부 링크 이미지 전송에 실패했습니다. 소스 사이트에 안티 리치 링크 메커니즘이있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것이 좋습니다. (img-zFwUSlVq-1614156224040) (https://upload-images.jianshu.io/ upload_images / 14735202-dbf3876add7dcb33? imageMogr2 / auto-orient / strip % 7CimageView2 / 2 / w / 1240)]

이게 왜 또? FromJson2List는 fromJson 메서드 만 호출하는데, 왜 fromJson이 할 수 있지만 fromJson2List는 실패합니다.

제목에 언급 된 Kotlin 버그입니까? 네, 아주 책임감있게 말 해주세요.

버그의 마법은 어디에 있습니까? 계속 아래를 봅니다.

버그의 마법

전체 이벤트를 재구성 해 보겠습니다. 위에서 먼저 두 가지 메소드를 정의하여 Json.kt 파일에 넣습니다. 완전한 코드는 다음과 같습니다.

@file:JvmName("Json")

package com.example.test

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

그런 다음 새 User 클래스를 만듭니다. 전체 코드는 다음과 같습니다.

package com.example.bean

class User {
    val name: String? = null
}

그런 다음 새 JsonTest.kt 파일을 만듭니다. 완료 코드는 다음과 같습니다.

@file:JvmName("JsonTest")

package com.example.test

fun main() {
    val user = fromJson<User>("""{"name": "张三"}""")
    val users = fromJson<List<User>>("""[{"name": "张三"},{"name": "李四"}]""")
    val userList = fromJson2List<User>("""[{"name": "张三"},{"name": "李四"}]""")
    print("")
}

참고 :이 3 개의 클래스는 동일한 패키지 이름과 동일한 모듈에 있습니다.

마지막으로 main 메소드를 실행하면 버그가 발견됩니다.

높은 에너지가 앞서 있습니다. 다음과 같이 Json.kt 파일을 기본 모듈에 복사합니다.

@file:JvmName("Json")

package com.example.base

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

그런 다음 앱 모듈의 Json.kt 파일에 다음과 같이 테스트 메서드를 추가합니다.

fun test() {
    val users = fromJson2List<User>("""[{"name": "张三"},{"name": "李四"}]""")
    val userList = com.example.base.fromJson2List<User>("""[{"name": "张三"},{"name": "李四"}]""")
    print("")
}

참고 : 기본 모듈의 Json.kt 파일에는 이러한 메서드가 없습니다.

위 코드에서는 앱 모듈과베이스 모듈의 fromJson2List 메소드가 각각 실행 되었으니 위 코드의 예상 결과를 추측 해보자.

위의 경우 첫 번째 명령문은 분명히 List 객체를 반환합니다. 두 번째 명령문은 어떻습니까? 논리적으로는 List 객체도 반환되어야합니다. 그러나 작업은 비생산적입니다. 다음과 같이 실행을 살펴 보겠습니다.

[외부 링크 이미지 전송에 실패했습니다. 소스 사이트에 안티 리치 링크 메커니즘이있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것이 좋습니다. (img-QZ0MeQqQ-1614156224044) (https://upload-images.jianshu.io/ upload_images / 14735202-651f1b934a63a1be? imageMogr2 / auto-orient / strip % 7CimageView2 / 2 / w / 1240)]

보시다시피 앱 모듈의 fromJson2List 메서드는 List를 deserialize하는 데 실패했지만 기본 모듈의 fromJson2List 메서드는 성공했습니다.

같은 코드인데 위치하는 모듈도 다르고 실행 결과도 달라 신이 마법이 아니라고?

확인 해봐

버그를 알고 버그의 마법을 알고 있으면 다음으로 살펴 보겠습니다. 왜 이런 일이 발생합니까? 어디서 시작하나요?

분명히 Json.kt 클래스의 바이트 코드 파일을보기 위해 다음과 같이 기본 모듈의 Json.class 파일을 먼저 살펴 보겠습니다.

참고 : 다음 바이트 코드 파일의 경우보기의 편의를 위해 일부 주석 정보가 삭제됩니다.

package com.example.base;

import com.google.gson.reflect.TypeToken;
import java.util.List;

public final class Json {

  public static final class Json$fromJson$type$1 extends TypeToken<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}
}

보시다시피 Json.kt의 두 인라인 메서드는 바이트 코드 파일로 컴파일 된 후 두 개의 정적 내부 클래스가되며 둘 다 TypeToken 클래스를 상속합니다.

다음과 같이 앱 모듈의 Json.kt 파일에 해당하는 바이트 코드 파일을 계속 살펴 봅니다.

package com.example.test;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

public final class Json {
  public static final void test() {
    List list;
    Object object = null;
    try {
      Type type = (new Json$fromJson2List$$inlined$fromJson$2()).getType();
      list = (List)(new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
    } catch (Exception exception) {
      list = null;
    } 
    (List)list;
    try {
      Type type = (new Json$test$$inlined$fromJson2List$1()).getType();
      object = (new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
    } catch (Exception exception) {}
    (List)object;
    System.out.print("");
  }

  public static final class Json$fromJson$type$1 extends TypeToken<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}

  public static final class Json$fromJson2List$$inlined$fromJson$2 extends TypeToken<List<? extends T>> {}

  public static final class Json$test$$inlined$fromJson2List$1 extends TypeToken<List<? extends User>> {}
}

바이트 코드 파일에는 1 개의 테스트 메서드 + 4 개의 정적 내부 클래스가 있습니다. 처음 두 개의 정적 내부 클래스는 Json.kt 파일에있는 두 개의 인라인 메서드의 컴파일 된 결과입니다. 이는 무시할 수 있습니다.

다음으로 테스트 메서드를 살펴 보겠습니다.이 메서드에는 두 개의 역 직렬화 프로세스가 있습니다. 처음에는 정적 내부 클래스 JsonfromJson2List KaTeX 구문 분석 오류 : 16 번 위치의 수학 모드에서 '$'함수를 사용할 수 없습니다. inlinedfromJson $ ̲2 , 정적 내부 클래스 Js ... inlinedfromJson2List $ 1이 두 번째 로 호출 됩니다 . 즉, 세 번째 및 네 번째 정적 내부 클래스가 각각 특정 제네릭 유형을 얻기 위해 호출됩니다.

이 두 정적 내부 클래스에 의해 선언 된 제네릭 유형은 동일하지 않습니다. 이들은 <List <? extends T >> 및 <List <? extends User >>입니다.이 시점에서 모든 사람이이를 이해하는 것으로 추정됩니다. time 제네릭 유형이 deserialization 중에 지워져 deserialization이 실패했습니다.

이 모듈에 의존하는 방법은 일반 T를 특정 클래스와 결합하면 일반 T가 지워집니다. 이에 대한 답변은 Kotlin 공식 웹 사이트에서해야합니다. 이유를 아시면 댓글 영역에 메시지.

넓히다

프로젝트가 Gson에 의존하지 않는 경우 다음과 같이 클래스를 사용자 정의하여 특정 제네릭 유형을 얻을 수 있습니다.

open class TypeLiteral<T> {
    val type: Type
        get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}

//用以下代码替换TypeToken类相关代码即可
val type = object : TypeLiteral<T>() {}.type

제네릭 조합의 경우 RxHttp 라이브러리의 ParameterizedTypeImpl 클래스를 사용할 수도 있습니다. 사용법은 다음과 같습니다.

https://github.com/liujingxing/okhttp-RxHttp

https://github.com/liujingxing/okhttp-RxHttp/blob/master/rxhttp/src/main/java/rxhttp/wrapper/entity/ParameterizedTypeImpl.kt

//得到 List<User> 类型
val type: Type = ParameterizedTypeImpl[List::class.java, User::class.java]

자세한 사용법은 Android 및 Java 일반 리터러시를 참조하십시오.

https://juejin.cn/post/6844903828219756552

요약

현재이 문제를 피하기 위해서는 해당 코드를 서브 모듈로 옮기기 만하면되며, 서브 모듈 코드를 호출해도 일반 삭제 문제가 발생하지 않습니다.

사실 Kotlin 1.3.x에서이 문제를 발견했습니다. 최신 버전은 항상 존재했습니다. 그 기간 동안 Bennyhuo에 문의했지만 나중에이 문제를 우회했기 때문에이 문제가 가까운 시일 내에 Kotlin 담당자에게 제출하여 최대한 빨리 문제를 해결하기를 바랍니다.

저자의 생각

Google이 Kotlin-First의 중요한 개념을 발표 한 이후, 관계자는 Kotlin이 Android 개발자가 선택한 언어라고 지속적으로 주장 해 왔습니다.

그러나 지금까지 많은 사람들의 마음에는 여전히 의문이 있습니다. 그렇다면 Android 개발에 참여하고 있습니다. 나중에 Kotlin을 배울 수 있을까요?

** 제 대답은 아니오입니다. ** Java는 여전히 Android 개발의 주류 언어입니다. 또한 C 언어도 매우 중요합니다 (오디오 및 비디오 개발 방향이 매우 유명합니다).

많은 사람들이 바퀴가 Android 개발에 사용될 수 있다고 생각하지만 바퀴는 만능이 아닙니다. 현재 Android 인재 시장은 가득 차 있습니다. 눈에 띄고 싶다면 여전히 마스터해야 할 것이 많습니다. 장기적인 개발을 원한다면 , 할 수 있기를 바랍니다. 아래에 나열된 사항을 잘 살펴보십시오.

298 일, 8 개 모듈, 3,382 페이지, 66 만 단어, Android 개발의 핵심 지식에 대한 메모가 필요했습니다! (이것은 다년간의 업무 경험을 바탕으로 거물들이 요약 한 것이며, Android 개발을 위해 습득해야 할 지식이기도합니다. 모두에게 도움이 되었으면합니다.)

추천

출처blog.csdn.net/BUGgogogo/article/details/114135899