C Sharp syntax
C# syntax encompasses the formal rules and conventions that govern the structure of programs written in C#, a simple, modern, object-oriented programming language developed by Microsoft and first released in July 2000 as part of the .NET Framework.[1] Designed with goals of robustness, durability, and productivity, C# syntax supports strong type checking, automatic garbage collection, and the creation of distributed software components while ensuring portability for developers familiar with C and C++.[1] The language is case-sensitive, uses semicolons (;) to terminate statements, and curly braces ({}) to delimit blocks, making it syntactically similar to C, C++, Java, and JavaScript.[2]
At its core, C# syntax organizes code into namespaces, which group related types such as classes, structs, interfaces, delegates, and enumerations to promote modularity and avoid naming conflicts.[3] Programs typically begin with using directives to import namespaces (e.g., using [System](/page/System);), followed by a namespace declaration, and contain at least one class or top-level statements serving as the entry point, often a static void Main method or implicit top-level code in a single file.[3] Built-in types like int, double, bool, and string form the foundation, with the language enforcing strong typing where variables must be explicitly declared and compatible operations are verified at compile time.[2] Comments are supported via single-line (//) or multi-line (/* */) formats, and keywords such as if, else, for, while, switch, and foreach drive control flow.[2]
C# syntax has evolved through standardized versions managed by Ecma International since 2000, incorporating features like pattern matching, LINQ queries, async/await for asynchronous programming, and tuples for lightweight data structures, all while maintaining backward compatibility.[1] These elements enable both imperative and functional programming paradigms, with automatic memory management handled by the .NET runtime to prevent common errors like memory leaks.[2] The definitive reference for syntax remains the C# Language Specification, which details expressions (value-producing constructs like x + y), statements (action-performing like int x = 42;), and the overall grammar for valid programs.[4]
Basics
Identifiers and Keywords
In C#, identifiers are names used to declare and reference elements such as variables, methods, classes, and namespaces.[5] They must follow specific lexical rules to ensure uniqueness and readability within the language's syntax.[6]
Valid identifiers begin with an Identifier_Start_Character, which includes the underscore (_) or any Unicode letter character from categories Lu (uppercase letter), Ll (lowercase letter), Lt (titlecase letter), Lm (modifier letter), or Nl (letter number).[5] Subsequent characters can be Identifier_Part_Characters, encompassing Unicode decimal digits (Nd), connecting punctuation (Pc), combining marks (Mn or Mc), non-spacing marks, or formatting characters (Cf), in addition to the starting characters.[5] C# supports Unicode identifiers, requiring normalization to Unicode Normalization Form C for consistency, and allows Unicode escape sequences (e.g., \u0041 for 'A') within identifiers.[7] Identifiers are case-sensitive, and while consecutive underscores are permitted by the rules, coding conventions recommend avoiding them for clarity.[6]
Keywords in C# are predefined, reserved identifiers that hold special meanings to the compiler and cannot be used directly as user-defined identifiers.[8] There are 79 reserved keywords, including literals like true, false, and null, as well as structural elements such as abstract, as, base, bool, break, byte, case, catch, char, checked, class, const, continue, decimal, default, delegate, do, double, else, enum, event, explicit, extern, finally, fixed, float, for, foreach, goto, if, implicit, in, int, interface, internal, is, lock, long, namespace, new, object, operator, out, override, params, private, protected, public, readonly, ref, return, sbyte, sealed, short, sizeof, stackalloc, static, string, struct, switch, this, throw, try, typeof, uint, ulong, unchecked, unsafe, ushort, using, virtual, void, volatile, and while.[9]
To use a reserved keyword as an identifier, prefix it with the verbatim identifier symbol @, which escapes the keyword without becoming part of the name itself; for example, @class can name a variable while class remains reserved for type declarations.[5] This mechanism allows interoperability with other languages or legacy code where keywords might conflict.
In addition to reserved keywords, C# features contextual keywords, which are not fully reserved but acquire special meanings in limited syntactic contexts, allowing them to serve as identifiers elsewhere.[8] There are 31 such keywords: add, alias, ascending, async, await, by, descending, dynamic, equals, from, get, global, group, into, join, let, nameof, notnull, on, orderby, partial, remove, select, set, unmanaged, value, var, when, where, yield, and field.[9][10] For instance, var is contextual in variable declarations but can name a type outside that scope, and the @ prefix can still be applied if needed in conflicting contexts.[8]
The introduction of contextual keywords evolved to minimize breaking changes in new language versions; for example, partial was added as a contextual keyword in C# 2.0 for partial classes, async and await were introduced similarly in C# 5.0 for asynchronous programming, and field was added in C# 13 for accessing backing fields in auto-implemented properties, without reserving them globally.[11][10] This design preserves backward compatibility while extending the language's expressiveness.[8]
Literals
In C#, literals are fixed values directly embedded in source code that represent constant data of various types, such as numbers, characters, strings, and booleans, without requiring computation or variable assignment.[12] They form the basis for initializing variables and constants, enabling straightforward representation of immutable values in expressions. The syntax for literals has evolved across C# versions to include modern features like digit separators and raw strings, enhancing readability and expressiveness.[13]
Boolean literals consist of the keywords true and false, which represent the two possible values of the bool type.[14] These literals are used in conditional expressions and variable initializations, such as bool isActive = true;.[15]
Integer literals support decimal (base-10), hexadecimal (base-16, prefixed with 0x or 0X), and binary (base-2, prefixed with 0b or 0B) notations, introduced in C# 7.0 for binary.[16] Their type is determined by the value and optional suffixes: no suffix defaults to int, uint, long, or ulong based on magnitude; u or U specifies unsigned (uint or ulong); l or L specifies signed long (long or ulong); and ul, UL, lu, or LU specifies ulong.[17] Digit separators using underscores (_) improve readability for large numbers, a feature added in C# 7.0, and can be placed between digits but not at the start or end.[17] Examples include:
csharp
int decimalLit = 42;
uint hexLit = 0x2A;
int binaryLit = 0b101010;
long largeLit = 1_000_000_000L; // Digit separator for clarity
ulong unsignedLit = 0xFFUL;
int decimalLit = 42;
uint hexLit = 0x2A;
int binaryLit = 0b101010;
long largeLit = 1_000_000_000L; // Digit separator for clarity
ulong unsignedLit = 0xFFUL;
Real literals represent floating-point values in decimal form with an optional fractional part and exponent (using e or E for scientific notation), supporting types double (default or suffixed with d or D), float (suffixed with f or F), and decimal (suffixed with m or M).[18] Digit separators are also allowed here.[19] Examples include:
csharp
double defaultDouble = 3.14159;
[float floatLit = 1.23f;](/page/Float)
[decimal decimalLit = 0.456M;](/page/Decimal)
double scientific = 1.23e4; // Equivalent to 12300
double separated = 1_234.567_89;
double defaultDouble = 3.14159;
[float floatLit = 1.23f;](/page/Float)
[decimal decimalLit = 0.456M;](/page/Decimal)
double scientific = 1.23e4; // Equivalent to 12300
double separated = 1_234.567_89;
Character literals are enclosed in single quotes and represent a single Unicode UTF-16 code unit of type char.[20] They support escape sequences for special characters: simple escapes like \', \", \\, \0, and \e (for ESCAPE, introduced in C# 13); hexadecimal escapes with \x followed by 1-4 hex digits; and Unicode escapes with \u or \U followed by exactly 4 or 8 hex digits, respectively.[21][10] Examples include:
csharp
char simple = 'A';
char escaped = '\n'; // Newline
char escape = '\e'; // ESCAPE (U+001B), C# 13
char unicode = '\u0041'; // 'A' in Unicode
char hex = '\x41'; // 'A' in hex
char simple = 'A';
char escaped = '\n'; // Newline
char escape = '\e'; // ESCAPE (U+001B), C# 13
char unicode = '\u0041'; // 'A' in Unicode
char hex = '\x41'; // 'A' in hex
String literals represent sequences of characters of type string and come in several forms. Regular string literals use double quotes and require escape sequences for special characters, such as \" for quotes or \n for newlines.[22] Verbatim string literals, prefixed with @, treat backslashes and quotes literally without escapes, ideal for paths or regex patterns.[23] Interpolated strings, prefixed with $, embed expressions inside curly braces {} for dynamic formatting, and can combine with verbatim ($@) or raw forms.[24] Raw string literals, introduced in C# 11, use at least three consecutive double quotes (""") to delimit content, allowing unescaped quotes, newlines, and indentation handling via delimiter alignment, which simplifies multiline text like JSON or SQL.[25] UTF-8 string literals, also from C# 11, append the u8 suffix to produce Utf8String for efficient byte-encoded strings.[25] Examples include:
csharp
string regular = "Hello, \"World\"!";
string verbatim = @"C:\Path\To\File";
string interpolated = $"Value: {42}";
string raw = """
{
"name": "Example"
}
"""; // Multiline JSON without escapes
Utf8String utf8Lit = "Hello"u8;
string regular = "Hello, \"World\"!";
string verbatim = @"C:\Path\To\File";
string interpolated = $"Value: {42}";
string raw = """
{
"name": "Example"
}
"""; // Multiline JSON without escapes
Utf8String utf8Lit = "Hello"u8;
The null literal is the keyword null, representing a reference that does not refer to any object or an undefined value for nullable types.[26] It serves as the default value for reference types and can be assigned to nullable value types like int?.[27] For example: string emptyRef = null;.
Collection expressions, introduced in C# 12, provide a concise literal syntax for initializing arrays, lists, spans, and other collections using square brackets [], supporting element lists, spreads (..), and nested collections.[28] They convert implicitly to supported types without needing explicit constructors.[29] Examples include:
csharp
int[] arrayLit = [1, 2, 3];
List<string> listLit = ["apple", "banana"];
int[][] jagged = [[1, 2], [3, 4]];
int[] spreadLit = [1, ..arrayLit, 4]; // Spreads elements from arrayLit
int[] arrayLit = [1, 2, 3];
List<string> listLit = ["apple", "banana"];
int[][] jagged = [[1, 2], [3, 4]];
int[] spreadLit = [1, ..arrayLit, 4]; // Spreads elements from arrayLit
Variables and Constants
In C#, variables are storage locations associated with a specific type that can hold values of that type during program execution. Local variables can be declared with explicit typing, specifying the type name followed by the variable name and optional initialization, as in int count = 0;.[30] Multiple variables of the same type can be declared in a single statement, such as int x = 1, y = 2;.[30] This explicit approach, available since C# 1.0, ensures the compiler verifies type compatibility at compile time.[30]
Implicit typing uses the var keyword to allow the compiler to infer the type from the initializer expression, introduced in C# 3.0. For example, var message = "Hello, World!"; infers string for message.[31] The inferred type is determined at compile time and remains strongly typed thereafter, supporting built-in types, anonymous types, and collection initializers like var numbers = new [List](/page/List)<int> { 1, 2, 3 };.[31] Limitations include requiring initialization in the same statement and prohibiting use for class members or untyped expressions like null alone.[31]
Local variable type inference extends to target-typed new expressions in C# 9.0, where the type is inferred from the declaration context rather than the new expression itself. For instance, List<int> list = new(); uses new() to create a List<int> without repeating the type.[32] This simplifies code when the target type is already known, such as in variable assignments or method returns, but requires a clear contextual type for inference.[32]
Constants in C# provide immutable values that cannot be modified after declaration. Local constants are declared using the const keyword and must be initialized with a compile-time constant expression, supporting only built-in types like numbers, booleans, strings, or null. An example is const double Pi = 3.14159;, available since C# 1.0.[33] Unlike variables, const members are evaluated at compile time and inlined wherever used, but they cannot be declared in methods with unsafe code or involve dynamic values.[33]
For runtime-initialized immutable values, particularly at the class or struct level, the readonly keyword is used on fields. These can be assigned during declaration or in constructors, supporting instance or static contexts, as in public static readonly [string](/page/String) Version = "1.0"; where the value might derive from DateTime.Now.[34] Static readonly fields behave like constants for runtime scenarios but allow evaluation at object creation or static initialization time.[34]
Variable scoping defines the region of code where a variable is accessible. Local variables, declared within methods or blocks, are scoped to their enclosing block, such as { int localVar = 10; }, and inaccessible outside it.[35] Their lifetime begins at scope entry and ends at exit, unless captured by anonymous functions or lambdas, which extends lifetime until the delegate is no longer reachable.[35] Local variables start unassigned and must be definitely assigned before use to avoid compiler errors.[35]
Field variables, declared at class or struct level, have class-wide scope and lifetime tied to the instance (for non-static) or application domain (for static).[35] They are automatically initialized to default values, such as 0 for integers. Parameter variables, passed to methods, are scoped to the method's execution and lifetime matches the call duration, with value parameters copied and reference/output parameters aliasing the original.[35] Input parameters (in), introduced in C# 7.2, are read-only aliases with similar scoping.[35]
Declaration expressions, added in C# 7.0, enable concise declarations in various contexts like if or using statements, including deconstruction for tuples or objects. For example, var (name, age) = GetPerson(); declares name and age from the returned tuple, inferring types from the expression.[30] This supports patterns like if (var (x, y) = ParsePoint(input)) for conditional declarations, where variables are scoped to the enclosing block.[30]
Code Blocks
In C#, code blocks are delimited by curly braces {} and serve to group one or more statements into a single compound statement, enabling the definition of local scopes. The syntax for a block is { statement_list? }, where statement_list consists of one or more statements, allowing multiple lines of code to be treated as a unit wherever a single statement is expected.[36] Empty blocks, containing no statements, simply transfer control to their end point upon execution.[36]
Local scoping rules ensure that variables and constants declared within a block are accessible only from the point of declaration to the end of that block, promoting encapsulation and preventing unintended access from outer scopes. For instance, a variable declared inside a block cannot be referenced outside it, even in enclosing blocks, which helps manage resource lifetimes and avoid naming conflicts.[36] This scoping applies uniformly to blocks, with labels within a block being visible throughout the block and any nested blocks.[37]
Many language constructs implicitly introduce blocks, such as method bodies, where the sequence of statements following the method signature is enclosed in braces to form a scope. Similarly, blocks appear implicitly in other declarative contexts to group related statements without requiring explicit braces in all cases.[38]
Introduced in C# 6.0, expression-bodied members provide a concise shorthand for defining members that consist of a single expression, using the lambda-like syntax => expression in place of a full block. This applies to methods, properties, constructors, and other members, omitting the need for braces and a return statement when the body is a simple expression. For example:
csharp
public string Name => $"{firstName} {lastName}".Trim();
public string Name => $"{firstName} {lastName}".Trim();
This syntax reduces verbosity while maintaining the same semantic behavior as a braced block containing the expression.[39]
Program Structure
Namespaces and Using Directives
In C#, namespaces provide a way to organize code by grouping related types, such as classes, interfaces, structures, enumerations, and delegates, into logical hierarchical containers. This organization helps manage the scope of type names, preventing conflicts in large projects where multiple developers or libraries might define types with the same name.[40] The namespace keyword declares such a scope, and all types defined within it belong to that namespace unless further nested.[41]
Namespace declarations can take two forms: block-scoped or file-scoped. In the traditional block-scoped syntax, introduced in C# 1.0, the namespace wraps its contents in curly braces, allowing multiple types and nested declarations within a single file or across files. For example:
csharp
[namespace](/page/Namespace) MyApp.Utilities
{
[public](/page/Public) [class](/page/Class) HelperClass { }
[namespace](/page/Namespace) Data
{
[public](/page/Public) [class](/page/Class) DataProcessor { }
}
}
[namespace](/page/Namespace) MyApp.Utilities
{
[public](/page/Public) [class](/page/Class) HelperClass { }
[namespace](/page/Namespace) Data
{
[public](/page/Public) [class](/page/Class) DataProcessor { }
}
}
This creates a nested namespace MyApp.Utilities.Data for DataProcessor. Block-scoped namespaces are explicitly public and can be split across multiple files to form the same namespace, such as MyApp.Utilities declared in separate compilation units.[41] File-scoped namespaces, introduced in C# 10.0, offer a more concise syntax for files containing a single primary namespace, using a semicolon terminator instead of braces. All top-level types in the file then belong to that namespace without additional wrapping. For instance:
csharp
namespace MyApp.Utilities;
public class HelperClass { }
namespace MyApp.Utilities.Data; // Invalid: Multiple file-scoped not allowed in one file
public class DataProcessor { } // Belongs to MyApp.Utilities
namespace MyApp.Utilities;
public class HelperClass { }
namespace MyApp.Utilities.Data; // Invalid: Multiple file-scoped not allowed in one file
public class DataProcessor { } // Belongs to MyApp.Utilities
File-scoped declarations must appear before any types in the file and cannot be nested directly; they simplify code in projects with one namespace per file but require careful placement of using directives, as those before the declaration apply globally to the file, while those after are scoped to the namespace.[42]
Using directives import namespaces or types to avoid repeatedly qualifying names with the full namespace path, such as referencing Console instead of System.Console. The standard using directive, available since C# 1.0, imports an entire namespace:
csharp
using System;
using System.Collections.Generic;
class Program
{
List<string> list = new(); // List from System.Collections.Generic
}
using System;
using System.Collections.Generic;
class Program
{
List<string> list = new(); // List from System.Collections.Generic
}
This facilitates access to types but does not import nested namespaces, requiring separate directives for them.[43] The using static directive, introduced in C# 6.0, imports static members and nested types from a specific type, enabling direct use without the type name prefix. For example:
csharp
using static System.Math;
class Program
{
double result = PI * Pow(2, 3); // Static members PI and Pow
}
using static System.Math;
class Program
{
double result = PI * Pow(2, 3); // Static members PI and Pow
}
It applies to any type with static members, regardless of instance members, and can be combined with global scope in later versions.[43] The using alias directive creates a shorthand for a namespace, type, or qualified name, useful for resolving ambiguities:
csharp
using Proj = MyCompany.Project;
using List = System.Collections.Generic.List<int>; // Alias for a specific type
class Program
{
Proj.Data data = new();
List items = new();
}
using Proj = MyCompany.Project;
using List = System.Collections.Generic.List<int>; // Alias for a specific type
class Program
{
Proj.Data data = new();
List items = new();
}
Aliases, available since C# 1.0 and enhanced in C# 12.0 to support more complex types, help in scenarios with conflicting names from different assemblies.[43] Global using directives, added in C# 10.0, apply project-wide to all files in an assembly, reducing repetition in multi-file projects:
csharp
global using System;
global using static System.Console;
global using System;
global using static System.Console;
These can be placed in any file but are typically centralized in a shared file like GlobalUsings.cs, and they support static and alias forms.[44]
To avoid namespace pollution—where excessive imports lead to name conflicts or cluttered global scope—developers should use targeted using directives rather than importing many overlapping namespaces, qualify names explicitly when ambiguities arise, and leverage aliases or file-scoped structures to maintain clean, hierarchical organization. This practice ensures type resolution remains efficient and predictable, especially in large-scale applications with multiple referenced libraries.[40] Identifiers can always be fully qualified with their namespace to resolve any such issues.[45]
Entry Point and Top-Level Statements
In C# applications, the entry point serves as the starting location for program execution, traditionally defined as a static method named Main within a class. This method must be declared as static and is invoked by the common language runtime (CLR) upon program startup. The standard signatures include static void Main() for programs without command-line arguments and static void Main([string](/page/String)[] args) to accept arguments as a string array, where args provides access to parameters passed when the program is run. Additionally, Main can return an integer exit code using static [int](/page/INT) Main() or static [int](/page/INT) Main([string](/page/String)[] args), allowing the program to signal success (typically 0) or failure to the operating system.[46]
Introduced in C# 7.1, asynchronous entry points extend the Main method to support async operations with signatures such as static async Task Main() or static async Task Main(string[] args), returning a Task to handle asynchronous initialization without blocking the thread. The integer-returning variants, static async Task<int> Main() and static async Task<int> Main(string[] args), enable async code while providing exit codes via await-able tasks. These forms maintain compatibility with the traditional Main while allowing modern asynchronous patterns, such as awaiting I/O operations at startup.[47]
C# 9.0 introduced top-level statements, simplifying program structure by permitting executable code directly at the file level without requiring an explicit Main method, enclosing class, or namespace declaration for console applications. The compiler automatically generates an appropriate Main entry point based on the code's content—for instance, producing static void Main(string[] args) if no await or return statements are present, or static async Task<int> Main(string[] args) if both asynchronous operations and integer returns are used. This feature is limited to a single file per project to avoid ambiguity, and the generated Main includes the args parameter for command-line arguments, accessible as a non-null string array with args.Length indicating the number of arguments. Return values in top-level statements are handled implicitly; an explicit return expression yields an integer exit code, while unhandled exceptions propagate as process failure.[48]
In top-level statement contexts, C# 10 enhances usability through implicit global using directives, automatically including common namespaces like System and System.Threading.Tasks for projects targeting .NET 6 or later, without needing explicit using statements at the file top. These implicit usings apply project-wide, reducing boilerplate in simple programs while preserving the ability to add custom global usings via the global using keyword or project file configuration. For more complex scenarios involving external types, namespaces may be referenced briefly to qualify names within the entry point code.[43]
csharp
// Traditional Main example
public class Program
{
public static void Main(string[] args)
{
if (args.[Length](/page/Length) > 0)
{
Console.WriteLine($"Hello, {args[0]}!");
}
else
{
Console.WriteLine("Hello, World!");
}
}
}
// Traditional Main example
public class Program
{
public static void Main(string[] args)
{
if (args.[Length](/page/Length) > 0)
{
Console.WriteLine($"Hello, {args[0]}!");
}
else
{
Console.WriteLine("Hello, World!");
}
}
}
csharp
// Top-level statements example (C# 9.0+)
Console.WriteLine("Hello, World!");
if (args.[Length](/page/Length) > 0)
{
Console.WriteLine($"Argument count: {args.[Length](/page/Length)}");
}
[return 0](/page/Return_0); // Explicit exit code
// Top-level statements example (C# 9.0+)
Console.WriteLine("Hello, World!");
if (args.[Length](/page/Length) > 0)
{
Console.WriteLine($"Argument count: {args.[Length](/page/Length)}");
}
[return 0](/page/Return_0); // Explicit exit code
csharp
// Async top-level statements example (C# 9.0+ with async)
var delayTask = Task.Delay(1000);
await delayTask;
Console.WriteLine("Async startup complete.");
// Async top-level statements example (C# 9.0+ with async)
var delayTask = Task.Delay(1000);
await delayTask;
Console.WriteLine("Async startup complete.");
Data Types
Value Types
Value types in C# represent data stored directly within a variable, typically allocated on the stack, which enables efficient access and avoids heap allocation overhead. Unlike reference types, value types exhibit copy-by-value semantics, meaning that when assigned or passed as arguments, a complete copy of the data is made, ensuring that modifications to the copy do not affect the original instance. This behavior promotes immutability in certain contexts but requires careful consideration for larger structs to prevent performance issues from excessive copying. Value types include both predefined simple types provided by the language and user-defined types such as structs and enums.[49][49]
Predefined Value Types
C# provides a set of built-in value types categorized as integral numeric types, floating-point types, decimal, Boolean, and character types, each with specific sizes and value ranges defined by the .NET runtime. These types are aliases for underlying .NET structs, such as int for System.Int32, and are optimized for common operations like arithmetic and comparisons. The following table summarizes their key characteristics:
| Type | Size (bits) | Range/Value |
|---|
| sbyte | 8 | -128 to 127 |
| byte | 8 | 0 to 255 |
| short | 16 | -32,768 to 32,767 |
| ushort | 16 | 0 to 65,535 |
| int | 32 | -2,147,483,648 to 2,147,483,647 |
| uint | 32 | 0 to 4,294,967,295 |
| long | 64 | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
| ulong | 64 | 0 to 18,446,744,073,709,551,615 |
| float | 32 | ±1.5e−45 to ±3.4e38, 7 digits precision |
| double | 64 | ±5.0e−324 to ±1.7e308, 15-16 digits precision |
| decimal | 128 | ±1.0 × 10−28 to ±7.9228 × 10^28, 28-29 digits precision |
| bool | 1 (effective) | true or false |
| char | 16 | Unicode characters, U+0000 to U+FFFF |
These predefined types support implicit conversions within compatible categories, such as from int to long, and are initialized to default values like 0 for numerics and false for bool.[50]
User-Defined Value Types
User-defined value types extend C#'s type system by allowing developers to create custom structs and enums that inherit the stack allocation and copy-by-value behavior of value types. Structs are versatile for encapsulating small, related data and methods, while enums provide named constants for discrete values.
Structs are declared using the struct keyword and can include fields, properties, methods, and constructors, but they cannot inherit from other types except interfaces. For example:
csharp
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Move(int dx, int dy)
{
X += dx;
Y += dy;
}
}
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Move(int dx, int dy)
{
X += dx;
Y += dy;
}
}
Instances of structs are mutable by default, allowing field modifications, but readonly structs restrict changes to prevent unintended copies. Due to their value semantics, passing a struct to a method by value creates a full copy, which is efficient for small data but can impact performance for larger ones.[51][52]
Enums are value types defined as a set of named constants backed by an underlying integral type, defaulting to int if unspecified. They are declared with the enum keyword and support explicit values for members. For instance:
csharp
public enum Color
{
Red = 1,
Green = 2,
Blue = 4
}
public enum Color
{
Red = 1,
Green = 2,
Blue = 4
}
Since C# 7.3, enums can specify a custom underlying type, such as byte, to optimize storage: public enum E : byte { A = 0, B = 1 }. Enum values are copied by value, and they support bitwise operations when used as flags.[53][54]
Tuples, introduced in C# 7.0, are lightweight value types for grouping a fixed number of elements of arbitrary types, declared using parentheses syntax. They are implemented via System.ValueTuple structs and support both unnamed and named elements for readability:
csharp
[var](/page/Var) pair = (1, "one"); // Unnamed tuple
[var](/page/Var) named = (Number: 1, Name: "one"); // Named tuple
[var](/page/Var) pair = (1, "one"); // Unnamed tuple
[var](/page/Var) named = (Number: 1, Name: "one"); // Named tuple
Tuple elements are public fields, making them mutable, and deconstruction allows easy unpacking, such as var (num, str) = pair;. They provide a concise alternative to custom structs for temporary data grouping.[55]
Ref structs, available since C# 7.2, are a specialized category of structs declared with the ref struct keyword, enforced to be stack-allocated only and preventing boxing to reference types like object. This restriction avoids heap escapes, enabling high-performance scenarios like spans:
csharp
public ref struct Coordinate
{
public int X;
public int Y;
}
public ref struct Coordinate
{
public int X;
public int Y;
}
Ref structs cannot be used in fields, arrays, or as type arguments in generics (prior to C# 13), ensuring they remain short-lived and stack-bound, which is crucial for types like Span<T>. Unlike ref structs, other value types can be boxed to reference types when needed, placing a copy on the heap.[56][49]
In C# 13, ref structs gained additional capabilities while preserving their stack-only nature: they can implement interfaces (though without implicit conversion to interface types to avoid boxing), be used in async methods and iterators (with restrictions preventing escape across await or yield boundaries), and serve as type arguments in generic types via the new allows ref struct constraint. The System.Threading.Lock type is also a ref struct, integrated with the lock statement for efficient synchronization.[10]
Reference Types
Reference types in C# are types that store references to their data rather than the data itself, with instances allocated on the managed heap and managed by the garbage collector.[57] This design enables reference semantics, where assigning one reference variable to another results in both pointing to the same object, and modifications through one affect the shared instance.[57] Unlike value types, reference types support null as a valid value, indicating no object is referenced, and nullable reference types can be annotated with ? to explicitly allow or disallow null assignments at compile time.[57][58]
Arrays are a fundamental reference type in C#, used to store multiple elements of the same type in contiguous memory.[59] Single-dimensional arrays are declared with syntax like type[] identifier;, and initialized using new type[size], for example:
csharp
int[] numbers = new int[5]; // Creates an array of 5 integers, initialized to 0
int[] numbers = new int[5]; // Creates an array of 5 integers, initialized to 0
Multidimensional arrays use comma-separated dimensions, such as int[,] for two-dimensional arrays:
csharp
int[,] matrix = new int[2, 3]; // 2 rows, 3 columns
int[,] matrix = new int[2, 3]; // 2 rows, 3 columns
Jagged arrays, which are arrays of arrays, are declared as type[][] identifier and allow varying lengths for inner arrays:
csharp
int[][] jagged = new int[3][]; // Array of 3 int arrays
jagged[0] = new int[2] { 1, 2 };
jagged[1] = new int[4] { 3, 4, 5, 6 };
int[][] jagged = new int[3][]; // Array of 3 int arrays
jagged[0] = new int[2] { 1, 2 };
jagged[1] = new int[4] { 3, 4, 5, 6 };
Starting in C# 12, collection expressions provide concise initialization using square brackets, applicable to arrays and other collections:
csharp
int[] numbers = [1, 2, 3, 4, 5]; // Single-dimensional
int[,] matrix = [[1, 2, 3], [4, 5, 6]]; // Multidimensional
int[][] jagged = [[1, 2], [3, 4, 5, 6]]; // Jagged
int[] numbers = [1, 2, 3, 4, 5]; // Single-dimensional
int[,] matrix = [[1, 2, 3], [4, 5, 6]]; // Multidimensional
int[][] jagged = [[1, 2], [3, 4, 5, 6]]; // Jagged
[59][28]
Classes define custom reference types, declared with the class keyword followed by an identifier and optional access modifiers.[60] A basic declaration is:
csharp
public [class](/page/Class) MyClass { }
public [class](/page/Class) MyClass { }
Objects are created using the new operator, which allocates memory on the heap and returns a reference:
csharp
MyClass instance = new(); // Target-typed new (C# 9+), or new MyClass()
MyClass instance = new(); // Target-typed new (C# 9+), or new MyClass()
This syntax invokes the parameterless constructor if available, producing a reference that can be null.[60][61]
Strings represent sequences of characters and are built-in reference types aliasing System.String, which is immutable—any modification creates a new string instance.[62] Declaration uses the string keyword, with special literals including ordinary quoted strings ("text"), verbatim strings (@"text" to escape backslashes), and raw string literals (C# 11+, """multiline text""" for unescaped content). UTF-8 string literals (C# 11+) use u8 suffix, producing ReadOnlySpan<byte>:
csharp
string s1 = "Hello"; // Ordinary
string s2 = @"C:\Path"; // Verbatim
string s3 = """Line1
Line2"""; // Raw
ReadOnlySpan<byte> utf8 = "Hello"u8;
string s1 = "Hello"; // Ordinary
string s2 = @"C:\Path"; // Verbatim
string s3 = """Line1
Line2"""; // Raw
ReadOnlySpan<byte> utf8 = "Hello"u8;
Equality uses value-based operators (==, !=), and indexing provides read-only access.[62]
Delegates are reference types that encapsulate methods with a specific signature, declared using the delegate keyword specifying return type and parameters.[63] A basic declaration is:
csharp
public delegate void MyDelegate(int value);
public delegate void MyDelegate(int value);
Instances are created by assigning compatible methods, anonymous methods, or lambda expressions, enabling type-safe function pointers.[63]
Other reference types include object, the base type for all types, declared simply as object o = null;.[62] The dynamic type, introduced in C# 4.0, allows runtime binding and bypasses compile-time type checking:
csharp
dynamic d = 5;
d = d + 10; // Resolved at [runtime](/page/Runtime)
dynamic d = 5;
d = d + 10; // Resolved at [runtime](/page/Runtime)
[62] In unsafe contexts, pointers are reference-like types declared with *, such as int* ptr;, requiring the unsafe modifier and compiler option for direct memory access.[64]
Type Conversions
C# supports conversions between compatible types to enable flexible data manipulation while ensuring type safety. These conversions can be implicit, where the compiler handles the transformation automatically without risk of data loss, or explicit, requiring programmer intervention via casting operators. Conversions apply to both value and reference types, with special handling for nullable types and scenarios involving the common base type object. The language specification defines built-in conversions for numeric types, reference assignments, and boxing, while allowing user-defined conversions for custom types.[65]
Implicit conversions occur without explicit syntax and are guaranteed not to lose information, making them safe for widening operations. For numeric types, these include promotions from smaller integral types to larger ones, such as int to long, or from integral to floating-point types like int to double. For example:
csharp
int value = 42;
long largerValue = value; // Implicit widening conversion
int value = 42;
long largerValue = value; // Implicit widening conversion
Reference types support implicit conversions from derived classes to base classes, and value types can implicitly convert to nullable versions of themselves. Nullable value types, declared as T?, allow implicit conversion from T to T?, accommodating null values in value-type contexts. For instance:
csharp
int? nullableInt = 42; // Implicit from int to int?
int? nullableInt = 42; // Implicit from int to int?
These conversions are performed at compile time and have no runtime overhead.[66][67]
Explicit conversions, also known as casts, use the syntax (targetType)expression and are required when data loss or overflow is possible, such as narrowing numeric types from double to int. This can truncate fractional parts or cause overflow in integral conversions. For example:
csharp
double pi = 3.14159;
int truncated = (int)pi; // Results in 3
double pi = 3.14159;
int truncated = (int)pi; // Results in 3
In checked contexts, explicit conversions that overflow throw an OverflowException; unchecked contexts silently wrap the value. Contexts can be specified with checked or unchecked keywords, or controlled project-wide via compiler settings, with unchecked as the default for performance. Example in a checked block:
csharp
int large = 300;
byte small = checked((byte)large); // Throws OverflowException
int large = 300;
byte small = checked((byte)large); // Throws OverflowException
This mechanism protects against unintended data corruption in critical sections.[66][68]
Boxing and unboxing provide conversions between value types and reference types, bridging the type system for polymorphism. Boxing implicitly converts a value type to object or an interface it implements, allocating a new object on the heap. For example:
csharp
int integer = 123;
object boxed = integer; // [Boxing](/page/Boxing) occurs
int integer = 123;
object boxed = integer; // [Boxing](/page/Boxing) occurs
Unboxing explicitly extracts the value back, requiring a compatible cast; mismatches throw InvalidCastException. Example:
csharp
int unboxed = (int)boxed; // [Unboxing](/page/Unboxing)
int unboxed = (int)boxed; // [Unboxing](/page/Unboxing)
These operations incur performance costs due to heap allocation (boxing) and runtime type checks (unboxing), so they should be minimized in performance-sensitive code, such as loops processing value types as objects.[69]
The is and as operators facilitate safe type checking and conditional casting, avoiding exceptions from direct casts. The is operator tests compatibility, returning true if the operand can be converted via reference, boxing, unboxing, or nullable conversions. For example:
csharp
object obj = "hello";
if (obj is string) { /* safe to cast */ }
object obj = "hello";
if (obj is string) { /* safe to cast */ }
The as operator attempts a cast to a reference or nullable type, returning null on failure rather than throwing an exception, useful for reference types. Example:
csharp
object input = "hello";
string result = input as string; // result is "hello"
object input = "hello";
string result = input as string; // result is "hello"
These operators promote robust code by enabling pattern-based type verification without overhead from failed casts.[70]
User-defined conversions allow classes and structs to define implicit or explicit operators for conversions to or from other types, extending built-in capabilities. These are declared as static methods with implicit or explicit modifiers, enabling seamless integration in expressions while respecting the safety rules of the type system. Details on implementation are covered in operator overloading sections.[71]
Operators
Arithmetic and Assignment Operators
C# provides a set of arithmetic operators for performing mathematical operations on numeric types, including integral types such as int, uint, long, and ulong, as well as floating-point types like float, double, and decimal. These operators include both unary forms for single operands and binary forms for two operands, supporting common computations like addition, subtraction, multiplication, division, and modulo. Unary operators also include increment (++) and decrement (--) for modifying values by 1.[72]
The unary plus (+) operator returns the value of its operand unchanged, while the unary minus (-) computes the numeric negation of its operand; the latter is not supported for ulong due to the absence of negative values in unsigned types. The increment (++) and decrement (--) operators can be used in prefix or postfix forms: in prefix form (e.g., ++x), the operand is modified before its value is used, whereas in postfix form (e.g., x++), the current value is used before modification. These operators are applicable to all numeric types, with smaller integral types like sbyte or short being promoted to int for the operation. For example:
csharp
int x = 5;
Console.WriteLine(++x); // Outputs: 6 (x is now 6)
Console.WriteLine(x++); // Outputs: 6 (x is now 7)
int x = 5;
Console.WriteLine(++x); // Outputs: 6 (x is now 6)
Console.WriteLine(x++); // Outputs: 6 (x is now 7)
Binary arithmetic operators include addition (+), which sums the operands and also supports string concatenation or delegate combination in non-numeric contexts; subtraction (-), which computes the difference and can remove delegates; multiplication (*), which scales the operands; division (/), which yields the quotient with integer division truncating toward zero and floating-point division producing a precise result; and the modulo (%), which returns the remainder with the sign matching the dividend for integers. Mixed-type operations undergo numeric promotions to ensure compatibility, such as promoting int and double to double. An illustrative example is:
csharp
int a = 10, b = 3;
double result = a / b; // 3.333... (promoted to double)
Console.WriteLine(a % b); // Outputs: 1
Console.WriteLine(-10 % 3); // Outputs: -1 (sign matches [dividend](/page/Dividend))
int a = 10, b = 3;
double result = a / b; // 3.333... (promoted to double)
Console.WriteLine(a % b); // Outputs: 1
Console.WriteLine(-10 % 3); // Outputs: -1 (sign matches [dividend](/page/Dividend))
Overflow in arithmetic operations on integral types can occur when the result exceeds the representable range, leading to undefined behavior in unchecked contexts or exceptions in checked ones. By default, non-constant expressions execute in an unchecked context, where overflow wraps around by truncating high-order bits, but this can be overridden using checked blocks or expressions to enforce runtime checks that throw OverflowException. Constant expressions are checked by default, producing compile-time errors on overflow, though unchecked can suppress this. Floating-point operations handle overflow by yielding positive or negative infinity, and division by zero results in NaN or infinity without regard to context; decimal operations always throw OverflowException or DivideByZeroException. For instance:
csharp
checked {
int max = int.MaxValue;
int overflow = max + 1; // Throws OverflowException
}
int uncheckedOverflow = unchecked(max + 1); // Wraps to int.MinValue
checked {
int max = int.MaxValue;
int overflow = max + 1; // Throws OverflowException
}
int uncheckedOverflow = unchecked(max + 1); // Wraps to int.MinValue
Assignment operators facilitate storing computed values, with the simple assignment (=) copying the right-hand operand's value to the left-hand operand—for value types, this duplicates the data, while for reference types, it copies the reference. Compound assignment operators combine arithmetic with assignment, such as += (equivalent to x = x + y), -=, *=, /=, and %=, as well as bitwise &=, |=, ^=, <<=, >>=, and >>>=, evaluating the left operand only once and applying implicit conversions if needed. These are particularly efficient for in-place modifications and support all arithmetic binary operators. An example demonstrates:
csharp
int sum = 10;
sum += 5; // sum is now 15
sum *= 2; // sum is now 30
int sum = 10;
sum += 5; // sum is now 15
sum *= 2; // sum is now 30
For computations exceeding the range of built-in integral types, the System.Numerics.BigInteger struct provides arbitrary-precision signed integer arithmetic, supporting the same operators (+, -, *, /, %) as well as methods like Add and Multiply for explicit calls. It is instantiated via constructors from numeric types, byte arrays, or strings, and handles large values without overflow by dynamically allocating storage. Integer literals in C# are limited to built-in types, but large literals can be converted to BigInteger instances. User-defined types can overload arithmetic operators to customize behavior for custom numeric classes.[73]
Relational and Logical Operators
C# provides relational operators to compare the order or equality of operands, returning a boolean value (true or false) based on the evaluation. These include the equality operator (==), inequality operator (!=), less than (<), greater than (>), less than or equal to (<=), and greater than or equal to (>=). They are applicable to numeric types such as integers (int), floating-point ([double](/page/Double), [float](/page/Float)), characters ([char](/page/Char)), and enums, as well as strings. For numeric types, comparisons evaluate the relative values directly; for example, 5 < 10 yields true, while 7.0 < 5.1 yields false.[74][74]
Type-testing operators include is, which returns true if the operand is compatible with the specified type or pattern, and as, which attempts to cast the operand to the specified type, returning the result if successful or null if not. The is operator, enhanced since C# 7.0 to support pattern matching (e.g., obj is string s), enables type-safe checks without exceptions. The as operator is used for reference or nullable conversions, such as string str = obj as string;, avoiding InvalidCastException. These operators apply to reference types, value types (via boxing if needed), and nullable types. For example:
csharp
object obj = "hello";
if (obj is string text)
{
Console.WriteLine(text.Length); // Outputs: 5
}
string result = obj as string ?? "default";
object obj = "hello";
if (obj is string text)
{
Console.WriteLine(text.Length); // Outputs: 5
}
string result = obj as string ?? "default";
String comparisons using these operators perform ordinal comparisons based on the Unicode code points of characters, treating strings as sequences of characters. For instance, "apple" < "banana" returns true because the first differing character ('a' code 97) is less than ('b' code 98). Special handling applies to floating-point values: comparisons involving NaN (Not a Number), such as double.NaN >= 5.1, always return false to avoid indeterminate results. Enums are compared based on their underlying integral values. User-defined types can overload these operators, but relational operators like < and > must be overloaded in pairs with <= and >= respectively for consistency.[74][74][74]
Logical operators in C# manipulate boolean values to perform negation, conjunction, disjunction, and exclusive disjunction. The unary logical negation operator (!) inverts a boolean operand, so !true evaluates to false and !false to true. The binary conditional logical AND (&&) returns true only if both operands are true, with short-circuit evaluation: if the left operand is false, the right is not evaluated, as in false && SecondOperand() where SecondOperand() is skipped. Similarly, the conditional logical OR (||) returns true if either operand is true, short-circuiting if the left is true.[75][75][75]
Non-conditional logical operators include the binary AND (&), OR (|), and exclusive OR (^), which evaluate both operands regardless of the left operand's value. For boolean types, & yields true if both are true, | if at least one is true, and ^ if exactly one is true; examples include true & false as false and true ^ false as true. These operators also function as bitwise operators on integral types, but when applied to bool, they perform logical operations. The unary bitwise complement (~) is not defined for bool and is used solely for bitwise negation on integers.[75][75][76]
Bitwise shift operators manipulate the bits of integral types (including char and enums). The left-shift operator (<<) shifts bits left by the specified amount, filling with zeros and equivalent to multiplication by 2^amount for positive values. The signed right-shift (>>) shifts right, preserving the sign bit (arithmetic shift for signed types), while the unsigned right-shift (>>>) fills with zeros (logical shift). The right operand (shift amount) is converted to int and masked to the operand's bit width (e.g., 5 bits for int, 6 for long). Shifts by amounts >= width yield zero for unsigned or sign-extended for signed >>. These apply only to integrals, with promotion for smaller types. For example:
csharp
int value = 8; // Binary: 00001000
Console.WriteLine(value << 1); // Outputs: 16 (00010000)
Console.WriteLine(-8 >> 1); // Outputs: -4 (sign preserved)
Console.WriteLine(8 >>> 1); // Outputs: 4 (11111000 >>>1 = 01111100, but for positive same)
int value = 8; // Binary: 00001000
Console.WriteLine(value << 1); // Outputs: 16 (00010000)
Console.WriteLine(-8 >> 1); // Outputs: -4 (sign preserved)
Console.WriteLine(8 >>> 1); // Outputs: 4 (11111000 >>>1 = 01111100, but for positive same)
The conditional operator (? :), also known as the ternary operator, provides a concise way to select one of two expressions based on a boolean condition. Its syntax is condition ? consequent : alternative, where the condition must evaluate to true or false; if true, the consequent is evaluated and returned, otherwise the alternative. For example, int temp = [15](/page/15); string message = temp < 20 ? "Cold" : "Warm"; assigns "Cold" to message. The types of consequent and alternative must be compatible, either identical or implicitly convertible to a common type, and only one branch is evaluated, similar to short-circuiting in && and ||. It is right-associative, meaning a ? b : c ? d : e parses as a ? b : (c ? d : e).[77][77][77]
These operators are foundational in control flow constructs, such as if statements, where relational and logical expressions determine execution paths; for instance, if (x > 0 && y < 10) evaluates the condition before proceeding. As of C# 14, enhancements allow explicit overloading of compound assignments for &, |, and ^ to improve efficiency in user-defined types.[75][75]
csharp
// Example: Relational operators with numbers and strings
int a = 5, b = 10;
Console.WriteLine(a < b); // True
string s1 = "hello", s2 = "world";
Console.WriteLine(s1 != s2); // True
// Logical operators
bool p = true, q = false;
Console.WriteLine(p && q); // False (short-circuits if p false)
Console.WriteLine(p || q); // True
Console.WriteLine(!p); // False
// Conditional operator
int score = 85;
string grade = score >= 90 ? "A" : "B";
Console.WriteLine(grade); // B
// Example: Relational operators with numbers and strings
int a = 5, b = 10;
Console.WriteLine(a < b); // True
string s1 = "hello", s2 = "world";
Console.WriteLine(s1 != s2); // True
// Logical operators
bool p = true, q = false;
Console.WriteLine(p && q); // False (short-circuits if p false)
Console.WriteLine(p || q); // True
Console.WriteLine(!p); // False
// Conditional operator
int score = 85;
string grade = score >= 90 ? "A" : "B";
Console.WriteLine(grade); // B
Special Operators
C# provides several special operators that enhance null safety, member and element access, delegate management, and parameter passing, distinct from standard arithmetic or relational operators. These operators, introduced across various language versions, address common programming scenarios such as avoiding null reference exceptions, efficient event handling, and reference semantics without requiring explicit pointer manipulation in most cases.[78]
The null-coalescing operator ??, available since C# 2.0, evaluates to the left-hand operand if it is not null; otherwise, it evaluates to the right-hand operand, enabling concise fallback values for nullable types or reference types. For example, string name = userName ?? "Anonymous"; assigns "Anonymous" if userName is null, preventing null propagation in expressions. This operator is right-associative, allowing chaining like a ?? b ?? c, and its right-hand side is not evaluated if the left is non-null, supporting lazy evaluation.[79][80]
Introduced in C# 8.0, the null-coalescing assignment operator ??= assigns the right-hand operand to the left-hand operand only if the left is null, combining null checks with assignment in one step. The syntax left ??= right; ensures the right operand is evaluated only when necessary, and the left must be assignable such as a variable, property, or indexer, but not a non-nullable value type. An example is List<int>? numbers = null; numbers ??= new List<int>(); numbers.Add(5);, which initializes the list only if it was null.[79]
For member access, the dot operator . , present since C# 1.0, accesses fields, properties, methods, or nested types on an instance or namespace, such as console.WriteLine("Hello");, but it throws a NullReferenceException if the instance is null. The null-conditional operator ?., added in C# 6.0, mitigates this by returning null if the left operand is null, short-circuiting further access in chains like customer?.Orders?.Count ?? 0. This operator works with members, methods, indexers, and even delegates (invoking only if non-null), and since C# 14, it supports assignment contexts. In unsafe code, the pointer member access operator -> accesses members through pointers, equivalent to (*pointer).member, requiring an unsafe context and used for low-level operations like fixed (char* pChars = chars) { *pChars = 'a'; } pChars->Length;.[81][82][83]
The index operator [], fundamental since C# 1.0, accesses array elements or custom indexer properties, with integer indices for arrays throwing IndexOutOfRangeException on bounds violation, as in int[] array = {1, 2}; int value = array[0];. The null-conditional indexer ?[], introduced in C# 6.0, applies null checking before indexing, returning null if the expression is null, useful for safe dictionary or collection access like dictionary?["key"] ?? defaultValue. Since C# 14, assignments like collection?[index] = value; are supported only if the collection is non-null.[84][85]
Delegate operators += and -= manage event subscriptions, where += adds a method as an event handler and -= removes it, leveraging multicast delegates for events declared with the event keyword. For instance, button.Click += OnClick; button.Click -= OnClick; attaches and detaches handlers without directly modifying the delegate, ensuring thread safety in event raising. These operators work on any delegate type but are idiomatic for events to prevent external modifications.[86]
Since C# 7.0, the ref keyword enables reference semantics in parameters, returns, and locals, allowing methods to modify caller variables directly, as in ref int max(ref int a, ref int b) { if (a > b) return ref a; return ref b; }, which returns a reference to the larger value for further manipulation without copying. ref variables alias existing storage, supporting efficient passing of large structures. The out keyword, also enhanced in C# 7.0, requires methods to assign values to parameters before return and allows declaration at the call site without prior initialization, like if (int.TryParse("1", out var number)) { /* use number */ }, facilitating multiple return values idiomatically.[87][88]
The discard _, introduced in C# 7.0, serves as a placeholder for unused variables in deconstruction, out parameters, or pattern matching, improving code clarity by explicitly ignoring values. In tuple deconstruction, (string name, _, int age) = person; discards the middle element; for out parameters, DateTime.TryParse(input, out _); suppresses the result; and in switch expressions, case var _: matches any input without binding. Using _ as a regular variable outside these contexts is allowed but may trigger warnings for unused identifiers.[89][90]
Control Flow
Conditional Statements
Conditional statements in C# enable decision-making by executing different code paths based on Boolean conditions or pattern matches. These include the if statement for simple branching, the switch statement for multi-way selection using labels or patterns, and the switch expression (introduced in C# 8.0) for concise value-based pattern matching. Conditions typically evaluate Boolean expressions, which may incorporate logical operators such as &&, ||, and ! for complex predicates.[91][92]
The if statement evaluates a Boolean expression and executes its associated block if the result is true. Its basic syntax is if (boolean_expression) statement, where statement can be a single expression or a block enclosed in curly braces { }. An optional else clause provides an alternative block for when the condition is false, using the syntax if (boolean_expression) statement else statement. Nested if statements allow for chained conditions, and the ternary conditional operator condition ? expr1 : expr2 offers a compact alternative for simple expressions. For example:
csharp
int temperature = 15;
if (temperature < 20) {
Console.WriteLine("It's cold.");
} else {
Console.WriteLine("It's warm.");
}
int temperature = 15;
if (temperature < 20) {
Console.WriteLine("It's cold.");
} else {
Console.WriteLine("It's warm.");
}
This executes the first block if temperature < 20 evaluates to true; otherwise, it runs the else block.[91]
The switch statement selects one of several code paths by matching an expression against case labels or patterns. In its traditional form (available since early C# versions), it uses constant labels with the syntax switch (expression) { case constant: statements; break; ... default: statements; break; }, where break prevents fall-through to subsequent cases, and default handles unmatched values. Enhanced in C# 7.0, the switch statement supports pattern matching, allowing non-constant patterns like type checks (with when clause from C# 7.0), with the syntax switch (expression) { case pattern when guard: statements; ... }. Later versions, such as C# 9.0 and beyond (including relational, list, and extended property patterns in C# 10.0–13.0), further expand pattern capabilities for ranges, collections, and objects. Execution proceeds to the first matching case and exits via break, return, or throw. An example of C# 7.0 pattern matching:
csharp
object value = 42;
switch (value)
{
case int i when i > 0:
Console.WriteLine("Positive integer.");
break;
case string s:
Console.WriteLine("String value.");
break;
default:
Console.WriteLine("Other type.");
break;
}
object value = 42;
switch (value)
{
case int i when i > 0:
Console.WriteLine("Positive integer.");
break;
case string s:
Console.WriteLine("String value.");
break;
default:
Console.WriteLine("Other type.");
break;
}
Here, type patterns (int, string) and a when guard are used. Multiple patterns can share a single case block, and the default label can appear anywhere. For advanced patterns like relational comparisons (e.g., case < 0), see C# 9.0 documentation.[91][92][93][25]
Introduced in C# 8.0, the switch expression provides a more concise, expression-oriented alternative to the switch statement, evaluating to a value rather than controlling flow. Its syntax is expression switch { pattern [when guard] => subexpression, ... }, where arms are separated by commas, and the result is the subexpression of the first matching arm. It requires exhaustiveness—all possible input values must be covered, often via a discard pattern _—to avoid runtime exceptions like SwitchExpressionException in .NET Core 3.0 and later. Guards use when clauses similarly to the statement form. For instance (C# 8.0 compatible):
csharp
string direction = "North";
Orientation result = direction switch
{
"North" => Orientation.North,
"South" => Orientation.South,
"East" => [Orientation](/page/Orientation).Horizontal,
"West" => [Orientation](/page/Orientation).Horizontal,
_ => throw new ArgumentException("Invalid direction.")
};
string direction = "North";
Orientation result = direction switch
{
"North" => Orientation.North,
"South" => Orientation.South,
"East" => [Orientation](/page/Orientation).Horizontal,
"West" => [Orientation](/page/Orientation).Horizontal,
_ => throw new ArgumentException("Invalid direction.")
};
This assigns Orientation.North if direction matches "North"; separate cases handle "East" and "West". The discard _ ensures completeness. Switch expressions support the same patterns as statements, including type, constant, and property patterns (with combinators like or added in C# 9.0), but cannot have empty arms and are evaluated in textual order.[94]
Iteration Statements
Iteration statements in C# provide mechanisms to repeatedly execute a block of code based on specified conditions or over collections of data, enabling efficient handling of repetitive tasks. These statements include the while, do, for, and foreach constructs, each designed for different iteration scenarios, from condition-based loops to traversing enumerable sequences. They are fundamental to control flow in C# programs, allowing developers to process data structures like arrays, lists, and other collections that implement IEnumerable or IEnumerable.[95][38]
The while statement executes its embedded statement or block as long as a given Boolean expression evaluates to true, checking the condition before each iteration. Its syntax is while (boolean_expression) embedded_statement, where the embedded statement can be a single statement or a block enclosed in curly braces. For example, the following code prints numbers from 0 to 4:
csharp
[int](/page/INT) n = 0;
while (n < 5)
{
Console.Write(n);
n++;
}
[int](/page/INT) n = 0;
while (n < 5)
{
Console.Write(n);
n++;
}
This construct is useful for loops where the number of iterations is unknown in advance and depends on a runtime condition. The condition is evaluated at the start of each iteration, so the loop body may execute zero times if the initial condition is false.[95][96]
In contrast, the do statement executes its body at least once before checking the Boolean condition, making it suitable for scenarios where initial execution is required regardless of the condition. Its syntax is do embedded_statement while (boolean_expression);, with the semicolon required after the condition. Using the same example logic:
csharp
int n = 0;
do
{
Console.Write(n);
n++;
} while (n < 5);
int n = 0;
do
{
Console.Write(n);
n++;
} while (n < 5);
This ensures the loop runs a minimum of one iteration, differing from while by postponing the condition check until after the body executes.[95][97]
The for statement combines initialization, condition checking, and iteration updating into a single construct, ideal for loops with a known or countable number of iterations. Its syntax is for (for_initializer_opt; for_condition_opt; for_iterator_opt) embedded_statement, where components are optional and separated by semicolons. A typical numeric loop is:
csharp
for (int i = 0; i < 3; i++)
{
Console.Write(i);
}
for (int i = 0; i < 3; i++)
{
Console.Write(i);
}
The initializer executes once before the loop starts, the condition is checked before each iteration (defaulting to true if omitted), and the iterator runs after each body execution. This structure promotes clear, compact code for index-based iterations.[95][98]
The foreach statement simplifies iteration over collections that implement IEnumerable or IEnumerable, automatically handling enumeration without explicit indexing. Its basic syntax is foreach (type identifier in expression) embedded_statement, where the expression yields an enumerable collection. For instance, iterating over an array of integers:
csharp
int[] fibNumbers = { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fibNumbers)
{
Console.Write(element + " ");
}
int[] fibNumbers = { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fibNumbers)
{
Console.Write(element + " ");
}
Starting in C# 7.0, foreach supports deconstruction, allowing iteration over tuples or objects by unpacking multiple values into variables, such as foreach ((int key, string value) in dictionary) { ... } for Dictionary entries. Break and continue statements can be used within any iteration statement to exit early or skip to the next iteration. Since C# 8.0, ranges (using the .. operator) enable slicing collections for targeted iteration, as in foreach (var word in words[1..4]) { ... } to process a subset of an array or string.[95][99][100]
Jump and Exception Handling
Jump statements in C# provide mechanisms to alter the normal flow of execution within methods, primarily by terminating or skipping parts of loops and switches or by returning control to the caller. These include the break, continue, goto, and return statements, which enable precise control over program flow without relying on conditional branching alone.[101] Break and continue are particularly useful within iteration statements like for, while, or foreach loops to interrupt or resume iterations, while goto allows unconditional jumps to labeled statements, though its use is generally discouraged in favor of structured alternatives due to potential for unstructured code.[101] The return statement exits the current method, optionally providing a value back to the caller if the method has a non-void return type.[101]
The break statement terminates the innermost enclosing loop or switch statement, transferring control to the statement following the terminated construct. For example, in a loop, it exits early when a condition is met:
csharp
for (int i = 0; i < 10; i++)
{
if (i == 5) break; // Exits the [loop](/page/Loop) when i is 5
Console.WriteLine(i);
}
for (int i = 0; i < 10; i++)
{
if (i == 5) break; // Exits the [loop](/page/Loop) when i is 5
Console.WriteLine(i);
}
This outputs numbers 0 through 4. In switch statements, break ensures fall-through does not occur between cases.[101]
The continue statement skips the remaining statements in the current iteration of the innermost enclosing loop and proceeds to the next iteration. It cannot be used in switch statements. For instance:
csharp
foreach (int i in Enumerable.Range(0, 10))
{
if (i % 2 == 0) continue; // Skips even numbers
Console.WriteLine(i); // Outputs odd numbers: 1, 3, 5, 7, 9
}
foreach (int i in Enumerable.Range(0, 10))
{
if (i % 2 == 0) continue; // Skips even numbers
Console.WriteLine(i); // Outputs odd numbers: 1, 3, 5, 7, 9
}
This advances the loop counter or enumerator without executing the rest of the body.[101]
The goto statement transfers control unconditionally to a labeled statement within the same method, using a label defined by an identifier followed by a colon. Labels can appear anywhere a statement is permitted, but goto is often avoided as it can lead to spaghetti code. An example:
csharp
int x = 0;
start: x++;
if (x < 5) goto start; // Loops until x is 5
Console.WriteLine(x); // Outputs 5
int x = 0;
start: x++;
if (x < 5) goto start; // Loops until x is 5
Console.WriteLine(x); // Outputs 5
Jumps into or out of try or catch blocks are restricted to prevent bypassing exception handling.[101]
The return statement ends execution of the current method and returns control to the calling code, optionally with a value matching the method's return type. In void methods, it simply exits early. For example:
csharp
int Add(int a, int b)
{
if (a < 0 || b < 0) return 0; // Early exit for invalid inputs
return a + b;
}
int Add(int a, int b)
{
if (a < 0 || b < 0) return 0; // Early exit for invalid inputs
return a + b;
}
If no return statement is reachable in a non-void method, a compiler error occurs. In async methods, return wraps the value in a Task.[101]
Exception handling in C# uses structured statements to detect, manage, and propagate runtime errors, preventing abrupt program termination. The try-catch-finally construct wraps potentially error-prone code, allowing custom responses to exceptions while ensuring cleanup. Exceptions are objects derived from System.Exception, thrown explicitly or by the runtime.[102]
The try statement encloses code that might throw an exception, followed by one or more catch blocks to handle specific exception types and an optional finally block for cleanup code that executes regardless of exceptions. Catch blocks filter by type, with the most specific first to avoid masking. For example:
csharp
try
{
int result = 10 / 0; // Throws DivideByZeroException
}
catch (DivideByZeroException e)
{
Console.WriteLine($"Error: {e.Message}"); // Handles [division by zero](/page/Division_by_zero)
}
catch (Exception e)
{
Console.WriteLine($"General error: {e.Message}"); // Catches others
}
finally
{
Console.WriteLine("Cleanup executed."); // Always runs
}
try
{
int result = 10 / 0; // Throws DivideByZeroException
}
catch (DivideByZeroException e)
{
Console.WriteLine($"Error: {e.Message}"); // Handles [division by zero](/page/Division_by_zero)
}
catch (Exception e)
{
Console.WriteLine($"General error: {e.Message}"); // Catches others
}
finally
{
Console.WriteLine("Cleanup executed."); // Always runs
}
The finally block runs even if no exception occurs, a jump statement executes, or the try block completes normally, making it ideal for releasing resources.[102]
The throw statement signals an error by creating and propagating an exception instance. It can initialize a new exception or rethrow a caught one. Prior to C# 7.0, rethrowing required throw e;, which loses the original stack trace; from C# 7.0, a simple throw; preserves it. Examples:
csharp
try
{
// Risky operation
}
catch (InvalidOperationException e)
{
Console.WriteLine("Handled locally.");
throw; // Rethrows, preserving stack trace (C# 7.0+)
}
try
{
// Risky operation
}
catch (InvalidOperationException e)
{
Console.WriteLine("Handled locally.");
throw; // Rethrows, preserving stack trace (C# 7.0+)
}
Or explicitly: throw new ArgumentException("Invalid argument");. Throw can also appear as an expression in conditional contexts since C# 7.0.[102]
The using statement simplifies resource management for types implementing IDisposable, automatically calling Dispose() at the end of the block to release unmanaged resources like file handles or database connections, even if exceptions occur. Introduced in C# 1.0 as a block form, C# 8.0 added using declarations for implicit scoping at the variable's lifetime end. Traditional block example:
csharp
using (var file = new FileStream("file.txt", FileMode.Open))
{
// Use file
} // Dispose called here
using (var file = new FileStream("file.txt", FileMode.Open))
{
// Use file
} // Dispose called here
With C# 8.0 declaration:
csharp
using var file = new FileStream("file.txt", FileMode.Open);
// Use file; Dispose called at end of enclosing scope
using var file = new FileStream("file.txt", FileMode.Open);
// Use file; Dispose called at end of enclosing scope
Multiple resources can be declared in one using, separated by commas. This syntactic sugar expands to try-finally under the hood.[103]
Classes and Structs
In C#, classes are reference types declared using the class keyword, optionally preceded by an access modifier such as public or private, followed by the class name and a body enclosed in curly braces.[60] The default access level for a class is internal, meaning it is accessible only within the same assembly, but explicit modifiers like public allow access from any assembly, while private restricts visibility to the containing type.[104] For example:
csharp
public class Person
{
public string Name { get; set; }
}
public class Person
{
public string Name { get; set; }
}
This declaration defines a basic class that can be instantiated and used across assemblies.[60]
Structs, in contrast, are value types declared with the struct keyword and similarly support access modifiers.[51] The primary distinction between classes and structs lies in their storage and behavior: classes are allocated on the heap and accessed via references, enabling shared instances and nullability, whereas structs are typically allocated on the stack and copied by value, which avoids heap allocation overhead but can lead to performance issues with large or complex data due to copying semantics.[105] Structs are recommended for small, immutable data structures like coordinates or dates where value equality and performance are critical, such as in high-frequency operations, while classes suit scenarios requiring polymorphism, inheritance, or object identity.[105] For instance:
csharp
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
Classes support inheritance from other classes, allowing derived types to extend base functionality.[60]
Introduced in C# 12, primary constructors provide a concise way to declare classes or structs by including parameters directly in the type declaration, making them available as fields throughout the type body without needing a separate constructor definition.[106] These parameters capture values at instantiation and can be used for initialization or as implicit backing fields, reducing boilerplate while maintaining mutability unless specified otherwise.[107] An example for a class is:
csharp
public class Container(int capacity)
{
private int _current = 0;
public void Add() => _current++; // capacity is accessible here
}
public class Container(int capacity)
{
private int _current = 0;
public void Add() => _current++; // capacity is accessible here
}
This feature extends to structs as well, promoting cleaner syntax for simple data holders.[28]
Records, added in C# 9.0, offer a specialized syntax for immutable reference types (record classes) or value types (record structs) that emphasize data equality and immutability, declared as record followed by the type name and optional primary constructor parameters.[108] A positional record like record Person([string](/page/String) Name, [int](/page/INT) Age); automatically generates properties, constructors, and overrides for equality comparison based on values, making it ideal for data transfer objects or when structural equality is needed over reference identity.[108] Records can be mutable if explicitly defined but default to immutability, and record structs provide value-type benefits with built-in deconstruction support.[109]
Also from C# 9.0, init-only properties enable immutability after object initialization by using the init accessor, which allows setting values only during construction via object initializers or constructors, declared as { get; init; }.[110] This prevents post-construction changes, enhancing thread safety and data integrity in classes or structs, as in:
csharp
public class ImmutablePoint
{
public int X { get; init; }
public int Y { get; init; }
}
// Usage: var p = new ImmutablePoint { X = 1, Y = 2 };
public class ImmutablePoint
{
public int X { get; init; }
public int Y { get; init; }
}
// Usage: var p = new ImmutablePoint { X = 1, Y = 2 };
Init-only properties integrate seamlessly with records and primary constructors to enforce immutability patterns without full record syntax.[111]
In C#, interfaces define contracts that specify the signatures of methods, properties, events, and indexers without providing implementations, enabling multiple types to adhere to a common set of behaviors.[112] An interface declaration begins with the interface keyword, followed by the interface name, and may include members such as method signatures; for example, public interface ILogger { void Log(string message); }.[113] Starting with C# 8.0, interfaces support default implementations for members, allowing a body to be provided for methods or properties, which implementers can choose to override if needed.[112] This feature facilitates evolutionary design by permitting additions to existing interfaces without breaking existing implementers.[114]
To implement an interface, a class or struct uses a colon (:) after its name to list the interface, followed by providing public implementations for all members; for instance, public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }.[112] Implementations must match the interface member signatures exactly, and if multiple interfaces declare the same member, the implementing type resolves any conflicts through explicit interface implementation, prefixing the member with the interface name, such as void ILogger.Log(string message) { ... }.[115] C# supports multiple inheritance of interfaces, allowing a type to implement several interfaces simultaneously, which promotes code reuse and polymorphism across unrelated types.[116]
Class inheritance in C# allows a derived class to extend a base class, inheriting its members and enabling polymorphism through the base keyword to access base class constructors or members, as in public class Derived : Base { public Derived() : base() { } }.[117] To support overriding, base class members are marked virtual, and derived classes use override to provide new implementations; for example, public virtual void Method() { } in the base and public override void Method() { } in the derived.[118] Overrides can be marked sealed to prevent further overriding in subclasses, enhancing control over inheritance hierarchies, such as public sealed override void Method() { }.[119]
Abstract classes serve as base classes that cannot be instantiated directly and may include abstract members requiring implementation in derived classes, declared with the abstract keyword; for example, public abstract class Shape { public abstract double Area(); }.[120] A derived class must provide concrete implementations for all abstract members, like public class Circle : Shape { public override double Area() { return Math.PI * radius * radius; } }, combining inherited non-abstract members with required overrides.[118] Unlike interfaces, abstract classes can include fields, constructors, and non-abstract methods, providing a partial implementation while enforcing contracts through abstraction.[117]
Members and Access
In C#, members of classes and structs include fields for data storage, properties for controlled access to data, methods for operations, constructors for initialization, indexers for array-like access, events for notifications, and destructors for cleanup. These members can be declared with access modifiers that determine their visibility and usage scope within assemblies and inheritance hierarchies. Access control promotes encapsulation by restricting direct access to internal state while allowing controlled interaction.[121]
Access modifiers in C# include public for unrestricted access from any code, private for access only within the same class or struct, protected for access within the class and derived classes, internal for access within the same assembly, protected internal for access within the same assembly or derived classes in other assemblies, and private protected (introduced in C# 7.2) for access within the same class or derived classes in the same assembly. These modifiers apply to all member types and can differ between accessors in properties and indexers. For instance, a property's get accessor might be public while its set is private. Fields typically use private to enforce encapsulation.[122][122]
Fields store data directly and are declared with a type and name, optionally initialized at declaration. They can be marked readonly to allow assignment only during declaration or in a constructor, preventing subsequent modification and enabling compile-time constants if initialized with literals. A readonly field ensures immutability after initialization, useful for configuration values or caches. For example:
csharp
public class Example
{
private int _x; // Mutable field
private readonly int _y = 42; // Readonly field initialized at declaration
}
public class Example
{
private int _x; // Mutable field
private readonly int _y = 42; // Readonly field initialized at declaration
}
In the constructor, _x can be assigned, but _y cannot after declaration. Readonly fields are evaluated at runtime if initialized in constructors, unlike const fields which are compile-time constants.[123]
Properties provide a flexible mechanism to read, write, or compute private field values, acting as smart fields with validation or computation logic in accessors. Auto-implemented properties, introduced in C# 3.0, simplify declaration by automatically generating a private backing field. Expression-bodied properties, available since C# 6.0, use => for concise read-only definitions. Accessors can include get for reading and set for writing, with optional different access modifiers. Starting with C# 13, properties and indexers can be declared as partial, allowing the declaration in one part of a partial class and the implementation (get/set accessors) in another, which is useful for generated code scenarios; for example, one file declares partial int MyProperty { get; } and another implements the accessors.[124] For example:
csharp
public class Example
{
private int _backingField;
public int AutoProperty { get; set; } // Auto-implemented
public int ExpressionProperty => _backingField * 2; // Expression-bodied since C# 6.0
public int ControlledProperty
{
get => _backingField;
private set => _backingField = value > 0 ? value : 0; // Validation in setter
}
}
public class Example
{
private int _backingField;
public int AutoProperty { get; set; } // Auto-implemented
public int ExpressionProperty => _backingField * 2; // Expression-bodied since C# 6.0
public int ControlledProperty
{
get => _backingField;
private set => _backingField = value > 0 ? value : 0; // Validation in setter
}
}
Properties support required modifiers (C# 11.0) to enforce initialization via object initializers or constructors.[125][39]
Methods define behavior and are declared with a return type, name, and parameters, optionally including modifiers like virtual or static. The params modifier allows a variable number of arguments as an array, placed last in the parameter list. Optional parameters provide default values, enabling callers to omit them. Local functions, introduced in C# 7.0, are private methods nested within another member for encapsulation of helper logic, supporting their own parameters and locals. For example:
csharp
public class Example
{
public int Sum(int a, int b = 0, params int[] extras)
{
int LocalHelper(int x, int y) => x + y; // Local function since C# 7.0
return LocalHelper(a, b) + extras.[Sum](/page/Sum)();
}
}
public class Example
{
public int Sum(int a, int b = 0, params int[] extras)
{
int LocalHelper(int x, int y) => x + y; // Local function since C# 7.0
return LocalHelper(a, b) + extras.[Sum](/page/Sum)();
}
}
Invocation uses Sum(1) for defaults or Sum(1, 2, 3, 4) for params. Local functions capture enclosing variables but can be marked static to avoid captures.[47][126][127][128]
Constructors initialize instances and are special methods with the class name as signature, optionally parameterized. Instance constructors can chain via this() or base() for base class invocation. Static constructors, parameterless, run once before first access to static members. Primary constructors, introduced in C# 12.0, declare parameters in the class header for direct use as fields or in other constructors. Object initializers, since C# 3.0, allow setting properties post-constructor invocation. For example:
csharp
public [class](/page/Class) Example([int](/page/INT) initialValue) // Primary constructor since C# 12.0
{
public [int](/page/INT) [Value](/page/Value) { get; set; } = initialValue;
public Example() : this(0) { } // Chains to primary
static Example() => Console.WriteLine("Static init"); // Static constructor
}
// Usage
var obj = new Example { [Value](/page/Value) = 42 }; // Object initializer since C# 3.0
public [class](/page/Class) Example([int](/page/INT) initialValue) // Primary constructor since C# 12.0
{
public [int](/page/INT) [Value](/page/Value) { get; set; } = initialValue;
public Example() : this(0) { } // Chains to primary
static Example() => Console.WriteLine("Static init"); // Static constructor
}
// Usage
var obj = new Example { [Value](/page/Value) = 42 }; // Object initializer since C# 3.0
Static constructors initialize static fields automatically if not provided.[129][130][107][131]
Indexers enable array-like access to class instances using this with parameters, similar to properties but with indexing syntax. They include get and set accessors and support multiple parameters for multidimensional access. For example:
csharp
public [class](/page/Class) IndexerExample
{
private string[] _values = new string[10];
public string this[int index]
{
get => _values[index];
set => _values[index] = value;
}
}
// Usage
var ex = new IndexerExample();
ex[0] = "Hello";
public [class](/page/Class) IndexerExample
{
private string[] _values = new string[10];
public string this[int index]
{
get => _values[index];
set => _values[index] = value;
}
}
// Usage
var ex = new IndexerExample();
ex[0] = "Hello";
Indexers can be declared in interfaces and overloaded by parameter types.[85]
Events are members that allow a class to notify subscribers of occurrences, declared with the event keyword and a delegate type, typically following the EventHandler pattern. They support add and remove accessors for subscription management, with field-like syntax for simplicity. Events prevent direct invocation outside the declaring class, ensuring controlled raising. Detailed delegate usage is covered elsewhere. For example:
csharp
public event EventHandler ValueChanged;
public event EventHandler ValueChanged;
Raising uses ValueChanged?.Invoke(this, EventArgs.Empty);.[132][133]
Destructors, known as finalizers, perform cleanup before garbage collection and are declared with ~ClassName(), parameterless and without access modifiers. They cannot be inherited or overloaded and run nondeterministically. Use is discouraged in favor of IDisposable for resource management. For example:
csharp
public class Example
{
~Example() => Console.WriteLine("Cleanup");
}
public class Example
{
~Example() => Console.WriteLine("Cleanup");
}
The compiler translates to protected override void Finalize().[134]
Generics and Collections
Generic Types and Methods
Generics in C# provide a mechanism for parametric polymorphism, allowing classes, interfaces, methods, and delegates to operate on data types specified as parameters, thereby promoting code reuse, type safety, and performance without the overhead of casting or boxing. Introduced in C# 2.0 as part of the .NET Framework 2.0, generics enable developers to define type placeholders, known as type parameters, which are replaced with actual types at compile time.[135] This feature is particularly prevalent in collection classes within the System.Collections.Generic namespace, such as List, where T represents the element type.[136]
Generic classes are declared by appending one or more type parameters in angle brackets after the class name. The basic syntax is class ClassName<T> { }, where T is the type parameter, which can appear in fields, properties, methods, or return types within the class. For example, a simple generic stack class might be defined as follows:
csharp
public class Stack<T>
{
private T[] elements;
private int top;
public Stack(int size)
{
elements = new T[size];
top = -1;
}
public void Push(T item)
{
if (top + 1 < elements.Length)
elements[++top] = item;
}
public T Pop()
{
if (top >= 0)
return elements[top--];
throw new InvalidOperationException("Stack is empty");
}
}
public class Stack<T>
{
private T[] elements;
private int top;
public Stack(int size)
{
elements = new T[size];
top = -1;
}
public void Push(T item)
{
if (top + 1 < elements.Length)
elements[++top] = item;
}
public T Pop()
{
if (top >= 0)
return elements[top--];
throw new InvalidOperationException("Stack is empty");
}
}
Instances are created by specifying the type argument, such as Stack<int> intStack = new Stack<int>(10);, resulting in a closed constructed type where T is replaced by int during compilation.[137] Generic classes can inherit from other generic or non-generic classes and implement generic interfaces, enhancing flexibility in object-oriented designs.[137]
Generic methods extend this polymorphism to individual methods, declared within or outside generic classes, using type parameters independent of the enclosing type. The syntax involves placing the type parameter list before the method's return type, as in T MethodName<T>(T arg) { }. Type inference allows the compiler to deduce the type from arguments, eliminating the need to explicitly specify it in calls. Consider this example of a swapping method:
csharp
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
Usage might be int x = 1, y = 2; Swap(ref x, ref y);, where the compiler infers T as int.[138] Generic methods support overloading based on type parameters and can be static or instance methods, with the type parameter scope limited to the method body.[138]
Type parameters can be constrained using the where clause to restrict the allowable types, ensuring access to specific members or behaviors and improving compile-time error detection. Constraints include interfaces (e.g., where T : IComparable<T>), base classes (e.g., where T : SomeBaseClass), reference types (where T : class), value types (where T : struct), unmanaged types (where T : unmanaged), types with parameterless constructors (where T : new()), and the ability to reference the type parameter itself (where T : U for another parameter U). Multiple constraints are chained, and the new() constraint must appear last. For instance:
csharp
public class Container<T> where T : class, IComparable<T>, new()
{
public T Value { get; set; } = new T();
public int Compare(T other)
{
return Value.CompareTo(other);
}
}
public class Container<T> where T : class, IComparable<T>, new()
{
public T Value { get; set; } = new T();
public int Compare(T other)
{
return Value.CompareTo(other);
}
}
This restricts T to reference types implementing IComparable with a default constructor.[139] In C# 13, an additional allows ref struct constraint permits ref struct types like Span in generic algorithms, broadening applicability to stack-allocated structures.[139]
The default value for a type parameter T is obtained via default(T), which yields null for reference types and zero-initialized values for value types, facilitating generic initialization without assumptions about the type. This is commonly used in fields or local variables, as in T item = default(T);.[140]
Generic interfaces define contracts with type parameters, similar to classes, using syntax like interface IRepository<T> { T GetById(int id); }. Implementing classes specify the type argument, such as public class UserRepository : IRepository<User> { }, enabling type-safe abstractions for collections or services.[141] Generic interfaces can inherit from other interfaces and support constraints, as seen in standard library types like IEnumerable.[141]
Generic delegates mirror this pattern, declared with type parameters for strongly typed callbacks or events, such as public delegate void [Action](/page/Action)<T>(T arg);. Invocation requires specifying the type, e.g., Action<string> print = s => Console.WriteLine(s); print("Hello");, avoiding boxing and enhancing performance in functional programming scenarios.[142]
Covariance and Contravariance
In C#, covariance and contravariance are type-safe mechanisms that enable implicit conversions between generic types, allowing for more flexible assignments while preserving compatibility with the type hierarchy. Covariance permits the use of a more derived type in place of a less derived one, typically for output positions like return types, whereas contravariance allows a less derived type to be used where a more derived one is expected, often for input positions such as method parameters. These features were introduced in C# 4.0 to support variance annotations on generic type parameters in interfaces and delegates, enhancing code reusability without runtime type errors.[143]
Covariance is declared using the out keyword for a generic type parameter, such as in the interface IEnumerable<out T>, which ensures that the type parameter appears only in output positions (e.g., as return types) and never as input parameters. This allows assignments like IEnumerable<object> enumerable = new List<string>();, where a collection of strings (more derived) can be treated as a collection of objects (less derived), since operations on IEnumerable<T> only read from the collection. The out modifier enforces that no methods in the interface accept T as an argument, preventing type-unsafe insertions.[144][145]
Contravariance uses the in keyword, as in IComparer<in T>, where the type parameter is used solely in input positions (e.g., method parameters) and not as return types. For instance, if Employee derives from Person, an IComparer<Employee> can be assigned to IComparer<Person>, enabling a comparer designed for employees to compare persons safely, since it will only receive more specific types. This is possible because contravariant positions ensure that inputs are treated as the more general base type.[146][145]
Variance annotations apply exclusively to reference types and are not supported for value types, as substituting value types could lead to boxing or unboxing issues that violate type safety. In delegates, covariance allows a method returning a more derived type to match a delegate expecting a less derived return type (e.g., Func<object> func = () => "string";), while contravariance permits a method accepting a less derived parameter to match a delegate expecting a more derived one (e.g., Action<string> action = obj => Console.WriteLine(obj); where the method takes object). Arrays support covariance for reference types, such as assigning string[] to object[], but this is not type-safe at compile time and may cause runtime exceptions if elements are modified incompatibly.[144][146][147]
Enumerators and Iterators
In C#, enumeration over collections is facilitated by the IEnumerable<T> and IEnumerator<T> interfaces, which provide a standardized way to iterate through sequences of elements without exposing the underlying data structure. The IEnumerable<T> interface, defined in the System.Collections.Generic namespace, declares a single method, GetEnumerator(), which returns an IEnumerator<T> object for sequential access to the collection's elements. This interface inherits from the non-generic IEnumerable and supports covariance on its type parameter T, allowing it to be used with derived types in iteration contexts.[148]
The IEnumerator<T> interface extends IEnumerator and IDisposable, enabling the actual traversal of the collection. It includes a Current property to access the element at the current position and methods such as MoveNext(), which advances the enumerator to the next element and returns true if one exists (otherwise false), and Reset(), which sets the enumerator back to its initial state before the first element—though implementations may throw a NotSupportedException if resetting is not supported. The enumerator begins positioned before the first element, requiring an initial call to MoveNext() to access the first item, and it enforces read-only access to prevent modifications during iteration. Modifying the collection while enumerating can lead to undefined behavior, and enumerators are not thread-safe by default.[149]
Iterator methods, introduced in C# 2.0, simplify the implementation of IEnumerable<T> by allowing developers to define sequences using the yield contextual keyword within methods, properties, or operators that return IEnumerable<T>. An iterator method appears as a standard method but uses yield return to produce each element on demand, pausing execution after each yield until the next element is requested. For example:
csharp
public IEnumerable<int> GetEvenNumbers(int start, int count)
{
for (int i = start; i < start + count; i += 2)
{
yield return i;
}
}
public IEnumerable<int> GetEvenNumbers(int start, int count)
{
for (int i = start; i < start + count; i += 2)
{
yield return i;
}
}
This method generates even numbers lazily without storing the entire sequence in memory. The yield break statement can be used explicitly to end iteration early, though it is optional as reaching the end of the method body implicitly terminates the sequence. The foreach loop in C# relies on these interfaces to consume iterators, translating into calls to GetEnumerator(), MoveNext(), and Current.[150][151]
Under the hood, the C# compiler transforms iterator methods containing yield return into a state machine to manage the deferred execution and resumption of the sequence. This involves generating a private nested class that implements IEnumerator<T>, complete with fields to track the current state (often via an integer enum or switch-based logic), local variables from the iterator, and the yielded value. The MoveNext() method in this generated class uses a switch statement to resume execution from the point of the last yield return, advancing the state accordingly—for instance, transitioning from an initial state to yield the first element, then to subsequent states for each additional yield, until completion or yield break. This compiler-generated code ensures efficient, resumable iteration without manual state management by the developer.[152]
Iterators enable deferral of computation in foreach loops, meaning the sequence is not fully evaluated until elements are actually consumed, promoting lazy evaluation for memory efficiency—particularly beneficial for large or infinite sequences. For example, in the GetEvenNumbers method above, execution pauses after each yield return, generating values only as the foreach loop requests them via MoveNext(). This deferral avoids upfront allocation of the entire result set, contrasting with traditional methods that return a complete collection.[153]
Functional and Anonymous Features
Delegates, Lambdas, and Anonymous Methods
In C#, delegates are type-safe function pointers that reference methods with a specific signature, enabling methods to be passed as parameters and facilitating callback mechanisms.[154] They are object-oriented wrappers around methods, supporting both static and instance methods, and can be multicast to invoke multiple methods sequentially.[155] Delegates are declared using the delegate keyword, specifying the return type and parameters, such as public delegate int PerformCalculation(int x, int y);.[63]
To instantiate a delegate, assign a compatible method to it, as in PerformCalculation calc = Add;, where Add is a method matching the signature public static [int](/page/INT) Add([int](/page/INT) a, [int](/page/INT) b) { return a + b; }.[63] Invocation occurs by calling the delegate instance like a method, e.g., int result = calc(5, 3);, which executes the referenced method and returns the value.[154] For multicast delegates, combine instances using the + or += operator, such as calc += Subtract;, and invoke to call all chained methods in order.[154]
Anonymous methods, introduced in C# 2.0, provide a way to define inline code blocks without naming the method, using the delegate keyword.[86] The syntax is delegate (parameters) { body }, optionally omitting parameters if none are needed.[86] For example, Action<string> printMessage = delegate(string msg) { Console.WriteLine(msg); }; printMessage("Hello"); creates and invokes an anonymous method that prints the message.[156] This form allows capturing outer variables but is largely superseded by lambda expressions for brevity.
Lambda expressions, added in C# 3.0, offer a more concise syntax for anonymous functions using the => operator to separate parameters from the body.[157] They come in expression form (x) => x * 2 for single expressions or statement form (x, y) => { return x + y; } for blocks.[158] An example is Func<int, int> square = x => x * x; int result = square(4);, which computes and returns 16.[158] Async lambdas, introduced in C# 5.0 with the async and await keywords, enable asynchronous anonymous methods, such as Func<Task> asyncOp = async () => { await Task.Delay(1000); };.[158]
Both anonymous methods and lambdas support capturing variables from the enclosing scope, forming closures that extend the lifetime of those variables until the delegate is garbage-collected.[159] For instance, int counter = 0; Action increment = () => counter++; increment(); Console.WriteLine(counter); outputs 1, as the lambda modifies the captured counter.[158] Captured variables must be definitely assigned beforehand and cannot include ref, out, or in parameters directly; use the static modifier on the lambda to prevent capture if needed.[158]
C# provides predefined generic delegates to simplify common scenarios: Action<T> for void-returning methods with up to 16 parameters, and Func<T, TResult> (and variants) for methods returning a value.[160] For example, Action<string> logger = msg => Console.WriteLine(msg); logger("Log entry"); uses Action for logging without a return.[160] Similarly, Func<int, bool> isEven = n => n % 2 == 0; bool even = isEven(4); employs Func for a predicate returning true.[161] These are often used in event handling, such as subscribing to events with eventHandler += obj => Process(obj);.[154]
LINQ and Query Expressions
LINQ, or Language Integrated Query, is a set of features in C# that enables querying data from various sources using a declarative syntax integrated directly into the language.[162] Query expressions provide a SQL-like syntax for operations such as filtering, sorting, grouping, and joining, allowing developers to write type-safe queries that are checked at compile time.[163] This syntax is built atop extension methods from the System.Linq namespace, facilitating a consistent model for data manipulation across in-memory collections, databases, and XML.[164]
The core of LINQ query expressions begins with the from clause, which specifies the data source and introduces a range variable to iterate over its elements.[165] For instance, from num in numbers declares numbers as the source and num as the variable representing each element.[163] Subsequent clauses refine the query: where applies a Boolean condition to filter elements, such as where num > 5; orderby sorts the results ascending or descending, e.g., orderby num descending; let introduces a variable to store an intermediate expression, like let doubled = num * 2; join correlates two sources based on key equality, as in join prod in products on cat.Id equals prod.CategoryId; group partitions elements by a key for aggregation, e.g., group num by num % 2; and select projects the final output shape, such as select new { Value = num, Doubled = doubled }.[166] These clauses can be combined in a fluent, readable manner, resembling declarative languages like SQL.[163]
Under the hood, query expressions are syntactic sugar that the C# compiler translates into method calls on standard query operators from System.Linq.Enumerable.[167] For example, the query from num in numbers where num > 5 select num compiles to numbers.Where(num => num > 5).Select(num => num).[163] Similarly, join maps to Join, group to GroupBy, and orderby to OrderBy or OrderByDescending.[167] This translation preserves the semantics while allowing the use of lambda expressions for conditions and projections.[166]
LINQ supports multiple providers for different data sources, with LINQ to Objects targeting in-memory collections that implement IEnumerable<T>, enabling queries on arrays, lists, and other enumerable types without external dependencies.[162] LINQ to XML, via the System.Xml.Linq namespace, allows querying and manipulating XML documents as object hierarchies, such as using XDocument and XElement with query expressions to extract nodes.[162] Other providers include LINQ to SQL (via Entity Framework) for database queries and custom IQueryable<T> implementations for web services or specialized stores.[164]
A key aspect of LINQ queries is deferred execution, where the query is not evaluated until its results are enumerated, such as in a foreach loop or via ToList().[162] This laziness improves performance by avoiding unnecessary computations. For IQueryable<T> sources, the compiler builds expression trees—data structures representing the query as code—rather than delegates, allowing providers to translate them into source-specific operations like SQL.[162] In contrast, LINQ to Objects uses delegate-based execution trees.[162]
csharp
// Example: LINQ to Objects query
int[] numbers = { 0, 1, 2, 3, 4, 5, 6 };
var evenSquares = from num in numbers
where num % 2 == 0
let square = num * num
orderby square
select square;
// Equivalent method syntax: numbers.Where(num => num % 2 == 0).Select(num => num * num).OrderBy(x => x)
foreach (int sq in evenSquares)
{
Console.WriteLine(sq); // Outputs on enumeration: 0, 4, 16, 36
}
// Example: LINQ to Objects query
int[] numbers = { 0, 1, 2, 3, 4, 5, 6 };
var evenSquares = from num in numbers
where num % 2 == 0
let square = num * num
orderby square
select square;
// Equivalent method syntax: numbers.Where(num => num % 2 == 0).Select(num => num * num).OrderBy(x => x)
foreach (int sq in evenSquares)
{
Console.WriteLine(sq); // Outputs on enumeration: 0, 4, 16, 36
}
This example demonstrates filtering even numbers, computing squares, sorting, and projecting, with execution deferred until the foreach.[163]
Extension Methods and Local Functions
Extension methods, introduced in C# 3.0, allow developers to add functionality to existing types without modifying their source code or creating derived types.[11] These are implemented as static methods within a non-nested, non-generic static class, where the first parameter is modified with the this keyword to indicate the type being extended. For instance, to extend the string type with a WordCount method, the declaration would appear as follows:
csharp
public static class StringExtensions
{
public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
public static class StringExtensions
{
public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
This enables invocation as if it were an instance method: "Hello world".WordCount(); returns 2.[168] Extension methods must be imported via a using directive to be accessible, and they have lower precedence than instance methods of the same name on the extended type. They cannot access private or protected members of the extended type and are particularly useful for enhancing framework types, such as those in LINQ where they extend IEnumerable<T> for query operations.[169] Guidelines recommend avoiding frivolous extensions on core types like object and using descriptive namespaces to prevent conflicts.[169]
Local functions, added in C# 7.0, provide a way to declare private, nested methods within another member, such as a method or accessor, restricting their scope to the containing member only.[11] Unlike lambdas, local functions are named and do not require delegate creation unless explicitly converted, potentially avoiding heap allocations. The syntax resembles a standard method declaration but without access modifiers:
csharp
public int CalculateSum(int start, int end)
{
int LocalSum(int i, int j) => i + j;
return Enumerable.Range(start, end).Sum(i => LocalSum(i, end - i));
}
public int CalculateSum(int start, int end)
{
int LocalSum(int i, int j) => i + j;
return Enumerable.Range(start, end).Sum(i => LocalSum(i, end - i));
}
Local functions can capture variables from the enclosing scope, improving encapsulation for helper logic.[128] In C# 8.0, the static modifier was introduced for local functions, preventing capture of local variables or instance state to enhance performance and clarity. A static local function example:
csharp
public void ProcessData()
{
static int Add(int a, int b) => a + b;
int result = Add(5, 3);
}
public void ProcessData()
{
static int Add(int a, int b) => a + b;
int result = Add(5, 3);
}
This ensures the function relies only on its parameters and static members.[128] Generic local functions, supporting type parameters for greater flexibility, were introduced in C# 7.0. For example:
csharp
public void Process<T>(T item) where T : IComparable<T>
{
bool IsGreater<U>(U value) where U : T => item.CompareTo(value) > 0;
// Usage within the method, e.g., if (IsGreater(someValue)) { ... }
}
public void Process<T>(T item) where T : IComparable<T>
{
bool IsGreater<U>(U value) where U : T => item.CompareTo(value) > 0;
// Usage within the method, e.g., if (IsGreater(someValue)) { ... }
}
Attributes can apply to local functions since C# 9.0, including on generic parameters and the function itself, aligning with broader method capabilities.[11] Local functions also handle exceptions more predictably in iterators and async contexts by surfacing them immediately rather than deferring via delegates.[128]
Asynchronous and Advanced Syntax
Async-Await Pattern
The async-await pattern in C# enables asynchronous programming by allowing methods to perform non-blocking operations, improving responsiveness in applications such as UI or server-side code. Introduced in C# 5.0 with .NET Framework 4.5, this pattern uses the async modifier to declare asynchronous methods and the await keyword to suspend execution until an asynchronous operation completes, without blocking the calling thread.[170][171]
An async method is declared by prefixing the method signature with async and typically returns a Task or Task<T> to represent the asynchronous operation. For example, the following method simulates a delay and returns an integer:
csharp
public async Task<int> GetDataAsync()
{
await Task.Delay(100); // Non-blocking delay
return 42;
}
public async Task<int> GetDataAsync()
{
await Task.Delay(100); // Non-blocking delay
return 42;
}
This declaration allows the method to use await internally and enables the compiler to generate a state machine for handling continuations. The await expression operates on an awaitable type, such as Task, suspending the method until the awaited operation completes or faults, then resuming with the result. Awaitables must implement INotifyCompletion or ICriticalNotifyCompletion for proper integration with the async context.[172][173]
The Task class represents an asynchronous operation, while Task<T> provides a result of type T. For performance-sensitive scenarios where frequent synchronous completions occur, ValueTask<T> can be used as a lightweight, allocation-free alternative, wrapping either a Task or a synchronous result. Introduced in .NET Core 2.1, ValueTask reduces garbage collection pressure in high-throughput code but requires careful handling to avoid multiple awaits on the same instance.[174][175]
Async lambdas extend this pattern to anonymous functions, declared with async before the parameter list, enabling asynchronous processing in delegates or LINQ queries. For instance:
csharp
var asyncLambda = async (int x) => await Task.FromResult(x * 2);
var asyncLambda = async (int x) => await Task.FromResult(x * 2);
Async iterators, added in C# 8.0, allow methods to yield values asynchronously using yield return within an async iterator that returns IAsyncEnumerable<T>. This supports streaming data from asynchronous sources, such as network responses, with consumption via await foreach.[158][176]
In C# 13 (November 2024), async methods gained support for ref locals and unsafe contexts, provided these do not cross await boundaries or interact with yield in iterators. This enables the use of ref structs like Span<T> in async code for better performance in scenarios involving memory manipulation, such as parsing or buffering. For example:
csharp
public async Task ProcessSpanAsync(Span<byte> buffer)
{
unsafe
{
fixed (byte* ptr = buffer)
{
// Process pointer synchronously
await Task.Delay(100); // ref/unsafe do not cross await
}
}
}
```[](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#ref-and-unsafe-in-iterators-and-async-methods)
Cancellation is integrated via `CancellationToken`, passed as a parameter to async methods to signal cancellation requests cooperatively. Methods like `Task.Delay` and HTTP clients check the token, throwing `OperationCanceledException` if canceled. For example:
```csharp
public async Task ProcessAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}
public async Task ProcessSpanAsync(Span<byte> buffer)
{
unsafe
{
fixed (byte* ptr = buffer)
{
// Process pointer synchronously
await Task.Delay(100); // ref/unsafe do not cross await
}
}
}
```[](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#ref-and-unsafe-in-iterators-and-async-methods)
Cancellation is integrated via `CancellationToken`, passed as a parameter to async methods to signal cancellation requests cooperatively. Methods like `Task.Delay` and HTTP clients check the token, throwing `OperationCanceledException` if canceled. For example:
```csharp
public async Task ProcessAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}
This ensures resources are released promptly and prevents unnecessary computation.[177][178]
Pattern Matching and Deconstruction
Pattern matching in C# provides a concise way to inspect and extract data from expressions based on their structure, type, or properties, enhancing code readability and reducing boilerplate compared to traditional type checks and casts. Introduced in C# 7.0, it allows testing an expression against patterns using the is operator or switch expressions, with subsequent enhancements in later versions adding support for relational comparisons, properties, lists, and recursion.[92][11]
The is pattern, debuted in C# 7.0, enables type testing combined with variable declaration in a single expression, such as if (obj is string s), which checks if obj is a string and assigns it to s if true, eliminating the need for separate casting.[179] In C# 9.0, this expanded to relational patterns, allowing comparisons like if (value is > 0 and < 10), which verifies if value falls within a numeric range without explicit conditional logic.[180] These patterns support logical combinators such as and, or, and not for more complex conditions.[180]
Switch expressions, introduced in C# 8.0, integrate pattern matching into control flow by allowing cases to match structural patterns, for example, case string { Length: > 5 } to handle strings longer than five characters.[92] This syntax promotes declarative matching over imperative checks and can be used in switch statements for exhaustive analysis of inputs.[92]
Deconstruction, also from C# 7.0, breaks down objects or tuples into individual variables using syntax like (int x, int y) = point;, where point must define a Deconstruct method to unpack its components.[181] This feature supports "into" declarations in queries or foreach loops for extracting elements, and it works with tuples natively, assigning elements to variables or discards (_) to ignore parts.[182]
Property patterns, available since C# 8.0, match based on object properties, such as case Person { Age: > 18 }, verifying the Age property exceeds 18 without full deconstruction.[92] C# 11 introduced list patterns for collections, enabling matches like is [1, .., 3] to check if a list starts with 1 and ends with 3, using .. for variable-length slices and discards for unspecified elements. Additionally, C# 11 extended pattern matching to Span<char> and ReadOnlySpan<char> against constant strings, allowing efficient, allocation-free checks like if (span is "hello") for substring matching in memory buffers.[92][183]
Recursive patterns, starting in C# 8.0, allow nesting patterns within others, such as matching a tree structure with case Node(int value, Node left, Node right): to recursively inspect subnodes.[92] This capability, extended in C# 11 to include list and positional patterns, facilitates handling of hierarchical data like JSON or ASTs efficiently.[92]
Attributes and Preprocessor Directives
In C#, attributes provide a declarative mechanism for adding metadata to code elements such as classes, methods, properties, and parameters, enabling reflection and compiler behaviors without altering the core functionality.[184] Attributes are applied using square brackets immediately preceding the target declaration, allowing multiple attributes in a single section separated by commas.[185] For instance, the built-in Obsolete attribute marks an element as deprecated, generating a compiler warning or error upon usage to discourage its invocation.[186]
The Obsolete attribute accepts a message string and an optional boolean flag to escalate to an error, as shown in the following example:
csharp
[System.Obsolete("This method is obsolete; use ProcessNew instead.", true)]
public void ProcessOld() { }
[System.Obsolete("This method is obsolete; use ProcessNew instead.", true)]
public void ProcessOld() { }
This syntax enforces deprecation at compile time, promoting code evolution while maintaining backward compatibility.[186] Custom attributes are defined by creating classes that inherit from System.Attribute, specifying valid targets via the AttributeUsage attribute, which restricts application to elements like classes or methods using the AttributeTargets enum.[187] These classes support positional parameters through constructors and named parameters via public properties or fields.
C# 11 (November 2022) introduced generic attributes, allowing attribute classes to be generic with type parameters and constraints, enabling stronger typing for metadata. For example:
csharp
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute<T> : Attribute where T : struct, IComparable<T>
{
public RangeAttribute(T min, T max) { /* ... */ }
}
// Usage
[Range(0, 100)]
public int Value { get; set; }
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute<T> : Attribute where T : struct, IComparable<T>
{
public RangeAttribute(T min, T max) { /* ... */ }
}
// Usage
[Range(0, 100)]
public int Value { get; set; }
This feature permits attributes to enforce type-specific validation at compile time.[188]
In C# 12 (November 2023), the ExperimentalAttribute was added to mark features or APIs as experimental, issuing a compiler warning (CS8772) when used, to signal potential instability. It can be applied to types, members, or parameters:
csharp
[System.Diagnostics.CodeAnalysis.Experimental("Preview feature")]
public void ExperimentalMethod() { }
[System.Diagnostics.CodeAnalysis.Experimental("Preview feature")]
public void ExperimentalMethod() { }
This aids in gradual rollout of new syntax or libraries.[189]
Attribute usage includes specifying targets explicitly with colons (e.g., [Help: "url"] for a method) and passing parameters in either positional or named forms for flexibility.[185] For example, a custom HelpAttribute might be defined and applied as follows:
csharp
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class HelpAttribute : System.Attribute
{
public string Url { get; }
public string Topic { get; set; }
public HelpAttribute(string url) => Url = url;
}
// Usage
[Help("https://example.com/docs", Topic = "Overview")]
public class DocumentationClass { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class HelpAttribute : System.Attribute
{
public string Url { get; }
public string Topic { get; set; }
public HelpAttribute(string url) => Url = url;
}
// Usage
[Help("https://example.com/docs", Topic = "Overview")]
public class DocumentationClass { }
This allows metadata like URLs or categories to be attached for tools like documentation generators or serializers.[190]
C# preprocessor directives facilitate conditional compilation, code organization, and warning management by processing source code before compilation, without executing as runtime code.[191] The #define directive establishes conditional compilation symbols at the file level, enabling environment-specific builds without altering the codebase.[192] Symbols like DEBUG are predefined by the compiler when enabled via project settings, allowing code inclusion or exclusion based on build configurations.
The #if, #elif, #else, and #endif directives evaluate these symbols logically (supporting !, ==, !=, &&, ||) to include or skip blocks conditionally.[193] An example demonstrates debug-only output:
csharp
#define VERBOSE
#if VERBOSE || DEBUG
Console.WriteLine("Debug information");
#elif RELEASE
// Release-specific code
#else
// Default fallback
#endif
#define VERBOSE
#if VERBOSE || DEBUG
Console.WriteLine("Debug information");
#elif RELEASE
// Release-specific code
#else
// Default fallback
#endif
This mechanism supports multi-target frameworks, such as including .NET-specific APIs only when symbols like NET8_0 are defined.[192]
The #region and #endregion directives outline collapsible sections in IDEs for better code navigation, without affecting compilation.[191] For instance:
csharp
#region Public Methods
public void Method1() { }
public void Method2() { }
#endregion
#region Public Methods
public void Method1() { }
public void Method2() { }
#endregion
The #pragma warning directive suppresses or restores specific compiler warnings by ID, useful for unavoidable issues like unused variables in generated code.[194] Example:
csharp
#pragma warning disable CS0168 // Variable declared but never used
int unused = 42;
#pragma warning restore CS0168
#pragma warning disable CS0168 // Variable declared but never used
int unused = 42;
#pragma warning restore CS0168
Conditional compilation symbols integrate with attributes like Conditional, which skips method calls unless the symbol is defined, enhancing syntax for debug tracing without runtime overhead.[186]
C# supports comments to annotate code without affecting compilation. Single-line comments begin with // and extend to the end of the line, allowing developers to add brief notes or explanations directly after code statements.[195] For instance:
csharp
[int](/page/INT) x = 5; // This initializes x to five
[int](/page/INT) x = 5; // This initializes x to five
Multi-line comments use /* to start and */ to end, enclosing blocks of text across multiple lines, which is useful for temporarily disabling code sections or providing longer descriptions.[195] An example is:
csharp
/* This is a multi-line comment
that spans several lines.
It can include explanatory text. */
/* This is a multi-line comment
that spans several lines.
It can include explanatory text. */
XML documentation comments, prefixed with ///, enable structured documentation integrated into the code, using XML tags to describe elements like classes, methods, and properties.[196] These comments support recommended tags such as <summary> for overall descriptions, <param name="parameterName"> for parameter details, and <returns> for return value information.[197] For example:
csharp
public int Add(int a, int b)
{
return a + b;
}
public int Add(int a, int b)
{
return a + b;
}
The C# compiler can extract these XML comments into a separate XML file during build, which tools like IntelliSense or documentation generators use to provide API references.[196] This is enabled via project settings like <GenerateDocumentationFile>true</GenerateDocumentationFile> in MSBuild.[198]
Starting with C# 11, XML documentation comments can incorporate raw interpolated string literals, allowing more readable and flexible formatting within tags by using triple quotes (""") combined with interpolation markers.[25] [199] For instance, a <summary> tag might use $""" to embed dynamic elements while preserving structure.[24] This enhancement applies standard string literal features to documentation, improving maintainability for complex descriptions.[200]