אחד החסרונות של מאפיינים הוא ההסתרה (מבחינה תחבירית) של פעולת חישוב. פעולת גישה למשתנה רגיל היא זולה מאוד, מבחינת זמן חישוב, אך פעולת גישה למאפיין עשויה לדרוש חישוב בלתי מוגבל בזיכרון או בזמן; השפה לא מונעת יצירה של לולאה אינסופית בתוך מאפיין. מהסיבה הזאת אין זה מקובל להגדיר מאפיינים המבצעים חישובים מורכבים.
העמסת אופרטורים.
C# מאפשרת העמסת אופרטורים - שהיא טכניקה המאפשרת לתת משמעות חדשה לרוב האופרטורים הקיימים בשפה, על מנת להקל על הקריאות של התוכנית. את האופרטור "[ ]" מעמיסים בצורה מעט שונה מהאחרים, בעזרת מנגנון הנקרא סדרן - Indexer הדומה בכתיבתו למאפיין:
private int arr = new int[100];
public int this[int i]
רשומות (Records).
החל מגרסה 9 של השפה, נוספה תמיכה ברשומות. רשומה היא טיפוס ייחוס שדומה למחלקה, שבה אין שיטות אלא רק מאפיינים פומביים - ואת אלה ניתן להגדיר אך ורק בזמן היצירה של הרשומה.
את הרשומה מגדירים באופן הבא:
public record Student(string Name, int Age);
כדי ליצור רשומה פשוט משתמשים ב-new, בדומה למחלקה:
Student stu1 = new Student("First Student", 22);
Student stu2 = new Student("Second Student", 24);
השוואה בין שתי רשומות נעשית באמצעות האופרטור == או באמצעות השיטה Equals, והשוואה זו, בניגוד לטיפוסי ייחוס אחרים (כגון מחלקות), משווה את הערכים ברשומה ולא את מיקום המצביע. לכן, אם ניצור שתי רשומות שונות עם פרמטרים זהים, ונשווה ביניהן, תוצאת ההשוואה תהיה true.
תכנות מונחה אירועים.
האירוע בשפה הוא למעשה מאפיין נוסף של המחלקה, ונחשב לחלק מהעצם כמו השדה והשיטה.
event (אירוע) הוא טיפוס Delegate המובנה בשפה בעל הרשאות גישה מיוחדות: ניתן לרשום לאירוע פונקציות גם מחוץ למחלקה בה הוא מוגדר, אך הקריאה אליהן מתבצעת רק מתוך המחלקה. ה-Delegate עצמו הוא מחלקה שמכילה רשימה של הפניות לפונקציות, שניתן באמצעותה להזניק פונקציות ממקומות שונים.
ה-event משמש בשפת #C לתמיכה ישירה בתכנות מונחה אירועים. כדי שיהיה אפשר לתקשר ולהעביר הודעות ומידע בין מחלקות שונות, (בעקבות שפת וויז'ואל בייסיק). במחלקה המתארחת ניתן ליצור אירועים בחותמות שונות, ולהזניק אותם במקום הרלוונטי במחלקה באמצעות קריאה בשמם, ובמחלקה המארחת ניתן ללכוד אירועים אלו, באמצעות רישום מוקדם של פונקציה מקומית (בעלת חתימה זהה) לאירוע של האובייקט לאחר ההכרזה עליו. התוצאה היא שבעת שאירוע מוזנק במחלקה המתארחת, הוא גורם למעשה להרצת הפונקציה במחלקה המארחת.
בדוגמה מוגדר event מטיפוס MultiplicationEventHandler במחלקה CalculationEvent, לאחר מכן מתבצע במחלקה המארחת HostCalculationEvent יצירת מופע של המחלקה ורישום האירוע MultiplicationEventHandler לפונקציה testCalculation_MultiplicationNum, כך שבעת הזנקה שלו תופעל פונקציה זו.
במחלקה המתארחת : CalculationEvent
class CalculationEvent
public delegate int MultiplicationEventHandler(int x, int y);
public event MultiplicationEventHandler MultiplicationNum;
public int RaiseCalc(int x, int y)
return MultiplicationNum(x, y);
במחלקה המארחת: HostCalculationEvent
class HostCalculationEvent
CalculationEvent testCalculation = new CalculationEvent();
public HostCalculationEvent()
testCalculation.MultiplicationNum += new CalculationEvent.MultiplicationEventHandler(testCalculation_MultiplicationNum);
int testCalculation_MultiplicationNum(int x, int y)
return x * y;
תכנות פונקציונלי.
נציג (Delegate) הוא טיפוס המובנה בשפה, המגדיר חתימה של פונקציה, וכל מופע שלו מחזיק רשימה של עצמים ברי-קריאה המתאימים לחתימה זאת. קריאה לנציג יוזמת קריאה של כל האובייקטים המוחזקים בו. ניתן לרשום (Subscribe) פונקציות לנציג בעזרת אופרטור =+, ולקרוא להן בעזרת תחביר רגיל של קריאה לפונקציה.
דוגמה לתוכנית "שלום עולם" בעזרת נציגים:
class HelloWorld
public delegate void Del();
static void Main(string[] args) {
Del x = delegate { Console.Write("hello "); };
x += delegate { Console.WriteLine("world"); };
טיפוס בסיסי זה מאפשר להשתמש בטכניקות של תכנות פונקציונלי. למשל הוא מאפשר לקרוא לפונקציה לא מוכרת במחלקה, ליצור אירועים, ולהפעיל תהליכון (Thread).
ישנם מספר מימושים גנריים ל-Delegates בספריה של #C:
דוגמאות:
Action&lt;&gt; printHello = delegate { Console.Write("hello world!"); };
Predicate&lt;int&gt; range255 = delegate(int x) {return x&gt;=0 &amp;&amp; x&lt;=255;};
Func&lt;string, string&gt; convert = delegate(string s) {return s.ToUpper();};
פונקציות אנונימיות וביטויי למדא.
החל מגרסה 2 שפת #C תומכת במתודות אנונימיות - פונקציה הנכתבת בשורת פקודה, ומוגדרת בזמן ההרצה עצמה. החל מגרסה 3 נוספה תמיכה בביטויי למדא לשפה. זוהי דרך נוספת תמציתית וגמישה לכתוב מתודה אנונימית. שימוש בפונקציות אנונימיות נעשה כאשר רוצים להשתמש בפונקציה באופן מקומי מבלי להגדירה במחלקה, או כאשר פרמטר של פונקציה מוגדר כפונקציה, ואז ניתן לכתוב את הפונקציה האנונימית בקריאה לפונקציה, מבלי להגדירה כלל כפונקציה במחלקה.
בדוגמה מימוש של הפונקציה שלעיל באמצעות ביטוי למדא. (האופרטור &lt;= מפעיל הלמדא, מפריד בין הפרמטרים של הקלט בצידו השמאלי לגוף הלמדא מצידו הימני)
Func&lt;string, string&gt; Convert = x =&gt; x.ToUpper();
בביטויי למדא נעשה שימוש רב ב-LINQ. בדוגמה איתור גודל המילה הקטנה ביותר במערך מחרוזתי מתקבל במשתנה shortestWordLength.
string[] wordsArray = {"Orange", "Apple", "Pineapple", "Pear", "Peach"};
int shortestWordLength = wordsArray.Min(w =&gt; w.Length);
תכנות גנרי.
השפה תומכת בפולימורפיזם פרמטרי: ניתן להגדיר מחלקה המקבלת טיפוסים כפרמטרים:
public class GenericList&lt;T&gt; {
ניתן להעביר כפרמטר כל טיפוס - פרימיטיבי, טיפוס-ערך או טיפוס-התייחסות:
class TestGenericList {
static void Main() {
// Declare a list of type int.
GenericList&lt;int&gt; list1 = new GenericList&lt;int&gt;();
// Declare a list of type string.
GenericList&lt;string&gt; list2 = new GenericList&lt;string&gt;();
// Declare a list of type ExampleClass.
GenericList&lt;ExampleClass&gt; list3 = new GenericList&lt;ExampleClass&gt;();
ניתן להגדיר אילוצים של טיפוסים באמצעות משפט where, כך למשל ניתן להגדיר שמחלקה מסוימת תהיה רק מטיפוס מחלקה, מבנה או טיפוס קונקרטי. וניתן לתת ערכי ברירת מחדל למשתנים הגנריים באמצעות המילה default:
לדוגמה מבטיח כי הטיפוס הפרמטרי יהיה "ניתן להשוואה", בכך שמאלצים אותו לרשת מהממשק המתאים.
מנגנון הגנריות ב-#C איננו מתבצע בעזרת תבניות בזמן הידור (כמו ב-++C) ואיננו נעלם בזמן ריצה (כמו בג'אווה) אלא מתבצע בזמן טעינה, וכל אובייקט שומר בתוכו מידע על הפרמטרים בעזרתם הוא נוצר גם בזמן ריצה, על מנת לאפשר שיקוף ובטיחות טיפוסים.
ניתן להעביר רק טיפוסים (ולא קבועים מספריים, למשל, כמו ב-++C). מטיפוסים אלה ניתן לדרוש דברים מסוימים, כגון ירושה ממחלקה או ממשק מסוים, או קיומו של בנאי נטול פרמטרים.
לא ניתן לבצע התמחות של מחלקה גנרית עבור טיפוס ספציפי. כתוצאה מכך, כוח החישוב של מנגנון הגנריות ב-#C חלש יותר משל התבניות בשפת ++C, והוא איננו מאפשר להגדיר חישובים מורכבים שיתבצעו בזמן הידור או טעינה - הוא איננו שלם-טיורינג; כל תפקידו הוא יצירה של טיפוסים, משתנים, נציגים ופונקציות בעזרת פרמטרים (במילים אחרות: הוא מאפשר תכנות גנרי, אך לא מטא-תכנות). חולשה זו מאפשרת להדר ולטעון קוד גנרי מהר יותר.
תכנות דינמי.
על אף שהשפה היא במהותה שפה סטטית, החל מגרסה 4 נוספה לה תמיכה בתכנות דינמי, דהיינו הגדרת האובייקטים כמו משתנים, מאפיינים, שיטות ואירועים בזמן ריצה, ולא בזמן ההידור. לצורך השימוש בתכנות דינמי נוסף טיפוס חדש מסוג dynamic.
בדוגמה הבאה ניתן לראות שטיפוס המשתנה d1 משתנה בעקבות ההשמה של התוכן, ממחרוזת למספר שלם.
dynamic d1 = "STRING";
Console.WriteLine(d1.GetType());
// Prints "System.String".
d1 = 60;
Console.WriteLine(d1.GetType());
// Prints "System.Int32".
ונוספו מחלקות חדשות לטיפול דינמי (באמצעות מרחב System.Dynamic). המחלקה DynamicObject שבאמצעותה ניתן להוסיף ולשנות מאפיינים תוך כדי ריצה, ובאמצעות המחלקה ExpandoObject שנותנת טיפול כולל יותר, ניתן להוסיף למחלקה בזמן ריצה כל אובייקט, מאפיין, שיטה ואף אירוע. מחלקה זו תומכת ב-LINQ ובתחשיב למדא. דרך נוספת לתכנות דינמי היא באמצעות יכולת השיקוף של ה-#C (אם כי דרך זו מורכבת יותר למימוש).
בדוגמה הבאה נוצרת באמצעות ExpandoObject מחלקה בזמן ריצה שיש לה שני מאפיינים (בשמות Name מסוג מחרוזת ו-Age מסוג מספרי) ושיטה אחת (בשם Increment) שמוסיפה 5 לערך שקיים במאפיין Age. בסוף הדוגמה יוצג בחלון Output התוכן הבא:
Name: John Smith, Age: 20
using System.Dynamic;
dynamic employee = new ExpandoObject() ;
employee.Name = "John Smith";
employee.Age = 15;
employee.Increment = (Action)(() =&gt; { employee.Age+=5; });
employee.Increment();
Console.WriteLine("Name: " + employee.Name + " " + ", Age: " + employee.Age);
התמיכה מאחורי הקלעים בתכנות הדינמי נעשית באמצעות מהדר אחר ה-DLR ולא ה-CLR. בשל כך באובייקטים דינמיים לא ניתן להשתמש בתכונת השיקוף ולא ב-Intellisense.