Summary#
x! says "I know this is here" about a value the compiler types as possibly absent — a Keeper? returned by a
function, a FirstOrDefault(), a nullable reference. It is C#'s null-forgiving operator with C#'s meaning: a
compile-time promise, answered by the two places the compiler asks about absence, and nothing else. It changes
no type, no stored value and no runtime behaviour — if the promise is wrong, the value is still null, and reading a
member off it throws NullReferenceException, exactly as it would without the !.
Signature#
value! // the value, promised present
Find(id)!.Name // chains like any postfix: calls, member reads, indexers
names[0]! // after an indexer
a!.Next!.Name // at every hop that needs it
x! != null // `!=` is still inequality — the lexer reads it as one tokenDescription#
When do I reach for it?#
When you know a value is present and the compiler cannot see why. A lookup by a key you just wrote, a row an
earlier step guarantees, a navigation whose target is [Required] in practice though the model allows none: the
value is typed nullable, the program is right, and the compiler has no way to prove it.
Two places ask about absence, and ! answers both:
| where | without ! | with ! |
|---|---|---|
a nullable value into a non-nullable local — Keeper k = Find(id); | refused: cannot initialize non-nullable 'Keeper k' | accepted |
a member read off a nullable in a function — Find(id).Name | the SHOULD lint correctness-member-read-off-a-nullable (C#'s CS8602): it throws at run time if the value is null | quiet — and it still throws if the promise is wrong |
entity Lighthouse {
[MaxLength(60)] string Name;
int Height;
Lighthouse? Relief; // the station that takes over when this one is dark
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
Lighthouse? Find(string name) {
return Lighthouse.FirstOrDefault(l => l.Name == name);
}
string ReliefFor(string name) {
Lighthouse station = Find(name)!; // refused without the `!`
return station.Relief!.Name; // and each hop that can be absent says so where it is read
}A var carries the promise#
var keeper = Find(id)!; makes keeper present for everything after it, the way C#'s flow state does: it can be
handed to a non-nullable local, and its members read without the lint.
string NameOf(string name) {
var station = Find(name)!;
Lighthouse again = station; // no refusal — `station` was promised present
return again.Name;
}It is a promise, not a check#
! does not test the value and does not itself throw. When the promise is wrong, the null arrives exactly as it
would have without the ! — a comparison with null is still true, a query that compares against it still matches
a null, and a member read off it does what a member read off null does: it throws. That is C#'s behaviour too, and
it is deliberate: the operator is for telling the compiler something, never for changing what runs.
A member read off null throws#
In a function body — a function, a class method, a page's action, a workflow body, a test — reading a member off
null with a plain . throws NullReferenceException, as C# does. The message names the member and what was null:
`station.Relief` is null, so `.Name` cannot be read off it. Check it for null first, or read with `?.`.
`name` is null, so `.Trim(…)` cannot be called on it. Check it for null first, or call with `?.`.It is the same exception and the same sentence on the server and in the browser, and a typed catch takes it.
string ReliefOrNone(string name) {
var station = Find(name);
try { return station!.Relief!.Name; } // throws when there is no such station, or no relief
catch (NullReferenceException) { return "none"; }
}
string ReliefIfAny(string name) {
return Find(name)?.Relief?.Name ?? "none"; // `?.` answers null instead, and skips the rest of the chain
}| what is read off null | what happens |
|---|---|
a field, a property, a reference to another row — station.Name, station.Relief.Name | throws |
a method — name.Trim(), name.Length, station.Describe() | throws |
an assignment — station.Name = "x" | throws |
a list — list.Count, list[0], list.Add(x) | throws |
with ?. — station?.Name, station?.Relief.Name, list?.Count | null; the rest of the chain is not read |
a function of the language called by name — Text.Trim(name), string.IsNullOrEmpty(name) | takes the null as an argument; no throw |
Two positions keep a rule of their own, because the code in them is not a function body:
- A slot of a page —
Text(order.Customer.Name)in arender— draws nothing for a null. One row with an empty reference must not take the whole page down. The same read in the page'sactionthrows. - A query clause over stored rows —
Order.Where(o => o.Customer.Name != "x")— is the database's: an empty reference is null there,!=keeps the row, and nothing throws. (A lambda over a list you already hold —boxes.Select(b => b.Label)— is a function body, and throws.)
Three reads C# also throws for still answer without throwing: .Value on a nullable number or date that is null
(it answers null), foreach over a null list (no iteration), and a LINQ call on a null list (an empty result).
When the value genuinely might be absent, say what absence means instead:
| you mean | write |
|---|---|
| absent is a case I handle | declare it nullable — Lighthouse? station = Find(name); — and test it: if (station == null) { … return; } |
| absent answers absent | Find(name)?.Name |
| absent is a bug that should fail loudly, here | First(…) rather than FirstOrDefault(…) — it throws when nothing matches |
It does not unwrap a number, a date or an enum#
! forgives a possibly-null reference — an entity, a string. It does not turn an int? into an int, a
DateTime? into a DateTime, or an enum? into its enum: C# refuses int n = maybe!; (CS0266) and so does
Osy#, and the refusal says why. The unwrap there is .Value (which throws when absent) or ?? (which supplies a
value):
int Highest() {
return Lighthouse.Max(l => l.Height) ?? 0; // `Max` answers null over no rows; `??` says what that means
}cannot initialize non-nullable 'int n' with the result of a `Max(…)` aggregate, … (`!` does not unwrap `int?`
— it forgives a possibly-null REFERENCE, and C# refuses this too, as CS0266. `.Value` unwraps, throwing when
absent; `??` supplies a value.)Lighthouse b = null!; is refused as well. C# uses null! for a field that a constructor fills in later; an
Osy# local has nothing that could, so the promise could only ever be broken.
Examples#
string Direct(string name) { return Find(name)!.Name; } // off a call
bool Known(string name) { var s = Find(name); return s! != null; } // `!=` is still inequality
string Indexed(string name) {
var names = new List<string?> { name };
return names[0]!; // after an indexer
}
string Chain(string name) { return Find(name)!.Relief!.Name; } // at every hopSee also#
- Optional and required members — how a type is spelled nullable in the first place
- entity members —
[Required]references, whose absence the model rules out rather than you promising it - Sum / Average / Min / Max / Count — why
Max/Min/Averageanswer null over no rows, and??for what that means