0

I've got an editor that lets the user edit the "simple" properties of objects (int, string, DateTime, etc.), so it iterates through the properties and then constructs for each simple property an object to support the editing:

#nullable enable

public class DataNode<T>
{
   protected PropertyInfo Prop;
   protected object Source;
   protected T Value;
   protected bool Modified;

   public DataNode(PropertyInfo prop, object source)
   {
      Prop = prop;
      Source = source;
      object? value = prop.GetValue(source);
      if (value is T t)
         Value = t;
      else
         Value = default;
      Modified = false;
   }

   public virtual bool IsValid()
   {
      return true;
   }

   public void Save()
   {
      if (Modified)
      {
         Prop.SetValue(Source, Value);
         Modified = false;
      }
   }
}

I then have subclasses for specific properties - for example, if there's an int property where I don't want the user to be able to enter a negative value, I can create a specific subclass:

public class NonNegativeIntNode : DataNode<int>
{
   public NonNegativeIntNode (PropertyInfo prop, object source)
   : base(prop, source)
   {
   }

   public override bool IsValid()
   {
      return Value >= 0;
   }
}

So that all works fine, and before you ask:

  1. The selection of which DataNode class to use is controlled by custom attributes on the properties
  2. The DataNode does more than this that I've left out for simplicity. For example the BoolDataNode class knows that to edit this value it should use a checkbox

The problem is that the compiler is grumbling that "Value = default;" is a possible null reference assignment, and that the DataNode constructor might be exiting with a null value for 'Value'.

I can keep the compiler happy by defining Value as "protected T? Value" but that adds unnecessary complication elsewhere to check for a null value when I know darn well that inside BoolDataNode that Value will never be null.

I tried splitting DataNode into two classes - NullableDataNode and NonNullableDataNode - but inside NonNullableDataNode, even though I specify "where T: notnull", the compiler is still worried about 'default'

It seems like saying "where T: notnull" means "I am never going to set Value to null" where what I want to tell the compiler is "T is a type that cannot be null" (like bool)

Is there a way to reassure the compiler that all is well, without simply turning off all the nullability warnings with pragmas?

Betty Crokker
  • 3,001
  • 6
  • 34
  • 68
  • 3
    I don't see why you think that `Value = default` assigns a non-null value to `Value`? What if `T` is `string`? The default value of `string` is exactly `null`. – Sweeper Mar 28 '22 at 14:47
  • 1
    What do you want to happen with `Value` if the the generic argument `T` is a `string` or some other reference type? – gunr2171 Mar 28 '22 at 14:47
  • `notnull` is the correct way to express that `T` cannot be a nullable type, but that still doesn't make `default` legal, because of course `default` for a non-nullable reference type is still `null`. You can restrict the type to *value* types, whose `default`s are never `null`, with `T : struct`. Whether that's what you want is another matter; simply overriding the assignment with `default!` is another, as is throwing an exception. – Jeroen Mostert Mar 28 '22 at 14:47
  • @Sweeper I don't think that "Value = default" assigns a non-null value to Value. I am well aware that the default value of string is null. The problem is telling the compiler to not worry about Value being set to null when T is string. – Betty Crokker Mar 28 '22 at 14:54
  • @gunr2171 When T is string, I expect that "Value = default" will set Value to null. That's exactly what should happen. The problem is telling the compiler to not worry about it (give me a warning) – Betty Crokker Mar 28 '22 at 14:55
  • 1
    But then if you had a `DataNode`, whose `Value` property is non-nullable `string`, if you set `Value = default`, then you're setting `Value` to `null`. But `Value` is a `string` and not a `string?`, so it can't be null. Hence the warning. Wouldn't it make more sense for `Value` to be `T?`? That way you declare that it can be `null`, even if `T` is a non-nullable type – canton7 Mar 28 '22 at 14:56
  • @JeroenMostert I tried add "where T: struct" but that means I can't have string types – Betty Crokker Mar 28 '22 at 14:57
  • As Jeroen mentioned, you can use the [null forgiveness operator](https://stackoverflow.com/questions/55525861/c8-what-does-default-do-on-generic-types), but canton7 brings up a good point - you're telling the compiler "don't worry it won't be null" as it assigns null. – gunr2171 Mar 28 '22 at 14:59
  • What you are trying to do goes against the whole point of NRTs... You might as well just do `default!`, or not use them at all, or rethink this over. – Sweeper Mar 28 '22 at 14:59
  • What is, if you use `T?` for the protected field Value. – Caveman74 Mar 28 '22 at 15:00
  • @canton7 Yeah, I thought about defining Value to be T?, but (1) that adds complication elsewhere where I have to do null checks on things that definitely aren't null, and (2) isn't really accurate - if I have a DataNode I know darn well that Value will never be null, and it feels wrong to say that it can be just to keep the compiler happy – Betty Crokker Mar 28 '22 at 15:01
  • @Sweeper Generally I really like NRTs, and I'm slowly going through my application and adding "#nullable enable", but there a places like this where I run up against a limitation of the feature. I'd be happy to "rethink this over" but can't come up with a scheme that doesn't run into the same issue – Betty Crokker Mar 28 '22 at 15:03
  • @BettyCrokker But it *can* be null, because you're setting it to null! Note that `T?` (where `T` is unconstrained) means "If `T` is a reference type, this can be null; if `T` is a value type, the `?` has no effect". So if `T` is a `bool`, then the `T?` just means the same as `T`, and you're not saying that `Value` can be a nullable bool. (Strictly, `T?` means "defaultable" rather than "nullable", i.e. it can contain that type's default value) – canton7 Mar 28 '22 at 15:03
  • How about splitting this into `ValueDataNode where T: struct` and `ReferenceDataNode where T: class`? In the latter, you can make the property of type `T?`, because that is indeed what it is. – Sweeper Mar 28 '22 at 15:04
  • @Sweeper No need, the `?` has no effect when `T` is a value type anyway – canton7 Mar 28 '22 at 15:04
  • @canton7 Huh? I was referring to making `ReferenceDataNode` ("the latter") have a `T?` property. – Sweeper Mar 28 '22 at 15:06
  • 1
    @Sweeper What I said above -- there's no point in having a separate `ValueDataNode` type where `Value` is a `T`, because the behaviour is **identical** to an unconstrained `DataNode` where `Value` is a `T?`. `DataNode.Value` will be a `bool`, not a `bool?`. [See here](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEUCuA7NAExAGoAfAYjxwBsaBDYGmAAhj0eYFgAoXvGAHcWAEXoZ6AOQiEYAHmAQINAHwAKAJQsA3iwBq9GjlYBeFtTosAvgG5evAAIBmFg4BMo8VJnyAKit5tXhYQ5xZfAH59Q2M7HiteIA=). The `?` in `T?` *only* has an effect when `T` is a reference type -- it's ignored when `T` is a value type – canton7 Mar 28 '22 at 15:08
  • @canton7 I have learned something new about C# today, thank you! Do you want to make an official answer so you get "credit"? – Betty Crokker Mar 28 '22 at 15:15
  • 1
    @canton7 Oh yeah you're right. I was mislead by the sentence "but that adds unnecessary complication elsewhere to check for a null value when I know darn well that inside `BoolDataNode` that Value will never be null." @BettyCrokker, wouldn't `BoolDataNode` inherit `DataNode`, and if so, how could "unnecessary complication"s ever be added? – Sweeper Mar 28 '22 at 15:15
  • @Sweeper I was under the impression that if I defined Value to be type T? that DataNode.Value would be bool?, and that would cause complication elsewhere as I added all these null checks. I'm busy shifting my brain now to this new understanding. – Betty Crokker Mar 28 '22 at 15:20
  • BTW, I'm not happy with this over-use of the '?' symbol. Generally c# has done a great job of not reusing syntactical elements, this feels like a place where they shouldn't have used the '?' symbol. My two cents worth. – Betty Crokker Mar 28 '22 at 15:25

1 Answers1

3

I can keep the compiler happy by defining Value as "protected T? Value" but that adds unnecessary complication elsewhere to check for a null value when I know darn well that inside BoolDataNode that Value will never be null.

The right thing to do is to make this a protected T? Value field, since if T is a reference type, you'll be assigning null. People accessing your Value field need to know that if T is a non-nullable reference type, they might still be getting a null out.

You're confused about the meaning of T? when T is unconstrained however. T? means that the value is "defaultable", not "nullable". That's a subtle difference, but means that you can assign a value of default(T). Another way of putting that is:

If T is a reference type, T? means that you can assign null. If T is a value type, the ? in T? effectively has no meaning.

In other words, DataNode<string>.Value is of type string?, but DataNode<bool>.Value is of type bool, not bool?.

(Technically, there's no way to have T?, when T is unconstrained and is a value type, mean Nullable<T>. For nullable value types, the compiler outputs a member of type Nullable<T> rather than T. However, generics are expanded by the runtime rather than the compiler.)


Note that this changes if T is constrained to be a value type: in that case, T? suddenly starts meaning Nullable<T>.

canton7
  • 37,633
  • 3
  • 64
  • 77