190. DataTime ParseExact

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

הדרך הפופולרית לעשות זאת היא באמצעות הפונקציות ToString וDateTime.Parse.

למשל:

1
2
DateTime rememberRemember = new DateTime(1997, 11, 5);
string rememberRememberString = rememberRemember.ToString();

ופרסור חזרה מהמחרוזת:

1
2
DateTime rememberRemember =
DateTime.Parse(rememberRememberString);

הדוגמה הזאת בדרך כלל עובדת, אבל היא יכולה גם לא לעבוד.

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

למשל, בארצות הברית כותבים את ה5 בנובמבר 1997 בצורה הבאה:

11/05/1997

ואילו בארץ כך:

05/11/1997

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

מה שיקרה זה אחד מהשניים:

  1. המחשב השני יקרא את התאריך לא נכון (למשל, יקרא ה11 למאי, במקום ה5 לנובמבר)
  2. המחשב השני יזרוק Exception כיוון שהתאריך לא תקין לפי הפורמט שהוא מצפה לקבל לפי הגדרות המחשב (למחשב, אם התאריך הוא ה13 בנובמבר, הוא ינסה לקרוא אותו בתור ה11 לחודש ה13 ויזרוק Exception)

בעיה זו היא בעיה די נפוצה בעבודה עם מחרוזות המייצגות תאריכים.

איך אפשר להתגבר על הבעיה? במקום להשתמש בToString וDateTime.Parse שלא מקבלים פרמטרים ומשתמשים בהגדרות של המחשב, נוכל להשתמש בOverload של ToString שמקבל פרמטר המציין כיצד לכתוב את הstring:

1
2
string rememberRememberString =
rememberRemember.ToString("dd/MM/yyyy HH:mm:ss");

ובפונקציה ההפוכה ששמה DateTime.ParseExact:

1
2
3
4
DateTime rememberRemember =
DateTime.ParseExact(rememberRememberString,
"dd/MM/yyyy HH:mm:ss",
null);

ככה נוכל להיות בטוחים שתאריכים שלנו נקראים ונכתבים למחרוזות בצורה הנכונה.

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

סוף שבוע המתפרסר בצורה מדויקת טוב

שתף

189. String.Split

עוד פונקציה נחמדה של string היא Split.

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

למשל,

1
2
string filePath =
@"\\netshare\first\second\third\fourth\textfile.txt";

נוכל לפצל אותו לפי התו \ ולקבל:

1
2
string[] tokens = filePath.Split('\\');
// tokens = new[] {"", "", "netshare", "first", "second", "third", "fourth", "textfile.txt"};

בנוסף אפשר לפצל לפי מספר תווים, למשל לפי התווים \ ו. ולקבל:

1
2
string[] tokens = filePath.Split('\\', '.');
// tokens = new[] {"", "", "netshare", "first", "second", "third", "fourth", "textfile", "txt"};

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

1
2
string[] tokens = filePath.Split(new char[] {'\\', '.'},StringSplitOptions.RemoveEmptyEntries);
// tokens = new[] {"netshare", "first", "second", "third", "fourth", "textfile", "txt"};

בנוסף, אפשר להעביר גם פרמטר המציין לכמה איברים לכל היותר אנחנו מעוניינים לפצל (אם למשל מעניינים אותנו רק 3 האיברים הראשונים). האיבר האחרון הוא מה שנשאר ללא פיצול:

1
2
string[] tokens = filePath.Split(new char[] {'\\', '.'}, 4, StringSplitOptions.RemoveEmptyEntries);
// tokens = new[] {"netshare", "first", "second", @"third\fourth\textfile.txt"};

אפשר גם לפצל לפי מחרוזות שלמות:

1
2
string[] tokens = filePath.Split(new string[] {"ir", "le"},StringSplitOptions.RemoveEmptyEntries);
//tokens = new[] {@"\\netshare\\f", @"st\second\th", @"d\fourth\textfi", @".txt"};

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

למשל, מימוש XPath, או מציאת כל השורות של טקסט מסוים (ע"י פיצול באמצעות Environment.NewLine, ראו גם טיפ מספר 184)

למעשה, אפשר לומר שזו הפעולה ההפוכה לstring.Join. ראו גם טיפ מספר 9.

המשך יום מתפצל לטובה

שתף

188. TrimStart, TrimEnd, Trim

בהמשך לטיפים הקודמים,

נניח שיש לנו מחרוזת כזאת:

1
string header = "====================Welcome!====================";

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

הפונקציות ההפוכות לPadLeft וPadRight בהתאמה הן TrimStart וTrimEnd.

למשל,

1
2
3
4
5
6
7
8
string headerLeftTrim = header.TrimStart('=');
// Welcome!====================
string headerRightTrim = header.TrimEnd('=');
// ====================Welcome!
string headerFullTrim = headerRightTrim.TrimStart('=');
// Welcome!

הפונקציות מקבלות params (ראו גם טיפ מספר של 135) תווים ומקזזות רצף שמכיל רק אותם מתחילת/סוף המחרוזת בהתאמה.

בנוסף, קיימת הפונקציה Trim שמבצעת קיזוז משני הצדדים:

1
2
string headerFullTrim = header.Trim('=');
// Welcome!

אם לפונקציות הנ"ל נקרא ללא ציון פרמטרים, יקרא overload הקורא המסיר את כל הWhitespaceים: (למשל רווחים, ירידות שורה, טאבים וכו’)

הסיבה לכך היא שזה השימוש הנפוץ של פונקציות אלה, למשל בפרסור מחרוזות שמכניס משתמש וכו’.

המשך יום מקוזז טוב

שתף

187. String repeat constructor

ראינו פעם שעברה את הפונקציות PadLeft וPadRight.

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

==================================

נראה שאפשר לעשות את זה ככה:

1
string header = string.Empty.PadLeft(20, '=');

או ככה:

1
string header = "=".PadLeft(20, '=');

אפשר במקום להשתמש בConstructor המתאים:

1
string header = new string('=', 20);

הConstructor מקבל תו ומספר חזרות.

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

המשך יום עם חזרות טוב

שתף

186. string PadLeft, string PadRight

מדי פעם אנשים כותבים קוד כזה:

1
2
3
4
5
6
7
8
string header = "Welcome!";
for (int i = 0; i < 20; i++)
{
header = "=" + header + "=";
}
// ====================Welcome!====================

במקום זאת אפשר להשתמש בפונקציות PadLeft וPadRight המדפנות את המחרוזת שלנו משמאל או מימין בהתאמה בתו שאנחנו מציינים:

1
2
3
4
5
string header = "Welcome!".PadLeft(20, '=');
// ============Welcome!
string header = "Welcome!".PadRight(20, '=');
// Welcome!============

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

לדוגמה, כאן הושלמה המחרוזת ל20 תווים ע"י הוספת התו = מספר נדרש של פעמים משמאל/מימין.

כך שאם נרצה לעשות משהו כמו בדוגמה הראשונה, נצטרך לעשות משהו כזה:

1
2
3
4
5
string welcome = "Welcome!";
string header =
welcome.PadRight(20 + welcome.Length, '=') // Add 20 characters
.PadLeft(40 + welcome.Length, '='); // Add 20 more characters
// ====================Welcome!====================

המשך יום לימין שור טוב

שתף

185. Modulo works on floats!

רובנו תכנתנו בעבר בשפת C.

לכן כאשר עברנו לשפת C# סביר להניח שהנחנו שאנחנו מכירים את כל הפעולות האלמנטאריות על פרימיטיביים.

ההתנהגות באמת זהה ברוב האופרטורים, אלא שיש משהו שכנראה לא שמנו לב אליו:

אופרטור המודולו (%) עובד על floatים!

לדוגמה:

1
2
3
4
5
double pi = Math.PI; // 3.14159265...
double piFraction = pi%1; // 0.14159265...
double anotherFraction = pi%2; // 1.14159265...

ההגדרה של מודולו (לa וb חיוביים) היא כהלן:

$ a \% b=\begin{cases} a & a<b\\ \left(a-b\right) \%b & a\ge b \end{cases} $

כלומר הפעולה מחזירה לנו את האיבר הראשון שקטן מb וההפרש בינו לa הוא כפולה שלמה של b.

הדבר שימושי בעיקר לחישובים דברים מחזוריים, למשל חישוב סינוס של מספר – מאחר וסינוס פונקציה מחזורית עם מחזור $ 2 \pi $ אפשר לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static double Sin(double arg, int numOfTerms)
{
arg = arg%(2*Math.PI);
int currentFactorial = 1;
int minus = 1;
double currentPower = arg;
double result = 0;
for (int i = 2; i <= numOfTerms; i+=2)
{
result += minus * currentPower / currentFactorial;
currentFactorial *= i * (i + 1);
currentPower *= arg * arg;
minus *= -1;
}
return result;
}

זה מחשב את הסינוס גם כאשר הארגומנט הוא גדול מאוד.

סופ"ש צף טוב

שתף

184. Environment NewLine

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

יש מספרי דרכים לעשות זאת:

הדרך הראשונה היא מה שלמדנו בשפת C:

1
string myString = "A new line is \ncoming";

אבל מסתבר שצריך לכתוב ככה:

1
string myString = "A new line is \r\ncoming";

הדרך השנייה היא שימוש ב@ (ראו טיפ מספר 23):

1
2
string myString = @"A new line is
coming";

ההזחה נדפקת…

במקום זאת אפשר להשתמש בProperty הסטטי ששמו Environment.NewLine המייצג ירידת שורה. למשל:

1
2
string myString = string.Format("A new line is {0}coming",
Environment.NewLine);

הProperty שווה ל"\r\n" במערכות ההפעלה Windows.

יתרון נחמד של שימוש בProperty הזה (בנוסף לקריאות), הוא שהוא תלוי במערכת ההפעלה. כך שאם למשל נעביר קוד שכתבנו לLinux (בעזרת Mono – מימוש Open source של הFramework), לא נצטרך לשנות אותו בכל מקום במידה ונשתמש בProperty זה.

חג שמח! עם הרבה שבירות שגרה (ושורה)

שתף

183. Type EmptyTypes

בהמשך לטיפ הקודם,

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

למשל מציאת Constructor דיפולטי:

1
2
3
4
5
6
public void MyMethod(Type givenType)
{
ConstructorInfo constructorInfo =
givenType.GetConstructor(new Type[0]);
// ...
}

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

במקום זאת, מסתבר שיש Property סטטי בשם Type.EmptyTypes, שכשמו הוא, מערך ריק של Type.

כעת בכל מקום שאנחנו רוצים להעביר מערך ריק, נוכל להשתמש בו:

1
ConstructorInfo constructorInfo = givenType.GetConstructor(Type.EmptyTypes);

שיהיה המשך יום טיפוסי לא ריק טוב

שתף

182. EventArgs Empty

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

זו הפונקציה שאיתה אנחנו מקפיצים:

1
2
3
4
5
6
7
private void RaiseMyEvent(EventArgs e)
{
if (MyEvent != null)
{
MyEvent(this, e);
}
}

ואנחנו קוראים לה כך:

1
RaiseMyEvent(new EventArgs());

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

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

  1. זה מבזבז זכרון, מאחר וכל הקפצה של אירוע יוצרת אובייקט חדש
  2. זה מבזבז זמן, כי לוקח זמן לבנות את האובייקט הנ"ל

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

למרבה המזל, עשו זאת כבר בשבילנו: קיים הProperty הסטטי EventArgs.Empty, שכשמו הוא – EventArgs ריקים.

לכן במקום להקפיץ את האירוע עם יצירת אובייקט חדש, עדיף לכתוב כך:

1
RaiseMyEvent(EventArgs.Empty);

שבוע מלא אירועים בעלי תוכן טוב

שתף

181. Fast property access

נניח שיש לנו Property שאנחנו ניגשים אליו הרבה פעמים בReflection.

הפעולה הזו די יקרה ויכולה להאט משמעותית את התכנית שלנו.

אחת הדרכים שנראה שאפשר לעשות כדי לפתור בעיה זו היא להשתמש בDelegate.CreateDelegate, אלא שיש פה בעיה:

אם הפרמטרים שלנו הם לא מהטיפוס המתאים, לא נוכל ליצור Delegate כזה.

לדוגמה, נניח שיש לנו את הProperty הבא:

1
2
3
4
5
6
7
8
public class Triangle : Shape
{
public Circle BoundedCircle
{
get;
set;
}
}

אז הקריאה הבאה תעבוד:

1
2
3
4
5
6
7
8
9
10
11
12
13
PropertyInfo boundedCircleProperty =
typeof(Triangle).GetProperty("BoundedCircle");
// We get this somehow through reflection
Func<Triangle, Circle> getBoundedCircle =
(Func<Triangle, Circle>)
Delegate.CreateDelegate(typeof(Func<Triangle, Circle>),
boundedCircleProperty.GetGetMethod());
Triangle myTriangle = new Triangle();
object boundedCircle =
getBoundedCircle(myTriangle);

אבל מה הבעיה? הבעיה היא שאנחנו לא מכירים את הטיפוס Triangle. זהו טיפוס שהתקבל בReflection. אם היינו מכירים אותו, היינו לעשות הסבה ולגשת לProperty ששמו BoundedCircle בעצמנו במהירות סבירה.

אם ננסה לעשות משהו כזה, נחטוף כמובן Exception:

1
2
3
4
Func<Shape, Circle> getBoundedCircle =
(Func<Shape, Circle>)
Delegate.CreateDelegate(typeof(Func<Shape, Circle>),
boundedCircleProperty.GetGetMethod());

כי הפונקציה מצפה לקבל Triangle ולא Shape.

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

הגישה הזאת תראה ככה:

1
2
Expression<Func<Shape, Circle>> boundedCircleExpression =
x => ((Triangle) x).BoundedCircle;

מה שמתמקפל למשהו כזה:

1
2
3
4
5
6
7
8
9
10
11
ParameterExpression parameter =
Expression.Parameter(typeof(Shape), "x");
PropertyInfo boundedCircleProperty =
typeof(Triangle).GetProperty("BoundedCircle");
Expression<Func<Shape, Circle>> boundedCircleExpression =
Expression.Lambda<Func<Shape, Circle>>
(Expression.Property
(Expression.Convert(parameterX, typeof(Triangle)), boundedCircleProperty),
new ParameterExpression[] { parameterX });

נוכל ליצור פונקציה שיוצרת ככה Delegate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Func<TSource, TResult> GetProperty<TSource, TResult>(PropertyInfo property)
{
ParameterExpression parameterX = Expression.Parameter(typeof(TSource), "x");
Expression<Func<TSource, TResult>> propertyAccessExpression =
Expression.Lambda<Func<TSource, TResult>>
(Expression.Property
(Expression.Convert(parameterX, property.ReflectedType), property),
new ParameterExpression[] { parameterX });
Func<TSource, TResult> propertyAccess = propertyAccessExpression.Compile();
return propertyAccess;
}

ולהשתמש בו:

1
2
3
4
5
6
7
8
9
PropertyInfo boundedCircleProperty =
typeof(Triangle).GetProperty("BoundedCircle");
Shape myTriangle = new Triangle();
Func<Shape, Shape> boundedCircleAccess =
GetProperty<Shape, Shape>(boundedCircleProperty);
Shape boundedCircle = boundedCircleAccess(myTriangle);

במהירות סבירה.

נוכל לעשות גם הסבה נוספת בפונקציה במקרה הצורך (למשל במקרה של boxing לobject) בצורה הזאת:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Func<TSource, TResult> GetProperty<TSource, TResult>(PropertyInfo property)
{
ParameterExpression parameterX = Expression.Parameter(typeof(TSource), "x");
Expression<Func<TSource, TResult>> propertyAccessExpression =
Expression.Lambda<Func<TSource, TResult>>
(Expression.Convert
(Expression.Property
(Expression.Convert(parameterX, property.ReflectedType),
property),
typeof (TResult)),
new ParameterExpression[] {parameterX});
Func<TSource, TResult> propertyAccess = propertyAccessExpression.Compile();
return propertyAccess;
}

סופ"ש דינאמי מצוין

שתף