Syntax
The syntax of C# is defined by ECMA-334 as a curly-brace, statement-oriented grammar that resembles C++ at the surface and shares lineage with C and Java. The lexical surface includes around eighty reserved keywords plus a growing set of contextual keywords whose status as keywords depends on grammatical position. Each subsequent revision of the language has expanded the syntax — properties, lambdas, async methods, top-level statements, file-scoped namespaces, raw strings, primary constructors — while preserving substantial backward compatibility. This page covers the surface a working programmer encounters routinely; the dedicated pages cover the major sub-grammars.
A complete program
The classical form, with a class, a Main method, and explicit using directives:
using System;
using System.Linq;
class Program {
static void Main(string[] args) {
var names = args.Length == 0
? new[] { "world" }
: args;
foreach (var name in names) {
Console.WriteLine($"Hello, {name}.");
}
}
}
Since C# 9 (.NET 5), top-level statements admit a program without an enclosing class:
using System;
if (args.Length == 0) {
Console.WriteLine("Hello, world.");
} else {
foreach (var name in args) {
Console.WriteLine($"Hello, {name}.");
}
}
The compiler synthesises a hidden Program class with a Main method containing the top-level code; the program behaves identically. Top-level statements are the conventional form for small programs and for the entry point of larger applications since they reduce ceremony.
Source character set
C# source is interpreted as Unicode (typically UTF-8 with optional BOM). The basic source set includes the ASCII letters, digits, and the conventional punctuation; identifiers may use any Unicode character that the standard’s grammar admits, though in practice most code restricts identifiers to ASCII.
Identifiers and keywords
An identifier consists of a Unicode letter or underscore followed by any number of letter, digit, underscore, or formatting characters. Identifiers prefixed with @ may use a reserved keyword as an identifier:
int @class = 0; // 'class' is a keyword; @class is the identifier 'class'
The mechanism is principally useful for interoperating with languages that use a different keyword set (the @ lets a C# program reference an identifier with a name that is a C# keyword).
The keyword list:
abstract do in protected true
as double int public try
base else interface readonly typeof
bool enum internal ref uint
break event is return ulong
byte explicit lock sbyte unchecked
case extern long sealed unsafe
catch false namespace short ushort
char finally new sizeof using
checked fixed null stackalloc virtual
class float object static void
const for operator string volatile
continue foreach out struct while
decimal goto override switch
default if params this
delegate implicit private throw
Plus the contextual keywords — words that are keywords only in certain grammatical positions: add, and, alias, ascending, args, async, await, by, descending, dynamic, equals, file, from, get, global, group, init, into, join, let, managed, nameof, nint, not, notnull, nuint, on, or, orderby, partial, record, remove, required, select, set, unmanaged, value, var, when, where, with, yield. Each is a regular identifier outside its specific syntactic role.
Comments
Three comment forms:
// a line comment
/* a block comment, possibly spanning multiple lines */
/// <summary>
/// An XML doc-comment. Tools extract these to produce documentation.
/// </summary>
XML doc-comments — /// immediately above a declaration — are the conventional form for API documentation. They are extracted by dotnet build /doc:Foo.xml (or the <GenerateDocumentationFile> MSBuild property) and consumed by IDE tooling.
Declarations and definitions
C# does not distinguish declarations from definitions for non-extern members: a class, struct, method, property, or field declaration is its own definition. The exceptions are:
externmethods (whose body is provided by an external library).partialtypes and methods (whose definitions may be split across multiple files).abstractmembers of abstract classes (which have no body in the declaring class).
public class Counter {
private int count = 0; // field with initialiser
public int Value => count; // expression-bodied property
public void Increment() => count++;
}
public abstract class Animal {
public abstract void Speak(); // abstract: derived class supplies the body
}
public partial class Document { // partial: another file completes the type
public string Title { get; init; }
}
Statements
The statement grammar covers the conventional forms plus several C#-specific additions:
expression; // expression statement
{ /* statements */ } // block
if (cond) statement // selection
if (cond) statement else statement
switch (expr) { /* cases */ }
while (cond) statement // iteration
do statement while (cond);
for (init; cond; step) statement
foreach (var x in collection) statement
break;
continue;
goto label;
goto case value;
goto default;
return expr;
throw expr;
try { … } catch (Exception e) { … } finally { … }
using (resource) statement // disposal
using resource; // C# 8: using declaration
yield return expr;
yield break;
await expr;
await foreach (var x in source) { … }
Each statement is terminated by a semicolon or, for compound statements, by a closing brace.
Type qualifiers and modifiers
Several modifiers refine declarations:
| Modifier | Effect |
|---|---|
const | Compile-time constant; value baked into callers. |
readonly | Field assigned only at declaration or in a constructor. |
static | Member belongs to the type, not to instances. |
virtual | (Method) admits override in derived classes. |
override | (Method) overrides a base-class virtual or abstract member. |
sealed | (Class) may not be inherited; (method) may not be further overridden. |
abstract | (Class) cannot be instantiated; (method) has no body. |
partial | (Class, method) declaration may be split across files. |
extern | (Method) body is supplied by an external library. |
volatile | (Field) reads and writes are not optimised across thread boundaries. |
unsafe | (Block, method, type) admits pointer manipulation and fixed statements. |
async | (Method, lambda) admits await expressions. |
Several modifiers were added in later revisions:
init— (C# 9) a property setter that may be called only during initialisation.required— (C# 11) a member that must be initialised in any constructor or initialiser.file— (C# 11) a top-level type visible only to its source file.
Storage-class semantics
C# does not have C’s storage-class specifiers as such. Instead:
- Local variables have automatic-like duration: they are valid for the duration of the method (more precisely, for as long as they are reachable), and the GC reclaims them when no references remain.
- Fields of an instance live as long as the instance.
- Static fields live as long as the type — typically the entire program.
- Allocated objects (instances of reference types, arrays) live as long as references to them exist; they are reclaimed by the GC.
The combination of automatic locals, GC-managed heap allocations, and the absence of explicit free is the substance of the C# memory model; the full treatment is in Memory and the CLR.
Attributes
Attributes are declarative metadata attached to types, members, parameters, and return values. They are the C# mechanism for serialisation hints, validation rules, ORM mappings, and similar declarative configuration:
[Serializable]
public class Document {
[Required]
public string Title { get; set; } = "";
[Range(1, 100)]
public int Pages { get; set; }
[Obsolete("Use SaveAsync instead", error: false)]
public void Save() { /* ... */ }
}
Attributes are applied with […] immediately before the declaration. Each attribute is a class derived from System.Attribute; the constructor parameters are part of the attribute syntax. The runtime exposes attributes through reflection; the compiler interprets some attributes (Obsolete, Conditional, MethodImpl) as instructions to itself.
The standard library and third-party libraries define a substantial set of attributes; user code defines its own when domain-specific declarative configuration is appropriate.
The preprocessor
C# has a preprocessor of substantially smaller scope than C’s. The directives:
| Directive | Purpose |
|---|---|
#define, #undef | Define or remove a conditional-compilation symbol. |
#if, #elif, #else, #endif | Conditional compilation. |
#region, #endregion | IDE-only region markers. |
#warning, #error | Diagnostics. |
#line | Override line numbers (typically generated). |
#pragma warning | Disable or restore specific warnings. |
#nullable | Control the nullable annotation context. |
The preprocessor does not admit object-like or function-like macros, file inclusion, or stringification. Its uses are conditional compilation, IDE region annotation, and warning control.
#if DEBUG
Console.WriteLine($"value at line {__LINE__}: {value}");
#endif
#pragma warning disable CS8618 // non-nullable field uninitialised
public string Name;
#pragma warning restore CS8618
#nullable enable
A note on undefined behaviour
C# has substantially less undefined behaviour than C or C++. Most operations are defined: integer overflow is well-defined (wraps for the unsigned types, throws or wraps for signed depending on the checked/unchecked context), null dereferences throw NullReferenceException, array bounds violations throw IndexOutOfRangeException, type cast failures throw InvalidCastException. The principal exception is unsafe code, which admits raw pointer manipulation and the corresponding undefined-behaviour categories.
The discipline of writing correct C# is therefore largely the discipline of using the standard library correctly, handling exceptions appropriately, and respecting the nullability annotations when they are enabled. The compiler and the runtime catch many errors that would compile silently in C; the cases that remain are typically logic errors and concurrency issues rather than memory-safety violations.