Nullability
All Elements languages have the ability to specify the nullability of type references. Type references can be variables, fields, properties or method parameters. For brevity, we'll use the term "variables" throughout this topic to refer to all four kinds of references.
Nullable variables may either contain a valid value or they may not — in the latter case they are considered to be nil
(in Oxygene and Swift parlance), null
(in C# and Java parlance) or Null
(Mercury). Non-nullable variables must always contain a value and cannot be null/nil.
The different languages have different ways of expressing the nullability of a variable.
Oxygene, C#, Java, and Mercury
In Oxygene, C#l, Java and Mercury the default nullability of a variable is determined by its type. For value types (structs, enums and simple numeric types), variables are assumed to be non-nullable by default, and always have a value. Reference types (i.e. classes or interfaces) are considered nullable by default, and may be null
/nil
. Please refer to Value Types vs. Reference Types.
For example:
var i: Int32; // non-nullable by default, initialized to 0
var b: Button; // nullable by default, will be nil
Int32 i; // non-nullable by default, initialized to 0
Button b; // nullable by default, will be null
Int32 i; // non-nullable by default, initialized to 0
Button b; // nullable by default, will be null
Dim i As Int32 ' non-nullable by default, initialized to 0
Dim b As Button ' nullable by default, will be null
Nullability of a type can be changed by explicitly amending the type name with a modifier. In Oxygene, value types can be made nullable with the nullable
keyword, and reference types can be made non-nullable with the not nullable
keyword combination. In C#, Java and Mercury, value types can be made nullable by appending ?
to the typename, and reference types can be made non-nullable by appending !
, respectively.
For example:
var i: nullable Int32; // nullable
var b: not nullable Button := new Button(); // not nullable
Int32? i; // nullable
Button! b = new Button() // not nullable
Int32? i; // nullable
Button! b = new Button() // not nullable
Dim i As Int32? ' nullable
Dim b As Button! = New Button() ' not nullable
In the above example, the value type Int32
was modified to make the declared variable nullable, while the reference type Button
(we're assuming Button
is a class) was made non-nullable. As you will also notice, the non-nullable variable requires immediate initialization (in this case by assigning a newly created Button
instance), because the default value of the type, nil
or null
, would be invalid for a non-nullable variable.
Note that type inference will always favor the type's default nullability. For example, in the code below, s
will be inferred to be a regular nullable string, even though it's being assigned a decidedly non-null value.
var s := `Hello`;
var s = "Hello";
var s = "Hello";
Dim s = "Hello";
Swift
The Swift language behaves slightly different. Regardless of the underlying type, all types are treated as being non-nullable by default – even reference types.
For example:
var i: Int32 = 0; // non-nullable by default
var b: Button = Button(); // non-nullable by default
In addition, you'll also notice that Swift requires any non-nullable variable to be explicitly initialized with a default value. Where Oxygene and C# would assume a default value of 0 for the integer, Swift requires an initial value to be provided.
Swift has two ways to mark a variable as nullable, and that is either by appending a !
or a ?
to the typename:
var i: Int32! // nullable
var b: Button! // nullable
var i2: Int32? // nullable
var b2: Button? // nullable
Conceptually, the nullable variables declared with !
and ?
are the same. Both i
and i2
are of type nullable integer and initially are nil
; both b
and b2
are of type nullable Button
and also initially nil
.
The difference lies in how the two sets of variables can be used in subsequent code.
The variables declared with !
are what Swift calls implicitly unwrapped nullables. That means that even though the variable may be nil
, you can use it as you please, and – for example – directly call members on it or (in the case of the integer) use it in an arithmetic expression. If you try to call into a member of i
or b
and the actual variable is nil
at runtime, a Null Reference Exception will be thrown. (This is how all nullable types behave in Oxygene and C#, meaning that all nullable types in those languages are implicitly unwrapped nullables.)
By contrast, the variables declared with ?
do not allow direct access to the type they may (or may not) reference. All you can do with variables i2
and b2
is to compare them to nil
, and to explicitly unwrap them in order to gain access to their content.
Note: While this syntax is not recommended for user code, the ¡
(inverted exclamation point) suffix can be used in Silver's Swift dialect to mark a type as being nullable only if it is a reference type – essentially giving it the same nullability behavior of the type name being used on its own in Oxygene or C#. This is mainly used by code generators to express this behavior where the kind of type is not known at generation time.
var i: Int32¡ // not nullable, since Int32 is a value type
var b: Button¡ // nullable, since Button is a reference type
Unwrapping them can be done in two ways:
Unwrapping a Nullable Inline
You can use the !
or ?
operator on the variable to explicitly unwrap them in place before calling a member. For example:
let t1 = b2!.title
let t2 = b2?.title
Using !
will check if the b2
variable has a value. If it does, the title
property on that Button
instance will be called and returned into t1
. If it does not, a Null Reference Exception will be thrown. Assuming the title
property was declared as type String
, then t1
will also be of type String
, i.e. it will be assumed to be non-null.
When using ?
instead, the variable will still be checked for a valid value, and if it has a valid value, that instance's 'title
property will be called. However, if b2
is nil
instead, the call to title
will be bypassed, and t2
will be initialized to nil
instead. As you might expect, this means t2
itself will be of type String?
–in other words a nullable String
. (In essence ?
behaves similarly to the Colon Operator (:
) in Oxygene and the similar "Elvis" operator (?.
) in C#.)
Conditionally Unwrapping a Nullable
If you are planning to do more extensive calls on a nullable variable, it often makes sense to conditionally unwrap it into a new non-nullable variable with Swift's if let
construct:
if let b3 = b2 {
let t3 = b3.title
}
The if let
construct checks if the right hand of the assignment contains a value or is nil
. If it contains a value, that value is assigned to a new, non-nullable variable (in this case b3
) for the scope of the following block, and that block is executed. If the right hand side of the assignment is nil
, the whole block is skipped.
Note how inside the block, members of b3
can be called without the need for !
or ?
.
Comparing Swift Nullables to the Other Languages
Conceptually, nullable Variables in Oxygene, C#, Java and Mercury behave equivalent to implicitly unwrapped nullables in Swift declared with !
. They can contain nil
, but you can still access all their members directly – at the risk of a Null Reference Exception (NRE) occurring, if you don't make sure a variable is not nil
.
The other languages have no equivalent for Swift's more strict wrapped nullable types declared with ?
, which cannot be acecssed without unwrapping
A second crucial difference to keep in mind, especially when working with both C#, Java or Mercury, but also with Swift, is that while Swift uses both ?
and !
to mark a variable as nullable, !
has in fact the opposite meaning in RemObjects C#, Java and Mercury, marking an otherwise nullable reference type as *not nullable.
Nullability of Types Compared:
Type | Value Type | Reference Type |
---|---|---|
Oxygene Non-Nullable | Int32 |
not nullable String |
Oxygene Unwrapped Nullable | nullable Int32 |
String |
Oxygene Wrapped Nullable | N/A | nullable String |
C#/Java/Mercury Non-Nullable | Int32 |
String! |
C#/Java/Mercury Unwrapped Nullable | Int32? |
String |
C#/Java/Mercury Wrapped Nullable | N/A | String? |
Swift Non-Nullable | Int32 |
String |
Swift Unwrapped Nullable | Int32! |
String! |
Swift Wrapped Nullable | Int32? |
String? |
The Oxygene Colon (:
) Operator
Oxygene pioneered the colon operator to allow safe member calls on potentially nil
variables. C#, Java, Mercry and Swift later took on the same concept by combining the ?
operator with a subsequent dot (.
) for member access.
For example:
var b: Button;
var i = b:title:length; // i is a nullable Int32
Button b;
var s = b?.title?.length; // i is an Int32?
let b: Button?
var i = b?.title?.length // i is an Int32?
Button b;
var i = b?.title?.length; // i is an Int32?
Dim b as Button
DIm i = b?.title?.length ' i is an Int32?
Exposing Wrapped Nullables to Swift from the Other Languages
For the purpose of being able to create APIs in all languages that play well with Swift, the languages provide a way to mark variables as wrapped, even though the languages themselves do not use the concept. If a class type is explicitly marked as "nullable", it will appear as a wrapped nullable when referred to from Swift:
var b: nullable Button; // already nullable by default, "nullable" makes it wrapped to Swift
Button? b; // already nullable by default, "?" makes it wrapped to Swift
Button? b; // already nullable by default, "?" makes it wrapped to Swift
Dim c As Button? ' already nullable by default, "?" makes it wrapped to Swift
Optional Nullability Warnings in the Other Languages
As discussed above, all nullables in languages other than Swift are considered unwrapped, meaning they can be accessed without explicit unwrapping, and they can be assigned to variables that are declared non-nullable – at the risk of generating an NRE) when an actual null value is encountered. This is of course par for the course for Oxygene, C#, Java and Mercury developers.
When working in those languages with APIs designed for Swift – that is, APIs that make explicit use of nullable and non-nullable type references a lot – the compiler will optionally emit warnings when assigning potential null values to variables that are declared nullable. This can help you spot potential NREs at compile time rather than when debugging.
Because in many cases these warnings are noisy and can provide false positives, they can be turned off in Project Settings via the "Warn on Implicit Not-Nullable Casts" option. When turned on (the default), warnings will be generated in cases such as the one below:
var b: Button! := ...;
var c: Button;
b := c; // warning here, as c might be nil
Button! b = ...;
Button c;
b = c; // warning here, as c might be null
Button! b = ...;
Button c;
b = c; // warning here, as c might be null
Dim b As Button! = ...
Dim c As Button
b = c ' warning here, as c might be Nothing
There are two ways to "fix" these warnings and let the compiler know that the assignment is safe, or that the developer is aware of the potential risk of an NRE. The first is to enclose the assignment in code that ensures the variable is not null in a way that the compiler can detect as part of code flow analysis, for example with an if statement:
if c ≠ nil then
b := c; // no warning here, we know c is assigned
if (c != null)
b = c; // no warning here, we know c is assigned
if (c != null)
b = c; // no warning here, we know c is assigned
vb
if c IsNot Null Then
b = c ' no warning here, we know c is assigned
End IF
`
The second option is to explicitly "cast" the value to a non-null version. This can be done either by using type cast syntax with a nullable type name, or by using a shorthand syntax provided by the language. In C#, the shorthand syntax uses the !
operator, similar to Swift. In Oxygene, the as not nullable
keyword combination, without an explicit type name, can be used:
b := c as not nullable Button; // type-cast to non-nullable type
b := c as not nullable; // convenience shorthand syntax
b = c as Button!; // type-cast to non-nullable type
b = c!; // convenience shorthand syntax
b = c as Button!; // type-cast to non-nullable type
b = c!; // convenience shorthand syntax
b = DirectCast(c, Button!) ' type-cast to non-nullable type
b = c!; ' convenience shorthand syntax
Exposing Wrapped nullable to Swift from the Other Languages
When writing APIs in Oxygene, C#, Java or Mercury that will (also) be consumed from Swift, it is sometimes (or rather, often) desirable to expose nullable types as wrapped nullables on the Swift side.
Even though these languages have no concept of wrapped nullables themselves, reference types (and only reference types) that are explicitly (and, in a sense, redundantly) marked as nullable in Oxygene and C# will be exported as wrapped nullable when seen from Swift. For example, a type of nullable String
(Oxygene) or String?
(C#, Java or Mercury) will appear in Swift as wrapped nullable String?
, while a regular nullable String
will appear to Swift as a unwrapped nullable String!
.
There is currently no way to expose nullable value types as wrapped nullables from Oxygene, C#, Java or Mercury.
Nullability Information in Cocoa SDKs
Starting with the 2015 releases of Apple's Cocoa SDKs, their Objective-C APIs have been annotated with nullability information, marking parameters as nullable or non-nullable (usually wrapped, in the latter case), where appropriate. This applies to iOS 9.0, watchOS 2.0, tvOS 9.0 and OS X 10.11 and later.
Elements has full support for these annotations in Objective-C, and carries the nullability of parameters, method results and properties over into the APIs seen by Oxygene, C# and Silver. Because of this, many APIs are much more expressive and precise when building against the newer SDKs. This also means that your own code consuming these standard Cocoa APIs might need adjustment for more explicit handling of nullability, for example when overriding platform methods, or when dealing with return types.
Nullability information is available in the new SDKs' .FX files, and also when importing custom third party libraries with FXGen.