1

According to the documentation:

The postfix ! operator has no runtime effect - it evaluates to the result of the underlying expression. Its only role is to change the null state of the expression, and to limit warnings given on its use.

Example:

IEnumerable<object?>? foo = GetFromSomewhere();
IEnumerable<object> bar = foo; // warning CS8619: Nullability of reference types in value of type 'IEnumerable<object?>' doesn't match target type 'IEnumerable<object>'.

(Notice that the warning doesn't correctly identify the value's type as IEnumerable<object?>?, but claims it is IEnumerable<object?>.)

When adding !:

IEnumerable<object?>? foo = GetFromSomewhere();
IEnumerable<object> bar = foo!; // No warning.

! seems to change the nullability of the concrete generic argument (from object? to object) too, not only the null state of the actual object instance denoted by the expression foo.

But only if I annotate the type of bar explicitly. When using var instead, the behavior is more like how I would interpret the documentation:

IEnumerable<object?>? foo = GetFromSomewhere();
var bar = foo!;
IEnumerable<object> baz = bar; // warning CS8619: Nullability of reference types in value of type 'IEnumerable<object?>' doesn't match target type 'IEnumerable<object>'.

So bar is inferred to be IEnumerable<object?>, removing only the outermost question mark.

What are the exact semantics of the C# 8.0 null-forgiving operator (!)?

domin
  • 1,192
  • 1
  • 7
  • 28
  • 1
    The type didn't change. `object?` is still an `object`, a reference type, that *can* be null. The `!` operator didn't change that. If you used *value* types instead, eg `int` or a `struct, adding `?` would generate a different type. `int` is an `Int32` while `int?` is a `Nullable` – Panagiotis Kanavos Nov 11 '19 at 10:30
  • Right! So how would you call the compile-time-thing that's comprised of a type and a nullability state? – domin Nov 11 '19 at 11:23
  • @PanagiotisKanavos Also, when `X?` is a type that can be null, and `X` is just a type, then what is `X`? A type that has a type-with-nullability argument? While the actual type is `X`? – domin Nov 11 '19 at 11:31
  • 1
    The definition of "type" didn't change with the addition of nullable reference types -- `object` and `object?` are still considered the same type, just with different nullability semantics. Is this potentially confusing? Yes, especially since `int` and `int?` *are* different types. NRTs are explicitly acknowledged as not being a "full" solution to nullability, just sort of the best thing possible without breaking everything. – Jeroen Mostert Nov 11 '19 at 11:33
  • 1
    @domin what you call `a type that can be null` is *always* a reference type. Enabling NRTs emits attributes that the compiler uses to see whether it needs to enforce nullability or not. So both `X` and `X?` will generate `X` with different nullability attribute values on *X*. `X` and `X` will generate `X` with different nullability attributes on `Y` – Panagiotis Kanavos Nov 11 '19 at 11:43

1 Answers1

1

As you quoted:

The postfix ! operator has no runtime effect - it evaluates to the result of the underlying expression. Its only role is to change the null state of the expression, and to limit warnings given on its use.

So, the operator will limit nullability warnings in general.

! seems to change the nullability of the concrete generic argument (from object? to object) too

You specified the type of bar explicitly as IEnumerable<object>, and used the ! operator as well, which suppressed the warnings.

Dave Cousineau
  • 12,154
  • 8
  • 64
  • 80
  • So you would say the semantics are determined by the **combination of `!` and the type annotation of the receiving variable**? Like when `!` is present it says "Ok, now its your choice as to what the nullability of each part of a composed type is–just tell me!"? With the extra rule that a `var` by default only removes the outermost null-qualifier? – domin Nov 11 '19 at 11:39
  • @domin no, you assigned a value of one type to a reference of a different type, and suppressed the warning about the conversion. `var` simply infers type, and `!` should normally only remove the outer nullability, yes, which `var` gets. `!` is not changing the type from `IEnumerable` to `IEnumerable`, but it is suppressing the warning about making that conversion explicitly. The only reason `IEnumerable` was introduced was because you introduced it. – Dave Cousineau Nov 11 '19 at 18:32
  • But isn't this exactly how I described it? If `!` just suppresses all kinds of warnings about nullability-related conversions, it is *as if* it allows me to specify the nullability of any parts myself by making an explicit type annotation. So `!` does two things: Firstly, performing a conversion by assuming non-nullness of the outermost type, **and** secondly suppressing all nullabilty-related warnings and therefore letting me take full control over the conversion. Side node for clarification: This all happens purely at compile-time as no actual runtime types are modified by this in any way. – domin Nov 11 '19 at 18:58
  • @domin You were or are making it sound like `!` can change the generic type parameter. It can't, but *you can*. There's a large difference between `!` changing the type parameter (which it does not do) and you simply assigning a value to an explicitly typed variable and suppressing all warnings about the conversion. If you used `#pragma` to disable a warning, you wouldn't then say that the semantics of `#pragma` include type conversions just because you suppressed the type conversion warning. – Dave Cousineau Nov 11 '19 at 20:26
  • When you are putting it like this, then we can say that in general, no types are actually changed by anything related to nullable reference types, since the nullability of a reference type is not considered part of the type itself. However, when treating the types and their compiler-inferred nullability-state as one coherent package (in fact, in other languages like Eiffel, nullability is a first-class citizen of their type system), then I can very well change the type parameter, just by asserting to the compiler that I know better (which `!` allows me to do) and it better trust me. – domin Nov 11 '19 at 20:51
  • If you like, you can look at a simple assignment like `IFoo foo = new Foo();` and observe that the actual type of `foo` is `Foo` and not `IFoo`, which is a superset of `Foo`. Casting it later (`(Foo)foo`) won't change the actual type of `foo`, just as `!` doesn't. If the cast to `(Foo)` fails, it throws. If the reference before `!` is actually `null`, a deref later throws too. Just like `Foo` is a more specific type for the value in `foo`, `object` is a more specific "type" as `object?` for the actual value approximated by it. – domin Nov 11 '19 at 21:01
  • @domin Not sure if that analogy works. At runtime there is an actual object of type `Foo`, but nullability doesn't exist at runtime, and it doesn't make any sense to think of the nullability of an object (well except to say that an object is not null and null is maybe null). Nullability is applicable to references not objects. The `!` operator is changing a reference from "maybe null" to "not null" (this is the outer `?`). But the loss of the inner `?` is only because you removed it yourself and hid the warning about the conversion; the `!` operator did not remove the inner `?`. – Dave Cousineau Nov 11 '19 at 21:14
  • If you will, `IFoo` doesn't really exist at runtime either. It's just "there" when using reflection, but this is kind of cheating. Yeah, but the type of a reference says "I am a reference pointing to something of the specified shape" having a value equal to a memory address containing that shape. The nullable variant of it says "Maybe I am a reference to something of the specified shape" having a equal to a memory address or null. – domin Nov 11 '19 at 21:21