Appendix E - C# Cheatsheet
This cheatsheet provides a concise overview of C# up to version 14, focusing on core language fundamentals, data structures, and object-oriented programming principles. It includes new features introduced in C# 12, C# 13, and C# 14, with special attention to the latest language enhancements.
1. Core Language Fundamentals
1.1 Variables and Data Types
Variables in C# must be declared before they are used. C# is a statically-typed language, meaning the type of a variable is known at compile time.
Common Data Types:
- Value Types:
int,double,float,char,bool,decimal,struct,enum - Reference Types:
string,object,class,interface,delegate,array
// Value types
int age = 30;
double price = 19.99;
bool isActive = true;
char initial = 'J';
// Reference types
string name = "John Doe";
object obj = null;
Nullable Value Types:
Allow value types to hold null.
int? nullableInt = null;
double? nullableDouble = 12.5;
if (nullableInt.HasValue)
{
Console.WriteLine(nullableInt.Value);
}
Nullable Reference Types:
Helps prevent NullReferenceException by providing warnings if a reference type might be null. Enabled at the project level or with #nullable enable.
#nullable enable
string? possiblyNullString = null;
string definitelyNotNullString = "Hello";
1.2 Operators and Expressions
C# supports a wide range of operators:
- Arithmetic:
+,-,*,/,% - Comparison:
==,!=,>,<,>=,<= - Logical:
&&,||,! - Assignment:
=,+=,-=,*=,/=,%= - Bitwise:
&,|,^,~,<<,>> - Null-coalescing:
??,??= - Conditional (Ternary):
?: - Null-conditional access:
?.,?[]
int a = 10;
int b = 5;
int sum = a + b; // 15
bool areEqual = (a == b); // false
string result = (a > b) ? "a is greater" : "b is greater or equal";
// Null-coalescing
string? displayName = null;
string userName = displayName ?? "Guest"; // userName will be "Guest"
// Null-conditional access (C# 6+)
string? name = null;
int? length = name?.Length; // null if name is null
var firstChar = name?[0]; // null if name is null
// C# 14: Null-conditional assignment
List<int>? numbersList = null;
// numbersList?.Add(1); // This would not execute Add() if numbersList is null
// Null-conditional assignment evaluates the right side only if the left side is not null
Customer? customer = null; // Or some method that might return null
customer?.OrderCount = 5; // Only assigns if customer is not null, right side not evaluated if customer is null
// Before C# 14, you would need to write:
if (customer != null)
{
customer.OrderCount = 5; // Right side is always evaluated
}
// C# 14: Null-conditional assignment with compound operators
Dictionary<string, int>? counts = new();
counts?["key"] += 1; // Increments value only if counts is not null
// Works with +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= but not ++ or --
// This simplifies code that would otherwise require null checking:
// if (counts != null) counts["key"] += 1;
1.3 Null-Conditional Assignment (C# 14)
C# 14 introduces null-conditional assignment, allowing you to assign values to members of potentially null objects. The right side of the assignment is only evaluated if the left side is not null. [2]
// Basic null-conditional assignment with property access (?.)
Customer? customer = null; // Could be from a method that might return null
customer?.Name = "John"; // Only assigns if customer is not null
// The right side is not evaluated if the left side is null
customer?.Order = new Order(); // Constructor is not called if customer is null
// Null-conditional assignment with indexers (?[])
Dictionary<string, int>? counts = new();
counts?["key"] += 1; // Increments value only if counts is not null
// Works with multi-level null checking
Order? order = null; // Could be from a method that might return null
// Note: This assumes Items[0] is guaranteed to exist if Items is not null
order?.Items?.FirstOrDefault()?.Quantity = 5; // Only assigns if order, order.Items, and the first item are not null
// Works with all compound assignment operators
counts?["a"] += 1; // Addition
counts?["b"] -= 1; // Subtraction
counts?["c"] *= 2; // Multiplication
counts?["d"] /= 2; // Division
counts?["e"] %= 10; // Modulo
counts?["f"] &= 0xFF; // Bitwise AND
counts?["g"] |= 0x0F; // Bitwise OR
counts?["h"] ^= 0x55; // Bitwise XOR
counts?["i"] <<= 2; // Left shift
counts?["j"] >>= 1; // Right shift
// Note: ++ and -- operators are not supported
// counts?["key"]++; // Error
// Before C# 14, you would need to write:
if (counts != null) counts["key"] += 1;
New Escape Sequence (C# 13): \e for the ESCAPE character (Unicode U+001B). [1]
char escapeChar = '\e';
Console.WriteLine($"This is an {escapeChar}[31mError{escapeChar}[0m message."); // Example for ANSI escape code
1.4 Control Flow
If/Else:
int number = 10;
if (number > 0)
{
Console.WriteLine("Positive");
}
else if (number < 0)
{
Console.WriteLine("Negative");
}
else
{
Console.WriteLine("Zero");
}
Switch (including pattern matching):
object shape = new Circle(5); // Assuming Circle and Square classes exist
switch (shape)
{
case Circle c:
Console.WriteLine($"Circle with radius {c.Radius}");
break;
case Square s when s.Side > 10:
Console.WriteLine($"Large square with side {s.Side}");
break;
case null:
Console.WriteLine("Shape is null");
break;
default:
Console.WriteLine("Unknown shape");
break;
}
Loops:
-
for:
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
} -
while:
int count = 0;
while (count < 3)
{
Console.WriteLine(count);
count++;
} -
do-while:
int j = 0;
do
{
Console.WriteLine(j);
j++;
} while (j < 3); -
foreach:
int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers)
{
Console.WriteLine(num);
}
New lock Object (C# 13): The lock statement can use System.Threading.Lock for improved thread synchronization. [1]
using System.Threading; // Required
// Traditional lock with object
private readonly object _legacyLock = new object();
public void LegacyCriticalSection()
{
lock (_legacyLock) // Uses Monitor.Enter/Exit
{
// Synchronized code
}
}
// New Lock type (.NET 9+)
private readonly Lock _modernLock = new Lock(); // .NET 9+
public void ModernMethod()
{
lock (_modernLock) // Uses Lock.EnterScope()
{
// Synchronized code with potentially better performance
}
// Alternatively, use the API directly:
using (_modernLock.EnterScope())
{
// Synchronized code
}
}
Best Practice: For new code targeting .NET 9+, consider using System.Threading.Lock for potentially better performance and diagnostics.
1.5 Methods and Parameters
Methods define reusable blocks of code.
public int Add(int a, int b)
{
return a + b;
}
// Method with optional parameters
public void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
Greet("Alice"); // Output: Hello, Alice!
// Expression-bodied members
public int Multiply(int a, int b) => a * b;
Optional Parameters in Lambdas (C# 12) & Modifiers on Simple Lambda Parameters (C# 14):
Lambda expressions can have default parameter values (C# 12) (C# 12). C# 14 allows modifiers like ref, out, in, ref readonly, and scoped without explicit types (except params still needs an explicit type). [2]
// C# 12+: Optional parameters in lambdas
var greetLambda = (string name, string prefix = "Ms.") => $"{prefix} {name}";
Console.WriteLine(greetLambda("Smith")); // Output: Ms. Smith
// C# 14+: Parameter modifiers without explicit types
delegate bool TryParseDelegate<T>(string text, out T result);
TryParseDelegate<int> parser = (text, out result) => int.TryParse(text, out result); // 'result' type inferred
// Previously required: (string text, out int result) => int.TryParse(text, out result)
// More examples of modifiers without types:
delegate int RefFunc(ref int x);
RefFunc doubleIt = (ref x) => { x *= 2; return x; }; // 'ref' modifier
delegate void InAction<T>(in T val);
InAction<int> readValue = (in val) => Console.WriteLine(val); // 'in' modifier
delegate void SpanAction<T>(Span<T> span);
SpanAction<byte> processData = (scoped span) => span[0] = 1; // 'scoped' modifier
// C# 14 breaking change: 'scoped' is always treated as a modifier in lambdas,
// even if there's a type named 'scoped'. Use '@scoped' to refer to a type named 'scoped'.
ref struct @scoped { } // Type named 'scoped'
var lambda = (scoped @scoped s) => { }; // 'scoped' is modifier, '@scoped' is type
params Collections (C# 13): params can be used with collection types like IEnumerable<T>, Span<T>, ReadOnlySpan<T>, not just arrays. [1]
public void PrintItems<T>(params IEnumerable<T> items)
{
foreach (var item in items) Console.Write($"{item} ");
Console.WriteLine();
}
PrintItems("Hello", "World", "!"); // Calls with individual string arguments
PrintItems(new List<string> { "One", "Two" }); // Calls with a List
public void ProcessSpan(params ReadOnlySpan<char> text)
{
Console.WriteLine(new string(text));
}
ProcessSpan('a', 'b', 'c'); // abc
ref readonly parameters (C# 12) & ref and unsafe in iterators and async methods (C# 13):
ref readonly improves performance for large structs. C# 13 allows ref locals and unsafe contexts in iterators and async methods (with restrictions, e.g., ref locals can't cross await or yield return). [1]
// C# 12
public void PrintLargeStruct(ref readonly LargeStructDetails details)
{
Console.WriteLine(details.Id); // Cannot modify 'details'
}
// C# 13: Iterator method with ref locals and unsafe code
public IEnumerable<int> GetNumbers()
{
int value = 42;
ref int localRef = ref value; // Allowed in C# 13+
unsafe
{
int* ptr = &value; // Unsafe code allowed in C# 13+
*ptr = 100;
}
yield return localRef; // 100
// Important: ref locals can't cross yield boundaries
// After this point, localRef can't be used safely
yield return 200;
}
// C# 13: Async method with ref locals
public async Task<int> GetValueAsync()
{
int value = 42;
ref int localRef = ref value; // Allowed in C# 13+
localRef = 100;
// Important: ref locals can't cross await boundaries
await Task.Delay(100);
// After this point, localRef can't be used safely
return value; // 100
}
1.6 Error Handling (try-catch-finally)
Handles exceptions that may occur during program execution.
try
{
int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Cannot divide by zero.");
}
catch (FormatException ex)
{
Console.WriteLine("Invalid number format.");
}
finally
{
Console.WriteLine("Cleanup code that always runs.");
}
1.7 Namespaces and using Directives
Namespaces organize code. using imports types.
C# 12: using alias any type: Aliases for any type, including tuple types. [1]
using System;
using System.Collections.Generic;
using Point = (int X, int Y); // C# 12
using StringList = System.Collections.Generic.List<string>; // C# 12
namespace MyProject
{
public class MyClass
{
public Point GetOrigin() => (0, 0);
public void ProcessNames(StringList names) { /* ... */ }
}
}
1.8 nameof with Unbound Generic Types (C# 14)
C# 14 allows nameof to refer to unbound generic types, making it easier to get the base name of generic types. [2]
// C# 14: nameof with unbound generic types
string listTypeName = nameof(List<>); // "List"
string dictionaryTypeName = nameof(Dictionary<,>); // "Dictionary"
// Before C# 14, you had to use a closed generic type:
string oldListTypeName = nameof(List<int>); // "List"
// This is useful for error messages, logging, and reflection:
void ValidateGenericType<T>() where T : IComparable<T>
{
Console.WriteLine($"Validating {nameof(T)} against {nameof(IComparable<>)} constraint");
}
// Also works with nested generic types:
string nestedTypeName = nameof(Dictionary<,>.KeyCollection<>); // "KeyCollection"
1.9 Method Group Natural Type Improvements (C# 13)
C# 13 optimizes overload resolution for method groups by pruning inapplicable methods at each scope. [1]
// Example of method group used as delegate:
void ProcessItems<T>(Action<T> processor, T item) => processor(item);
// These methods exist in scope:
void Process(int x) { Console.WriteLine($"Processing int: {x}"); }
void Process<T>(T x) where T : struct { Console.WriteLine($"Processing struct: {x}"); } // Generic with constraint
void Process(string s, bool flag = false) { Console.WriteLine($"Processing string: {s}"); } // With optional parameter
// In C# 13+:
ProcessItems(Process, 42); // Resolves to Process(int) directly
// In C# 12, all methods named Process would be considered,
// even those that couldn't possibly match (like the string overload).
// C# 13 prunes inapplicable methods at each scope:
// 1. First, it eliminates Process(string, bool) because it doesn't match the Action<int> signature
// 2. Then, it considers Process(int) and Process<T>(T) where T : struct
// 3. Since both are applicable, it chooses Process(int) as the non-generic method is preferred
1.10 Overload Resolution Priority (C# 13)
The OverloadResolutionPriorityAttribute allows library authors to designate one overload as better than another. [1]
using System.Runtime.CompilerServices;
public class StringUtility
{
// Legacy method (lower priority)
[OverloadResolutionPriority(1)]
public static string Join(string[] values)
{
return string.Join(",", values);
}
// New, more performant method (higher priority)
[OverloadResolutionPriority(2)]
public static string Join(ReadOnlySpan<string> values)
{
// More efficient implementation
return string.Join(",", values);
}
}
// When recompiled with C# 13+, this will use the ReadOnlySpan<string> overload
// even though both are applicable, because it has higher priority
string result = StringUtility.Join(new[] { "a", "b", "c" });
2. Data Structures
2.1 Arrays
Fixed-size collections of elements of the same type.
Single-dimensional: int[] numbers = { 1, 2, 3 };
Multi-dimensional: int[,] matrix = { { 1, 2 }, { 3, 4 } };
Jagged: int[][] jagged = new int[2][]; jagged[0] = new int[] {1,2}; jagged[1] = new int[] {3,4,5};
C# 12: Inline Arrays: Fixed-size arrays embedded in a struct (for performance-critical scenarios). [1]
[System.Runtime.CompilerServices.InlineArray(10)]
public struct MyBuffer
{
private int _element0; // Compiler generates _element1 through _element9
// Proper indexer implementation for access
public int this[int index]
{
get
{
if ((uint)index >= 10) // Unsigned check for range optimization
throw new IndexOutOfRangeException();
// Use Unsafe.Add to access any element in the inline array
return System.Runtime.CompilerServices.Unsafe.Add(ref _element0, index);
}
set
{
if ((uint)index >= 10)
throw new IndexOutOfRangeException();
// Use Unsafe.Add to access any element in the inline array
System.Runtime.CompilerServices.Unsafe.Add(ref _element0, index) = value;
}
}
}
// Usage example:
// var buffer = new MyBuffer();
// buffer[0] = 42;
// int value = buffer[0]; // 42
Note: InlineArray attribute cannot be applied to a record struct (C# 13 breaking change). [4]
C# 12: Collection Expressions: Concise syntax for creating common collection values. [1]
// Creates an array:
int[] arr1 = [1, 2, 3];
// Creates a List<int>:
List<int> list1 = [4, 5, 6];
// Creates a Span<int>:
Span<int> span1 = ['a', 'b', 'c']; // char literals are implicitly convertible to int
// Spread operator ..
int[] row1 = [1, 2, 3];
int[] row2 = [10, 11, 12, 13];
int[] combined = [..row1, ..row2, 14, 15]; // [1, 2, 3, 10, 11, 12, 13, 14, 15]
Note: C# 13 introduced changes to collection expression overload resolution. [4] In C# 14, overload resolution over Span<T> in most cases, and exact element type matches are preferred over all else.
First-Class Span<T> and ReadOnlySpan<T> Support (C# 14):
C# 14 introduces first-class support for these types with new implicit conversions and improved overload resolution. [2]
// 1. New implicit conversions:
void ProcessData(ReadOnlySpan<byte> data) { /* ... */ }
byte[] myArray = { 1, 2, 3 };
ProcessData(myArray); // Implicit conversion from byte[] to ReadOnlySpan<byte>
// C# 14 adds these implicit conversions:
// - T[] to ReadOnlySpan<T>
// - T[] to Span<T> (when array is not readonly)
// - Span<T> to ReadOnlySpan<T>
// 2. Improved overload resolution:
void Process<T>(IEnumerable<T> items) { /* ... */ }
void Process<T>(Span<T> items) { /* ... */ }
void Process<T>(ReadOnlySpan<T> items) { /* ... */ } // This overload is preferred for arrays
string[] strings = ["hello"];
object[] objects = strings; // Array covariance
Process(objects); // In C# 13: Calls Span<T> overload, crashes at runtime with ArrayTypeMismatchException
// In C# 14: Calls ReadOnlySpan<T> overload, works correctly
// 3. Span types as extension method receivers:
public static class SpanExtensions
{
public static int CountPositive(this Span<int> span)
{
int count = 0;
foreach (int i in span) if (i > 0) count++;
return count;
}
}
int[] numbers = [1, -2, 3, -4, 5];
int positiveCount = numbers.CountPositive(); // Array implicitly converted to Span<int>
// 4. Better generic type inference:
T FindFirst<T>(ReadOnlySpan<T> items, Func<T, bool> predicate)
{
foreach (var item in items)
if (predicate(item)) return item;
throw new InvalidOperationException("No matching item found");
}
string[] names = ["Alice", "Bob", "Charlie"];
string result = FindFirst(names, name => name.StartsWith("B")); // "Bob"
// Type inference works with the implicit conversion
// Breaking changes with Span<T> in C# 14:
// 1. LINQ and arrays:
int[] numbersArray = [1, 2, 3];
// numbersArray.Reverse(); // In C# 13: Calls Enumerable.Reverse (returns new sequence)
// In C# 14: Calls MemoryExtensions.Reverse (in-place, returns void)
// Workarounds:
var reversed1 = System.Linq.Enumerable.Reverse(numbersArray); // Returns IEnumerable<int>
var reversed2 = numbersArray.AsEnumerable().Reverse(); // Returns IEnumerable<int>
// 2. Array variance and Span<T> (continued):
// When using arrays with variance, ReadOnlySpan<T> is preferred in C# 14
void Process<T>(IEnumerable<T> items) { /* ... */ }
void Process<T>(Span<T> items) { /* ... */ }
void Process<T>(ReadOnlySpan<T> items) { /* ... */ }
// Process(objects); // In C# 13: Calls Span<T> overload, crashes at runtime with ArrayTypeMismatchException
// In C# 14: Calls ReadOnlySpan<T> overload, works correctly
// 3. Overload resolution changes:
// - ReadOnlySpan<T> is preferred over Span<T> in most cases
// - Exact element type matches are preferred over all else
// - These changes help avoid runtime errors with array variance
Implicit Index Access ^ in Collection Initializers (C# 13): Index from end syntax [^index] can be used in collection initializers. [1]
// Array initialization with index from end
int[] items = new int[5];
items[^1] = 10; // Last element
items[^2] = 20; // Second-to-last element
// items is now {0, 0, 0, 20, 10}
// Can also be used in collection initializers
var dict = new Dictionary<Index, string> { [^1] = "last", [^2] = "second-to-last" };
2.2 Collections (System.Collections.Generic)
Dynamic collections.
List<T>: Dynamically sized list.List<string> names = ["Alice", "Bob"];Dictionary<TKey, TValue>: Key-value pairs.Dictionary<string, int> ages = new() { {"Alice", 30}, {"Bob", 25} };HashSet<T>: Unordered unique elements.HashSet<int> numbers = [1, 2, 3, 2];(contains 1, 2, 3)Queue<T>: FIFO.Queue<string> tasks = new(["A", "B"]); var t = tasks.Dequeue(); // "A"Stack<T>: LIFO.Stack<string> history = new(["Page1", "Page2"]); var p = history.Pop(); // "Page2"
2.3 LINQ (Language Integrated Query)
Powerful query capabilities.
Query Syntax:
List<int> numbers = [1, 2, 3, 4, 5, 6];
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
Method Syntax:
var oddNumbers = numbers.Where(num => num % 2 != 0).ToList();
// C# 14: Span<T> and ReadOnlySpan<T> conversions affect LINQ
int[] array = [1, 2, 3, 4, 5];
// In C# 13: array.Reverse() calls Enumerable.Reverse (returns new sequence)
// In C# 14: array.Reverse() calls MemoryExtensions.Reverse (in-place, returns void)
// Workaround: use explicit method call
var reversed = Enumerable.Reverse(array); // Explicitly call Enumerable.Reverse
// C# 14: More partial members (constructors and events)
// See the "Partial Constructors and Events" section for details
3. Object-Oriented Programming (OOP) Principles
3.1 Classes and Objects
Class is a blueprint; object is an instance.
C# 12: Primary Constructors for Classes/Structs: Concise constructor syntax. [1]
public class Student(string id, string name)
{
public string Id { get; } = id;
public string Name { get; set; } = name; // Can be mutable
public void DisplayInfo()
{
// Parameters 'id' and 'name' are in scope for initialization
// and can be captured by methods/properties.
Console.WriteLine($"Student: {id}, Name: {Name}");
}
}
var student = new Student("S101", "Maria");
student.DisplayInfo(); // Student: S101, Name: Maria
3.2 Encapsulation
Bundling data and methods. Access modifiers (public, private, protected, internal, protected internal, private protected).
Properties: Controlled access to fields.
public class Account
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set => _balance = value; // Can only be set within the class
}
// C# 14: 'field' keyword for auto-property backing field access
public string Status
{
get => field; // Accesses the compiler-synthesized backing field for Status
set => field = string.IsNullOrWhiteSpace(value) ? "Default" : value;
}
// Method to deposit money
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
}
}
### 3.3 The `field` Keyword (C# 14)
The `field` keyword provides direct access to the compiler-synthesized backing field in property accessors. It was a preview feature in C# 13 and is fully supported in C# 14. [2]
```csharp
public class Person
{
// Traditional approach with explicit backing field
private string _name = "";
public string Name
{
get => _name;
set => _name = value?.Trim() ?? "";
}
// C# 14 approach with 'field' keyword
public string Email
{
get => field; // Access the backing field directly
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Email cannot be empty")
: value.ToLowerInvariant();
}
// Property with custom getter but auto-implemented setter
public int Age
{
get => field < 0 ? 0 : field; // Ensure age is never negative
set; // Auto-implemented setter still uses the same backing field
}
// Warning CS9258 or Error CS9272 if a member named 'field' exists in scope
private string field = "Example"; // Member named 'field'
public string Description
{
get => @field; // Use '@field' to refer to the member, not the keyword
set => this.field = value; // Use 'this.field' to refer to the member
}
}
// Breaking change in C# 14: The 'field' keyword is now fully supported
// and will generate warnings/errors if it conflicts with existing members
public class Account
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set => _balance = value;
}
public Account(decimal initialBalance) => _balance = initialBalance;
public void Deposit(decimal amount) { if (amount > 0) Balance += amount; }
}
3.4 Inheritance
Derived classes inherit from base classes.
public class Animal
{
public virtual void MakeSound() => Console.WriteLine("Generic sound");
}
public class Cat : Animal
{
public override void MakeSound() => Console.WriteLine("Meow");
}
3.5 Polymorphism
Objects treated as instances of a common base class/interface.
- Method Overloading: Same method name, different parameters.
- Method Overriding: (See
MakeSound()in Inheritance).
3.6 Abstraction
Hiding implementation, showing essential features.
-
Abstract Classes: Cannot be instantiated; can have abstract methods.
public abstract class Shape
{
public abstract double GetArea(); // Must be implemented
public void Display() => Console.WriteLine("Displaying shape");
}
public class Square : Shape // Assume constructor and side property
{
public double Side {get; }
public Square(double side) => Side = side;
public override double GetArea() => Side * Side;
} -
Interfaces: Define a contract.
public interface ILoggable
{
void Log(string message);
}
public class ConsoleLogger : ILoggable
{
public void Log(string message) => Console.WriteLine(message);
}
ref struct Interfaces and allows ref struct (C# 13):
C# 13 introduces two related features:
ref structtypes can implement interfaces [1]- Generic type parameters can use the
allows ref structconstraint [1]
// 1. ref struct implementing an interface
public interface IMySpanProcessor
{
void Process(Span<int> data);
}
public ref struct MySpanProcessorImpl : IMySpanProcessor // C# 13
{
public void Process(Span<int> data)
{
for(int i = 0; i < data.Length; i++) data[i] *= 2;
}
}
// 2. Generic class with 'allows ref struct' constraint
public class GenericProcessor<T> where T : allows ref struct
{
public void ProcessScoped(scoped T value)
{
// Can safely use T as a ref struct
Console.WriteLine($"Processing {typeof(T).Name}");
}
}
// Use the generic constraint to work with ref struct implementations
public void UseProcessor<T>(T processor, Span<int> data)
where T : IMySpanProcessor, allows ref struct // Both constraints
{
processor.Process(data);
}
// This won't work - ref structs can't be converted to interface types:
// IMySpanProcessor processor = new MySpanProcessorImpl(); // Error!
// But this works:
var processor = new MySpanProcessorImpl();
UseProcessor(processor, new int[] { 1, 2, 3 });
// And this works too:
var genericProc = new GenericProcessor<Span<int>>();
genericProc.ProcessScoped(new int[] { 1, 2, 3 });
3.7 Structs and Records
Structs: Value types for small, lightweight objects.
public struct Point2D(double x, double y) // C# 12 primary constructor for struct
{
public double X { get; } = x;
public double Y { get; } = y;
}
Records (Record Class / Record Struct): Concise syntax for data-centric types with immutability and value-based equality.
// Record class (reference type)
public record Person(string FirstName, string LastName);
var p1 = new Person("Sam", "Hill");
var p2 = p1 with { LastName = "Valley" }; // Non-destructive mutation
// Record struct (value type, C# 10+)
public readonly record struct Color(byte R, byte G, byte B);
// Primary constructors (C# 12+) are natural for records and record structs.
// C# 14 breaking change: record types cannot have pointer fields
// This was always in the specification but is now properly enforced
unsafe
{
// This will not compile in C# 14:
// record struct UnsafeRecord(int* Ptr); // Error CS8908 in C# 14
// Even with custom Equals implementation:
// record struct UnsafeRecordWithEquals(int* Ptr)
// {
// public bool Equals(UnsafeRecordWithEquals other) => true;
// }
}
Note: record and record struct types cannot define pointer type members in C# 14, even with custom Equals implementations (breaking change). [3]
Partial Members (Expanded in C# 13 & C# 14)
Allows defining a class, struct, interface, or method in multiple files.
- C# 13: Partial Properties and Indexers. [1]
- C# 14: Partial Instance Constructors and Events. [2]
// File1.cs
public partial class MyData
{
// C# 13: Declaring partial property
public partial string? Description { get; set; }
// C# 14: Declaring partial constructor
public partial MyData(string initialValue);
// C# 14: Declaring partial event
public partial event EventHandler? DataChanged;
}
// File2.cs
public partial class MyData
{
private string? _backingDescription;
// C# 13: Implementing partial property
public partial string? Description
{
get => _backingDescription;
set
{
_backingDescription = value?.Trim();
DataChanged?.Invoke(this, EventArgs.Empty);
}
}
// C# 14: Implementing partial constructor
public partial MyData(string initialValue)
{
this.Description = initialValue;
Console.WriteLine("MyData partial constructor initialized.");
}
// C# 14: Implementing partial event
private EventHandler? _dataChangedHandler;
public partial event EventHandler? DataChanged
{
add => _dataChangedHandler += value;
remove => _dataChangedHandler -= value;
}
}
Breaking Change: Partial interface properties and events are now implicitly virtual and public in C# 14. [3] This matches the behavior of non-partial interface members but may change how your code behaves.
// Before C# 14:
partial interface I
{
partial int P { get; } // Was implicitly private
partial int P => 1; // Was implicitly non-virtual
}
// In C# 14:
partial interface I
{
partial int P { get; } // Now implicitly public
partial int P => 1; // Now implicitly virtual
}
// To maintain previous behavior, explicitly specify modifiers:
partial interface I
{
private partial int P { get; } // Explicitly private
sealed partial int P => 1; // Explicitly non-virtual
}
Extension Members (C# 14)
C# 14 introduces a powerful new syntax for defining extension members beyond just methods, including properties, indexers, and static extensions. This feature significantly expands the capabilities of extension functionality. [2]
Extension members allow you to:
- Add properties, indexers, and methods to existing types without modifying them
- Define static extension members that appear as static members of the extended type
- Group related extension members in a single block for better organization
Breaking Change: In C# 14, extension is a contextual keyword and cannot be used as a type name, type parameter name, or alias. Use @extension to escape it when needed as an identifier. [3]
// These will not compile in C# 14:
using extension = System.String; // Error: alias may not be named "extension"
class extension { } // Error: type may not be named "extension"
class C<extension> { } // Error: type parameter may not be named "extension"
// Workaround: Use @ to escape the identifier
using @extension = System.String; // OK
class @extension { } // OK
class C<@extension> { } // OK
The new extension syntax uses blocks to group related extension members:
public static class StringExtensions
{
// C# 14: Extension block for instance members
extension(string str) // Target type is string, parameter name is str
{
// Extension method
public bool IsNullOrEmpty() => string.IsNullOrEmpty(str);
// Extension method that acts like a property
public string? Truncate(int maxLength) =>
str.Length <= maxLength ? str : str.Substring(0, maxLength) + "...";
// Extension property
public char FirstChar => str.Length > 0 ? str[0] : throw new InvalidOperationException();
// Extension indexer
public char this[Index index] => str[index];
}
// C# 14: Extension block for static members (receiver type only)
extension(string) // Target type is string, no parameter name (static context)
{
// Static extension property
public static string DefaultGreeting => "Hello, World!";
// Static extension method
public static string Combine(string first, string second) => $"{first} {second}";
}
}
// Usage examples:
// 1. Instance extension methods and properties
string text = "Hello, World!";
Console.WriteLine(text.IsNullOrEmpty()); // False
Console.WriteLine(text.Truncate(5)); // "Hello..."
Console.WriteLine(text.FirstChar); // 'H'
Console.WriteLine(text[^1]); // '!'
// Extension methods can be called on null instances (unlike instance methods)
string? nullText = null;
Console.WriteLine(nullText.IsNullOrEmpty()); // True - works even though nullText is null
// 2. Static extension members (called as if they were static members of the type)
Console.WriteLine(string.DefaultGreeting); // "Hello, World!"
Console.WriteLine(string.Combine("Hello", "World")); // "Hello World"
// Before C# 14, you would need to write:
// Console.WriteLine(StringExtensions.DefaultGreeting);
// Console.WriteLine(StringExtensions.Combine("Hello", "World"));
// Extension blocks with generic type parameters:
public static class EnumerableExtensions
{
// Generic extension block for instance members
extension<T>(IEnumerable<T> items)
{
public bool IsEmpty => !items.Any();
public T FirstOrDefault(T defaultValue) => items.Any() ? items.First() : defaultValue;
// Extension method with multiple parameters
public IEnumerable<T> TakeIfAny(int count) =>
IsEmpty ? Enumerable.Empty<T>() : items.Take(count);
}
// Generic extension block for static members
extension<T>(IEnumerable<T>)
{
// Static extension method
public static IEnumerable<T> CreateSingleton(T item) => new[] { item };
// Static extension property
public static IEnumerable<T> Empty => Enumerable.Empty<T>();
}
}
// 3. Usage with generic extension members:
List<int> numbers = [1, 2, 3];
Console.WriteLine(numbers.IsEmpty); // False
Console.WriteLine(numbers.FirstOrDefault(0)); // 1
var firstTwo = numbers.TakeIfAny(2).ToList(); // [1, 2]
// Empty list example
List<int> emptyList = [];
Console.WriteLine(emptyList.IsEmpty); // True
Console.WriteLine(emptyList.FirstOrDefault(42)); // 42 (default value)
var noItems = emptyList.TakeIfAny(5).ToList(); // [] (empty)
// 4. Static extension members with generic types:
Console.WriteLine(IEnumerable<int>.Empty.Count()); // 0
var singleton = IEnumerable<string>.CreateSingleton("Hello"); // ["Hello"]
// This is a major improvement over traditional extension methods:
// Before C# 14:
// Console.WriteLine(EnumerableExtensions.Empty<int>().Count());
// var singleton = EnumerableExtensions.CreateSingleton("Hello");
4. Additional C# 13 and C# 14 Features and Breaking Changes
This section highlights unique features and breaking changes not covered in detail elsewhere in this cheatsheet.
4.1 Enumerator Behavior Changes (C# 14)
In C# 14, calling MoveNext() on a disposed enumerator properly returns false without executing any more user code. [3]
// Before C# 14:
var enumerator = GetEnumerator();
Console.Write(enumerator.MoveNext()); // True
Console.Write(enumerator.Current); // 1
enumerator.Dispose();
Console.Write(enumerator.MoveNext()); // True, and would execute more code
// In C# 14:
var enumerator = GetEnumerator();
Console.Write(enumerator.MoveNext()); // True
Console.Write(enumerator.Current); // 1
enumerator.Dispose();
Console.Write(enumerator.MoveNext()); // False, no more code executed
static IEnumerator<int> GetEnumerator()
{
yield return 1;
Console.Write("This won't execute after disposal in C# 14");
yield return 2;
}
4.2 Obsolete Methods in await foreach (C# 14)
C# 14 changes how await foreach handles IAsyncEnumerable<T> implementations with obsolete methods. [3]
// If AsyncEnumerator.DisposeAsync() is marked [Obsolete],
// C# 14 will report the obsolete warning when using await foreach
await foreach (var item in new AsyncCollection()) { }
// Example with obsolete DisposeAsync method:
class AsyncCollection
{
public AsyncEnumerator GetAsyncEnumerator(CancellationToken token = default) => new();
public sealed class AsyncEnumerator : IAsyncDisposable
{
public int Current => 42;
public ValueTask<bool> MoveNextAsync() => new(true);
[Obsolete("Use the new DisposeSafelyAsync method instead")]
public ValueTask DisposeAsync() => new(); // C# 14 will report this as obsolete
}
}
5. References
[1] What's new in C# 13
[2] What's new in C# 14
[3] C# compiler breaking changes since C# 13
[4] C# compiler breaking changes since C# 12
This cheatsheet covers many fundamental and new aspects of C# up to version 14. For more in-depth information, always refer to the official Microsoft C# documentation.