ממשקי API לא חוסמים מבקשים לבצע עבודה, ואז מעבירים את השליטה בחזרה לשרשור הקורא כדי שיוכל לבצע משימות אחרות לפני השלמת הפעולה המבוקשת. ממשקי ה-API האלה שימושיים במקרים שבהם העבודה המבוקשת עשויה להיות מתמשכת, או במקרים שבהם צריך להמתין להשלמת פעולות הקלט/פלט או תקשורת ה-IPC, לזמינות של משאבי מערכת שנמצאים במאבק תחרותי או לקלט של משתמש לפני שאפשר להמשיך בעבודה. ממשקי API במיוחד שתוכננו היטב מספקים דרך לבטל את הפעולה המתבצעת ולהפסיק את העבודה בשם מבצע הקריאה המקורי, כדי לשמור על תקינות המערכת ועל חיי הסוללה כשאין יותר צורך בפעולה.
ממשקי API אסינכרוניים הם אחת מהדרכים להשגת התנהגות ללא חסימה. ממשקי API אסינכרונים מקבלים סוג כלשהו של המשך או קריאה חוזרת (callback) שמקבלת הודעה כשהפעולה מסתיימת, או על אירועים אחרים במהלך הפעולה.
יש שתי סיבות עיקריות לכתיבה של ממשק API אסינכרוני:
- ביצוע כמה פעולות בו-זמנית, כאשר צריך להתחיל את הפעולה ה-N לפני שהפעולה ה-N-1 מסתיימת.
- הימנעות מחסימה של שרשור קריאה עד לסיום הפעולה.
ב-Kotlin מומלץ מאוד להשתמש בביצוע בו-זמנית (concurrency) מובנה, סדרה של עקרונות ו-API שנוצרו על סמך פונקציות השהיה (suspend) שמפרקות את הקשר בין ביצוע קוד סינכרוני ואסינכרוני לבין התנהגות חסימה של חוטים. פונקציות ההשהיה הן לא חוסמות וסינכרוניות.
השעיית פונקציות:
- לא לחסום את שרשור הקריאה שלהם, אלא להעביר את שרשור הביצוע שלהם כפרט בהטמעה בזמן שממתינים לתוצאות של פעולות שמבוצעות במקום אחר.
- ביצוע סינכרוני, בלי לחייב את מבצע הקריאה ל-API ללא נעילה להמשיך את הביצוע בו-זמנית עם עבודה ללא נעילה שהתקבלה מהקריאה ל-API.
בדף הזה מפורטות ציפיות בסיסיות למפתחים שאפשר לעמוד בהן בבטחה כשעובדים עם ממשקי API אסינכררוניים ולא חוסמים. לאחר מכן מפורטות כמה שיטות ליצירת ממשקי API שעומדים בציפיות האלה בשפות Kotlin או Java, בפלטפורמת Android או בספריות Jetpack. אם יש לכם ספק, תוכלו להתייחס לציפיות של המפתחים כדרישה לכל ממשק API חדש.
הציפיות של מפתחים לממשקי API אסינכררוניים
הציפיות הבאות נכתבות מנקודת המבט של ממשקי API שלא מושהים, אלא אם צוין אחרת.
ממשקי API שמקבלים קריאות חזרה (callbacks) הם בדרך כלל אסינכרונים.
אם ממשק API מקבל קריאה חוזרת (callback) שלא מתועדת כקריאה שתתבצע רק במקום (כלומר, קריאה שתתבצע רק על ידי שרשור הקריאה לפני שהקריאה ל-API עצמה תוחזר), ההנחה היא שה-API הוא אסינכרוני, והוא צריך לעמוד בכל הציפיות האחרות המתועדות בקטעים הבאים.
דוגמה לקריאה חוזרת שנקראת תמיד במקום היא פונקציית מיפוי או סינון ברמה גבוהה יותר שמפעילה מיפוי או תנאי לכל פריט באוסף לפני שהיא חוזרת.
ממשקי API אסינכרונים צריכים לחזור בהקדם האפשרי
מפתחים מצפים שממשקי API אסינכררוניים יהיו לא חוסמים ויחזרו במהירות אחרי שליחת הבקשה לביצוע הפעולה. תמיד צריך להיות בטוח לקרוא ל-API אסינכרוני, ואף פעם לא אמורה להיות תוצאה של קריאה ל-API אסינכרוני בפריימים לא חלקים או ב-ANR.
הפלטפורמה או הספריות יכולות להפעיל הרבה פעולות ואותות מחזור חיים על פי דרישה, ואי אפשר לצפות ממפתח שיהיה לו ידע גלובלי לגבי כל אתרי הקריאה הפוטנציאליים לקוד שלו. לדוגמה, אפשר להוסיף Fragment
ל-FragmentManager
בעסקה סינכרונית בתגובה למדידת View
ולפריסה שלה, כשצריך לאכלס את תוכן האפליקציה כדי למלא את המרחב הזמין (למשל RecyclerView
). LifecycleObserver
שמגיב לקריאה החוזרת של מחזור החיים onStart
של החלק הזה עשוי לבצע כאן פעולות הפעלה חד-פעמיות, ויכול להיות שהן יהיו על נתיב קוד קריטי ליצירת פריים של אנימציה ללא תנודות. מפתחים תמיד יכולים להיות בטוחים שהפעלת כל API אסינכרוני בתגובה לקריאות חזרה מסוגים כאלה של מחזור חיים לא תגרום לפריימים לא חלקים.
המשמעות היא שהעבודה שמתבצעת על ידי API אסינכררוני לפני החזרה צריכה להיות קלה מאוד. לכל היותר, צריך ליצור תיעוד של הבקשה והקריאה החוזרת המשויכת, ולרשום אותה במנוע הביצוע שמבצע את העבודה. אם ההרשמה לפעולה אסינכררונית דורשת IPC, ההטמעה של ה-API צריכה לכלול את כל האמצעים הנדרשים כדי לעמוד בציפייה הזו של המפתחים. הבקשה יכולה לכלול אחד או יותר מהפרטים הבאים:
- הטמעת IPC בסיסי כקריאה חד-כיוונית של Binder
- ביצוע קריאה דו-כיוונית של מקשר לשרת המערכת, שבה השלמת ההרשמה לא מחייבת נעילה תחת תחרות גבוהה
- פרסום הבקשה לשרשור עבודה בתהליך האפליקציה כדי לבצע רישום חסימה דרך IPC
ממשקי API אסינכררוניים צריכים להחזיר void ולהשליך רק במקרה של ארגומנטים לא חוקיים
ממשקי API אסינכררוניים צריכים לדווח על כל התוצאות של הפעולה המבוקשת ל-callback שסופק. כך המפתח יכול להטמיע נתיב קוד יחיד לטיפול בהצלחה ובשגיאות.
ממשקי API אסינכררוניים עשויים לבדוק אם הארגומנטים הם null ולהשליף את השגיאה NullPointerException
, או לבדוק אם הארגומנטים שסופקו נמצאים בטווח תקין ולהשליף את השגיאה IllegalArgumentException
. לדוגמה, פונקציה שמקבלת float
בטווח 0
עד 1f
עשויה לבדוק שהפרמטר נמצא בטווח הזה ולהשליף את הערך IllegalArgumentException
אם הוא מחוץ לטווח. לחלופין, עשויה להתבצע בדיקה של String
קצר כדי לוודא שהוא תואם לפורמט תקין, כמו אותיות וסימנים אלפאנומריים בלבד. (חשוב לזכור ששרת המערכת אף פעם לא צריך לבטוח בתהליך האפליקציה. כל שירות מערכת צריך לחזור על הבדיקות האלה בשירות המערכת עצמו).
כל שגיאה אחרת צריכה להיות מדווחת בקריאה החוזרת (callback) שסיפקתם. בין היתר, אסור:
- הכישלון הסופי של הפעולה המבוקשת
- חריגות אבטחה לגבי הרשאות או הסמכות חסרות שנדרשות לביצוע הפעולה
- חרגת מהמכסה לביצוע הפעולה
- תהליך האפליקציה לא מספיק 'בחזית' כדי לבצע את הפעולה
- החומרה הנדרשת נותקה
- כשלים ברשת
- חסימות זמניות
- מוות של Binder או תהליך מרוחק לא זמין
ממשקי API אסינכרוניים צריכים לספק מנגנון ביטול
ממשקי API אסינכררוניים צריכים לספק דרך לציין לפעולה שפועלת שהמבצע כבר לא מעוניין בתוצאה. פעולת הביטול הזו אמורה לסמן שני דברים:
צריך לשחרר הפניות קשות לקריאות חוזרות (callbacks) שסופקו על ידי מבצע הקריאה החוזרת
קריאות חזרה (callbacks) שסופקו לממשקי API אסינכררוניים עשויות להכיל הפניות קבועות (hard reference) לתרשים אובייקטים גדול, ועבודה מתמשכת שמכילה הפניה קבועה לאותה קריאה חוזרת יכולה למנוע את האיסוף של תרשים האובייקטים הזה. אם משחררים את ההפניות ל-callback בזמן הביטול, ייתכן שגרפי האובייקטים האלה יהיו כשירים לאיסוף אשפה הרבה יותר מוקדם מאשר אם העבודה הייתה מתבצעת עד הסוף.
מנוע הביצוע שמבצע עבודה עבור מבצע הקריאה עשוי להפסיק את העבודה הזו
עבודה שמתחילה בקריאות אסינכררוניות ל-API עשויה להיות כרוכה בעלות גבוהה של צריכת חשמל או משאבים אחרים במערכת. ממשקי API שמאפשרים למבצעי הקריאה לסמן מתי כבר אין צורך בעבודה הזו מאפשרים להפסיק את העבודה הזו לפני שהיא תמשיך לצרוך משאבי מערכת נוספים.
שיקולים מיוחדים לגבי אפליקציות שנשמרו במטמון או קפואות
כשאתם מתכננים ממשקי API אסינכררוניים שבהם קריאות חזרה מקורן בתהליך מערכת ומועברות לאפליקציות, כדאי לשקול את הדברים הבאים:
- תהליכים ומחזור החיים של האפליקציה: יכול להיות שתהליך האפליקציה המקבלת נמצא במצב שמאוחסן במטמון.
- הקפאת אפליקציות שנשמרו במטמון: יכול להיות שתהליך האפליקציה של הנמען קפוא.
כשתהליך של אפליקציה נכנס למצב שמאוחסן במטמון, המשמעות היא שהוא לא מארח באופן פעיל רכיבים שגלויים למשתמשים, כמו פעילויות ושירותים. האפליקציה נשארת בזיכרון למקרה שהיא תהיה שוב גלויה למשתמשים, אבל בינתיים היא לא אמורה לבצע פעולות. ברוב המקרים, מומלץ להשהות את שליחת הקריאות החוזרות (callbacks) של האפליקציה כשהיא נכנסת למצב שמאוחסן במטמון, ולהמשיך את השליחה כשהיא יוצאת מהמצב הזה, כדי לא לגרום לעבודה בתהליכים של האפליקציה שמאוחסנים במטמון.
אפליקציה שנשמרה במטמון עשויה גם היא להיות קפואה. כשאפליקציה קפואה, היא לא מקבלת זמן מעבד בכלל ולא יכולה לבצע שום פעולה. כל הקריאות להודעות החזרה (callbacks) הרשמות של האפליקציה הזו נשמרות במאגר ונשלחות כשהאפליקציה מפשירה.
ייתכן שטרנזקציות שנשמרו במאגר לקריאות חזרה (callbacks) של האפליקציה יהיו לא עדכניות עד שהאפליקציה תופשר ותטפל בהן. מאגר הזיכרון הוא מוגבל, ואם הוא יתמלא, האפליקציה המקבלת תקרוס. כדי למנוע עומס על האפליקציות עם אירועים לא עדכניים או כדי למנוע הצפה של מאגרי הנתונים שלהן, אל תשלחו קריאות חזרה (callbacks) לאפליקציות בזמן שהתהליך שלהן קפוא.
בבדיקה:
- מומלץ להשהות את שליחת הקריאות החוזרות של האפליקציה בזמן שהתהליך של האפליקציה מאוחסן במטמון.
- חובה להשהות את שליחת הקריאות החוזרות של האפליקציה בזמן שהתהליך של האפליקציה קפוא.
מעקב אחר סטטוס
כדי לעקוב אחרי הרגעים שבהם אפליקציות נכנסות למצב ששמור במטמון או יוצאות ממנו:
mActivityManager.addOnUidImportanceListener(
new UidImportanceListener() { ... },
IMPORTANCE_CACHED);
כדי לעקוב אחרי הפעמים שבהן אפליקציות קופאות או מפשירות:
IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);
אסטרטגיות להמשך שליחת קריאות חזרה לאפליקציה
בין אם משהים את שליחת הקריאות החוזרות (callbacks) של האפליקציה כשהיא נכנסת למצב ששמור במטמון או למצב קפוא, כשהיא יוצאת מהמצב הרלוונטי צריך להמשיך לשלוח את הקריאות החוזרות הרשויות של האפליקציה עד שהאפליקציה תבטל את הרישום של הקריאה החוזרת או שתהליך האפליקציה יסתיים.
לדוגמה:
IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
shouldSendCallbacks = false;
} else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
shouldSendCallbacks = true;
}
});
לחלופין, אפשר להשתמש ב-RemoteCallbackList
, שמבטיח שלא יישלחו קריאות חזרה לתהליך היעד כשהוא קפוא.
לדוגמה:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));
callback.foo()
מופעלת רק אם התהליך לא קפוא.
אפליקציות שומרות לרוב עדכונים שקיבלו באמצעות קריאות חזרה (callbacks) כתמונה מיידית של המצב האחרון. נניח ש-API היפותטי לאפליקציות עוקב אחרי אחוז הטעינה שנותר בסוללה:
interface BatteryListener {
void onBatteryPercentageChanged(int newPercentage);
}
ניקח לדוגמה תרחיש שבו מתרחשים כמה אירועים של שינוי מצב כשאפליקציה קפואה. כשהאפליקציה תופשר, צריך להעביר אליה רק את המצב העדכני ביותר ולבטל שינויים אחרים במצב שהם לא עדכניים. ההעברה הזו אמורה להתבצע באופן מיידי אחרי שהאפליקציה תופשר, כדי שהיא תוכל 'לסגור את הפער'. אפשר לעשות זאת באופן הבא:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));
במקרים מסוימים, אפשר לעקוב אחרי הערך האחרון שנשלח לאפליקציה כדי שלא תצטרכו לשלוח לאפליקציה הודעה על אותו ערך אחרי שתבטלו את ההקפאה.
המצב יכול להתבטא כנתונים מורכבים יותר. נניח שיש ממשק API היפותטי שמאפשר לאפליקציות לקבל התראות על ממשקי רשת:
interface NetworkListener {
void onAvailable(Network network);
void onLost(Network network);
void onChanged(Network network);
}
כשמשהים התראות מאפליקציה, חשוב לזכור את קבוצת הערוצים והמצבים שהאפליקציה ראתה בפעם האחרונה. כשממשיכים, מומלץ להודיע לאפליקציה על רשתות ישנות שאבדו, על רשתות חדשות שהפכו לזמינות ועל רשתות קיימות שהמצב שלהן השתנה – לפי הסדר הזה.
לא להודיע לאפליקציה על רשתות שהפכו לזמינות ואז נעלמו בזמן שהקריאות החוזרות הושהו. אפליקציות לא צריכות לקבל דיווח מלא על אירועים שהתרחשו בזמן שהן היו קפואות, ותיעוד ה-API לא צריך להבטיח העברה ללא הפסקה של שידורי אירועים מחוץ למצבים מפורשים של מחזור חיים. בדוגמה הזו, אם האפליקציה צריכה לעקוב באופן רציף אחרי זמינות הרשת, היא צריכה להישאר במצב מחזור חיים שימנע ממנה להישמר במטמון או להקפיא.
לסיכום, צריך לאחד אירועים שהתרחשו אחרי ההשהיה ולפני שחידוש ההתראות, ולשלוח את המצב העדכני בקצרה ל-callbacks הרשומים של האפליקציה.
שיקולים לגבי תיעוד למפתחים
יכול להיות שיהיו עיכובים בשליחת אירועים אסינכררוניים, כי השולח השהה את השליחה לתקופה מסוימת כפי שמתואר בקטע הקודם, או כי לאפליקציה המקבלת לא הועברו מספיק משאבי המכשיר כדי לעבד את האירוע בזמן.
מומלץ להניא מפתחים מלעשות הנחות לגבי הזמן שחולף בין קבלת ההתראה על האירוע באפליקציה לבין הזמן שבו האירוע התרחש בפועל.
מה מצפים המפתחים כשממשקי API מושהים
מפתחים שמכירים את התכנות בו-זמנית המובנית של Kotlin מצפים להתנהגויות הבאות מכל ממשק API עם השהיה:
פונקציות מושהות צריכות להשלים את כל העבודה המשויכת לפני שהן מחזירות ערכים או גורמות להשלכה
התוצאות של פעולות לא חסימות מוחזרות כערכים רגילים של פונקציות, והדיווח על שגיאות מתבצע באמצעות השלכת חריגות. (בדרך כלל, המשמעות היא שלא צריך פרמטרים של קריאה חוזרת).
פונקציות השהיה צריכות להפעיל פרמטרים של קריאה חוזרת רק במקום
פונקציות השהיה תמיד צריכות להשלים את כל העבודה המשויכת לפני שהן חוזרות, ולכן אסור להפעיל בהן פונקציית קריאה חוזרת או פרמטר אחר של פונקציה, או לשמור הפניה אליה אחרי שהפונקציה הושהתה.
פונקציות השהיה שמקבלות פרמטרים של קריאה חוזרת צריכות לשמור על ההקשר, אלא אם צוין אחרת במסמכים
קריאה לפונקציה בפונקציית השהיה גורמת לה לפעול ב-CoroutineContext
של מבצע הקריאה. מאחר שפונקציות ההשהיה צריכות להשלים את כל העבודה המשויכת לפני שהן מחזירות ערכים או גורמות להשלכה, וצריכות להפעיל את הפרמטרים של פונקציית ה-callback רק במקום, ההנחה שמוגדרת כברירת מחדל היא שכל פונקציית ה-callback כזו גם פועלת ב-CoroutineContext
הקורא באמצעות המפנה המשויך שלו. אם מטרת ה-API היא להריץ קריאה חוזרת (callback) מחוץ ל-CoroutineContext
הקורא, צריך לתעד את ההתנהגות הזו בצורה ברורה.
פונקציות ההשהיה צריכות לתמוך בביטול משימות של kotlinx.coroutines
כל פונקציית השהיה שמוצעת צריכה לפעול בשיתוף עם ביטול המשימה כפי שמוגדר ב-kotlinx.coroutines
. אם המשימה של הקריאה של פעולה מתמשכת מבוטלת, הפונקציה צריכה להמשיך עם CancellationException
בהקדם האפשרי כדי שהמבצע יוכל לנקות ולהמשיך בהקדם האפשרי. הטיפול בכך מתבצע באופן אוטומטי על ידי suspendCancellableCoroutine
ועל ידי ממשקי API אחרים להשעיה ש-kotlinx.coroutines
מציעה. בדרך כלל, לא מומלץ להשתמש ב-suspendCoroutine
ישירות בהטמעות של ספריות, כי הוא לא תומך בהתנהגות הביטול הזו כברירת מחדל.
פונקציות השהיה שמבצעות עבודה חוסמת ברקע (לא בשרשור הראשי או בשרשור של ממשק המשתמש) חייבות לספק דרך להגדיר את המפנה שבו נעשה שימוש
לא מומלץ לגרום לפונקציה חוסמת להשהות לחלוטין כדי לעבור בין שרשורים.
קריאה לפונקציית השהיה לא אמורה לגרום ליצירה של עוד חוטים בלי לאפשר למפתח לספק את החוט או מאגר החוטים שלו כדי לבצע את העבודה הזו. לדוגמה, ב-constructor יכול להיות שיקבלו CoroutineContext
שמשמש לביצוע עבודה ברקע בשביל השיטות של הכיתה.
אם אתם משתמשים בפונקציות השהיה שמקבלות פרמטר CoroutineContext
או Dispatcher
אופציונלי רק כדי לעבור למפזר הבקשות הזה ולבצע עבודה חוסמת, כדאי לחשוף במקום זאת את פונקציית החסימה הבסיסית ולהמליץ למפתחים שמבצעים את הקריאה להשתמש בקריאה משלהם ל-withContext כדי להפנות את העבודה למפזר הבקשות שנבחר.
כיתות שמפעילות פונקציות קורוטין
כדי לבצע את פעולות ההפעלה האלה, למחלקות שמפעילות קורוטינים צריך להיות CoroutineScope
. כדי לפעול בהתאם לעקרונות של בו-זמניות מובנית, צריך להשתמש בדפוסים המבניים הבאים כדי לקבל את ההיקף הזה ולנהל אותו.
לפני שכותבים כיתה שמפעילה משימות בו-זמנית בהיקף אחר, כדאי לשקול דפוסים חלופיים:
class MyClass {
private val requests = Channel<MyRequest>(Channel.UNLIMITED)
suspend fun handleRequests() {
coroutineScope {
for (request in requests) {
// Allow requests to be processed concurrently;
// alternatively, omit the [launch] and outer [coroutineScope]
// to process requests serially
launch {
processRequest(request)
}
}
}
}
fun submitRequest(request: MyRequest) {
requests.trySend(request).getOrThrow()
}
}
חשיפת suspend fun
לביצוע עבודה בו-זמנית מאפשרת למבצע הקריאה להפעיל את הפעולה בהקשר שלו, וכך אין צורך ב-MyClass
שמנהל CoroutineScope
. קל יותר לסדר את העיבוד של הבקשות בסדרת רצפים, ולרוב המצב יכול להתקיים כמשתנים מקומיים של handleRequests
במקום כמאפייני כיתה, שבמקרה אחר ידרשו סנכרון נוסף.
בכיתות שמנהלות קורוטינים צריך לחשוף את השיטות close ו-cancel
כיתות שמפעילות קורוטינים כפרטי הטמעה חייבות לספק דרך לסגור בצורה מסודרת את המשימות המתמשכות והמקבילות האלה, כדי שהן לא יגרמו לדליפת עבודה בו-זמנית לא מבוקרת להיקף הורה. בדרך כלל, היצירה מתבצעת על ידי יצירת צאצא Job
של CoroutineContext
שסופק:
private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)
fun cancel() {
myJob.cancel()
}
אפשר גם לספק שיטה join()
כדי לאפשר לקוד המשתמש להמתין להשלמת כל עבודה בו-זמנית שמתבצעת על ידי האובייקט.
(הפעולות האלה עשויות לכלול עבודות ניקוי שמבוצעות על ידי ביטול פעולה).
suspend fun join() {
myJob.join()
}
מתן שמות לפעולות בטרמינל
השם של השיטות שמאפשרות לסגור בצורה נקייה משימות בו-זמניות שנמצאות בבעלות של אובייקט מסוים ועדיין נמצאות בתהליך, צריך לשקף את האופן שבו הן מבצעות את הסגירה:
משתמשים ב-close()
כשפעולות מתמשכות עשויות להסתיים, אבל לא ניתן להתחיל פעולות חדשות אחרי שהקריאה ל-close()
חוזרת.
משתמשים ב-cancel()
כשפעולות מתמשכות עשויות להתבטל לפני שהן מסתיימות.
אסור להתחיל פעולות חדשות אחרי שהקריאה ל-cancel()
חוזרת.
ב-constructors של הכיתות אפשר להשתמש ב-CoroutineContext, ולא ב-CoroutineScope
כשאסור להפעיל אובייקטים ישירות בהיקף הורה שסופק, היתרון של CoroutineScope
כפרמטר של קונסטרוקטור מתבטל:
// Don't do this
class MyClass(scope: CoroutineScope) {
private val myJob = Job(parent = scope.`CoroutineContext`[Job])
private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)
// ... the [scope] constructor parameter is never used again
}
CoroutineScope
הופך למעטפת מיותרת ומטעה, שבמקרים מסוימים של תרחישים לדוגמה יכולה להיווצר רק כדי להעביר כפרמטר של קונסטרוקטור, ולאחר מכן להיזרק:
// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))
ברירת המחדל של הפרמטרים של CoroutineContext היא EmptyCoroutineContext
כשפרמטר CoroutineContext
אופציונלי מופיע בממשק API, ערך ברירת המחדל חייב להיות Empty`CoroutineContext`
sentinel. כך אפשר ליצור שילובים טובים יותר של התנהגויות API, כי הערך Empty`CoroutineContext`
של מבצע הקריאה מטופל באותו אופן כמו קבלת ברירת המחדל:
class MyOuterClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val innerObject = MyInnerClass(`CoroutineContext`)
// ...
}
class MyInnerClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val job = Job(parent = `CoroutineContext`[Job])
private val scope = CoroutineScope(`CoroutineContext` + job)
// ...
}