Nullability, equality, and conversions
Grammar
Section titled “Grammar”NullableType = Type "?" .SafeCast = Expression "as" Type .AssertCast = Expression "as!" Type .TypeTest = Expression "is" Type .T? denotes an optional value. For a CLR value type it is System.Nullable<T>; for a reference type it is
the underlying reference with E# nullability information. nil is assignable to a nullable type, a reference
type, and *T, but not to a plain non-nullable value type.
Optional values
Section titled “Optional values”A T implicitly promotes to T? as the present case. A T? shall not implicitly convert to T; a program
must prove or choose its absent behavior through match, ??, ?., a let-guard, or type/null flow analysis.
This prevents a dereference or value extraction from being hidden by assignment.
func label(name: string?) -> string = name ?? "anonymous"*T is distinct from T?: it is a pointer to a T location and can be nil, while T? describes an optional
T value. A class is already a reference and therefore cannot be the target of *.
Equality and identity
Section titled “Equality and identity”| Category | equality behavior | identity |
|---|---|---|
| primitive / enum / tuple | value equality | none |
struct with derive equality | field-by-field Equals, hash, ==, != | none |
| struct without derive | CLR default | none |
| class / ref union | reference equality by default | each object has identity |
derive equality and derive debug are declaration-time member synthesis. A class or struct may instead
declare the fixed operator set in its companion static T facet; explicit ==/!= conflicts with
derive equality. Operator functions do not introduce implicit conversions. There are no user-defined cast
operators; a type exposes a named method/factory when a conversion would otherwise be needed.
Reference narrowing and casts
Section titled “Reference narrowing and casts”An implicit conversion exists from a class to a base class and from a declared interface implementer to that named interface. Downward conversions are explicit:
| Form | result on mismatch | use |
|---|---|---|
x is T | false; flow narrows stable x on success | test and smart cast |
x as T | nil | safe optional cast |
x as! T | throws InvalidCastException | asserted boundary cast |
match x { (v: T) { ... } } | proceeds to another arm | multi-way type dispatch |
Narrowing is valid only for a stable path such as a let, parameter, or immutable field. A mutable var
cannot be assumed unchanged after an arbitrary call. Generic arguments remain part of the runtime test because
E# generics are reified.
Numeric, tuple, delegate, and boxing conversions
Section titled “Numeric, tuple, delegate, and boxing conversions”Numeric conversions are explicit: E# does not implicitly widen one nonliteral numeric value to another
numeric type. The native spelling is T(value) for the primitive numeric targets, and integral destinations
are checked. For example, double(x), decimal(x), and byte(x) are explicit; a dynamic out-of-range
byte(x) throws OverflowException. A literal inside T(...) is parsed directly in the target context, so
decimal(0.1) is exact. See Numerics and performance.
Tuple conversion is element-wise between compatible tuples of the same arity.
An enum and its integers convert in both directions through the same T(value) spelling, never
implicitly. int(codec) reads the enum’s underlying value__, then applies the ordinary integer
conversion of that underlying type to the target — so a signed underlying type
sign-extends and an unsigned one zero-extends, exactly as the same widening would between the primitives.
Conversely, Codec(raw) reinterprets an integral value as the enum with no range check (the C# (Codec)raw
cast), so an out-of-range integer yields a defined-but-unnamed enum value rather than a throw. The pair
round-trips: Codec(int(c)) == c for every c.
enum Codec: byte { Raw = 0, Deflate = 1 }
let c = Codec.Deflate()let n = int(c) // 1 — underlying byte read, then widened to intlet back = Codec(n) // Codec.Deflate — reinterpret, no range checkBoxing occurs when the CLR requires a reference, such as assigning a struct to an interface/object parameter.
The boxed value is a copy. Unboxing occurs at an explicit/asserted cast or a type-pattern binding. Delegate
conversion is target-typed: a lambda or method group converts when a specific delegate type is known from a
parameter, annotated local, return type, or event. Nominal delegate func types do not implicitly convert to
structurally similar Func<...> types.