Osy#betaRun it on Osyrin: osyrin.com

Reference / Types

x! — the null-forgiving operator

<value>! · F()!.Name · Keeper k = Find(id)!;

A postfix `!` tells the compiler a value it types as possibly-null is present — C#'s null-forgiving operator. It is a compile-time promise only: it changes no type and no behaviour, so when the promise is wrong a member read off the null throws `NullReferenceException`, as in C#.

stable5 examples compiled by CItypesoperatorauthoring

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 token

Description#

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:

wherewithout !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).Namethe SHOULD lint correctness-member-read-off-a-nullable (C#'s CS8602): it throws at run time if the value is nullquiet — 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 nullwhat happens
a field, a property, a reference to another row — station.Name, station.Relief.Namethrows
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?.Countnull; 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 a render — draws nothing for a null. One row with an empty reference must not take the whole page down. The same read in the page's action throws.
  • 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 meanwrite
absent is a case I handledeclare it nullable — Lighthouse? station = Find(name); — and test it: if (station == null) { … return; }
absent answers absentFind(name)?.Name
absent is a bug that should fail loudly, hereFirst(…) 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 hop

See also#

Related

Optional and required members

A member is required or optional by how you spell its type. A bare value type with a natural zero reads that zero; a…

entity members

The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type…

Sum / Average / Min / Max / Count

Fold rows down to a single number — a query or a `List<T>` you already hold. The one thing to know before you use them:…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…