December 26, 2022

C# - Lazy<T>


Lazy T

מחלקת ה-Lazy<T> נותנת לנו יכולת ליצור מופעים רק כשאנחנו צריכים אותם.

המאפיין Value יוצר את המחלקה.
ואם אנחנו רוצים לבדוק אם המופע נוצר ניתן לקרוא את IsValueCreated.

1
2
3
4
5
6
7
8
Lazy<int> myInt = new Lazy<int>();

var val = myInt.Value;

if(myInt.IsValueCreated)
{
Console.WriteLine("Has value!");
}

העקרונות מאחורי המחלקה

שימוש יעיל במשאבים.
יצירת מופעים בצורה מסונכרנת.

  • להשתמש במחלקה קיימת כדי לאתחל משתנה אשר צריך Thread safety (נדבר על זה עוד מעט).
  • לאתחל משתנה גלובלי סטטי - בגלל היכולת שלו להיות Thread safe.
  • כשמשאב הוא יקר והשימוש בו לא רב
  • כשהמשאב הוא יקר וצריך לבצע פעולה כלשהי כדי לאתחל אותו (כמו לקרוא מקובץ).

כיצד ליצור את המחלקה

מחלקת Lazy יכולה להיות מאותחלת במגוון דרכים:

Lazy - בנאי ריק

הבנאי הריק יוצר משתנה אשר יש לו בנאי ריק.

1
2
3
4
5
6
class MyClass
{
public string Name { get;set;}
}
Lazy<MyClass> lazyClass = new Lazy<MyClass>();
var myClassInstance = lazyClass.Value;

Lazy<T>(bool isThreadSafe) - עם סנכרון

ניתן להעביר בוליאני שאומר אם אנחנו רוצים שה-Lazy יהיה בעל הגנה לתהליכונים מרובים או לא.
המחיר?
משתנים שהם Thread-Safe סובלים מבעיות ביצועים.

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
{
public string Name { get;set;}
}
Lazy<MyClass> lazyClass = new Lazy<MyClass>(true);

for(int i=0;i<10;i++)
{
Task.Run(()=>{
var myClassInstance = lazyClass.Value;
});
}

Lazy<T>(LazyThreadSafetyMode) - סוג סנכרון

ניתן לקנפג את ההתנהגות בהתאם למצב שאנו רוצים.

LazyThreadSafetyMode.None

אין Thread safety בכלל.
מצב זה גורם לריבוי תהליכונים להיות - Undefined.
לכן אם יש לכם יותר מתהליכון אחד אל תשתמשו בזה!
אולם - זה מהיר יותר אז אם אתם יודעים שרק תהליכון אחד ישתמש ב-Lazy ניתן להשתמש ב-None.

LazyThreadSafetyMode.PublicationOnly

עבור תהליכונים רבים - הראשון שמשלים את תהליך האתחול הוא יסיים את התהליך.

הנה דוגמא מעניינת - מה לדעתכם יודפס?

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
public class MyClass
{
private Random mRand = new Random();

public MyClass()
{
var toWait = mRand.Next(100, 1000);
Console.WriteLine("MyClass" + toWait.ToString());
Thread.Sleep(toWait);
}
}

public static void Main()
{
Lazy<MyClass> lazy = new(LazyThreadSafetyMode.PublicationOnly);

List<Task> allTasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
allTasks.Add(Task.Run(() =>
{
var myClassInstance = lazy.Value;
}));
}

Task.WaitAll(allTasks.ToArray());
}

LazyThreadSafetyMode.ExecutionAndPublication

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

Lazy<T>(Func<T> function) - בניית המופע

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

למשל אם נרצה לקרוא מקובץ קודם:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass
{
private string mValue;
MyClass(string val) { mValue = val;}
}

Lazy<MyClass> classLazy = new Lazy<MyClass>(() => {
var value = File.ReadAllText("MyFile.txt");
return new MyClass(value);
});
classLazy.Value; // Reads file

Lazy(T value) - ערך מאותחל מראש

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass
{
public string Name { get;set; }
}
Lazy<MyClass> lazyClass = new Lazy<MyClass>(new MyClass() { Name = "Bob" });

for(int i=0;i<10;i++)
{
Task.Run(()=>{
var myClassInstance = lazyClass.Value;
Console.WriteLine(myClassInstance.Name);
});
}

דוגמאות לשימוש

יצירת סינגלטון

כמקובל יצירת סינגלטון סטטי נעשתה בצורה הזו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyHandler
{
private static object mLocker = new object();
private static MyHandler Object;

public static MyHandler GetHandler()
{
lock(mLocker)
{
if(Object == null)
{
lock(mLocker)
{
Object = new MyHandler();
}
}
}
return Object;
}
}

Lazy חוסך לנו את כל זה!

1
2
3
4
5
6
7
8
9
public class MyHandler
{
private static Lazy<MyHandler> StaticLazy = new Lazy<MyHandler>(true);

public static MyHandler GetHandler()
{
return StaticLazy.Value;
}
}

חסכון במשאב יקר

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 ExpensiveClass
{
private byte[] mBytes = new byte[100000000];
private long CurrentIndex = 0;

public long AddToCache()
{
// Add to bytes
// Return index
}
}

public class Consumer
{
private Lazy<ExpensiveClass> mCacheService = new Lazy<ExpensiveClass>(true);

public void Do()
{
byte[] someData = //...
if(shouldCache)
{
mCacheService.Value.AddToCache(someData);
}
}
}

יצירת מחלקות

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

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
public class Connection
{
public void SetOption(Option flag, object value);

public void Open(IpAdress address){ /*...*/ }
}


public class Client
{
private Lazy<Connection> mConnection;

public Client()
{
mConnection = new Lazy<Connection>(()=>{
Connection c = new Connection();
c.SetOption(Option.UseMinMtu);
c.Open("localhost");
return c;
});
}

// Somewhere else

public void Response(Message m)
{
var connection = mConnection.Value;
connection.Send(SerializeIntoBytes(m));
}
}

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


עצלנות זוהי דרך מעולה לנצל ביעילות רק מה שדרוש באותו רגע.
איזה עוד מחלקות מממשות את העקרון הזה?
רמז - Enumerable!

תודה על הקריאה!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#CSharp