Skip to content

Nullability, equality, and conversions

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.

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 *.

Categoryequality behavioridentity
primitive / enum / tuplevalue equalitynone
struct with derive equalityfield-by-field Equals, hash, ==, !=none
struct without deriveCLR defaultnone
class / ref unionreference equality by defaulteach 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.

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:

Formresult on mismatchuse
x is Tfalse; flow narrows stable x on successtest and smart cast
x as Tnilsafe optional cast
x as! Tthrows InvalidCastExceptionasserted boundary cast
match x { (v: T) { ... } }proceeds to another armmulti-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 int
let back = Codec(n) // Codec.Deflate — reinterpret, no range check

Boxing 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.