ראינו פעם שעברה שאפשר לקבל את כל הערכים של Enum מסוים בעזרת הפונקציה Enum.GetValues.
מי שיציץ בReflector על הפונקציה הזאת יגלה מספר דברים מעניינים:
החיסרון העיקרי כאן הוא שמוחזר לנו מערך של object ואנחנו צריכים לבצע הסבה לטיפוס של הEnum שלנו, מה שגורר Boxing ויכול להשפיע על הביצועים בצורה משמעותית, במידה ואנחנו קוראים לפונקציה זו מספר רב של פעמים.
פתרון אפשרי לבעיה זו הוא ליצור מחלקה סטטית גנרית שתכיל לנו את הערכים האלה לכל Enum בצד:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class EnumValues<TEnum>
where TEnum : struct, IConvertible
{
privatestatic TEnum[] mEnumValues = InitValues();
privatestatic TEnum[] InitValues()
{
TEnum[] result = (TEnum[])Enum.GetValues(typeof(TEnum));
return result;
}
publicstatic TEnum[] GetValues()
{
TEnum[] arrayCopy = new TEnum[mEnumValues.Length];
mEnumValues.CopyTo(arrayCopy, 0);
return arrayCopy;
}
}
שימו לב שנוצרת מחלקה סטטית עבור כל סוג Enum שנכניס במשתנה הגנרי (ראו גם טיפ מספר 177).
בנוסף, שימו לב כי שמנו Constraint שTEnum הוא Value Type ומממש IConvertible, זאת מאחר ואי אפשר להכריח בConstraint שמשתנה גנרי הוא Enum. (ראו גם טיפ 29)
הדבר הזה מאפשר לנו לקבל בצורה יותר מהירה, בלי Boxing מערך שהוא Type safe של הערכים של הEnum שביקשנו.
אנחנו בעצם מחפשים את כל הרשומים לפונקציה שהם WeakEventHandler עם Method וTarget שמתאימים לזה שאנחנו קיבלנו. (אם קיבלנו Reference לTarget כלשהו, זה אומר שהTarget עדיין בחיים ולכן הWeak Reference לא היה אמור למות)
לאחר מכן אנחנו מוצאים את הראשון ומסירים את הרישום שלו מהDelegate.
אפשר כמובן להפוך את זה לExtension Method שיהיה אפשר להשתמש בו גם למקרים שבהם הפרמטר הגנרי הוא לא EventArgs אלא משהו אחר.
נראה לי שהסדרה הזאת מסתיימת כאן, אלא אם כן אני אחשוב על עוד משהו לכתוב.
אני רוצה לציין שהסדרה מבוססת על הפוסט Solving the Problem with Events: Weak Event Handlers מהבלוג DidItWithNet. יש הבדלים בין הדרכים שהצגתי לדרכים שהוא מציג שם, משום שמהבדיקות שעשיתי ראיתי שהוא לא ממש צודק, ולכן לא נכנסתי לכל מיני דברים כמו unbound delegates. אתם מוזמנים לקרוא את הפוסט הזה, הוא די טוב.
כשנרשמים לEvent שלנו אנחנו בעצם נרשמים לMember שלנו עם Weak Event מתאים בעזרת הExtension Method שרשמנו פעם שעברה. (טיפ מספר 205)
למה אנחנו מחזיקים במחלקה event שהוא private ולא סתם Delegate? הסיבה העיקרית היא שראינו שEventים ממומשים בצורה קצת שונה ממימוש סטנדרטי, במיוחד בFramework 4.0 (טיפ מספר 195).
אז השתמשנו בprivate event בשביל לקבל את היכולות האלה.
באשר לRemove, כרגע אין שם קוד שמסיר את ההרשמה לEvent. זה דבר קצת פחות טריוואלי לכתוב כי המתודה שאליה מצביע הWeak Event שונה מהמתודה שאליה מצביע הEvent שאנחנו מקבלים (הvalue).
ראינו בפעמים הקודמות כיצד ניתן להמנע מדליפות זכרון של הרשמות לEventים באמצעות מימוש של Weak Event.
אמרתי שעדיין יש בעיה – האובייקט שנרשם לאירוע אמנם כבר לא דולף, אבל הWeakEventHandlerים דולפים. הסיבה לכך היא שהEvent עדיין מחזיק Reference לWeakEventHandler ולכן הGarbage Collector לא אוסף אותו. (כמו בטיפ 201)
אז איך אפשר לפתור את הבעיה? פתרון אפשרי הוא להעביר לWeakEventHandler איזשהו Delegate שיבצע הסרת רישום מהEvent ברגע שהאובייקט מת:
source.EventRaised += new RuntimeWeakEventHandler<EventArgs>(OnEventRaised);
נכתוב ככה:
1
source.EventRaised += new RuntimeWeakEventHandler<EventArgs>(OnEventRaised, x => source.EventRaised -= x);
מה שבעצם קורה זה שברגע שהאובייקט שלנו מת, אנחנו יודעים לבטל את הרישום של הWeakEventHandler לEvent. אז מה שקורה זה שאין הצבעות לWeakEventHandler ולכן הGarbage Collector יכול לאסוף אותו.
מאחר והסינטקס לא כל כך להיט, אפשר ליצור Extension Method שיהפוך אותו לטיפה יותר יפה (ראו גם טיפים מספר 68-69):
פעם שעברה התחלנו ליצור מחלקה שתייצג את הWeak-event שלנו.
מה שאנחנו רוצים לעשות זה שהReference של האובייקט שהמתודה שייכת אליו יוחזק בתור WeakReference, כדי שהוא לא יחשב בתור Reference בספירה של הGarbage Collector.
נוסיף למחלקה שלנו לכן Data Member מסוג זה:
1
2
3
4
5
6
7
8
9
privatereadonly WeakReference mTarget;
publicobject Target
{
get
{
return mTarget.Target;
}
}
הProperty ששמו Target הוא דומה לProperty שיש לDelegateים ששמו Target.
אנחנו צריכים גם לאתחל משהו שייצג את המתודה של האובייקט (של האובייקט שמחזיק mTarget) שאנחנו מעוניינים לקרוא לה בכל הקפצת אירוע.
הדרך הפשוטה היא לשמור כMember את handler, אלא שזה לא יעבוד. למה? כי אז יש לנו Reference לhandler, שלו יש Reference לאובייקט שהמתודה שייכת אליו, ולכן שוב הGarbage Collector לא יאסוף אותנו.
מה שנוכל לעשות במקום זה איכשהו לשמור מידע המייצג איזו מתודה אנחנו מעוניינים להריץ.
זה אמור להזכיר לכם את הטיפוס MethodInfo המייצג Metadata של מתודה (טיפים מספר 146-150), ואכן לDelegate יש Property ששמו Method המחזיר את הMethodInfo של המתודה שאליה מצביע הDelegate.
אז נשמור אותו בצד:
1
privatereadonly MethodInfo mHandlerMethodInfo;
ונאתחל גם אותו בConstructor:
1
2
3
4
5
Public WeakEventHandler(EventHandler<TEventArgs> handler)
{
mTarget = new WeakReference(handler.Target);
mHandlerMethodInfo = handler.Method;
}
שימו לב שלמחלקה שלנו אין Reference ישיר לTarget של הDelegate, אלא רק WeakReference. (כי MethodInfo אינו מצביע לinstance של אובייקט, אלא מתאר מתודה באופן כללי)
עכשיו כל מה שנשאר לעשות זה לכתוב מה יקרה כאשר יקפוץ האירוע:
מה שהיינו רוצים לעשות זה פשוט לקרוא לInvoke של המתודה:
1
2
3
4
5
publicvoidInvoke(object sender, TEventArgs e)
{
mHandlerMethodInfo.Invoke(mTarget.Target,
newobject[] {sender, e});
}
אלא שאנחנו צריכים לבדוק שהאובייקט שלנו עדיין בחיים.
כשרואים שלWeakReference יש Property בשם IsAlive, מפתה לכתוב משהו כזה:
1
2
3
4
5
6
7
8
publicvoidInvoke(objectsender, TEventArgs e)
{
if (mTarget.IsAlive)
{
mHandlerMethodInfo.Invoke(mTarget.Target,
newobject[] {sender, e});
}
}
IsAlive הוא Property המחזיר האם האובייקט שהWeakReference מצביע אליו עדיין בחיים, או שנאסף ע"י הGarbage Collector.
יש בעיה עם הכתיבה הזאת כיוון שהיא גורמת לRace Condition עם הGarbage Collector – יכול להיות שבתנאי הTarget עדיין היה בחיים, אבל שורה אחרי זה, כשאנחנו ניגשים לProperty ששמו Target, הReference כבר נאסף ע"י הGarbage Collector.
לכן הדרך היותר נכונה לכתוב זאת היא כך:
1
2
3
4
5
6
7
8
9
10
publicvoidInvoke(object sender, TEventArgs e)
{
object target = mTarget.Target;
if (target != null)
{
mHandlerMethodInfo.Invoke(target,
newobject[] {sender, e});
}
}
אנחנו מכניסים את הReference שאליו מצביע הWeakReference למשתנה לוקאלי. כעת יש אליו Reference אמיתי (לא WeakReference) ולכן הGarbage Collector לא ינסה לאסוף אותו.
אחרי זה אנחנו קוראים לInvoke של mHandlerMethodInfo, כלומר מקפיצים את הפונקציה שנרשמו איתה לEvent שלנו.
לבסוף מאחר וtarget הוא משתנה לוקאלי, הוא נאסף ע"י הGarbage Collector ביציאה מהפונקציה, ולכן אחרי הקפצת האירוע, מספר הReferenceים שיש לאובייקט שאליו שייכת המתודה חוזר להיות מה שהיה לפני הקריאה למתודה.
זהו מימוש די פשוט לWeak-event. כעת אם נקרא למתודה, באמת תקרא המתודה איתה נרשמנו, לדוגמה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
publicclassSubject
{
public eventEventHandler<EventArgs> EventRaised;
publicvoidRaise()
{
if (EventRaised != null)
{
EventRaised(this,EventArgs.Empty);
}
}
}
publicclassObserver
{
publicObserver(Subject source)
{
source.EventRaised += new WeakEventHandler<EventArgs>(OnEventRaised);
בעיה אחת היא העניין של ביצועים – ידוע הרי שקריאה לInvoke של MethodInfo היא הרבה יותר איטית מקריאה לDelegate.
בעיה שנייה היא שכרגע אמנם הזכרון שלנו לא מתנפח מאובייקטים שאנחנו נרשמים איתם לEvent (הם משתחררים כי הם מוחזקים כWeakReference), אבל הWeakEventHandlerים עצמם לא משתחררים (כי הEvent מחזיק Reference אליהם), כך שעדיין איזשהו סוג של דליפה.
פעם שעברה ראינו כיצד שימוש לא זהיר בEventים יכול לגרום לדליפת זיכרון.
ראינו גם שאפשר למנוע את הדליפה ע”י קריאה לפונקציה שמבטלת את ההרשמה לאירועים רגע לפני שהאובייקט עוזב.
בסדרת הפוסטים הקרובה נראה כיצד אפשר לפתור את הבעיה בצורה אחרת…
השיטה היא באמצעות שימוש בטיפוס שנקרא WeakReference.
מה זה בדיוק WeakReference?
WeakReference היא טיפוס המצביע לReference כלשהו, אלא שבניגוד לשימוש רגיל בobject, מופע של WeakReference אינו נספר בתור Reference לאובייקט מבחינת הGarbage Collector. לכן, אם לאף אחד חוץ מלWeakReference אין Reference לאובייקט מסוים, האובייקט ייאסף ע”י הGarbage Collector.
נשמע טוב, לא?
אז מה הקשר לEventים?
אמרנו שמה שגורם לדליפת זכרון הוא שכל Delegate מחזיק Reference לאובייקט אליו שייכת המתודה. לכן, מבחינת ספירה, הGarbage Collector לעולם לא יאסוף את האובייקט שהמתודה שייכת אליו, שהרי עדיין לDelegate יש Reference אליו.
מה שאנחנו יכולים לעשות זה ליצור מעין EventHandler משלנו בו הTarget יהיה WeakReference ואז איכשהו להפעיל את המתודה שלו.
ככה הReference לאובייקט לא יספר בספירה של הGarbage Collector, ולכן יאסף.
אז נתחיל:
דבר ראשון, אנחנו מעוניינים שהטיפוס שלנו יחקה את הDelegateששמו EventHandler. כדי לעשות זאת ניצור מחלקה גנרית כזאת:
Eventים יכולים לגרום לדליפת זיכרון אם לא משתמשים בהם נכון.
איך זה קורה?
ובכן הטיפוס הנחמד ששמו Delegate המייצג מצביע לפונקציה, מחזיק Reference לאובייקט שמחזיק את הפונקציה שאנחנו רוצים להריץ.
מה שיכול לקרות זה שאנחנו נרשמים לאובייקט שחי יותר זמן מאיתנו. מה שזה גורר זה שהאובייקט שחי יותר זמן, מחזיק Delegate שמצביע לאובייקט שלנו. כתוצאה מכך הוא מחזיק גם Reference לאובייקט שלנו. מה שזה גורר זה שהGarbage Collector לא אוסף אותנו, כי הוא רואה שלמישהו (האובייקט שחי יותר זמן מאיתנו) יש Reference אלינו.
כתוצאה מכך נמנעת פעולת הCollect על המחלקה שלנו, ולכן אנחנו לא נאספים, דבר שגורם לדליפת זכרון.
להלן דוגמה הממחישה את הסיפור:
יש את המחלקות הבאות:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicclassSubject
{
publicevent EventHandler EventRaised;
}
publicclassObserver
{
publicObserver(Subject source)
{
source.EventRaised += new EventHandler(OnEventRaised);
שימו לב שהזכרון רק גדל ובהרבה. מה שהיינו מצפים שבהשמה של null בobservers, לא יהיו יותר אובייקטים שמצביעים לObserverים שיצרנו כאן, אלא שsubject דווקא עדיין מצביע על כל הObserverים.
בדוגמה הזאת מדובר באובייקטים שאינם כבדים ולא שוקלים הרבה, כך שזה פחות מורגש (שימו לב שיצרתי 50000 אובייקטים בשביל לגדול בפחות מ3 מגה)
בחיים האמיתיים מה שקורה זה שהאובייקטים הרבה יותר כבדים, הם יכולים להיות Formים למשל שנרשמים לEventים אחרים וככה הדליפה יכולה להיות מורגשת…
הדרך הקלאסית לפתור את בעיה זו היא ליצור פונקציה שמבטלת את הרישום לEventים אלה:
נוכל, למשל, לממש IDisposable (ראו טיפ מספר 62):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Subject subject = new Subject();
long memoryBefore = GC.GetTotalMemory(true);
List<Observer> observers = new List<Observer>(50000);