Iterable และ Sequence ของ Kotlin มีลักษณะเหมือนกันทุกประการ ทำไมต้องมีสองประเภท?


88

อินเทอร์เฟซทั้งสองนี้กำหนดวิธีการเดียวเท่านั้น

public operator fun iterator(): Iterator<T>

เอกสารบอกว่าSequenceตั้งใจจะขี้เกียจ แต่ไม่Iterableขี้เกียจด้วย (เว้นแต่จะได้รับการสนับสนุนจาก a Collection)?

คำตอบ:


138

กุญแจสำคัญในความแตกต่างอยู่ในความหมายและการใช้งานฟังก์ชั่นการขยาย STDLIB สำหรับและIterable<T>Sequence<T>

  • สำหรับSequence<T>ฟังก์ชันส่วนขยายจะดำเนินการอย่างเฉื่อยชาหากเป็นไปได้เช่นเดียวกับการดำเนินการขั้นกลางของ Java Streams ตัวอย่างเช่นSequence<T>.map { ... }ส่งคืนอีกรายการSequence<R>และไม่ประมวลผลรายการจนกว่าจะมีการเรียกใช้งานเทอร์มินัลเหมือนtoListหรือfoldถูกเรียก

    พิจารณารหัสนี้:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    มันพิมพ์:

    before sum 1 2
    

    Sequence<T>มีไว้สำหรับการใช้งานที่ขี้เกียจและการไปป์ไลน์ที่มีประสิทธิภาพเมื่อคุณต้องการลดงานที่ทำในการดำเนินการเทอร์มินัลให้มากที่สุดเท่าที่จะทำได้เช่นเดียวกับ Java Streams อย่างไรก็ตามความเกียจคร้านทำให้เกิดค่าใช้จ่ายบางส่วนซึ่งเป็นสิ่งที่ไม่พึงปรารถนาสำหรับการเปลี่ยนคอลเลคชันขนาดเล็กโดยทั่วไปและทำให้มีประสิทธิภาพน้อยลง

    โดยทั่วไปไม่มีวิธีที่ดีในการระบุว่าเมื่อใดจำเป็นดังนั้นใน Kotlin stdlib ความเกียจคร้านจึงถูกทำให้ชัดเจนและดึงข้อมูลไปยังSequence<T>อินเทอร์เฟซเพื่อหลีกเลี่ยงการใช้งานกับIterables ทั้งหมดโดยค่าเริ่มต้น

  • สำหรับIterable<T>ในทางตรงกันข้าม, ฟังก์ชั่นการขยายกับกลางความหมายการดำเนินการทำงานกระหาย, Iterableการประมวลผลรายการได้ทันทีและกลับอีก ตัวอย่างเช่นIterable<T>.map { ... }ส่งคืน a List<R>พร้อมกับผลลัพธ์การแมป

    รหัสเทียบเท่าสำหรับทำซ้ำได้:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    สิ่งนี้พิมพ์ออกมา:

    1 2 before sum
    

    ตามที่กล่าวไว้ข้างต้นIterable<T>ไม่ขี้เกียจโดยค่าเริ่มต้นและโซลูชันนี้แสดงให้เห็นได้ดี: ในกรณีส่วนใหญ่มีการอ้างอิงที่ดีดังนั้นการใช้ประโยชน์จากแคชของ CPU การคาดการณ์การดึงข้อมูลล่วงหน้าเป็นต้นเพื่อให้แม้แต่การคัดลอกคอลเลกชันหลาย ๆ ชุดก็ยังทำงานได้ดี เพียงพอและทำงานได้ดีขึ้นในกรณีง่ายๆที่มีคอลเล็กชันขนาดเล็ก

    หากคุณต้องการการควบคุมมากขึ้นสำหรับไปป์ไลน์การประเมินผลมีการแปลงอย่างชัดเจนเป็นลำดับขี้เกียจพร้อมIterable<T>.asSequence()ฟังก์ชัน


3
อาจเป็นเรื่องน่าประหลาดใจสำหรับแฟน ๆJava(ส่วนใหญ่Guava)
Venkata Raju

@VenkataRaju สำหรับคนทำงานที่พวกเขาอาจแปลกใจกับทางเลือกของคนขี้เกียจโดยค่าเริ่มต้น
Jayson Minard

9
โดยค่าเริ่มต้น Lazy จะมีประสิทธิภาพน้อยกว่าสำหรับคอลเลกชันที่มีขนาดเล็กและใช้กันทั่วไป สำเนาอาจเร็วกว่าการประเมินที่ขี้เกียจหากใช้ประโยชน์จากแคชของ CPU เป็นต้น ดังนั้นสำหรับกรณีการใช้งานทั่วไปจะดีกว่าที่จะไม่ขี้เกียจ และน่าเสียดายที่สัญญาทั่วไปสำหรับฟังก์ชั่นชอบmap, filterและอื่น ๆ ที่ไม่ได้ดำเนินการข้อมูลเพียงพอที่จะตัดสินใจอื่น ๆ นอกเหนือจากชนิดแหล่งที่มาของคอลเลกชันและตั้งแต่คอลเลกชันมากที่สุดนอกจากนี้ยังมี Iterable ที่ไม่ได้เป็นเครื่องหมายที่ดีสำหรับ "ขี้เกียจ" เพราะมันเป็น โดยทั่วไปทุกที่ ขี้เกียจต้องชัดเจนจึงจะปลอดภัย
Jayson Minard

1
@naki ตัวอย่างหนึ่งจากการประกาศล่าสุดของ Apache Spark พวกเขากังวลเกี่ยวกับเรื่องนี้อย่างเห็นได้ชัดโปรดดูหัวข้อ "Cache-Aware Computation" ที่databricks.com/blog/2015/04/28/… ... แต่พวกเขากังวลเกี่ยวกับหลายพันล้าน สิ่งที่วนซ้ำดังนั้นพวกเขาจึงต้องดำเนินการอย่างเต็มที่
Jayson Minard

3
นอกจากนี้ข้อผิดพลาดทั่วไปที่เกิดจากการประเมินแบบเกียจคร้านคือการรวบรวมบริบทและจัดเก็บการคำนวณที่เกียจคร้านที่เกิดขึ้นในสนามพร้อมกับชาวบ้านที่ถูกจับทั้งหมดและสิ่งที่พวกเขามี ดังนั้นจึงยากที่จะแก้ปัญหาการรั่วไหลของหน่วยความจำ
Ilya Ryzhenkov

50

การกรอกคำตอบของปุ่มลัด:

สิ่งสำคัญคือต้องสังเกตว่าลำดับและวนซ้ำได้อย่างไรในองค์ประกอบของคุณ:

ตัวอย่างลำดับ:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

ผลการบันทึก:

ตัวกรอง - แผนที่ - แต่ละรายการ; ตัวกรอง - แผนที่ - แต่ละรายการ

ตัวอย่างที่ทำซ้ำได้:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

ตัวกรอง - ตัวกรอง - แผนที่ - แผนที่ - แต่ละรายการ


5
นั่นเป็นตัวอย่างที่ดีของความแตกต่างระหว่างทั้งสอง
Alexey Soshin

นี่เป็นตัวอย่างที่ยอดเยี่ยม
frye3k

2

Iterableถูกแมปกับjava.lang.Iterableอินเทอร์เฟซบนไฟล์ JVMและนำไปใช้โดยคอลเลกชันที่ใช้กันทั่วไปเช่นรายการหรือชุด ฟังก์ชันส่วนขยายคอลเลกชันในสิ่งเหล่านี้ได้รับการประเมินอย่างกระตือรือร้นซึ่งหมายความว่าทุกคนประมวลผลองค์ประกอบทั้งหมดในข้อมูลที่ป้อนทันทีและส่งคืนคอลเล็กชันใหม่ที่มีผลลัพธ์

นี่คือตัวอย่างง่ายๆของการใช้ฟังก์ชันคอลเลกชันเพื่อรับชื่อของบุคคล 5 คนแรกในรายการที่มีอายุอย่างน้อย 21 ปี:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

แพลตฟอร์มเป้าหมาย: JVMRunning บน kotlin v. 1.3.61 ขั้นแรกการตรวจสอบอายุจะทำสำหรับทุกคนในรายการโดยผลลัพธ์จะอยู่ในรายการใหม่เอี่ยม จากนั้นการแมปกับชื่อของพวกเขาจะทำสำหรับทุกคนที่ยังคงอยู่หลังจากตัวดำเนินการตัวกรองซึ่งจะลงท้ายด้วยรายการใหม่อีกรายการ (ตอนนี้คือ a List<String>) ในที่สุดก็มีรายการใหม่ล่าสุดที่สร้างขึ้นเพื่อให้มีห้าองค์ประกอบแรกของรายการก่อนหน้านี้

ในทางตรงกันข้าม Sequence เป็นแนวคิดใหม่ใน Kotlin เพื่อแสดงการรวบรวมค่าที่ประเมินอย่างเฉื่อยชา ส่วนขยายคอลเลคชันเดียวกันพร้อมใช้งานสำหรับSequenceอินเทอร์เฟซ แต่จะส่งคืนอินสแตนซ์ Sequence ทันทีที่แสดงสถานะประมวลผลของวันที่ แต่ไม่มีการประมวลผลองค์ประกอบใด ๆ ในการเริ่มต้นการประมวลผล Sequenceจะต้องถูกยกเลิกด้วยตัวดำเนินการเทอร์มินัลซึ่งโดยพื้นฐานแล้วเป็นการร้องขอไปยังลำดับเพื่อทำให้ข้อมูลที่แสดงเป็นรูปธรรมในรูปแบบที่เป็นรูปธรรม ตัวอย่าง ได้แก่toList , toSetและsumพูดถึงเพียงไม่กี่ เมื่อมีการเรียกสิ่งเหล่านี้จำนวนองค์ประกอบขั้นต่ำที่ต้องการเท่านั้นที่จะถูกประมวลผลเพื่อให้ได้ผลลัพธ์ที่ต้องการ

การเปลี่ยนคอลเลกชันที่มีอยู่ให้เป็นลำดับนั้นค่อนข้างตรงไปตรงมาคุณเพียงแค่ต้องใช้asSequenceส่วนขยาย ดังที่ได้กล่าวไว้ข้างต้นคุณต้องเพิ่มตัวดำเนินการเทอร์มินัลมิฉะนั้น Sequence จะไม่ทำการประมวลผลใด ๆ (อีกแล้วขี้เกียจ!)

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

แพลตฟอร์มเป้าหมาย: JVMRunning บน kotlin v.3.3.61 ในกรณีนี้อินสแตนซ์บุคคลในลำดับจะถูกตรวจสอบอายุของพวกเขาหากผ่านไปพวกเขาจะมีการแยกชื่อของพวกเขาแล้วเพิ่มลงในรายการผลลัพธ์ สิ่งนี้จะทำซ้ำสำหรับแต่ละคนในรายการเดิมจนกว่าจะมีคนพบห้าคน ณ จุดนี้ฟังก์ชัน toList จะส่งกลับรายการและคนที่เหลือในไฟล์Sequenceจะไม่ถูกประมวลผล

นอกจากนี้ยังมีสิ่งพิเศษที่ Sequence สามารถทำได้: สามารถบรรจุรายการได้ไม่ จำกัด จำนวน ด้วยมุมมองนี้จึงสมเหตุสมผลที่ตัวดำเนินการจะทำงานในแบบที่พวกเขาทำ - ตัวดำเนินการบนลำดับที่ไม่มีที่สิ้นสุดจะไม่สามารถย้อนกลับได้หากมันทำงานอย่างกระตือรือร้น

ตัวอย่างเช่นต่อไปนี้เป็นลำดับที่จะสร้างพาวเวอร์ของ 2 ได้มากตามที่ตัวดำเนินการเทอร์มินัลต้องการ (โดยไม่สนใจว่าสิ่งนี้จะล้นอย่างรวดเร็ว)

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

ท่านสามารถหาข้อมูลเพิ่มเติมได้ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.