Lexical Structure and Style
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page covers the mechanics that apply to every other page: how the compiler reads your text, how names are scoped, how files are organised, and the naming and layout conventions the rest of this section follows.
Identifiers and Keywords
An identifier starts with a letter or and continues with letters, digits, or a few Unicode categories.
C# is case-sensitive, and identifiers are compared after Unicode normalisation.
Reserved keywords (class, if, return, int, …) can never be identifiers. Contextual keywords are
keywords only where the grammar expects them, so they remain usable as names elsewhere: var, value,
yield, async, await, nameof, when, record, required, file, scoped, and/or/not in
patterns, and — new in C# 14 — field and extension.
// `record` and `value` are contextual keywords: still legal as identifiers.
int record = 1;
string value = "still fine";
// A reserved keyword needs the @ prefix to be used as an identifier.
int @class = 3;
string @string = "verbatim identifier";
Console.WriteLine($"{record} {value} {@class} {@string}");
@ identifiers exist mainly for interop: a library written in another .NET language may expose a member called
event or params, and @ is how you name it. Do not use @ to dodge a naming conflict you could simply
rename away.
|
C# 14 makes |
Literals
Numeric literals accept _ as a digit separator anywhere between digits, and a suffix to pin the type:
int plain = 1_000_000;
int hex = 0x00FF_1A2B;
int binary = 0b1010_0101;
long big = 9_000_000_000L;
uint unsigned = 42U;
ulong huge = 18_000_000_000_000_000_000UL;
float f = 3.14f;
double d = 3.141_592_653_589;
decimal money = 19.99m; // base-10, 28-29 significant digits -- use for currency
double sci = 6.022e23;
char c = 'A';
char tab = '\t';
char unicode = '\u00E9'; // é
bool yes = true;
object? nothing = null;
string plainText = "escapes: \n \t \\ \"";
string verbatim = @"C:\temp\no-escapes"; // @ suppresses escape sequences
string raw = """He said "no escaping needed" and meant it.""";
ReadOnlySpan<byte> utf8 = "UTF-8 bytes"u8; // a UTF-8 literal, not a string
decimal is the only base-10 floating type and the only correct choice for money;
Basic Types and Variables explains why. Raw
string literals and u8 literals are covered in
Strings and Text.
Comments and XML Documentation
// A single-line comment.
/* A delimited comment.
It may span lines but does not nest. */
/// <summary>
/// Computes the arithmetic mean of <paramref name="values"/>.
/// </summary>
/// <param name="values">The values to average. Must not be empty.</param>
/// <returns>The arithmetic mean.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="values"/> is empty.</exception>
public static double Average(IReadOnlyCollection<double> values)
{
if (values.Count == 0)
{
throw new ArgumentException("At least one value is required.", nameof(values));
}
return values.Sum() / values.Count;
}
/// comments are XML documentation comments. With <GenerateDocumentationFile>true</GenerateDocumentationFile>
the compiler extracts them to an XML file that IntelliSense, DocFX and NuGet consume — and starts warning about
undocumented public members and broken cref references, which makes them checkable rather than decorative.
Coding Conventions and
Documentation covers the full tag set.
Statements, Expressions and Blocks
An expression produces a value; a statement performs an action. C# blurs the line deliberately — switch,
throw, assignment and await all have expression forms — which is what makes expression-bodied members and
concise LINQ possible.
public sealed class Counter
{
private int _count;
// Expression-bodied members: `=>` instead of a block.
public int Count => _count;
public void Increment() => _count++;
public override string ToString() => $"Counter({_count})";
public string Describe(int n) => n switch // switch *expression*
{
< 0 => "negative",
0 => "zero",
_ => "positive",
};
}
A block { … } introduces a scope. A local variable is in scope from its declaration to the end of the
enclosing block, and may not be shadowed by another local in a nested block:
public static void Scope()
{
int x = 1;
{
int y = 2; // visible only inside this block
Console.WriteLine(x + y);
}
// Console.WriteLine(y); // error CS0103: y is not in scope here
}
Pattern variables and out variables are scoped to the enclosing block from their point of declaration, which
is why if (int.TryParse(s, out int n)) { … } works and n remains usable after the if.
Namespaces
A namespace groups types and prevents name collisions. Modern C# uses the file-scoped form — one namespace per file, no extra indentation:
namespace Irurueta.Sample.Geometry;
public readonly record struct Point(double X, double Y);
public readonly record struct Circle(Point Centre, double Radius);
The block form is still valid and is required when a file genuinely needs two namespaces:
namespace Irurueta.Sample.Outer
{
public class First;
namespace Inner // nested -- full name is Irurueta.Sample.Outer.Inner.Second
{
public class Second;
}
}
Namespaces need not match the directory layout, but by convention they do:
<Company>.<Product>.<Feature>, matching the default root namespace derived from the project name.
using Directives
using System.Text; // plain: bring a namespace into scope
using static System.Math; // static: bring a type's static members into scope
using Json = System.Text.Json.JsonSerializer; // alias: rename a type
using Coordinates = (double Lat, double Lon); // alias any type, including tuples (C# 12)
public static class UsingDemo
{
public static string Run()
{
double h = Sqrt(Pow(3, 2) + Pow(4, 2)); // `using static System.Math`
Coordinates where = (40.4168, -3.7038);
var sb = new StringBuilder().Append(h).Append(' ').Append(where.Lat);
return Json.Serialize(sb.ToString());
}
}
A global using applies to every file in the project and belongs in one dedicated file:
// GlobalUsings.cs
global using System.Collections.Generic;
global using System.Linq;
<ImplicitUsings>enable</ImplicitUsings> — on by default in new projects — makes the SDK add a set of
global using directives appropriate to the project type (System, System.Linq,
System.Collections.Generic, System.Threading.Tasks and more). This is why templates contain no using
lines at all. You can add to or remove from that set in the project file:
<ItemGroup>
<Using Include="System.Text.Json" />
<Using Remove="System.Net.Http" />
</ItemGroup>
Directives must precede type declarations, and using directives inside a namespace apply only within it.
Entry Points
Every executable needs exactly one entry point. Either write it explicitly:
namespace EntryPointDemo;
internal static class Program
{
// Any of: void, int, Task, Task<int>; with or without string[] args.
private static async Task<int> Main(string[] args)
{
await Task.Delay(1);
return args.Length;
}
}
…or use top-level statements, in which case the compiler synthesises the class and Main for you. Only one
file in a project may have them, args is implicitly available, and await works directly at the top level.
Statements must come before any type declaration in that file.
Coding Conventions
Microsoft’s C# coding conventions are what analyzers, dotnet format and this section follow:
| Element | Casing | Example |
|---|---|---|
Namespace, type, method, property, event, enum member |
|
|
Public / protected field (rare — prefer a property) |
|
|
Private / internal field |
|
|
Static private field |
|
|
Constant |
|
|
Parameter, local variable |
|
|
Interface |
|
|
Generic type parameter |
|
|
Attribute class |
|
|
Async method |
|
|
Layout rules that matter in practice:
-
Allman braces — the opening brace on its own line. (Expression-bodied members and single-line initialisers are the exception.)
-
Four spaces per indent level, no tabs.
-
One type per file, named after the type.
-
usingdirectives at the top,System.*first, outside the namespace. -
Prefer the language keyword (
string,int) over the BCL name (String,Int32). -
Use
varwhen the type is obvious from the right-hand side; write the type when it is not. -
Braces even on single-statement
ifbodies.
All of this is enforceable. Put it in an .editorconfig file and dotnet format will apply it and CI can
check it — see Coding Conventions
and Documentation.
See Also
-
Basic Types and Variables — what the literals above actually produce.
-
Namespaces, Assemblies and Projects — how namespaces relate to assemblies and
internal. -
Coding Conventions and Documentation —
.editorconfig, analyzers and the full XML documentation tag set. -
Preprocessor Directives and Compilation — the
#directives that also affect how a file is read.