The following describes the representation of nullable annotations in metadata.
Type references are annotated in metadata with a NullableAttribute.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.GenericParameter |
AttributeTargets.Parameter |
AttributeTargets.Property |
AttributeTargets.ReturnValue,
AllowMultiple = false,
Inherited = false)]
public sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte flag)
{
NullableFlags = new byte[] { flag };
}
public NullableAttribute(byte[] flags)
{
NullableFlags = flags;
}
}
}The NullableAttribute type is for compiler use only - it is not permitted in source.
The type declaration is synthesized by the compiler if not already included in the compilation.
Each type reference in metadata may have an associated NullableAttribute with a byte[] where each byte
represents nullability: 0 for oblivious, 1 for not annotated, and 2 for annotated.
The byte[] is constructed as follows:
- Reference type: the nullability (0, 1, or 2), followed by the representation of the type arguments in order including containing types
- Nullable value type: the representation of the type argument only
- Non-generic value type: skipped
- Generic value type: 0, followed by the representation of the type arguments in order including containing types
- Array: the nullability (0, 1, or 2), followed by the representation of the element type
- Tuple: the representation of the underlying constructed type
- Type parameter reference: the nullability (0, 1, or 2, with 0 for unconstrained type parameter)
Note that non-generic value types are represented by an empty byte[].
However, generic value types and type parameters constrained to value types have an explicit 0 in the byte[] for nullability.
The reason generic types and type parameters are represented with an explicit byte is to simplify metadata import.
Specifically, this avoids the need to calculate whether a type parameter is constrained to a value type when
decoding nullability metadata, since the constraints may include a (valid) cyclic reference to the type parameter.
If the byte[] is empty, the NullableAttribute is omitted.
If all values in the byte[] are the same, the NullableAttribute is constructed with that single byte value. (For instance, NullableAttribute(1) rather than NullableAttribute(new byte[] { 1, 1 })).)
Each type parameter definition may have an associated NullableAttribute with a single byte:
notnullconstraint:NullableAttribute(1)classconstraint in#nullable disablecontext:NullableAttribute(0)classconstraint in#nullable enablecontext:NullableAttribute(1)class?constraint:NullableAttribute(2)- No
notnull,class,struct,unmanaged, or type constraints in#nullable disablecontext:NullableAttribute(0) - No
notnull,class,struct,unmanaged, or type constraints in#nullable enablecontext (equivalent to anobject?constraint):NullableAttribute(2)
NullableContextAttribute can be used to indicate the nullability of type references that have no NullableAttribute annotations.
namespace System.Runtime.CompilerServices
{
[System.AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Delegate |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Struct,
AllowMultiple = false,
Inherited = false)]
public sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte flag)
{
Flag = flag;
}
}
}The NullableContextAttribute type is for compiler use only - it is not permitted in source.
The type declaration is synthesized by the compiler if not already included in the compilation.
The NullableContextAttribute is optional - nullable annotations can be represented in metadata with full fidelity using NullableAttribute only.
NullableContextAttribute is valid in metadata on type and method declarations.
The byte value represents the implicit NullableAttribute value for type references within that scope
that do not have an explicit NullableAttribute and would not otherwise be represented by an empty byte[].
The nearest NullableContextAttribute in the metadata hierarchy applies.
If there are no NullableContextAttribute attributes in the hierarchy,
missing NullableAttribute attributes are treated as NullableAttribute(0).
The attribute is not inherited.
The C#8 compiler uses the following algorithm to determine which NullableAttribute and
NullableContextAttribute attributes to emit.
First, NullableAttribute attributes are generated at each type reference and type parameter definition by:
calculating the byte[], skipping empty byte[], and collapsing byte[] to single byte where possible.
Then at each level in metadata hierarchy starting at methods:
The compiler finds the most common single byte value across all the NullableAttribute attributes at that level
and any NullableContextAttribute attributes on immediate children.
If there are no single byte values, there are no changes.
Otherwise, a NullableContext(value) attribute is created at that level where value is most common
value (preferring 0 over 1 and preferring 1 over 2), and all NullableAttribute and NullableContextAttribute attributes with that value are removed.
Note that an assembly compiled with C#8 where all reference types are oblivious will have no
NullableContextAttribute and no NullableAttribute attributes emitted.
That is equivalent to a legacy assembly.
// C# representation of metadata
[NullableContext(2)]
class Program
{
string s; // string?
[Nullable({ 2, 1, 2 }] Dictionary<string, object> d; // Dictionary<string!, object?>?
[Nullable(1)] int[] a; // int[]!
int[] b; // int[]?
[Nullable({ 0, 2 })] object[] c; // object?[]~
}To reduce the size of metadata, the C#8 compiler can be configured to not emit attributes
for members that are inaccessible outside the assembly (private members, and also internal members
if the assembly does not contain InternalsVisibleToAttribute attributes).
The compiler behavior is configured from a command-line flag.
For now a feature flag is used: -features:nullablePublicOnly.
If private member attributes are dropped, the compiler will emit a [module: NullablePublicOnly] attribute.
The presence or absence of the NullablePublicOnlyAttribute can be used by tools to interpret
the nullability of private members that do not have an associated NullableAttribute attribute.
For members that do not have explicit accessibility in metadata (specifically for parameters, type parameters, events, and properties), the compiler uses the accessibility of the container to determine whether to emit nullable attributes.
namespace System.Runtime.CompilerServices
{
[System.AttributeUsage(AttributeTargets.Module, AllowMultiple = false)]
public sealed class NullablePublicOnlyAttribute : Attribute
{
public readonly bool IncludesInternals;
public NullablePublicOnlyAttribute(bool includesInternals)
{
IncludesInternals = includesInternals;
}
}
}The NullablePublicOnlyAttribute type is for compiler use only - it is not permitted in source.
The type declaration is synthesized by the compiler if not already included in the compilation.
IncludesInternal is true if internal members are annotated in addition to public and protected members.
The nullable metadata does not include an explicit version number. Where possible, the compiler will silently ignore attribute forms that are unexpected.
The metadata format described here is incompatible with the format used by earlier C#8 previews:
- Concrete non-generic value types are no longer included in the
byte[], and NullableContextAttributeattributes are used in place of explicitNullableAttributeattributes.
Those differences mean that assemblies compiled with earlier previews may be read incorrectly by later previews, and assemblies compiled with later previews may be read incorrectly by earlier previews.