יש אובייקטים שמחזיקים משאבים שאוסף הזבל לא מנהל: קבצים פתוחים, sockets ברשת, חיבורים למסד נתונים, ידיות של מערכת ההפעלה. הם מממשים IDisposable, ומשחררים את המשאב בקריאה ל-Dispose(). פקודת using קוראת לה בשבילכם בנקודה קבועה, גם אם נזרקת חריגה.
העמוד הזה עוסק בפקודת using. השורות using System; בראש קובץ הן תכונה אחרת, הנחיית using, שמייבאת מרחב שמות.
בלוק using
using (declaration) { body } יוצרת את האובייקט, מריצה את הגוף, ואז קוראת ל-Dispose. מחלקה קטנה שמדפיסה מתוך Dispose מראה בדיוק מתי זה קורה:
פלט:
open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled
הבלוק השני זורק, ו-"close api" עדיין מודפס, לפני שה-catch מטפל בחריגה. ההבטחה הזו היא הסיבה להשתמש ב-using במקום לקרוא ל-Dispose() בעצמכם בשורה האחרונה: קריאה ידנית מדולגת על ידי כל חריגה שנזרקת לפניה.
למה using מתקמפלת
הקומפיילר פורש בלוק using ל-try/finally. שתי הצורות האלה שקולות:
using (var writer = new StreamWriter("log.txt"))
{
writer.WriteLine("started");
}
// is compiled as:
{
var writer = new StreamWriter("log.txt");
try
{
writer.WriteLine("started");
}
finally
{
if (writer != null) ((IDisposable)writer).Dispose();
}
}
שלושה פרטים נובעים מהפרישה הזו. המשתנה מוגבל לטווח של הבלוק והוא לקריאה בלבד בתוכו (אי אפשר להשים מחדש ל-writer). האובייקט חייב לממש IDisposable, אחרת הקוד לא מתקמפל. וערך null מותר: Dispose פשוט מדולג, וזה שימושי כשפונקציית factory עלולה לא להחזיר כלום.
קבצים וזרמים
מחלקות של קבצים וזרמים הן המקרה היומיומי. StreamWriter שומר טקסט בחוצץ בזיכרון, ו-Dispose כותב את החוצץ לדיסק וסוגר את ידית הקובץ:
פלט:
id,total
1001,59.90
1002,12.50
בלי Dispose, השורות האחרונות של ה-writer עלולות עדיין לשבת בחוצץ שלו, וב-Windows הידית הפתוחה מונעת מתוכניות אחרות (ומה-File.Delete הבא שלכם) לגעת בקובץ. מתודות עזר כמו File.WriteAllText ו-File.ReadAllLines פותחות ומשחררות בעצמן, כך שהן לא צריכות using. ראו files להרחבה.
כמה משאבים: סדר השחרור
ערמו פקודות using בלי סוגריים מסולסלים ביניהן כדי לפתוח כמה משאבים בבלוק אחד. הם משתחררים בסדר הפוך לסדר היצירה:
פלט:
acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a
סדר הפוך הוא מה שמשאבים בשכבות צריכים: writer עוטף buffer שעוטף קובץ, כך שה-writer חייב לרוקן את עצמו לתוך ה-buffer לפני שה-buffer כותב לקובץ, ולפני שהקובץ נסגר.
הצהרת ה-using (C# 8)
C# 8 הוסיפה צורה בלי סוגריים מסולסלים. המשתנה משתחרר כשהטווח העוטף מסתיים, בדרך כלל בסוף המתודה:
static void ExportReport(string path, IEnumerable<string> rows)
{
using var writer = new StreamWriter(path);
writer.WriteLine("Report");
foreach (string row in rows)
{
writer.WriteLine(row);
}
} // writer.Dispose() runs here, at the end of the method
היא חוסכת רמת הזחה אחת, וזה מצטבר כשמתודה משתמשת בשניים או שלושה משאבים. כלל סדר השחרור זהה: הצהרות באותו טווח משתחררות בסדר הפוך. המחיר הוא שהמשאב נשאר פתוח עד שהטווח מסתיים, כך שכשקובץ צריך להיסגר לפני שהמתודה עושה עוד עבודה, השאירו את צורת הבלוק או שימו את ההצהרה בבלוק { } משלה.
לאובייקטים שהניקוי שלהם אסינכרוני (IAsyncDisposable, כמו הרבה חיבורים למסדי נתונים וזרמים), ב-C# 8 יש גם await using, שמבצעת await על DisposeAsync() באותו מקום.
מימוש IDisposable במחלקה שלכם
ממשו IDisposable כשהמחלקה שלכם מחזיקה בבעלותה משהו שצריך שחרור (היא יצרה זרם, טיימר, חיבור) ולכן חייבת לשחרר אותו בתורה. במקרה הנפוץ, זו מתודה קצרה:
פלט:
1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException
הכללים שהקוד הזה עוקב אחריהם: Dispose משחררת את כל מה שהאובייקט מחזיק בבעלותו; קריאה לה יותר מפעם אחת לא עושה כלום בפעם השנייה; ושימוש באובייקט אחרי השחרור זורק ObjectDisposedException. using (log) מראה גם שהבלוק יכול לקבל משתנה קיים במקום הצהרה.
"תבנית ה-dispose" הארוכה יותר, עם מתודה protected virtual void Dispose(bool disposing) ו-finalizer, נחוצה רק כשמחלקה מחזיקה ישירות ידית לא מנוהלת (מצביע גולמי מקוד native). ב-.NET מודרני, עטפו ידיות כאלה במחלקה שיורשת מ-SafeHandle במקום זאת, והגרסה הפשוטה שלמעלה היא כל מה שהמחלקה שלכם צריכה.
טעויות נפוצות
- החזרה של אובייקט שנוצר בבלוק
using. הוא משתחרר כשהבלוק מסתיים, כך שהקורא מקבל זרם סגור. החזירו את הנתונים, או תנו לקורא להחזיק את ה-using. - שכחה של
usingעל זרמים ו-writers. נתונים שבחוצץ יכולים ללכת לאיבוד וקבצים נשארים נעולים. - שחרור של אובייקט משותף. שחררו רק את מה ששייך לכם.
HttpClientארוך חיים, למשל, נועד להיות משותף ולשימוש חוזר, לא להיווצר ולהשתחרר בכל בקשה. - שימוש באובייקט אחרי הבלוק. מתודות על אובייקט משוחרר זורקות
ObjectDisposedException. - הסתמכות על אוסף הזבל. הוא משחרר זיכרון, לא קבצים או sockets, ואף פעם לא קורא ל-
Dispose.
שאלות נפוצות
מה פקודת using עושה ב-C#?
using (var x = ...) { ... } קוראת ל-x.Dispose() כשהבלוק מסתיים, בין אם הוא מסתיים כרגיל, דרך return, או בגלל שנזרקה חריגה. הקומפיילר הופך אותה ל-try/finally עם הקריאה ל-Dispose בתוך ה-finally, כך שהמשאב משוחרר בנקודה ידועה ולא מתי שאוסף הזבל ירוץ.
מה זה IDisposable ב-C#?
IDisposable הוא ממשק עם מתודה אחת, void Dispose(). מחלקה מממשת אותו כשהיא מחזיקה משהו שחייבים לשחרר במפורש: ידית לקובץ, socket ברשת, חיבור למסד נתונים, טיימר. כל אובייקט שהטיפוס שלו מממש IDisposable צריך להשתחרר כשמסיימים איתו, בדרך כלל עם using.
מה ההבדל בין using ל-using var?
using (var x = ...) { } משחררת בסוף הבלוק שלה. הצהרת ה-using using var x = ...; (C# 8 ומעלה) היא בלי בלוק משלה: היא משחררת כשהטווח העוטף מסתיים, בדרך כלל בסוף המתודה. היא חוסכת קינון כשמשאב צריך לחיות עד סוף המתודה.
באיזה סדר משתחררים כמה אובייקטים של using?
בסדר הפוך לסדר היצירה. עם using (var a = ...) using (var b = ...) { }, b משתחרר ראשון ואחריו a. זה הסדר שרוצים כש-b תלוי ב-a, כמו StreamWriter שעוטף FileStream.
האם אוסף הזבל קורא ל-Dispose?
לא. אוסף הזבל משחרר זיכרון, והוא עשוי להריץ finalizer בזמן מאוחר שאי אפשר לצפות, אבל הוא אף פעם לא קורא ל-Dispose. קובץ שלא שוחרר יכול להישאר נעול, והנתונים שבחוצץ שלו עלולים לא להיכתב לעולם. בשביל זה קיימת using.