2

I have this simple code and it generates a warning:

private void MyMethod()
{
    IDictionary<string, object> notNullable = new Dictionary<string, object>();
    Test(notNullable);
}

private void Test(IDictionary<string, object?> nullable)
{
}

I get this warning when I try to compile (it does work with ! though):

Argument of type 'Dictionary< string, object>' cannot be used for parameter 'nullable' of type 'IDictionary' in '...' due to differences in the nullability of reference types

Now I can see the problem doing it the other way around, but how is it a problem that I send a not nullable to a nullable parameter? Just a limitation of C# compiler, or perhaps a bug?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • Isn't this an issue more with covariance/contravariance? – Dai Dec 30 '19 at 15:54
  • Its not a bug. U can cast non nullable to nullable and then pass. – Gauravsa Dec 30 '19 at 15:55
  • You're trying to pass a `Dictionary` to a method that takes an `IDictionary>`... why would that ever be allowed? – Powerlord Dec 30 '19 at 15:56
  • 3
    @Powerlord in C# 8.0, `Object?` is not the same thing as `Nullable`. – Dai Dec 30 '19 at 15:59
  • Yes, behind the scenes there is no difference at all. Nullable reference types are not the same as nullable value types – Ilya Chernomordik Dec 30 '19 at 16:26
  • If you pass a non-nullable type to a function expecting a nullable type, and that function sets the value to null as part of its operation, what do you think should happen? Best case scenario when allowing non-nullable types to be passed in in place of a nullable type and you attempt to set it to null you'd see a runtime exception. But if you already know that this would throw such an exception in advance, like the compiler does, then why not throw an error on the attempt and not at runtime? Compiler errors are much more desirable than runtime errors. –  Dec 30 '19 at 16:56

2 Answers2

5

This is the same problem as generic-type argument covariance/contravariance problem because IDictionary supports "in and out" movement of data (compared to IReadOnlyDictionary which is an "out" container).

The reason it doesn't compile gives a warning is because it would allow this:

// This code requires a C# 8.0 compiler!

private void MyMethod()
{
    IDictionary<String,Object> cannotContainNulls = new Dictionary<String,Object>();

    Test( cannotContainNulls );

    assert( cannotContainNulls[ "this has a null value" ] == null ); // this shouldn't be possible!
}

private void Test( IDictionary<String,Object?> canContainNulls )
{
    canContainNulls.Add( key: "this has a null value", value: null );
}

If you change your Test method to accept an IReadOnlyDictionary (where TValue is marked as out for contravariance (or covariance, I forget which is which) it should work.

Note that only interfaces and delegates can have their generic type parameters annotated with in and out, whereas concrete types (including abstract classes) cannot. This isn't a problem provided that programs that consume generic types expecting type argument variance are programmed to work with the interfaces instead of concrete types.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • 1
    Although you are right about `IDictionary` being invariant in its types, AFAIK the types of `object` and `object?` are identical, since `object?` is synctactic sugar for the benefit of the compiler, not a different type (they are both still just `object`): object foo = new object(); object? bar = new object(); Debug.Assert(foo.GetType() == bar.GetType()); (and as per the OP, it does compile, and run, he just gets a warning) – StuartLC Dec 30 '19 at 16:12
  • @StuartLC, seems that the reason for the warning is the same as for covariance stuff. It will fully work of course, it's not even a syntactic sugar, it's just a warning generator really, but the warning is correctly generated since it can get into troubles later (no guarantee it won't be null) – Ilya Chernomordik Dec 30 '19 at 16:24
  • Though you could fix "don't compile" to "gives a warning", perhaps that's wha @StuartLC meant :) – Ilya Chernomordik Dec 30 '19 at 16:25
  • 1
    I'm fairly familiar with [covariance *stuff* :)](https://stackoverflow.com/a/42660356/314291), but it's a bit sad to see that Code Contracts have died - the static checking was amazing, and not just for nulls. TBH I rather prefer the way that Scala / Java handle 'missing' data through use of the Option type / monad. – StuartLC Dec 30 '19 at 16:35
  • 1
    @StuartLC You're making me depressed :( (Code-contracts were amazing, but hard to sell to people unfamiliar with the underlying concepts - and a "problem" with CC-heavy code is you end up using C# to write functional code and then you end-up asking yourself why you weren't using F# in the first place. – Dai Dec 30 '19 at 16:39
  • @StuartLC I'm in love with things like [`OneOf`](https://github.com/mcintyre321/OneOf) but they're still so cumbersome to use in C# - for example: having to use lambdas for each branch to abuse scopes for safety instead of a more succint syntax with no runtime overhead, or how we don't have a reasonable way to implement exhaustive `switch` , and so on. I was hoping that Roslyn's pluggable design would mean projects could introduce new syntax but so far all we have a new analyzers (which are nice, but have a long way to go). – Dai Dec 30 '19 at 16:44
-1

Until C# 7.0 you don't need this object? because the type object accept nullable values, same as strings. If it was for example a int then you need to do that IDictionary<string, int?>

Diogo Magro
  • 84
  • 2
  • 9