一段值的Kotlin之旅

小小小小小粽子-發表於2019-03-28

我們偶爾可能會遇到需要使用一段值的場景,比如在寫演算法時,輸入1~10,往常使用Java的時候,我們得初始化一個包含1~10的陣列,我在查詢Kotlin集合文件的時候,在眾多的語法糖中發現了Range類。

Range代表了一個範圍,這個範圍由最大值跟最小值定義。我們來看它的用法:

 val range = 1..10
複製程式碼

沒錯,就是這麼簡單,這樣我們便可以表示一到十。

當然了,它可以用來表示一大段值,比如:

val value = args[0].toInt()
when(value) {   
    in 100..200 -> println("Informational responses")   
    in 200..300 -> println("Success")    
    in 300..400 -> println("Redirection")    
    in 400..500 -> println("Client error")    
    in 500..600 -> println("Server error")
}
複製程式碼

在這個情況下,用來判斷HTTP狀態碼是不是很方便?

..操作符對應rangeTo方法,此處返回了一個IntRange。注意想用它生成一段倒序的值是沒有效果的,編譯器也會提示我們這生成的是一個空的Range

val range=(3..1)//錯誤用法
複製程式碼

因為range一旦發現它first > last,就不做處理了:

override fun isEmpty(): Boolean = first > last
複製程式碼

想要一個倒序的物件我們得呼叫downTo方法。除了downTo方法最常用的就是step方法了,step表示步長,或者說是當前值跟下一個值的差,

(1..3 step 2)//表示的範圍裡只有1跟3兩個數
複製程式碼

順便我們就來好好檢視IntRange的原始碼,可以發現它繼承自IntProgression

public class IntRange(start: Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> {
    override val start: Int get() = first
    override val endInclusive: Int get() = last

    override fun contains(value: Int): Boolean = first <= value && value <= last

    override fun isEmpty(): Boolean = first > last

    override fun equals(other: Any?): Boolean =
        other is IntRange && (isEmpty() && other.isEmpty() ||
        first == other.first && last == other.last)

    override fun hashCode(): Int =
        if (isEmpty()) -1 else (31 * first + last)

    override fun toString(): String = "$first..$last"

    companion object {
        /** An empty range of values of type Int. */
        public val EMPTY: IntRange = IntRange(1, 0)
    }
}
複製程式碼

IntProgression又實現了Iterable介面:

public open class IntProgression
    internal constructor
    (
            start: Int,
            endInclusive: Int,
            step: Int
    ) : Iterable<Int> {
    init {
        if (step == 0) throw kotlin.IllegalArgumentException("Step must be non-zero.")
        if (step == Int.MIN_VALUE) throw kotlin.IllegalArgumentException("Step must be greater than Int.MIN_VALUE to avoid overflow on negation.")
    }

    /**
     * The first element in the progression.
     */
    public val first: Int = start

    /**
     * The last element in the progression.
     */
    public val last: Int = getProgressionLastElement(start.toInt(), endInclusive.toInt(), step).toInt()

    /**
     * The step of the progression.
     */
    public val step: Int = step

    override fun iterator(): IntIterator = IntProgressionIterator(first, last, step)

    /** Checks if the progression is empty. */
    public open fun isEmpty(): Boolean = if (step > 0) first > last else first < last

    override fun equals(other: Any?): Boolean =
        other is IntProgression && (isEmpty() && other.isEmpty() ||
        first == other.first && last == other.last && step == other.step)

    override fun hashCode(): Int =
        if (isEmpty()) -1 else (31 * (31 * first + last) + step)

    override fun toString(): String = if (step > 0) "$first..$last step $step" else "$first downTo $last step ${-step}"

    companion object {
        /**
         * Creates IntProgression within the specified bounds of a closed range.

         * The progression starts with the [rangeStart] value and goes toward the [rangeEnd] value not excluding it, with the specified [step].
         * In order to go backwards the [step] must be negative.
         *
         * [step] must be greater than `Int.MIN_VALUE` and not equal to zero.
         */
        public fun fromClosedRange(rangeStart: Int, rangeEnd: Int, step: Int): IntProgression = IntProgression(rangeStart, rangeEnd, step)
    }
}

複製程式碼

這個類被包含在Progressions.kt檔案下,這個檔案下還有LongProgressionCharProgression,結構大體類似,我們不做額外的分析。

這個類重寫了iterator()方法,返回了一個IntProgressionIterator類,在同一個檔案下還有LongProgressionIteratorCharProgressionIterator,分別對應於LongProgressionCharProgression類的iterator()方法。

我們來看看IntProgressionIterator的原始碼:

internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
    private val finalElement = last
    private var hasNext: Boolean = if (step > 0) first <= last else first >= last
    private var next = if (hasNext) first else finalElement

    override fun hasNext(): Boolean = hasNext

    override fun nextInt(): Int {
        val value = next
        if (value == finalElement) {
            if (!hasNext) throw kotlin.NoSuchElementException()
            hasNext = false
        }
        else {
            next += step
        }
        return value
    }
}
複製程式碼

很簡短,跟我們常見的迭代器實現差不多,nextInt()方法會檢查下面是否還有值,設定hasNext欄位。

根據上面的分析,我們知道Range除了繼承下來的contains等方法外,可以使用標準庫為Iterable提供的諸多擴充套件方法了。我們來瞎玩玩:

class Main {
    fun main(args: Array<String>) {
    val input = args[o].toInt()
    if (input in (1..10)) {
        print(input)
    }

    (1..10).forEach {
      print(it)
      } 
    }
}
複製程式碼

我們來看看反編譯的Java程式碼:

public final class Main {
   public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
  String var3 = args[0];
 int input = Integer.parseInt(var3);
 if (1 <= input) {
         if (10 >= input) {
            System.out.print(input);
  }
      }

      byte var9 = 1;
  Iterable $receiver$iv = (Iterable)(new IntRange(var9, 10));
  Iterator var4 = $receiver$iv.iterator();   while(var4.hasNext()) {
         int element$iv = ((IntIterator)var4).nextInt();
 int var7 = false;
  System.out.print(element$iv);
  }

   }
}
複製程式碼

啊咧,第一個判斷輸入是否在給定range的例子沒有生成Range物件,直接拿數值作了比較,而第二個列印出range裡所有值的例子按照預期建立了IntRange,並使用了它的iterator來迭代。

我不敢相信自己的眼睛,我只宣告一個range物件來看看:

val range = 1..2
複製程式碼

但是結果讓我更加迷糊了:

public final class Main {
   public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
 byte var3 = 1;
 new IntRange(var3, 2);
  }
}
複製程式碼

我猜,可能我們的輸入是一個整型,而我們這裡建立的Range中的值也是一個整型,所以編譯器又悄咪咪地幫我們做了一些事,直接省略了物件的建立轉而使用最大最小值比較?而上面的程式碼由於我把它賦值給了一個變數,所以編譯器也給我建立了物件?順著猜想我來做驗證,我把input宣告成一個可能是null的變數,,我不信你編譯器還能斷定我輸入的是一個整型:

class Main {
    fun main(args: Array<String>) {
        val input = args[0].toIntOrNull()
        if (input in (1..10)) {
            print(input)
        }
    }
}
複製程式碼

這時候我們來看反編譯的位元組碼:

public final class Main {
  public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Integer input = StringsKt.toIntOrNull(args[0]);
      byte var3 = 1;
      IntRange var4 = new IntRange(var3, 10);
      if (input != null && var4.contains(input)) {
         System.out.print(input);
      }
   }
}
複製程式碼

果然,這時候我如願看到了IntRange物件的建立!果然編譯器無法肯定input是一個整型數字時,它會建立Range物件來做邏輯判斷。

我又試了一下把輸入值改成Double型別:

val input = args[0].toDouble()
複製程式碼

這種情況下,編譯器也會給我們建立物件:

public final class Main {
   public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
  String var4 = args[0];
 double input = Double.parseDouble(var4);
 byte var5 = 1;
 if (RangesKt.intRangeContains((ClosedRange)(new IntRange(var5, 10)), input)) {
         System.out.print(input);
  }
   }
}
複製程式碼

我們來比較簡單情況下建立物件跟不建立物件的效能:

@State(Scope.Thread)
open class MyState {
 val value = 3;
}

@Benchmark
fun benchmark1(blackhole: Blackhole, state: MyState) {
 val range = 0..10    
 if (state.value in range) {
        blackhole.consume(state.value)
    }

 if (state.value in range) {
        blackhole.consume(state.value)
    }
}

@Benchmark
fun benchmark2(blackhole: Blackhole, state: MyState) {

 if (state.value in 0..10) {
        blackhole.consume(state.value)
    }

 if (state.value in 0..10) {
        blackhole.consume(state.value)
    }
}
複製程式碼

就結果來看,方法執行時間差不多:

Benchmark  Mode   Cnt  Score    Error  Units
benchmark1 avgt   200  4.828 ±  0.018  ns/op
benchmark2 avgt   200  4.833 ±  0.045  ns/op
複製程式碼

只不過其中一種多建立了物件佔用了記憶體罷了。

到這裡謎團都解開了,我們使用了一些編譯器需要由iterator來實現的方法,或者range中值的型別跟拿來傳入range方法的引數型別不一致時(之前Double與Int混用或者傳入引數可為空),或者我們把rangeTodownTo方法返回的物件賦值給一個變數,這些時候編譯器都會給我們建立Range物件,佔用記憶體。 不管怎麼說,記憶體能省則省,我們應當盡力避免這些情況。

最後按照慣例我們來做一下BenchMark,跟陣列作比較,程式碼如下:

val range = 0..1_000 
val array = Array(1_000) { it } 
@Benchmark 
fun rangeLoop(blackhole: Blackhole) {
    range.forEach {
  blackhole.consume(it)
    } }

@Benchmark
fun rangeSequenceLoop(blackhole: Blackhole) {
    range.asSequence().forEach {
  blackhole.consume(it)
    } }

@Benchmark
fun arrayLoop(blackhole: Blackhole) {
    array.forEach {
  blackhole.consume(it)
    } }

@Benchmark
fun arraySequenceLoop(blackhole: Blackhole) {
    array.asSequence().forEach {
  blackhole.consume(it)
    } }
複製程式碼

反編譯成Java大概是這樣:

@Benchmark
public final void rangeLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Iterable $receiver$iv = (Iterable)MyBenchmarkKt.getRange();
   Iterator var3 = $receiver$iv.iterator();

   while(var3.hasNext()) {
      int element$iv = ((IntIterator)var3).nextInt();
      blackhole.consume(element$iv);
   }

}

@Benchmark
public final void rangeSequenceLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Sequence $receiver$iv = CollectionsKt.asSequence((Iterable)MyBenchmarkKt.getRange());
   Iterator var3 = $receiver$iv.iterator();

   while(var3.hasNext()) {
      Object element$iv = var3.next();
      int it = ((Number)element$iv).intValue();
      blackhole.consume(it);
   }

}

@Benchmark
public final void arrayLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Object[] $receiver$iv = (Object[])MyBenchmarkKt.getArray();
   int var3 = $receiver$iv.length;

   for(int var4 = 0; var4 < var3; ++var4) {
      Object element$iv = $receiver$iv[var4];
      int it = ((Number)element$iv).intValue();
      blackhole.consume(it);
   }

}

@Benchmark
public final void arraySequenceLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Sequence $receiver$iv = ArraysKt.asSequence((Object[])MyBenchmarkKt.getArray());
   Iterator var3 = $receiver$iv.iterator();

   while(var3.hasNext()) {
      Object element$iv = var3.next();
      int it = ((Number)element$iv).intValue();
      blackhole.consume(it);
   }

}
複製程式碼

都是一次迴圈迭代來完成任務。 再看看結果:

Benchmark                  Mode Cnt Score      Error   Units
arrayLoop                  avgt 200 2640.670 ± 8.357   ns/op
arraySequenceLoop.         avgt 200 2817.694 ± 44.780  ns/op
rangeLoop                  avgt 200 3156.754 ± 27.725  ns/op
rangeSequenceLoop          avgt 200 5286.066 ± 81.330  ns/op
複製程式碼

這次反而是轉化成Sequence之後耗時更多,不過也難免,只有一次迴圈迭代的情況下,Sequence的實現並沒有效能上的優勢。 關於Sequence的效能問題,參考這篇分析Kotlin使用優化

我們再來一個呼叫多個方法的版本:

@Benchmark 
fun rangeLoop(blackhole: Blackhole)
        = range
  .map { it * 2 }
  .first { it % 2 == 0 }     
  
  @Benchmark 
fun rangeSequenceLoop(blackhole: Blackhole)
        = range.asSequence()
            .map { it * 2 }
  .first { it % 2 == 0 }  
  
    @Benchmark
fun arrayLoop(blackhole: Blackhole)
    = array
            .map { it * 2 }
  .first { it % 2 == 0 }  
  
    @Benchmark
fun arraySequenceLoop(blackhole: Blackhole)
    = array.asSequence()
            .map { it * 2 }
  .first { it % 2 == 0 }
複製程式碼

來看看編譯器生成的程式碼:

@Benchmark
public final int rangeLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Iterable $receiver$iv = (Iterable)MyBenchmarkKt.getRange();
   Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
   Iterator var5 = $receiver$iv.iterator();

   while(var5.hasNext()) {
      int item$iv$iv = ((IntIterator)var5).nextInt();
      Integer var12 = item$iv$iv * 2;
      destination$iv$iv.add(var12);
   }

   $receiver$iv = (Iterable)((List)destination$iv$iv);
   Iterator var3 = $receiver$iv.iterator();

   Object element$iv;
   int it;
   do {
      if (!var3.hasNext()) {
         throw (Throwable)(new NoSuchElementException("Collection contains no element matching the predicate."));
      }

      element$iv = var3.next();
      it = ((Number)element$iv).intValue();
   } while(it % 2 != 0);

   return ((Number)element$iv).intValue();
}

@Benchmark
public final int rangeSequenceLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Sequence $receiver$iv = SequencesKt.map(CollectionsKt.asSequence((Iterable)MyBenchmarkKt.getRange()), (Function1)null.INSTANCE);
   Iterator var3 = $receiver$iv.iterator();

   Object element$iv;
   int it;
   do {
      if (!var3.hasNext()) {
         throw (Throwable)(new NoSuchElementException("Sequence contains no element matching the predicate."));
      }

      element$iv = var3.next();
      it = ((Number)element$iv).intValue();
   } while(it % 2 != 0);

   return ((Number)element$iv).intValue();
}

@Benchmark
public final int arrayLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Object[] $receiver$iv = (Object[])MyBenchmarkKt.getArray();
   Object[] $receiver$iv$iv = $receiver$iv;
   Collection destination$iv$iv = (Collection)(new ArrayList($receiver$iv.length));
   int it = $receiver$iv.length;

   for(int var6 = 0; var6 < it; ++var6) {
      Object item$iv$iv = $receiver$iv$iv[var6];
      int it = ((Number)item$iv$iv).intValue();
      Integer var13 = it * 2;
      destination$iv$iv.add(var13);
   }

   Iterable $receiver$iv = (Iterable)((List)destination$iv$iv);
   Iterator var15 = $receiver$iv.iterator();

   Object element$iv;
   do {
      if (!var15.hasNext()) {
         throw (Throwable)(new NoSuchElementException("Collection contains no element matching the predicate."));
      }

      element$iv = var15.next();
      it = ((Number)element$iv).intValue();
   } while(it % 2 != 0);

   return ((Number)element$iv).intValue();
}

@Benchmark
public final int arraySequenceLoop(@NotNull Blackhole blackhole) {
   Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
   Sequence $receiver$iv = SequencesKt.map(ArraysKt.asSequence((Object[])MyBenchmarkKt.getArray()), (Function1)null.INSTANCE);
   Iterator var3 = $receiver$iv.iterator();

   Object element$iv;
   int it;
   do {
      if (!var3.hasNext()) {
         throw (Throwable)(new NoSuchElementException("Sequence contains no element matching the predicate."));
      }

      element$iv = var3.next();
      it = ((Number)element$iv).intValue();
   } while(it % 2 != 0);

   return ((Number)element$iv).intValue();
}

複製程式碼

看看這迴圈的數量,我不看結果也知道sequence系列方法完勝了。

Benchmark             Mode  Cnt  Score      Error     Units
arrayLoop             avgt  200  6490.003 ± 124.134   ns/op
arraySequenceLoop     avgt  200  14.841   ± 0.483     ns/op
rangeLoop             avgt. 200  8268.058 ± 179.797   ns/op
rangeSequenceLoop     avgt  200  16.109   ± 0.128     ns/op
複製程式碼

最後的最後,我們來做個總結,雖然都能用來表示一段值,Range大兄弟在整體表現上是不如陣列來的快,而且Range表示的這一段值根據我們使用的方式不同,編譯器最後給我們生成的表現形式也不同。編譯器悄咪咪地給我們做了太多事,可能也會默默地增加我們資源的消耗,小小的Range就能扒拉出這麼多東西,大夥兒在平時使用的時候,一定要注意自己的用法,有時間可以看看位元組碼,總會有一些新收穫。

相關文章