Kotlin編譯調校之WarningsAsErrors

  • 2020 年 1 月 21 日
  • 筆記

這之前的文章中,我們介紹過如果通過Kotlin編譯器參數實現將所有的warnings按照errors對待,主要的實現方法是這樣

//Code to be added    kotlinOptions {        allWarningsAsErrors = true    }  

那麼問題可能就會被提出來,開啟這一選項有什麼好處呢,畢竟我需要修改很多文件。

通常情況下,開啟後的作用,我們可以歸納為如下

  • 發現更多的潛在問題和崩潰
  • 減少不必要的代碼(變量,參數)
  • 發現不好的編碼實踐
  • 發現更多的API棄用問題
  • 最終增加代碼的健壯性和優雅程度

如下,我們會通過一些實踐來說明一些問題

Nothing to Inline(作用不大的內聯)

@Suppress("NOTHING_TO_INLINE")    inline fun String?.isNotNullNorEmpty(): Boolean {        //Expected performance impact of inlining        // 'public inline fun String?.isNotNullNorEmpty(): Boolean        // defined in com.example.warningsaserrorscases in file NothingToInlineWarnings.kt'        // is insignificant.        // Inlining works best for functions with parameters of functional types        return this != null && this.isNotEmpty()    }  
  • Kotlin的inline關鍵字會將對應的方法內聯到調用者的方法體,減少進棧出棧操作
  • inline最好的場景是處理函數類型參數,比如lambda
  • 刻意的inline可能導致方法體膨脹,增大class文件大小。
  • 處理這種警告,建議是去除inline關鍵字
  • 如果執意inline時,使用@Suppress("NOTHING_TO_INLINE")壓制編譯器警告

INACCESSIBLE_TYPE(不可訪問的類型)

public class RequestManager {        public static RequestManager sInstance = new RequestManager();            private static class TimelineRequest {            public String from;        }            public TimelineRequest getTimelineRequest() {            return new TimelineRequest();        }    }  
fun testInaccessibleType() {        //Type RequestManager.TimelineRequest! is inaccessible in this context        // due to: private open class TimelineRequest defined        // in com.example.warningsaserrorscases.RequestManager        @Suppress("INACCESSIBLE_TYPE")        RequestManager.sInstance.timelineRequest    }  
  • 上述的testInaccessibleType無法訪問TimelineRequest的屬性和方法
  • 具體的解決辦法,可以是設置TimelineRequest為public,而非private
  • 必要時可以使用@Suppress("INACCESSIBLE_TYPE")壓制警告

UNCHECKED_CAST(未檢查的類型轉換)

fun <T> Any.toType(): T? {        //Unchecked cast: Any to T        @Suppress("UNCHECKED_CAST")        return this as? T    }  
  • 上面this as? T屬於未檢查的類型轉換,可能在運行時拋出轉換異常
  • 不推薦使用@Suppress("UNCHECKED_CAST")壓制警告
  • 推薦使用reified方式處理
//a better way    inline fun <reified T> Any.toType(): T? {        return if (this is T) {            this        } else {            null        }    }  

WHEN_ENUM_CAN_BE_NULL_IN_JAVA(Enum 可能為null)

fun testEnum1() {        //Enum argument can be null in Java, but exhaustive when contains no null branch        when(SeasonUtil.getCurrentSeason()) {            Season.SPRING -> println("Spring")            Season.SUMMER -> println("Summer")            Season.FALL -> println("Fall")            Season.WINTER -> println("Winter")            //else -> println("unknown")        }    }        fun testEnum2() {        //Enum argument can be null in Java, but exhaustive when contains no null branch        @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")        when(SeasonUtil.getCurrentSeason()) {            Season.SPRING -> println("Spring")            Season.SUMMER -> println("Summer")            Season.FALL -> println("Fall")            Season.WINTER -> println("Winter")        }    }  
  • 上述的SeasonUtil.getCurrentSeason()可能為null
  • 建議增加else -> println("unknown")處理when的缺省情況
  • 不建議使用@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")壓制警告

PARAMETER_NAME_CHANGED_ON_OVERRIDE(方法重寫修改參數名)

interface OnViewClickedListener {        fun onViewClicked(viewId: Int)    }        fun testParameterNameChangedOnOverride() {        // The corresponding parameter in the supertype 'OnViewClickedListener'        // is named 'viewId'.        // This may cause problems when calling this function with named arguments.        object : OnViewClickedListener {            override fun onViewClicked(@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") id: Int) {                println("onViewClicked id=$id")            }        }    }  
  • 出問題的情況是當我們使用具名變量指定參數值時,可能出問題。
  • 建議方法參數與源方法保持一致。
  • 不建議壓制警告

Name shadowing(命名遮擋)

fun testNameShadowing(message: String) {        run {            //Name shadowed: message            @Suppress("NAME_SHADOWING") val message = "Hello World"            println(message)        }    }  
  • 當run方法後面的lambda中的message與testNameShadowing的message命名一致時,就發生了所謂的Name shadowing(命名遮擋)
  • Name shadowing很容易導致問題,且排查起來不易察覺。
  • 建議主動通過命名差異來解決這個問題
  • 不建議壓制警告

Uncessary cases (不必要的編碼場景)

UNNECESSARY_SAFE_CALL(不必要的安全調用)

fun testUnnecessarySafeCall(message: String) {        @Suppress("UNNECESSARY_SAFE_CALL")        println(message?.toIntOrNull())    }  
  • 上述的安全調用其實是顯得多餘,因為Kotlin內部會有Intrinsics做參數非空的與判斷
  • 另外安全調用會增加if條件檢查
  • 建議主動移不必要的安全調用
  • 不建議壓制警告

SENSELESS_COMPARISON(無意義的比較)

fun testSenselessComparison(message: String) {        //Condition 'message != null' is always 'true'        @Suppress("SENSELESS_COMPARISON")        if (message != null) {            }    }  
  • 和前面的例子一樣,這種檢查是多餘的,因為Kotlin內部會有Intrinsics做參數非空的與判斷
  • 建議主動移除無意義的比較
  • 不建議壓制警告

UNNECESSARY_NOT_NULL_ASSERTION(不需要的非空斷言)

fun testUncessaryNotNullAssertion(message: String) {        //Unnecessary non-null assertion (!!) on a non-null receiver        // of type String        @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")        println(message!!.toIntOrNull())    }  
  • 這種斷言是多餘的,因為Kotlin內部會有Intrinsics做參數非空的與判斷
  • 建議主動移除不需要的非空斷言
  • 不建議壓制警告

USELESS_IS_CHECK(沒有用的實例類型檢查)

fun testUselessIsCheck(message: String) {        //Check for instance is always 'true'        @Suppress("USELESS_IS_CHECK")        if (message is String) {            }    }  
  • 沒有意義的類型檢查,因為Kotlin內部會有Intrinsics做參數非空的與判斷
  • 建議主動移除不必要的檢查
  • 不建議壓制警告

VARIABLE_WITH_REDUNDANT_INITIALIZER(變量初始化多餘)

fun testVariableWithRedundantInitializer() {        //Variable 'message' initializer is redundant        @Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER") var message: String? = null;        message = System.currentTimeMillis().toString()        println(message)    }  
  • 建議手動移除多餘的初始化
  • 不建議壓制警告

Deprecation (方法棄用)

fun testGetDrawable(context: Context) {        @Suppress("DEPRECATION")        context.resources.getDrawable(R.mipmap.ic_launcher)    }  

建議的方法是尋找替代棄用方法的其他方法,以getDrawable為例,我們可以使用

  • ContextCompat.getDrawable(getActivity(), R.drawable.name);
  • ResourcesCompat.getDrawable(getResources(), R.drawable.name, null);
  • ResourcesCompat.getDrawable(getResources(), R.drawable.name, anotherTheme);
  • 必要時可以選擇壓制警告

unsued cases(開發者編碼沒有用到的情況)

Parameter 『extra』 is never used(參數沒有使用)

fun showMessage(message: String, extra: String?) {        println(message)    }  

解決方法

  • 移除extra參數
  • 方法中使用extra參數
  • 使用@Suppress("UNUSED_PARAMETER")壓制警告

Parameter 『index』 is never used, could be renamed to _(匿名參數沒有使用,可以使用佔位符)

fun forEachList() {        listOf<String>("Hello", "World").forEachIndexed { index, s ->            println(s)        }    }  
  • index改成佔位符_
  • 使用@Suppress("UNUSED_ANONYMOUS_PARAMETER")壓制警告

Variable 『currentTimeStamp』 is never used(變量未使用)

fun unusedVariable() {        @Suppress("UNUSED_VARIABLE") val currentTimeStamp = System.currentTimeMillis()        println("unusedVariable")    }  
  • 移除變量
  • 使用@Suppress壓制警告

The expression is unused(表達式未使用)

fun test(status: Int) {        when(status) {            1 -> "First"            2 -> "Second"            else -> "Else"        }    }  
  • 移除不用的表達式
  • 使用Suppress壓制警告

UNUSED_VALUE && ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE (未使用的值,賦值後未使用的變量)

fun testUnusedValue() {        // The value '"Hello"' assigned to 'var message: String?        // defined in com.example.warningsaserrorscases.test' is never used        @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") var message: String? = null        @Suppress("UNUSED_VALUE")        message = "Hello"    }  
  • 移除不用變量
  • 使用Suppress壓制警告

關於@Suppress

  • 不建議濫用,因優先考慮其他的更好的解決問題的方式
  • 及時使用一定要限定最小作用範圍,通常的選擇範圍盡量限制在變量(variable),參數(parameter)或者語句(statement)上。
  • 上面代碼中出現了很多@Suppress主要目的是顯示警告的名稱,而不是提倡大家使用壓制的方式處理警告。

以上。