מחנה קוטלין למתכננים 5.1: תוספים

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

מבוא

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

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

דברים שחשוב לדעת

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

מה תלמדו

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

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

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

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

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

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

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

  1. פותחים את ה-REPL (כלים &gt ; 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

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

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

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

  1. רשימות ורשימות שעברו שינויים נוספו בשיעור קודם. הם מבנה נתונים שימושי מאוד, ולכן קוטלין מספק מספר פונקציות מובנות לרשימות. כדאי לעיין ברשימה החלקית הזו של פונקציות לרשימות. ניתן למצוא כרטיסי מוצר מלאים בתיעוד של קוטלין עבור 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() באמצעות פונקציית למדה, כדי לסכם את האורך של כל מחרוזת. שם ברירת המחדל של ארגומנט למדה הוא 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: מנסים להשתמש במפות גיבוב (hash)

בקוטלין אפשר למפות כמעט כל דבר אחר באמצעות hashMapOf(). מפות גיבוב (hash) הן סוג של רשימה של זוגות, שבהן הערך הראשון משמש כמפתח.

  1. צור מפת גיבוב שתואמת לתסמינים, למפתחות ולמחלות של דגים, הערכים.
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. מפה ממשית מאפשרת לכם להסיר פריטים ולהסיר אותם. משמעותו של מושג היא שניתן לשנות, המשמעות של 'לא ניתן לשינוי' היא 'לא ניתן לשינוי'.

  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, ולא בכיתות רגילות. ניתן להשתמש באפשרות הזו כדי ליצור קובץ או אובייקט Singleton שמכיל רק קבועים, ולייבא אותם לפי הצורך.

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

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

קוטלין לא מגדיר את הקבועים ברמת הכיתה.

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

  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&#39.

  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

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

שלב 3: הוספת נכס תוסף

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

  1. עדיין בתהליך של REPL, יש להוסיף נכס תוסף isGreen אל AquariumPlant, שהוא true אם הצבע ירוק.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

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

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

שלב 4: הכרת מקלטים שניתן לבטל

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

  1. עדיין בתהליך של REPL, יש להגדיר שיטת pull() שמקבלת מקלט שניתן לביטול. זו נקודה עם סימן שאלה ? אחרי הסוג, לפני הנקודה. בתוך הגוף, אפשר לבדוק אם this אינו null באמצעות
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. אפשר למקם אותם ברמה העליונה, לארגן אותן באובייקט יחיד או למקם אותן באובייקט נלווה.
  • אובייקט נלווה הוא אובייקט Singleton בתוך הגדרת כיתה, המוגדר באמצעות מילת המפתח companion.
  • פונקציות ונכסים של תוספים יכולים להוסיף פונקציונליות לכיתה. למשל:
    fun String.hasSpaces() = find { it == ' ' } != null
  • מקלט שניתן לביטול מאפשר ליצור תוספים בכיתה (null). ניתן להתאים את האופרטור ?. עם apply כדי לבדוק את הפונקציה null לפני הפעלת הקוד. למשל:
    this?.apply { println("removing $this") }

התיעוד של Kotlin

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

מדריכים בקוטלין

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

קורס של Udacity

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

IntelliJ IDEA

ניתן למצוא תיעוד עבור IntelliJ IDEA באתר JetBrains.

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

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

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

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

מענה על השאלות האלה

שאלה 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?

הוספה ברמה העליונה של הקובץ

▬ בכיתות הרגילות

הוספה עם אובייקט יחיד

▸באובייקטים נלווים

המשך לשיעור הבא: 5.2 גנרי

סקירה כללית של הקורס, כולל קישורים למעבדות קוד אחרות, זמינה במאמר "Kotlin Bootcamp for Programmers: ברוך הבא לקורס."