פונקציות מוטבעות בקוטלין

1. סקירה כללית

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

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

2. צרות בגן עדן

2.1. תקורה של למבדות בקוטלין

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

כדי לחקור את הצד האפל של lambdas, בואו להמציא את הגלגל מחדש על ידי הכרזת פונקציה הרחבה לְסַנֵן אוספים:

Collection.filter מהנה (פרדיקט: (T) -> בוליאני): אוסף = // הושמט

עכשיו, בואו נראה איך הפונקציה שלמעלה מתכנסת ל- Java. התמקדו ב לְבַסֵס פונקציה שעוברת כפרמטר:

מסנן אוסף סופי סטטי ציבורי (Collection, kotlin.jvm.functions.Function1);

שימו לב איך לְבַסֵס מטופל באמצעות פונקציה 1 מִמְשָׁק?

עכשיו, אם נקרא לזה בקוטלין:

sampleCollection.filter {it == 1}

משהו דומה לדברים הבאים יופק בכדי לעטוף את קוד למבדה:

פילטר (sampleCollection, פונקציה 1 חדשה) {@Override הפעלה ציבורית בוליאנית (פרמטר שלם) {return param == 1;}});

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

מדוע קוטלין עושה זאת במקום, נניח, באמצעות ייעוד דינמי כמו איך Java 8 עושה עם lambdas? במילים פשוטות, Kotlin הולך על תאימות Java 6, וכן ייעוד דינמי לא זמין עד Java 7.

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

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

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

  1. לפחות מופע אחד מסוג מיוחד נוצר ומאוחסן בערימה
  2. שיחת שיטה נוספת תמיד תתרחש

הקצאת מופע אחת ועוד שיחת שיטה וירטואלית אחת לא נראה כל כך רע, נכון?

2.2. סגירות

כפי שראינו קודם, כאשר אנו מעבירים למבדה לפונקציה, ייווצר מופע מסוג פונקציה, בדומה ל שיעורים פנימיים אנונימיים בג'אווה.

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

הקצאות הזיכרון הנוספות מחמירות עוד יותר כאשר למבדה לוכדת משתנה: ה- JVM יוצר מופע מסוג פונקציה בכל קריאה. עבור לאמבות שאינן לוכדות, תהיה מופע אחד בלבד, א קְלָף בּוֹדֵד, מאותם סוגי פונקציות.

איך אנחנו כל כך בטוחים בזה? בואו נמציא גלגל נוסף על ידי הכרזת פונקציה להחלת פונקציה על כל רכיב אוסף:

Collection.each (block: (T) -> Unit) {for (e in this) block (e)} מהנה

כמה שזה נשמע טיפשי, כאן נכפיל כל אלמנט אוסף במספר אקראי:

כיף ראשי () {val numbers = listOf (1, 2, 3, 4, 5) val random = אקראי () numbers.each {println (אקראי * it)} // לכידת המשתנה האקראי}

ואם ניקח הצצה לתוך קוד הביץ באמצעות javap:

>> javap -c MainKt public class class MainKt {public static final void main (); קוד: // הושמט 51: חדש # 29 // class MainKt $ main $ 1 54: dup 55: fload_1 56: invokespecial # 33 // שיטה MainKt $ main $ 1. "" :( F) V 59: checkcast # 35 // מחלקה kotlin / jvm / פונקציות / Function1 62: invokestatic # 41 // אוספי שיטות Kt.each: (Ljava / util / Collection; Lkotlin / jvm / functions / Function1;) V 65: return

אז נוכל לזהות ממדד 51 ש- JVM יוצר מופע חדש של MainKt $ main $מעמד פנימי אחד לכל קריאה. כמו כן, אינדקס 56 מראה כיצד קוטלין לוכד את המשתנה האקראי. המשמעות היא שכל משתנה שנתפס יועבר כטיעוני קונסטרוקטור, וכך ייווצר תקרת זיכרון.

2.3. הקלד מחיקה

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

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

3. פונקציות מוטבעות

3.1. הסרת תקורה של למבדות

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

האם עלינו לבחור בין הפשטה ליעילות?

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

inline fun Collection.each (חסום: (T) -> יחידה) {עבור (ה בזה) בלוק (ה)}

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

לדוגמה, המהדר מתרגם:

val numbers = listOf (1, 2, 3, 4, 5) numbers.each {println (it)}

למשהו כמו:

val numbers = listOf (1, 2, 3, 4, 5) עבור (מספר במספרים) println (מספר)

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

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

3.2. ללא מוטבע

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

ino fun foo (מוטבע: () -> יחידה, noinline לא מוטבע: () -> יחידה) {...}

3.3. אימות מוטבע

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

כל שעלינו לעשות הוא לסמן את פרמטר הסוג באמצעות ה- אישר מילת מפתח:

כיף מוטבע Any.isA (): בוליאני = זה T

לְלֹא בשורה ו אישר, ה הוא הפונקציה לא תיאסף, כפי שמסביר ביסודיות במאמר Kotlin Generics שלנו.

3.4. החזרות לא מקומיות

בקוטלין, אנחנו יכולים להשתמש ב- לַחֲזוֹר ביטוי (ידוע גם כלא מוסמך לַחֲזוֹר) רק כדי לצאת מפונקציה בשם או מפי אנונימי:

fun calledFunction (): Int {return 42} fun anonymous (): () -> Int {// function anonymous return return (): Int {return 42}}

בשתי הדוגמאות, ה- לַחֲזוֹר הביטוי תקף מכיוון שהפונקציות נקראות או אנונימיות.

למרות זאת, אנחנו לא יכולים להשתמש בלא מוסמך לַחֲזוֹר ביטויים ליציאה מביטוי למבדה. כדי להבין זאת טוב יותר, בואו נמציא עוד גלגל נוסף:

List.eachIndexed מהנה (f: (Int, T) -> יחידה) {עבור (i במדדים) {f (i, זה [i])}}

פונקציה זו מבצעת את גוש הקוד הנתון (פונקציה f) על כל אלמנט, ומספק את האינדקס הרציף עם האלמנט. בואו נשתמש בפונקציה זו כדי לכתוב פונקציה אחרת:

כיף List.indexOf (x: T): Int {eachIndexed {index, value -> if (value == x) {return index}} return -1}

פונקציה זו אמורה לחפש ברכיב הנתון ברשימה המקבלת ולהחזיר את האינדקס של האלמנט שנמצא או -1. למרות זאת, מכיוון שאנחנו לא יכולים לצאת ממבדה עם לא מוסמך לַחֲזוֹר ביטויים, הפונקציה אפילו לא תיאסף:

קוטלין: 'להחזיר' אסור כאן

כפתרון למגבלה זו, אנו יכולים בשורה ה כל אינדקס פוּנקצִיָה:

כיף מוטבע List.eachIndexed (f: (Int, T) -> יחידה) {עבור (i במדדים) {f (i, זה [i])}}

אז אנחנו יכולים להשתמש ב- אינדקס של פוּנקצִיָה:

val נמצא = numbers.indexOf (5)

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

4. מגבלות

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

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

כיף מוטבע CharSequence.replace (regex: Regex, transform noinline: (MatchResult) -> CharSequence): String = regex.replace (this, transform) // עובר לפונקציה רגילה

הקטע שלמעלה עובר את הלמדה, שינוי צורה, לתפקוד רגיל, החלףומכאן noinline.

5. סיכום

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

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

כרגיל, כל הדוגמאות זמינות ב- GitHub.


$config[zx-auto] not found$config[zx-overlay] not found