Kotlin Bootcamp for Programmers 5.1: Extensions

ה-codelab הזה הוא חלק מקורס Kotlin Bootcamp for Programmers. כדי להפיק את המרב מהקורס הזה, מומלץ לעבוד על ה-codelabs לפי הסדר. אם יש לכם ידע בנושא, יכול להיות שתוכלו לדלג על חלק מהקטעים. הקורס הזה מיועד למתכנתים שמכירים שפה מונחית-אובייקטים ורוצים ללמוד Kotlin.

מבוא

ב-codelab הזה נציג לכם כמה תכונות שימושיות שונות ב-Kotlin, כולל זוגות, אוספים ופונקציות הרחבה.

במקום ליצור אפליקציה לדוגמה אחת, השיעורים בקורס הזה נועדו להרחיב את הידע שלכם, אבל הם גם עצמאיים למחצה, כך שתוכלו לדלג על חלקים שאתם מכירים. כדי לקשור בין הדוגמאות, רבות מהן מתבססות על נושא האקווריום. אם אתם רוצים לראות את הסיפור המלא של האקווריום, כדאי לעיין בקורס Kotlin Bootcamp for Programmers ב-Udacity.

מה שכדאי לדעת

  • התחביר של פונקציות, מחלקות ושיטות ב-Kotlin
  • איך עובדים עם REPL (Read-Eval-Print Loop) של Kotlin ב-IntelliJ IDEA
  • איך יוצרים כיתה חדשה ב-IntelliJ IDEA ומריצים תוכנית

מה תלמדו

  • איך עובדים עם זוגות ועם שלשות
  • מידע נוסף על אוספים
  • הגדרה ושימוש בקבועים
  • כתיבת פונקציות של תוספים

הפעולות שתבצעו:

  • מידע על זוגות, שלשות ומפות גיבוב ב-REPL
  • דרכים שונות לארגון קבועים
  • כתיבת פונקציית הרחבה ומאפיין הרחבה

במשימה הזו תלמדו על צמדים ועל שלשות, ועל פירוק שלהם. זוגות ושלשות הם מחלקות נתונים מוכנות מראש ל-2 או ל-3 פריטים כלליים. לדוגמה, זה יכול להיות שימושי אם רוצים שפונקציה תחזיר יותר מערך אחד.

נניח שיש לכם List של דגים, ופונקציה isFreshWater() לבדיקה אם הדג הוא דג מים מתוקים או דג מים מלוחים. הפונקציה List.partition() מחזירה שתי רשימות: אחת עם הפריטים שבהם התנאי הוא true, והשנייה עם הפריטים שבהם התנאי הוא false.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

שלב 1: יוצרים כמה זוגות ושלשות

  1. פותחים את REPL‏ (Tools (כלים) > Kotlin > Kotlin REPL).
  2. יוצרים זוג, משייכים ציוד לשימוש שלו ואז מדפיסים את הערכים. כדי ליצור זוג, יוצרים ביטוי שמקשר בין שני ערכים, כמו שני מחרוזות, באמצעות מילת המפתח to, ואז משתמשים ב-.first או ב-.second כדי להתייחס לכל ערך.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. יוצרים משולש ומדפיסים אותו עם toString(), ואז ממירים אותו לרשימה עם toList(). יוצרים שלישייה באמצעות Triple() עם 3 ערכים. משתמשים בערכים .first, .second ו-.third כדי להתייחס לכל ערך.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

בדוגמאות שלמעלה, כל החלקים של הצמד או השלשה הם מאותו סוג, אבל זה לא חובה. החלקים יכולים להיות מחרוזת, מספר או רשימה, למשל – ואפילו עוד זוג או שלישייה.

  1. ליצור זוג שבו החלק הראשון הוא בעצמו זוג.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

שלב 2: פירוק של כמה זוגות ושלשות

הפרדה של זוגות ושלשות לחלקים שלהם נקראת פירוק. מקצים את הצמד או השלשה למספר המתאים של משתנים, ו-Kotlin תקצה את הערך של כל חלק לפי הסדר.

  1. מפרקים צמד ומדפיסים את הערכים.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. מפרקים טריפל ומדפיסים את הערכים.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

שימו לב: פירוק של זוגות ושלשות פועל באותו אופן כמו פירוק של מחלקות נתונים, כפי שמוסבר ב-codelab הקודם.

במשימה הזו נלמד על אוספים, כולל רשימות, ועל סוג חדש של אוסף, מפות גיבוב.

שלב 1: מידע נוסף על רשימות

  1. הסבר על רשימות ורשימות שניתנות לשינוי מופיע בשיעור קודם. זוהי מבנה נתונים שימושי מאוד, ולכן Kotlin מספקת מספר פונקציות מובנות לרשימות. כדאי לעיין ברשימה החלקית הזו של פונקציות לרשימות. רשימות מלאות זמינות במסמכי התיעוד של Kotlin בנושא List ו-MutableList.

פעולה

מטרה

add(element: E)

הוספת פריט לרשימה שניתן לשנות.

remove(element: E)

הסרת פריט מרשימה שניתן לשנות.

reversed()

הפונקציה מחזירה עותק של הרשימה עם הרכיבים בסדר הפוך.

contains(element: E)

הפונקציה מחזירה true אם הפריט מופיע ברשימה.

subList(fromIndex: Int, toIndex: Int)

הפונקציה מחזירה חלק מהרשימה, מהאינדקס הראשון ועד לאינדקס השני (לא כולל).

  1. עדיין עובדים ב-REPL, יוצרים רשימה של מספרים ומפעילים עליה את הפונקציה sum(). הסכום של כל הרכיבים.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. יוצרים רשימה של מחרוזות ומסכמים את הרשימה.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. אם הרכיב הוא לא משהו שפונקציית List יודעת לסכם ישירות, כמו מחרוזת, אפשר לציין איך לסכם אותו באמצעות .sumBy() עם פונקציית lambda, למשל, כדי לסכם לפי האורך של כל מחרוזת. שם ברירת המחדל של ארגומנט lambda הוא it, וכאן it מתייחס לכל רכיב ברשימה במהלך המעבר על הרשימה.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. אפשר לעשות עוד הרבה דברים עם רשימות. אחת הדרכים לראות את הפונקציונליות הזמינה היא ליצור רשימה ב-IntelliJ IDEA, להוסיף את הנקודה ואז לעיין ברשימת ההשלמה האוטומטית בתיאור הכלים. הפעולה הזו אפשרית לכל אובייקט. כדאי לנסות את זה עם רשימה.

  1. בוחרים באפשרות listIterator() מהרשימה, ואז עוברים על הרשימה עם הצהרת for ומדפיסים את כל הרכיבים מופרדים ברווחים.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

שלב 2: ניסיון של מפות גיבוב

ב-Kotlin, אפשר למפות כמעט כל דבר לכל דבר אחר באמצעות hashMapOf(). מפות גיבוב הן כמו רשימה של צמדים, שבהם הערך הראשון משמש כמפתח.

  1. יוצרים טבלת גיבוב (hash table) שמתאימה בין סימפטומים (המפתחות) לבין מחלות של דגים (הערכים).
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. אחר כך אפשר לאחזר את ערך המחלה על סמך מפתח הסימפטום באמצעות get() או אפילו באמצעות סוגריים מרובעים [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. נסו לציין סימפטום שלא מופיע במפה.
println(cures["scale loss"])
⇒ null

אם מפתח לא נמצא במפה, ניסיון להחזיר את המחלה התואמת יחזיר null. בהתאם לנתוני המיפוי, יכול להיות שלא תהיה התאמה למפתח אפשרי. במקרים כאלה, Kotlin מספקת את הפונקציה getOrDefault().

  1. נסו לחפש מפתח שלא נמצאה לו התאמה, באמצעות getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

אם אתם צריכים לעשות יותר מאשר להחזיר ערך, Kotlin מספקת את הפונקציה getOrElse().

  1. צריך לשנות את הקוד כך שישתמש ב-getOrElse() במקום ב-getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

במקום להחזיר ערך ברירת מחדל פשוט, מופעל כל קוד שנמצא בין הסוגריים המסולסלים {}. בדוגמה, הפונקציה else פשוט מחזירה מחרוזת, אבל היא יכולה להיות מורכבת יותר, למשל למצוא דף אינטרנט עם תרופה ולהחזיר אותו.

בדומה ל-mutableListOf, אפשר גם ליצור mutableMapOf. מפה שניתן לשנות מאפשרת להוסיף פריטים ולהסיר אותם. המונח 'ניתן לשינוי' (mutable) פשוט אומר שאפשר לשנות, והמונח 'לא ניתן לשינוי' (immutable) אומר שאי אפשר לשנות.

  1. ליצור מפת מלאי שאפשר לשנות, ולמפות מחרוזת של ציוד למספר הפריטים. יוצרים אותו עם רשת דגים, מוסיפים 3 מברשות לניקוי האקווריום למלאי עם put() ומסירים את רשת הדגים עם remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

במשימה הזו נלמד על קבועים ב-Kotlin ועל דרכים שונות לארגן אותם.

שלב 1: מידע על const לעומת val

  1. ב-REPL, מנסים ליצור קבוע מספרי. ב-Kotlin, אפשר ליצור קבועים ברמה העליונה ולהקצות להם ערך בזמן ההידור באמצעות const val.
const val rocks = 3

הערך מוקצה ואי אפשר לשנות אותו, וזה דומה מאוד להצהרה על val רגיל. אז מה ההבדל בין const val לבין val? הערך של const val נקבע בזמן הקומפילציה, ואילו הערך של val נקבע במהלך ביצוע התוכנית. כלומר, אפשר להקצות ל-val ערך באמצעות פונקציה בזמן הריצה.

כלומר, אפשר להקצות ערך ל-val מפונקציה, אבל אי אפשר לעשות את זה ל-const val.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

בנוסף, const val פועל רק ברמה העליונה, ובמחלקות סינגלטון שהוגדרו באמצעות object, ולא במחלקות רגילות. אפשר להשתמש בזה כדי ליצור קובץ או אובייקט יחיד שמכיל רק קבועים, ולייבא אותם לפי הצורך.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

שלב 2: יצירת אובייקט נלווה

ב-Kotlin אין מושג של קבועים ברמת המחלקה.

כדי להגדיר קבועים בתוך מחלקה, צריך להוסיף אותם לאובייקטים נלווים שהוגדרו באמצעות מילת המפתח companion. אובייקט נלווה הוא בעצם אובייקט יחיד בתוך המחלקה.

  1. יוצרים כיתה עם אובייקט נלווה שמכיל קבוע מחרוזת.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

ההבדל הבסיסי בין אובייקטים נלווים לבין אובייקטים רגילים הוא:

  • אובייקטים נלווים מאותחלים מהבונה הסטטי של המחלקה המכילה, כלומר הם נוצרים כשהאובייקט נוצר.
  • אובייקטים רגילים מאותחלים באופן עצלני בגישה הראשונה לאובייקט הזה, כלומר כשהם בשימוש בפעם הראשונה.

יש עוד, אבל בשלב הזה צריך לדעת רק שצריך להוסיף קבועים למחלקות באובייקט נלווה.

במשימה הזו תלמדו איך להרחיב את ההתנהגות של מחלקות. מקובל מאוד לכתוב פונקציות עזר כדי להרחיב את ההתנהגות של מחלקה. ‫Kotlin מספקת תחביר נוח להצהרה על פונקציות עזר כאלה: פונקציות הרחבה.

פונקציות הרחבה מאפשרות להוסיף פונקציות למחלקה קיימת בלי לגשת לקוד המקור שלה. לדוגמה, אפשר להצהיר עליהם בקובץ Extensions.kt שהוא חלק מהחבילה. הפעולה הזו לא משנה את המחלקה בפועל, אבל היא מאפשרת להשתמש בסימון הנקודה כשקוראים לפונקציה באובייקטים של המחלקה הזו.

שלב 1: כותבים פונקציית הרחבה

  1. עדיין עובדים ב-REPL, כותבים פונקציית הרחבה פשוטה, hasSpaces() כדי לבדוק אם מחרוזת מכילה רווחים. לשם הפונקציה מתווסף התחילית של המחלקה שהיא פועלת עליה. בתוך הפונקציה, this מתייחס לאובייקט שהפונקציה מופעלת עליו, ו-it מתייחס לאיטרטור בקריאה של find().
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. אפשר לפשט את הפונקציה hasSpaces(). אין צורך ב-this באופן מפורש, ואפשר לצמצם את הפונקציה לביטוי יחיד ולהחזיר אותו, כך שגם הסוגריים המסולסלים {} סביבו לא נדרשים.
fun String.hasSpaces() = find { it == ' ' } != null

שלב 2: מידע על המגבלות של התוספים

לפונקציות של תוספים יש גישה רק ל-API הציבורי של המחלקה שהן מרחיבות. אי אפשר לגשת למשתנים שהערך שלהם הוא private.

  1. אפשר לנסות להוסיף פונקציות של תוספים לנכס שמסומן בתווית private.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. בודקים את הקוד שבהמשך ומנסים להבין מה הוא ידפיס.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() הדפסות GreenLeafyPlant. יכול להיות שתצפו שגם aquariumPlant.print() יודפס כ-GreenLeafyPlant, כי הוקצה לו הערך plant. אבל הסוג נקבע בזמן ההידור, ולכן הערך AquariumPlant מודפס.

שלב 3: מוסיפים מאפיין של תוסף

בנוסף לפונקציות הרחבה, ב-Kotlin אפשר גם להוסיף מאפייני הרחבה. בדומה לפונקציות של תוספים, מציינים את המחלקה שמרחיבים, ואחריה נקודה ושם המאפיין.

  1. עדיין עובדים ב-REPL, מוסיפים מאפיין הרחבה isGreen ל-AquariumPlant, שהוא true אם הצבע ירוק.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

אפשר לגשת למאפיין isGreen בדיוק כמו למאפיין רגיל. כשניגשים אליו, מתבצעת קריאה ל-getter של isGreen כדי לקבל את הערך.

  1. מדפיסים את המאפיין isGreen של המשתנה aquariumPlant ומעיינים בתוצאה.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

שלב 4: מידע על מקבלים שניתן להגדיר להם ערך null

הכיתה שמרחיבים נקראת מקבל, ואפשר להגדיר אותה ככיתה שניתן להקצות לה ערך null. אם תעשו את זה, המשתנה this שמשמש בגוף יכול להיות null, לכן חשוב לבדוק את זה. כדאי להשתמש במקבל שיכול להיות null אם אתם מצפים שהמתקשרים ירצו להפעיל את שיטת ההרחבה שלכם על משתנים שיכולים להיות null, או אם אתם רוצים לספק התנהגות ברירת מחדל כשהפונקציה שלכם מופעלת על null.

  1. עדיין עובדים ב-REPL, מגדירים שיטה pull() שמקבלת מקלט שניתן להגדרה כ-nullable. הסימן לכך הוא סימן שאלה ? אחרי הסוג, לפני הנקודה. בתוך הגוף, אפשר לבדוק אם this לא שווה ל-null באמצעות questionmark-dot-apply ?.apply.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. במקרה כזה, לא תהיה פלט כשמריצים את התוכנית. מכיוון ש-plant הוא null, הפונקציה הפנימית println() לא נקראת.

פונקציות הרחבה הן רבות עוצמה, ורוב הספרייה הסטנדרטית של Kotlin מיושמת כפונקציות הרחבה.

בשיעור הזה למדתם על אוספים ועל קבועים, וקיבלתם טעימה מהעוצמה של פונקציות ומאפיינים של הרחבות.

  • אפשר להשתמש בזוגות ובשלשות כדי להחזיר יותר מערך אחד מפונקציה. לדוגמה:
    val twoLists = fish.partition { isFreshWater(it) }
  • ל-Kotlin יש הרבה פונקציות שימושיות ל-List, כמו reversed(),‏ contains() ו-subList().
  • אפשר להשתמש ב-HashMap כדי למפות מפתחות לערכים. לדוגמה:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • מצהירים על קבועים בזמן הידור באמצעות מילת המפתח const. אפשר להציב אותם ברמה העליונה, לארגן אותם באובייקט יחיד או להציב אותם באובייקט נלווה.
  • אובייקט נלווה הוא אובייקט יחיד בהגדרת מחלקה, שמוגדר באמצעות מילת המפתח companion.
  • פונקציות ומאפיינים של תוסף יכולים להוסיף פונקציונליות לכיתה. לדוגמה:
    fun String.hasSpaces() = find { it == ' ' } != null
  • מקבל שיכול להיות null מאפשר לכם ליצור תוספים בכיתה שיכולים להיות null. אפשר לשלב את האופרטור ?. עם apply כדי לבדוק אם null קיים לפני שמריצים את הקוד. לדוגמה:
    this?.apply { println("removing $this") }

תיעוד של Kotlin

אם אתם רוצים לקבל מידע נוסף על נושא כלשהו בקורס הזה, או אם נתקעתם, https://kotlinlang.org הוא המקום הכי טוב להתחיל בו.

מדריכים ל-Kotlin

באתר https://try.kotlinlang.org יש הדרכות מפורטות שנקראות Kotlin Koans, מפרש מבוסס-אינטרנט וסט מלא של מסמכי עזר עם דוגמאות.

קורס של Udacity

כדי לצפות בקורס של Udacity בנושא הזה, אפשר לעבור אל Kotlin Bootcamp for Programmers.

IntelliJ IDEA

מסמכי התיעוד של IntelliJ IDEA זמינים באתר JetBrains.

בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:

  • אם צריך, מקצים שיעורי בית.
  • להסביר לתלמידים איך להגיש מטלות.
  • בודקים את שיעורי הבית.

אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.

אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.

עונים על השאלות הבאות

שאלה 1

איזו מהאפשרויות הבאות מחזירה עותק של רשימה?

add()

remove()

reversed()

contains()

שאלה 2

איזו מהפונקציות הבאות של התוסף ב-class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) תגרום לשגיאת קומפילציה?

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

שאלה 3

באילו מהמקומות הבאים אי אפשר להגדיר קבועים באמצעות const val?

‫▢ ברמה העליונה של קובץ

‫▢ בכיתות רגילות

‫▢ באובייקטים מסוג singleton

‫▢ באובייקטים נלווים

עוברים לשיעור הבא: 5.2 Generics

סקירה כללית של הקורס, כולל קישורים ל-Codelabs אחרים, זמינה במאמר "Kotlin Bootcamp for Programmers: Welcome to the course".