מה שקורה כאן זה בעצם שאין שום קשר של ירושה בין int וdouble, אבל מתבצעת הסבה ע"י Custom explicit cast. (ראו גם טיפ מספר 197)
בעיה שבד"כ נתקלים בה בהקשר זה היא הבאה: נניח שיש לנוobject שמחזיק double:
1
2
3
double real = 3.14;
object boxed = real;
int integer = (int) boxed; // Boom!
משום מה, הקוד הזה שנראה בערך אותו הדבר, לא עובד…
אבל אם נעשה משהו כזה:
1
2
3
double real = 3.14;
object boxed = real;
int integer = (int) (double) boxed; // 3
זה יעבוד.
מה בעצם קורה כאן?
כאשר אנחנו מכניסים לobject אובייקט שהוא Value-Type, מתבצע תהליך שנקרא Boxing. בתהליך הזה הValue-Type נעטף ע"י איזשהו אובייקט על הHeap שמצביע אליו.
נניח שיש לנו טיפוס A וטיפוס B.
נניח שיש לנו אובייקט מסוג A ואנחנו מעוניינים להמירו בטיפוס מסוג B בצורה Explicit.
הקומפיילר יודע להבחין בין שני סוגים של הסבות:
הסבה ראשונה: Up-cast – הסבה כזו אומרת את הדבר הבא: יש לי יותר ידע מהקומפיילר, אני יודע שהאובייקט A שאני מחזיק הוא למעשה אובייקט מסוג B, ואני דורש מהקומפיילר להתייחס אל האובייקט כאובייקט מסוג B.
הסבה שנייה: Custom cast – הסבה כזו אומרת את הדבר הבא: אני יודע שהאובייקט שאני מחזיק הוא מסוג A, אבל אני יודע שיש איזשהי דרך להמיר אותו לסוג B. אני דורש מהקומפיילר לבצע את ההמרה.
כעת, בדוגמה השנייה למעלה, כאשר אנחנו מבצעים הסבה לint, אנחנו בעצם מבקשים מהקומפיילר לעשות unboxing לint, למרות שזה לא הסוג האמיתי שלו.
שאלה המתבקשת היא למה? למה הקומפיילר מפרש את הקריאה שלנו בתור בקשה לעשות unboxing לint? למה שלא ינסה לבדוק מהו הטיפוס האמיתי של האובייקט boxed וינסה משם לחפש דרך אחרת להפוך אותו לint?
התשובה היא כזאת: ביצוע פעולה של unboxing היא פעולה יחסית בזבזנית. כעת נניח שאנחנו רוצים לבצע unboxing ואז לחפש אופרטור הסבה מתאים, ואז להפעיל אותו. דבר ראשון, הפעולה שמתוארת כאן היא אפילו יותר כבדה מunboxing, שהרי אחר כך אנחנו מחפשים פונקציית המרה מתאימה וקוראים לה. דבר שני, הפעולה התמימה הזאת של "הסבה לint", הייתה מתקמפלת בסופו של דבר למספר שורות בIL, בניגוד לפעולה בודדת של unbox:
1
2
3
ldloc.1
unbox.any int32
stloc.2
אוקיי, ואם בכל זאת בא לי שהקומפיילר יבצע את התהליך המסובך שתואר למעלה?
אז זה אפשרי, בשביל זה בדיוק קיימת הפונקציה Convert.ToInt32, המבצעת חישוב הדומה לחישוב המסובך הנ"ל 😃:
אחת הפרדיגמות העיצוב החשובות היא פרדיגמה בשם Convention over configuration.
פרדיגמה זו מעודדת להקטין את מספר ההחלטות שמפתח המשתמש בFramework שלנו צריך לקבל, מה שמביא לפשטות, אבל לאו דווקא ע”י איבוד גמישות.
למה הכוונה?
לדוגמה, נניח שיש לנו איזשהו Framework המאפשר לנו לשלוף מהDatabase אובייקטים משלנו. (שם קוד: DAL)
אחת הבעיות הראשונות שעשויות להיות לDAL כזה היא שהשמות בDatabase של הטבלאות והשמות של העמודות שלהן לאו דווקא מזדהים עם השמות של המחלקות והProperties של האובייקטים שלנו.
לכן בד”כ Framework כזה ידרוש מאיתנו מיפוי בין המחלקות והProperties שלנו לטבלאות או העמודות בDatabase:
המיפוי בדרך כלל יהיה באמצעות קובץ XML מעצבן, או בתוך הקוד באמצעות Metadata:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable(Name = "PEOPLE")]
publicclassPerson
{
[DbColumn(Name = "FIRST_NAME")]
publicstring Name
{
get;
set;
}
[DbColumn(Name ="LAST_NAME")]
publicstring LastName
{
get;
set;
}
[DbColumn(Name = "AGING")]
publicint Age
{
get;
set;
}
}
במקרה זה המיפויים של השמות של המחלקות הProperties לא תואם לשמות בDatabase. אבל אם הוא היה תואם, הקוד שלנו היה צריך להראות כך:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable(Name = "Person")]
publicclassPerson
{
[DbColumn(Name = "Name")]
publicstring Name
{
get;
set;
}
[DbColumn(Name ="LastName")]
publicstring LastName
{
get;
set;
}
[DbColumn(Name = "Age")]
publicint Age
{
get;
set;
}
}
מה שקורה במקרה כזה, זה שנוצרת פה הרבה מאוד כפילות – אנחנו נצטרך להגדיר מיפוי של כל עמודה וטבלה מספר פעמים!
מה שConvention over configuration אומר, זה את הדבר הבא:
הFramework שלנו יאפשר למפות את המחלקות/Properties לטבלאות/עמודות, אבל יאפשר גם הגדרה אוטומטית המבוססת קונבנציה:
אם לא מגדירים את שם הטבלה/עמודה הממופה, פשוט נשתמש בשם של המחלקה/הProperty.
זה יכול להתבצע במספר שיטות. לדוגמה, במקרה שלנו אופציה אפשרית אחת היא להכריח לשים את הAttributeים, אבל לא להכריח למלא אותם:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[DbTable]
publicclassPerson
{
[DbColumn]
publicstring Name
{
get;
set;
}
[DbColumn]
publicstring LastName
{
get;
set;
}
[DbColumn]
publicint Age
{
get;
set;
}
}
אופציה נוספת יכולה להיות לא להכריח לשים כלום:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
publicclassPerson
{
publicstring Name
{
get;
set;
}
publicstring LastName
{
get;
set;
}
publicint Age
{
get;
set;
}
}
בכל מקרה, שתי האופציות מאפשרות למשתמשים שלנו למנוע כפילות מיותרת.
מה שחשוב, זה שההגדרה האוטומטית מתבצעת בנוסף לאפשרות קינפוג, כלומר עדיין אפשר לעשות משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[DbTable(Name = "PEOPLE")]
publicclassPerson
{
publicstring Name
{
get;
set;
}
[DbColumn(Name = "LAST_NAME")]
publicstring LastName
{
get;
set;
}
publicint Age
{
get;
set;
}
}
אבל אין חובה לעשות זאת אם השמות זהים!
למה זה טוב?
הקונבנציה בד"כ מבוססת על Best Practices, ועוזרת למתכנתים לעשות עיצוב טוב יותר
זה יוצר תבנית קבועה, כך שאם מישהו חדש מגיע לקוד (אבל מכיר את הקונבנציה), הוא אמור להבין דברים בצורה קלה יותר
זה מאפשר למתכנתים לתכנת ברמת אבסטרקציה גבוהה יותר, בלי צורך לבצע הרבה משימות שדורשת הקונפיגורציה
כמו שהשם מרמז, הקונבנציה היא רק בונוס, שמנסה לעזור. עדיין אפשר קיימת אופציה לקינפוג.
הבעיה העיקרית עם פרדיגמה זו היא לאנשים שחדשים לשפה/לFramework שלנו. הם עשויים לפספס את הפרטים אודות הקונבנציה ולקבל התנהגות מוזרה, או לחלופין להבין מה חלק מהקוד עושה (או איך העיצוב בדיוק עובד).
אמנם כתבתי בפעם הקודמת שזה כנראה הפוסט האחרון בסדרה, אבל מאחר וחשבתי על עוד איזשהו רעיון, להלן עוד טיפ על מימוש Fluent Syntax.
ראינו בעבר כי אחד הדברים השימושיים והיפים בC# הוא היכולת להוסיף Extension Methods לInterfaces (טיפ מספר 84).
מה שאולי לא טריוויאלי הוא העובדה שאפשר לנצל עובדה זו גם עבור Fluent syntax:
נניח שיש לנו את הFluent Syntax הטיפ מסובך שיצרנו בחלק 2.5 של הסדרה:
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
publicinterfaceIQ1
{
IQ2<T> WithColumn<T>(string name);
}
public interface IQ2<T>
{
IQ2<S> WithColumn<S>(string name);
IQ3<T> MakePrimaryKey();
IQ4 WithDefaultValue(T value);
}
public interface IQ3<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
publicinterfaceIQ4
{
IQ5 MakePrimaryKey();
IQ2<S> WithColumn<S>(string name);
}
publicinterfaceIQ5
{
IQ6<T> WithColumn<T>(string name);
}
public interface IQ6<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
נוכל להוסיף Extension Method לInterfaceים. לדוגמה, יש לנו את הפונקציה WithColumn שיוצרת לנו עמודה באיזשהו שם וטיפוס. יש לנו את הפונקציה MakePrimaryKey שהופכת את העמודה גם להיות Primary Key של הטבלה.
למה זה בעצם לא מתקמפל? הסיבה לכך היא שמאחר והטיפוס IQ2 הוא בעל פרמטר גנרי, יש בידינו שתי אפשרויות: או שכל הטיפוסים יזוהו ע"י הקומפיילר בזמן קימפול באופן Implicitly, או שאנחנו נציין Explicitly את כל הפרמטרים בזמן קימפול.
מאחר ויש כאן פרמטר שלא יכול להיות מזוהה בזמן קימפול באופן Implicitly (זה הפרמטר בשם T), עלינו לציין את כל הפרמטרים בזמן קימפול באופן Explicitly.
פתרון אפשרי לבעיה יכול להיות למשל שIQ2 ירש מטיפוס לא גנרי (למשל IQ1).
(כנראה זה הפתרון הנכון, וגם עדיף שIQ4 יירש מIQ1, ככה גם יהיה לנו רק Extension Method בודד, במקום 3 שצוינו מעלה)
השאלה היא האם הדבר הזה טוב? למה צריך את זה בכלל? מצד אחד זה טוב, מאחר וזה מאפשר לנו להוסיף עוד Syntax לFluent Syntax קיים וכך לעשות את הFluent Syntax אפילו טוב יותר!
מצד שני, זה יכול להיות קשה להוסיף Fluent Syntax משלנו (למשל, אולי נצטרך להוסיף עוד ממשקים/מחלקות), ואם הFluent Syntax כבר שייך לנו, אז מה אכפת לנו להוסיף לו כבר עוד פונקציות?
לדעתי אין תשובה חד משמעית. אם הFluent Syntax לא שייך לנו (למשל שייך לאיזשהו Open Source כמו Rhino Mocks או Ninject), אז הדבר הזה מאפשר לנו להרחיב את הSyntax כך שיתאים לצרכים שלנו. אם הSyntax שייך לנו, אין לזה כל כך יתרון מלהוסיף את הפעולות הרלוונטיות לממשקים של הSyntax, אבל אולי זה בכל זאת יכול להתאים: למשל, במקרה כאן, כל מי שאי פעם ירש מהממשקים שציינתי כאן, יקבל חינם את הפונקציהWithPrimaryKeyColumn, כך שאולי זה כן שווה את זה…
טוב, זה כנראה הפוסט האחרון בסדרה, אלא אם כן אני אחשוב על משהו מעניין שלא כתבתי, או שמישהו יצביע לי על משהו כזה.
מה היה לנו עד כה?
הכרנו מה זה Fluent Syntax. ראינו איך מממשים כזה בעצמנו. ראינו שיש לנו את הפריבילגיה של לשמור Stateים לגבי הפעולות שביצענו, כדי שנציג אותן במידת הצורך. ראינו שאפשר להשמיט את המתודות של Object כדי שלא יפריעו למשתמשים שלנו להשתמש בSyntax שלנו. ראינו שגם לא קשה לממש Fluent Syntax, מאחר ואנחנו מסוגלים לממש מחלקה בודדת.
משהו שאני מרגיש שלא כל כך ברור, הוא מה לעשות כאשר יש לנו פעולה שמתמשכת על גבי כמה מתודות בSyntax:
למשל בטיפ הראשון ראינו משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
dataSet.
AddTable("DEPARTMENT").
WithColumn<int>("PK").
MakePrimaryKey().
WithColumn<string>("NAME").
AddTable("EMPLOYER").
WithColumn<string>("NAME").
WithColumn<int>("DEPARTMENT_FK").
RelatedTo("DEPARTMENT").
WithParentKey("PK").
WithChildKey("DEPARTMENT_FK").
Named("EMPLOYER_TO_DEPARTMENT");
זו פעולה בודדת שנפרשת על כמה מתודות, שהרי היא מתרגמת למשהו כזה:
באחת הפעמים הראשונות ראינו שבמימוש מסוים של Fluent Syntax אנחנו יוצרים מחלקה לכל ממשק, ולכן יש לנו הרבה מאוד ממשקים $ \impliedby $ יש לנו הרבה מאוד מחלקות $ \impliedby $ יש לנו הרבה מאוד קוד.
כמובן, זה מצב לא רצוי (שכידוע, פחות קוד, זה יותר טוב).
אחת האופציות לפתור את הבעיה היא הבאה:
נניח שיש לנו את הממשקים הבאים:
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
publicinterfaceIQ1
{
IQ2<T> WithColumn<T>(string name);
}
public interface IQ2<T>
{
IQ2<S> WithColumn<S>(string name);
IQ3<T> MakePrimaryKey();
IQ4 WithDefaultValue(T value);
}
public interface IQ3<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
publicinterfaceIQ4
{
IQ5 MakePrimaryKey();
IQ2<S> WithColumn<S>(string name);
}
publicinterfaceIQ5
{
IQ6<T> WithColumn<T>(string name);
}
public interface IQ6<T>
{
IQ5 WithDefaultValue(T value);
IQ6<S> WithColumn<S>(string name);
}
במקום לממש מחלקה עבור כל ממשק, נוכל לכתוב מחלקה אחת שמממשת את כל הממשקים!:
טוב, זה קצת ארוך אבל הרעיון הוא מאוד פשוט! יש שלוש פונקציות שאנחנו מממשים ושאר הפונקציות קוראות לפונקציות הנ"ל.
הפונקציות האלה הן SetDefaultValue, MakePrimaryKey וAddColumn. הן מחזירות לנו Instance מתאים של המחלקה שלנו (את הInstance הנוכחי, אם אנחנו עדיין עובדים על אותה עמודה, או Instance חדש אם עברנו לעבוד על עמודה חדשה)
בנוסף, הן עושות את הפעולה שאנחנו מצפים שהן יעשו.
למה בעצם יש פה שתי מחלקות? זוכרים את הExtension Method הזה?
הוא מחזיר Instance של IQ1, שזה בסה"כ ממשק המציין פעולה על טבלה (כלומר, הוספת עמודה). מאחר וזה קורה לפני שעוד יש לנו עמודה ביד, זה מרגיש קצת לא נכון לממש את IQ1 כבר בTableOrColumnDefinitionSyntax<T>. למה? כי מה יהיה הפרמטר הגנרי? אפשר לשים בפרמטר הגנרי כל דבר, אבל זה מרגיש לי קצת לא נכון.
בכל מקרה, עכשיו אחרי שיש לנו את המימוש הזה, אפשר לממש בכיף את הExtension Method הזה: