Pattern matching בודק ערך מול צורה: "האם זה Circle?", "האם זה null?", "האם ה-Status שלו הוא "paid" וה-Total שלו מעל 100?". כשהבדיקה מצליחה, התבנית יכולה גם לחלץ חלקים מהערך למשתנים חדשים. זה התחיל בקטן ב-C# 7 עם האופרטור is וגדל בכל גרסה מאז, ולכן הדף הזה מציין את הגרסה של כל צורה.
תבנית הטיפוס עם is (C# 7)
לפני C# 7, בדיקת טיפוס ושימוש בו דרשו שני צעדים: as ועוד בדיקת null, או is ועוד המרה. תבנית הטיפוס עושה את שניהם בבת אחת ונותנת לכם משתנה מהטיפוס הנכון:
פלט:
card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5
p is CardPayment card נכון רק כש-p הוא CardPayment שאינו null (או מחלקה שנגזרת ממנו), ואז card מחזיק אותו עם הטיפוס הסטטי הנכון. ערך null אף פעם לא מתאים לתבנית טיפוס, ולכן התשלום שהוא null ממשיך הלאה עד לבדיקת ה-null.
טווח והשמה ודאית
משתנה התבנית קיים בבלוק העוטף, אבל הקומפיילר מאפשר לכם לקרוא אותו רק היכן שידוע שההתאמה הצליחה. זה מה שמאפשר את צורת "סעיף השמירה", שבה יוצאים מוקדם ומשתמשים במשתנה אחר כך:
פלט:
19.99
42 is over 40
price must be text
boxed is int n && n > 40 מראה את הצורה הנפוצה השנייה: המשתנה שמיש מימין ל-&&, כי הצד הזה רץ רק כשההתאמה הצליחה. מימין ל-|| הוא לא מוגדר, והקומפיילר אומר את זה (CS0165).
תבניות גם מבצעות unboxing: boxed is int n מצליח עבור int ארוז ונותן לכם את הערך בלי המרה שעלולה לזרוק חריגה. בטיפוסי ערך nullable, maybe is int n מצליח בדיוק כשיש ב-maybe ערך.
תבניות קבוע ו-null
גם קבוע הוא תבנית. x is null היא הנפוצה ביותר, ו-C# 7 מתירה גם x is 0, status is "paid" או איבר של enum:
if (customer is null) return; // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();
צוותים רבים מעדיפים את is null על פני == null כי העמסת אופרטור על הטיפוס לא יכולה להסיט אותו. C# 9 מוסיפה את הצורה השלילית, is not null.
תבניות בהוראות switch (C# 7)
אותן תבניות טיפוס יכולות לשמש תוויות של case, וסעיף when מוסיף תנאי. ה-cases נבדקים מלמעלה למטה, אז סדרו אותם מהספציפי לכללי:
static string Describe(object o)
{
switch (o)
{
case null: return "nothing";
case int n when n < 0: return "negative number";
case int n: return $"number {n}";
case string s when s.Length == 0: return "empty text";
case string s: return $"text of {s.Length} chars";
case IEnumerable<int> list: return "a sequence of numbers";
default: return o.GetType().Name;
}
}
Describe(-4); // negative number
Describe(""); // empty text
Describe(new List<int>()); // a sequence of numbers
Describe(2.5); // Double
הקומפיילר בודק את הסדר: case שלעולם לא יכול להתאים כי case קודם מכסה אותו הוא שגיאה CS8120. default תמיד נבדק אחרון, לא משנה איפה הוא מופיע. אותה לוגיקה כשהיא כתובה עם שרשראות if/is, כמו בדוגמת התשלומים למעלה, עובדת בכל גרסה של C# מ-7 והלאה.
ביטויי switch (C# 8)
ביטוי switch הוא הצורה הקומפקטית ל"חישוב ערך אחד מכמה מקרים". הערך בא קודם, אחריו switch, ואחריו זרועות של pattern => result מופרדות בפסיקים, עם _ כתופס הכול:
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
_ => throw new ArgumentException("Unknown shape", nameof(shape)),
};
Area(new Rectangle(3, 4)); // 12
אם אף זרוע לא מתאימה בזמן ריצה, נזרקת SwitchExpressionException, והקומפיילר מזהיר (CS8509) כשהוא יכול לראות שקלט כלשהו לא מטופל. הדף על switch expression מסביר את התחביר עצמו; שאר הדף הזה עוסק בתבניות שאפשר לשים בזרועות.
תבניות מאפיינים (C# 8, הורחבו ב-C# 10)
תבנית מאפיינים מתאימה את המאפיינים של אובייקט לתבניות מקוננות, בתוך סוגריים מסולסלים. האובייקט חייב גם להיות לא null, כך ש-{ } לבדו אומר "לא null":
public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);
static string Shipping(Order order) => order switch
{
{ Status: "cancelled" } => "no shipment",
{ ShipTo.Country: "PT", Total: >= 50m } => "free, domestic", // C# 10 dotted form
{ ShipTo.Country: "PT" } => "4.90, domestic",
{ Total: > 200m } => "free, international",
_ => "12.00, international",
};
Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2)); // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2)); // free, international
ב-C# 8 ו-9 נדרשת הצורה המקוננת: { ShipTo: { Country: "PT" } }. C# 10 מתירה את הקיצור עם הנקודה שבדוגמה. תבניות מאפיינים עובדות גם עם is, וזה הופך בדיקות עם כמה תנאים לקריאות כמו תיאור: if (order is { Status: "paid", Items: > 0 }).
תבניות יחס ותבניות לוגיות (C# 9)
C# 9 הוסיפה השוואות (<, <=, >, >=) ואת הצירופים and, or ו-not, שהופכים טווחים לתבניות:
static string Grade(int score) => score switch
{
< 0 or > 100 => "invalid",
>= 90 => "A",
>= 75 and < 90 => "B",
>= 50 => "C",
_ => "F",
};
Grade(82); // B
Grade(101); // invalid
if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... } // and binds tighter than or
המקבילה ב-C# 7 היא שרשרת if/else if או switch עם סעיפי when (case int s when s >= 90:).
תבניות tuple ותבניות מיקום (C# 8)
switch על כמה ערכים בבת אחת נעשה באמצעות switch על tuple:
static string Quadrant(int x, int y) => (x, y) switch
{
(0, 0) => "origin",
(> 0, > 0) => "I",
(< 0, > 0) => "II",
(< 0, < 0) => "III",
(> 0, < 0) => "IV",
_ => "on an axis",
};
Quadrant(-2, 5); // II
תבנית מיקום עושה את אותו הדבר לכל טיפוס עם מתודת Deconstruct, כולל records: case Point(0, 0): או p is Rectangle(var w, var h) && w == h.
תבניות רשימה (C# 11)
תבניות רשימה מתאימות מערכים ורשימות לפי האיברים שלהם. .. מתאים לכל מספר של איברים ("פרוסה"), וכל מיקום של איבר יכול להכיל כל תבנית אחרת:
static string Route(string[] args) => args switch
{
[] => "help",
["add", var item] => $"add {item}",
["remove", var item, ..] => $"remove {item}",
[var cmd, ..] => $"unknown command {cmd}",
};
Route(new[] { "add", "milk" }); // add milk
Route(new[] { "remove", "milk", "now" }); // remove milk
int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last); // 4
איזו גרסה הוסיפה מה
| תבנית | דוגמה | גרסה |
|---|---|---|
| תבנית טיפוס עם משתנה | o is Circle c | C# 7.0 |
| תבנית קבוע | x is null, n is 0 | C# 7.0 |
תבנית var | x is var v | C# 7.0 |
תבניות בתוויות case, שומרי when | case int n when n < 0: | C# 7.0 |
ביטוי switch, השלכה _ | x switch { ... } | C# 8 |
| תבנית מאפיינים | { Status: "paid" } | C# 8 |
| תבנית tuple | (x, y) switch { (0, 0) => ... } | C# 8 |
| תבנית מיקום | Point(0, 0) | C# 8 |
| תבנית יחס | > 100, <= 0 | C# 9 |
| תבניות לוגיות | not null, >= 1 and <= 5, 'a' or 'b' | C# 9 |
| תבנית טיפוס חשופה | case Circle:, Circle => ... | C# 9 |
| תבנית מאפיינים מורחבת | { ShipTo.City: "Porto" } | C# 10 |
| תבנית רשימה ופרוסה | [first, .., last] | C# 11 |
טעויות נפוצות
- סידור cases מהכללי לספציפי.
case Payment p:לפניcase CardPayment c:הופך את השני לבלתי ניתן להגעה (CS8120). הספציפי קודם. - ציפייה שתבנית טיפוס תתאים ל-null.
null is string sהוא false. טפלו ב-nullעם case משלו או עםis null. - קריאה של משתנה התבנית היכן שההתאמה אולי נכשלה. אחרי
a is int n || ...,nלא מוגדר (CS0165). - switch ארוכים על טיפוסים במקום מתודות וירטואליות. אם כל תת-מחלקה חדשה מחייבת לערוך את אותו switch בחמישה מקומות, ההתנהגות שייכת להיררכיית המחלקות. תבניות מצטיינות בנתונים שמגיעים מבחוץ (מבני JSON, הודעות, tuples של קלטים) ובקבוצות סגורות של טיפוסים שבשליטתכם.
- שכחה של תופס הכול בביטוי switch. קלט שלא הותאם זורק חריגה בזמן ריצה; שימו לב לאזהרה CS8509.
שאלות נפוצות
מה זה pattern matching ב-C#?
pattern matching בודק ערך מול צורה, וכשיש התאמה הוא יכול לחלץ חלקים ממנו למשתנים באותו צעד. if (shape is Circle c) בודק את הטיפוס ונותן לכם c מטיפוס מתאים; order is { Status: "paid", Total: > 100 } בודק מאפיינים. תבניות מופיעות בביטויי is, בהוראות switch ובביטויי switch.
איך משתמשים ב-is עם משתנה ב-C#?
if (obj is Customer c) { ... } בודק ש-obj הוא Customer (ולא null) ומציב אותו ב-c, מטיפוס Customer, בתוך ה-if. הוא מחליף את הזוג הישן var c = obj as Customer; if (c != null). המשתנה מוגדר בוודאות רק היכן שידוע שהבדיקה נכונה, ולכן if (!(obj is Customer c)) return; משאיר את c שמיש אחרי ה-if.
מה ההבדל בין is null ל-== null ב-C#?
x is null תמיד בודק את ההפניה עצמה. x == null קורא לאופרטור == של הטיפוס אם הוא מעמיס אחד, וזה בדרך כלל נותן אותה תשובה אבל לא מובטח. להפך, כתבו x != null, !(x is null), או x is not null מ-C# 9.
איך עובד pattern matching ב-switch עם when ב-C#?
תווית case יכולה להכיל תבנית ועוד שומר when: case Order o when o.Total > 100:. ה-case מתאים רק אם התבנית מתאימה והתנאי נכון, וה-cases נבדקים מלמעלה למטה, אז שימו את הספציפיים יותר קודם. הקומפיילר מדווח CS8120 כש-case לעולם לא יכול להתקיים כי case קודם כבר מכסה אותו.
איזו גרסה של C# הוסיפה אילו תבניות?
C# 7.0: תבניות טיפוס, קבוע ו-var ב-is וב-case, ועוד when. C# 8: ביטויי switch, תבניות מאפיינים, tuple ומיקום. C# 9: תבניות יחס (> 5), לוגיות (and, or, not) ותבניות טיפוס חשופות. C# 10: תבניות מאפיינים מורחבות ({ Address.City: "Porto" }). C# 11: תבניות רשימה ([1, .., var last]).