210. Computing a power set of a collection

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

ניתן לעשות זאת בצורה מעניינת בעזרת מספר Featureים של השפה:

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
26
27
28
29
30
31
32
33
34
35
36
public static IEnumerable<IEnumerable<T>> GetPowerSet<T>(IEnumerable<T> collection)
{
if (!collection.Any())
{
// There is exactly one subset of the empty set,
// the empty set itself.
yield return Enumerable.Empty<T>();
}
else
{
// excluded will be the excluded item
// of our set.
// We now find the power set of all
// elements except him.
T excluded = collection.First();
IEnumerable<T> withoutCurrent =
collection.Skip(1);
// Get the power set (using recursive call)
IEnumerable<IEnumerable<T>> subsetsWithoutCurrent =
GetPowerSet(withoutCurrent);
// Iterate over the power set of the rest elements.
foreach (IEnumerable<T> currentSet in subsetsWithoutCurrent)
{
// Each set yields two sets: one without the excluded
// item and the second with the excluded item.
IEnumerable<T> currentSetWithoutExcluded = currentSet;
IEnumerable<T> currentSetWithExcluded = currentSet.Concat(new T[] {excluded});
yield return currentSetWithoutExcluded;
yield return currentSetWithExcluded;
}
}
}

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

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

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

זה די מזכיר את הקוד של הפרמוטציות (ראו גם טיפ מספר 172). הוספתי תיעוד כך שהקוד די מסביר את עצמו.

מה שטוב כאן, זה שהחישוב הוא Lazy, לכן הוא לא יתבצע עד שנעבור על האיבר המתאים בforeach (או נקרא לMoveNext המתאים). ראו גם טיפים מספר 54-55.

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

מומלץ לקרוא בויקיפדיה גם על משפט קנטור (יש שם הסבר אינטואיטיבי נחמד) ו האלכסון של קנטור.

סופ"ש חזק.

שתף

209. Tweaking Enum GetValues

ראינו פעם שעברה שאפשר לקבל את כל הערכים של 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
{
private static TEnum[] mEnumValues = InitValues();
private static TEnum[] InitValues()
{
TEnum[] result = (TEnum[])Enum.GetValues(typeof(TEnum));
return result;
}
public static 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 שביקשנו.

המשך יום גנרי ערכי סטטי.

שתף

208. Enum.GetValues, Enum.GetNames

מדי פעם יש לנו Enum ואנחנו מעוניינים לעבור על כל הערכים האפשריים שלו.

יש מספר דרכים לעבור על הערכים:

  1. בתור ערכי הEnum
  2. בתור ערכים מספריים
  3. בתור ערכי המחרוזות

למשל, נניח שיש לנו את הEnum הבא:

1
2
3
4
5
6
7
8
9
10
public enum Days
{
Sunday = 1,
Monday = 2,
Tuesday = 3,
Wednesday = 4,
Thursday = 5,
Friday = 6,
Saturday = 7
}

כדי לעבור על כל ערכי הEnum נוכל להשתמש בפונקציה Enum.GetValues פונקציה זו מחזירה לנו מערך של object המכיל את כל ערכי הEnum האפשריים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Days[] days = (Days[])Enum.GetValues(typeof(Days));
foreach (Days day in days)
{
Console.WriteLine(day);
}
//Sunday
//Monday
//Tuesday
//Wednesday
//Thursday
//Friday
//Saturday

כדי לעבור על כל ערכי הEnum בתור ערכים נומריים, נצטרך לבצע הסבה:

1
2
3
4
5
6
int[] daysValues = (int[])Enum.GetValues(typeof(Days));
foreach (int day in daysValues)
{
Console.WriteLine(day);
}

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

כדי לעבור על כל השמות של הEnum נוכל להשתמש בפונקציה Enum.GetNames:

1
2
3
4
5
6
string[] daysNames = Enum.GetNames(typeof(Days));
foreach (string day in daysNames)
{
Console.WriteLine(day);
}

המשך יום ערכי טוב

שתף

207. Solving the problem with events - part 6

אמרתי פעם שעברה שאני אראה כיצד אפשר להסיר באופן ייזום Weak event שנרשם לאירוע שלנו.

ובכן, כדי לעשות זאת נצטרך להוסיף PropertyלWeakEventHandler:

1
2
3
4
5
6
7
public MethodInfo Method
{
get
{
return mHandlerMethodInfo;
}
}

האנלוגי לProperty בשם Method שיש לDelegate כלשהו.

כעת בפונקצית הסרת הרישום נעשה משהו כזה:

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
26
public event EventHandler<EventArgs> EventRaised
{
add
{
mEventRaised += value.MakeWeak(x => mEventRaised -= x);
}
remove
{
Delegate[] subscribers = mEventRaised.GetInvocationList();
IEnumerable<WeakEventHandler<EventArgs>> weakSubscribers =
subscribers.Select(x => x.Target).OfType<WeakEventHandler<EventArgs>>();
IEnumerable<WeakEventHandler<EventArgs>> requestedSubscribers =
weakSubscribers.Where(x => (x.Method == value.Method) &&
(x.Target == value.Target));
WeakEventHandler<EventArgs> requestedSubscriber =
requestedSubscribers.FirstOrDefault();
if (requestedSubscriber != null)
{
mEventRaised -= requestedSubscriber.Invoke;
}
}
}

אנחנו בעצם מחפשים את כל הרשומים לפונקציה שהם WeakEventHandler עם Method וTarget שמתאימים לזה שאנחנו קיבלנו. (אם קיבלנו Reference לTarget כלשהו, זה אומר שהTarget עדיין בחיים ולכן הWeak Reference לא היה אמור למות)

לאחר מכן אנחנו מוצאים את הראשון ומסירים את הרישום שלו מהDelegate.

אפשר כמובן להפוך את זה לExtension Method שיהיה אפשר להשתמש בו גם למקרים שבהם הפרמטר הגנרי הוא לא EventArgs אלא משהו אחר.


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

אני רוצה לציין שהסדרה מבוססת על הפוסט Solving the Problem with Events: Weak Event Handlers מהבלוג DidItWithNet. יש הבדלים בין הדרכים שהצגתי לדרכים שהוא מציג שם, משום שמהבדיקות שעשיתי ראיתי שהוא לא ממש צודק, ולכן לא נכנסתי לכל מיני דברים כמו unbound delegates. אתם מוזמנים לקרוא את הפוסט הזה, הוא די טוב.

המשך יום מלא אירועים בלי שום דליפות טוב.

שתף

206. Solving the problem with events - part 5

פעם שעברה ראינו כיצד ניתן לגרום לWeak event להסיר את עצמו מEvent ברגע שהEvent קופץ והTarget של הDelegate מת.

אמרתי שאפשר לעשות את הסינטקס קצת יותר יפה.

ובכן, אם מדובר בEvent שלנו, אפשר לדאוג לכך שכל ההרשמות אליו יהיו Weak.

איך אפשר לעשות זאת? בעזרת הAccessorים של AddוRemove. (ראו גם טיפ מספר 194)

למשל, משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
private event EventHandler<EventArgs> mEventRaised;
public event EventHandler<EventArgs> EventRaised
{
add
{
mEventRaised += value.MakeWeak(x => mEventRaised -= x);
}
remove
{
// TODO: Write removal code here.
}
}

כמה הסברים:

כשנרשמים לEvent שלנו אנחנו בעצם נרשמים לMember שלנו עם Weak Event מתאים בעזרת הExtension Method שרשמנו פעם שעברה. (טיפ מספר 205)

למה אנחנו מחזיקים במחלקה event שהוא private ולא סתם Delegate? הסיבה העיקרית היא שראינו שEventים ממומשים בצורה קצת שונה ממימוש סטנדרטי, במיוחד בFramework 4.0 (טיפ מספר 195).

אז השתמשנו בprivate event בשביל לקבל את היכולות האלה.

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

נראה פעם הבאה את המימוש לAccesor זה.

שבוע בעל אירועים חסרי דליפות טוב

שתף

205. Solving the problem with events - part 4

ראינו בפעמים הקודמות כיצד ניתן להמנע מדליפות זכרון של הרשמות לEventים באמצעות מימוש של Weak Event.

אמרתי שעדיין יש בעיה – האובייקט שנרשם לאירוע אמנם כבר לא דולף, אבל הWeakEventHandlerים דולפים. הסיבה לכך היא שהEvent עדיין מחזיק Reference לWeakEventHandler ולכן הGarbage Collector לא אוסף אותו. (כמו בטיפ 201)

אז איך אפשר לפתור את הבעיה? פתרון אפשרי הוא להעביר לWeakEventHandler איזשהו Delegate שיבצע הסרת רישום מהEvent ברגע שהאובייקט מת:

אם נחזור למימושים מאתמול, ניצור Delegate כזה:

1
public delegate void UnregisterDelegate(EventHandler<TEventArgs> subscription);

נקבל אותו בConstructor:

1
protected WeakEventHandler(EventHandler<TEventArgs> handler, UnregisterDelegate unregisterHandler)

נחזיק אותו:

1
protected readonly UnregisterDelegate mUnregisterHandler;

נאתחל אותו בConstructor:

1
mUnregisterHandler = unregisterHandler;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Invoke(object sender, TEventArgs eventArgs)
{
object target = mTarget.Target;
if (target != null)
{
InnerInvoke(target, sender, eventArgs);
}
else
{
mUnregisterHandler(this);
}
}

נשנה כמובן גם בConstructorים של הבנים את החתימה:

1
2
3
public RuntimeWeakEventHandler(EventHandler<TEventArgs> handler,
UnregisterDelegate unregisterHandler)
: base(handler, unregisterHandler)

כעת איך משתמשים?

ברישום לEvent במקום לכתוב כמו שכתבנו עד עכשיו:

1
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):

1
2
3
4
5
6
7
public static RuntimeWeakEventHandler<TEventArgs> MakeWeak<TEventArgs>
(this EventHandler<TEventArgs> eventHandler,
WeakEventHandler<TEventArgs>.UnregisterDelegate unregisterDelegate)
where TEventArgs : EventArgs
{
return new RuntimeWeakEventHandler<TEventArgs>(eventHandler, unregisterDelegate);
}

ואז להשתמש בסינטקס הזה:

1
source.EventRaised += new EventHandler<EventArgs>(OnEventRaised).MakeWeak(x => source.EventRaised -= x);

שזה די סביר.

בהמשך נראה שיפור נוסף שאפשר לעשות בסינטקס,

בינתיים שיהיה סופ"ש מלא אירועים לא חלשים טוב.

שתף

204. Solving the problem with events - part 3

פעם שעברה ראינו איך אפשר לממש Weak event בסיסי בעזרת שימוש בWeakReference והפעלת המתודה בעזרת הפעלת Invoke של MethodInfo.

אמרתי שיש עם בעיה זו שתי בעיות:

  1. אמנם כרגע לא דולף האובייקט שהDelegate מצביע אליו, אבל דולפת המחלקה WeakEventHandler.
  2. הפעלת מתודה בעזרת Invoke היא יותר איטית מהפעלה ישירה.

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

ראשית נהפוך את המחלקה WeakEventHandlerלאבסטרקטית. נוסיף לה מתודה בשם InnerInvoke:

1
protected abstract void InnerInvoke(object target, object sender, TEventArgs eventArgs);

ונשנה את המימוש של הפונקציה Invoke כך שיקרא לפונקציה זו כאשר הReference עדיין בחיים:

1
2
3
4
5
6
7
8
9
public void Invoke(objectsender, TEventArgs eventArgs)
{
object target = mTarget.Target;
if (target != null)
{
InnerInvoke(target, sender, eventArgs);
}
}

בנוסף נהפוך את הMemberים לprotected.

כעת את המימוש שראינו אתמול, אפשר לכתוב ככה:

1
2
3
4
5
6
7
8
9
10
11
12
public class ReflectionWeakEventHandler<TEventArgs>: WeakEventHandler<TEventArgs>
where TEventArgs : EventArgs
{
public ReflectionWeakEventHandler(EventHandler<TEventArgs> handler) : base(handler)
{
}
protected override void InnerInvoke(object target,object sender, TEventArgs eventArgs)
{
mHandlerMethodInfo.Invoke(target,new object[] {sender, eventArgs});
}
}

כפי שציינתי בעבר (טיפים מספר 148,151,181), קריאה לפונקציה Invoke היא קריאה יחסית איטית.

הטריק שאפשר לעשות הוא ליצור מתודה בזמן ריצה עם החתימה של InnerInvoke, שInnerInvoke תקרא לה בלי Dynamic Binding:

ניצור מחלקה כזאת:

1
2
3
4
public class RuntimeWeakEventHandler<TEventArgs> : WeakEventHandler<TEventArgs>
where TEventArgs : EventArgs
{
}

בה יהיה Delegate עם החתימה של InnerInvoke:

1
private delegate void RaiseEventHandler(object target, object sender, TEventArgs eventArgs);

ונחזיק Member כזה:

1
private readonly RaiseEventHandler mRaisingHandler;

שנקרא לו בפונקציה InnerInvoke:

1
2
3
4
protected override void InnerInvoke(object target, object sender, TEventArgs eventArgs)
{
mRaisingHandler(target, sender, eventArgs);
}

כעת נותר לאתחל את mRaisingHandler בConstructor:

1
2
3
4
5
public RuntimeWeakEventHandler(EventHandler<TEventArgs> handler)
: base(handler)
{
mRaisingHandler = CreateRaiseEventHandler();
}

הפונקציה שמאתחלת את הDelegate נראית כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private RaiseEventHandler CreateRaiseEventHandler()
{
ParameterExpression target = Expression.Parameter(typeof(object),"target");
ParameterExpression sender = Expression.Parameter(typeof(object),"sender");
ParameterExpression e = Expression.Parameter(typeof(TEventArgs),"e");
UnaryExpressionconvertedTarget =
Expression.Convert(target, mHandlerMethodInfo.DeclaringType);
MethodCallExpression eventRaise =
Expression.Call(convertedTarget,
mHandlerMethodInfo,
sender,
e);
Expression<RaiseEventHandler> raiserLambda =
Expression.Lambda<RaiseEventHandler>(eventRaise,
target,
sender,
e);
return raiserLambda.Compile();
}

עושים פה הסבה של target לטיפוס האמיתי שלו (ראו גם טיפ מספר 161) ואז קוראים לפונקציה mHandlerMethodInfo עם הפרמטרים sender וe.

אחר כך הופכים את כל זה לLambda Expression ואז מחזירים את התוצאה המקומפלת. ראו גם טיפים על Expression Trees: 173-181.

כעת הקפצה של RuntimeWeakEventHandlerכזה יותר מהירה מהקפצה של ReflectionWeakEventHandler.

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

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

1
2
private static readonly Dictionary<MethodInfo,RaiseEventHandler> mMethodInfoToRaisingHandler =
new Dictionary<MethodInfo, RaiseEventHandler>();

במקום לקרוא לפונקציה CreateRaiseEventHandler נקרא לפונקציה אחרת:

1
2
3
4
public RuntimeWeakEventHandler(EventHandler<TEventArgs> handler) : base(handler)
{
mRaisingHandler = GetRaiser();
}

ותוכן הפונקציה GetRaiser הוא פשוט:

1
2
3
4
5
6
7
8
9
10
11
12
private RaiseEventHandler GetRaiser()
{
RaiseEventHandler raiseEventHandler;
if(!mMethodInfoToRaisingHandler.TryGetValue(mHandlerMethodInfo, out raiseEventHandler))
{
raiseEventHandler = CreateRaiseEventHandler();
mMethodInfoToRaisingHandler[mHandlerMethodInfo] = raiseEventHandler;
}
return raiseEventHandler;
}

למי שמעוניין במספרים:

בהקפצה של אירוע שרשומים אליו 100 רשומים (בלי תוכן בפונקציה הנרשמת):

הקפצה ישירה לוקחת בממוצע 0.000805100000000011 מילישניות

הקפצה שלReflectionWeakEventHandler לוקחת בממוצע 0.3630364 מילישניות

הקפצה של RuntimeWeakEventHandler לוקחת בממוצע 0.00414629999999997 מילישניות

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

המשך יום מלא אירועים לא כל כך חלשים

שתף

203. Solving the problem with events - part 2

פעם שעברה התחלנו ליצור מחלקה שתייצג את הWeak-event שלנו.

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

נוסיף למחלקה שלנו לכן Data Member מסוג זה:

1
2
3
4
5
6
7
8
9
private readonly WeakReference mTarget;
public object Target
{
get
{
return mTarget.Target;
}
}

הProperty ששמו Target הוא דומה לProperty שיש לDelegateים ששמו Target.

אנחנו נאתחל אותו בConstructor:

1
2
3
4
public WeakEventHandler(EventHandler<TEventArgs> handler)
{
mTarget = new WeakReference(handler.Target);
}

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

הדרך הפשוטה היא לשמור כMember את handler, אלא שזה לא יעבוד. למה? כי אז יש לנו Reference לhandler, שלו יש Reference לאובייקט שהמתודה שייכת אליו, ולכן שוב הGarbage Collector לא יאסוף אותנו.

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

זה אמור להזכיר לכם את הטיפוס MethodInfo המייצג Metadata של מתודה (טיפים מספר 146-150), ואכן לDelegate יש Property ששמו Method המחזיר את הMethodInfo של המתודה שאליה מצביע הDelegate.

אז נשמור אותו בצד:

1
private readonly 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
public void Invoke(object sender, TEventArgs e)
{
mHandlerMethodInfo.Invoke(mTarget.Target,
new object[] {sender, e});
}

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

כשרואים שלWeakReference יש Property בשם IsAlive, מפתה לכתוב משהו כזה:

1
2
3
4
5
6
7
8
public void Invoke(objectsender, TEventArgs e)
{
if (mTarget.IsAlive)
{
mHandlerMethodInfo.Invoke(mTarget.Target,
new object[] {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
public void Invoke(object sender, TEventArgs e)
{
object target = mTarget.Target;
if (target != null)
{
mHandlerMethodInfo.Invoke(target,
new object[] {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
public class Subject
{
public eventEventHandler<EventArgs> EventRaised;
public void Raise()
{
if (EventRaised != null)
{
EventRaised(this,EventArgs.Empty);
}
}
}
public class Observer
{
public Observer(Subject source)
{
source.EventRaised += new WeakEventHandler<EventArgs>(OnEventRaised);
}
private void OnEventRaised(object sender,EventArgs e)
{
Console.WriteLine(DateTime.Now);
}
}

אז קריאה כזאת:

1
2
3
Subject subject = new Subject();
Observer observer = new Observer(subject);
subject.Raise(); // Writes the current time

תדפיס את התאריך הנוכחי.

הכל נראה אחלה, אז מה הבעיה?

בעיה אחת היא העניין של ביצועים – ידוע הרי שקריאה לInvoke של MethodInfo היא הרבה יותר איטית מקריאה לDelegate.

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

בהמשך נדבר על פתרונות אפשריים לבעיות אלה,

המשך יום מלא אירועים חסרי דליפות טוב.

שתף

202. Solving the problem with events - part 1

פעם שעברה ראינו כיצד שימוש לא זהיר ב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. כדי לעשות זאת ניצור מחלקה גנרית כזאת:

1
2
3
4
5
6
7
8
public class WeakEventHandler<TEventArgs>
where TEventArgs : EventArgs
{
public WeakEventHandler(EventHandler<TEventArgs> handler)
{
// ...
}
}

זאת כבר התחלה טובה, כי קוד כזה יתקמפל:

1
2
WeakEventHandler<EventArgs> myEvent =
new WeakEventHandler<EventArgs>(EventRaised);

למרבה הצער, אנחנו לא יכולים לרשת מ"הטיפוסים המיוחדים" ששמם Delegate, MulticastDelegate וEventHandler<TEventArgs>, או מכל Delegate אחר.

לכן נצטרך למצוא איזשהו Workaround, שהרי היינו רוצים שהשורה הבאה תתקמפל:

1
2
EventHandler<EventArgs> myEvent =
new WeakEventHandler<EventArgs>(EventRaised);

הפתרון, למקרה שלא ניחשתם הוא להשתמש בimplict cast:

1
2
3
4
public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> source)
{
return null;
}

כמובן, צריך לשים פה מימוש מתאים, שהרי ככה זה לא יעבוד 😃

בואו נדבר על המימוש:

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

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

מה שנעשה זה ניצור פונקציה אליה יכנסו בכל קריאה לWeak-event שלנו. בה אנחנו נבצע את הבדיקה וכו’:

1
2
3
4
5
public void Invoke(objectsender, TEventArgs e)
{
// TODO: check here if the reference is still alive
// TODO: and call its method.
}

ואז בהסבה לEventHandler<TEventArgs>ניצור פשוט Delegate לפונקציה זו:

1
2
3
4
public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> source)
{
return new EventHandler<TEventArgs>(source.Invoke);
}

עכשיו מה שיקרה זה שכל פעם שיקפיצו את הWeak-event שלנו, תקרא הפונקציה Invoke. אם למשל נכתוב בה קוד כזה:

1
2
3
4
public void Invoke(object sender, TEventArgs e)
{
Console.WriteLine(DateTime.Now);
}

אז אם נרשם לאיזשהו Event ככה:

1
2
subject.EventRaised +=
new WeakEventHandler<EventArgs>(EventRaised);

אז כשיוקפץ הEvent הנ"ל, נכנס לפונקציה Invoke ולכן יכתבו למסך התאריך והשעה.

בהמשך נראה מה בדיוק נכתוב בפונקציה Invoke ואיך זה מתקשר לאותם WeakReferenceים.

המשך יום מלא אירועים לא חלשים טוב

שתף

201. Event leaks

הכרנו בעבר (טיפ מספר 193) את המושג של Eventים.

אחת הבעיות הנפוצות עם Eventים היא הבאה:

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
public class Subject
{
public event EventHandler EventRaised;
}
public class Observer
{
public Observer(Subject source)
{
source.EventRaised += new EventHandler(OnEventRaised);
}
private void OnEventRaised(object sender, EventArgs e)
{
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Subject subject = new Subject();
long memoryBefore = GC.GetTotalMemory(true);
List<Observer> observers = new List<Observer>(50000);
for (int i = 0; i < 50000; i++)
{
observers.Add(new Observer(subject));
}
observers = null;
GC.Collect();
long memoryAfter = GC.GetTotalMemory(true);
Console.WriteLine(memoryAfter - memoryBefore); // 2662244

שימו לב שהזכרון רק גדל ובהרבה. מה שהיינו מצפים שבהשמה של 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);
for (int i = 0; i < 50000; i++)
{
observers.Add(new Observer(subject));
}
foreach (Observer observer in observers)
{
observer.Dispose();
}
observers = null;
GC.Collect();
long memoryAfter = GC.GetTotalMemory(true);
Console.WriteLine(memoryAfter - memoryBefore); // 800068

כפי שאתם רואים, הפעם השתחרר לנו יותר זכרון.

הפתרון הזה סבבה והכל, אבל יש איתו רק בעיה אחת – הוא דוחה.

למה דוחה?

הרגילו אותנו לחשוב שהשפה היא שפה Managed, ושהGarbage Collector יודע לנהל את הזכרון עבורנו.

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

בהמשך אנחנו נראה פתרון אחר לבעיה זו,

בינתיים שיהיה המשך יום מלא באירועים חסרי דליפות טוב

שתף