2

Consider this class

public sealed record IdValuePair<TId, TValue>
{
    public IdValuePair(TId id, TValue value)
    {
        EnsureArg.HasValue(value, nameof(value));

        Id = id;
        Value = value;
    }

    public IdValuePair(TValue value)
    {
        EnsureArg.HasValue(value, nameof(value));

        Id = default!;
        Value = value;
    }

    public TId Id { get; }
    public TValue Value { get; }
}

Why is it that if I add the generic constraint

where TId : notnull

I am unable to do this assignment, due to the below error:

Id = default;

CS8601 Possible null reference assignment.

Doesn't the notnull constraint ensure that TId is not null?

Bassie
  • 9,529
  • 8
  • 68
  • 159
  • 3
    `where TId : notnull` promises that any properties of type `TId` will not be null. However, you're setting `Id` (which is of type `TId`) to `null`. So you're getting a warning, because you're violating the `notnull` constraint – canton7 Mar 10 '22 at 11:19
  • Also, if TId is (at runtime) a reference type (as an example) - the default value is null https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/default-values – Andrew Corrigan Mar 10 '22 at 11:22
  • @canton7 but I get the error even without `notnull` constraint - if I exclude the `!` when setting to `default` – Bassie Mar 10 '22 at 11:24
  • @Bassie Because someone could create e.g. an `IdValuePair`, where `TId` is a non-nullable string. So assigning `null` to it results in a warning. If you want the `Id` property to be nullable even if `TId` is a non-nullable type, then declare it as `public TId? Id { get; }` – canton7 Mar 10 '22 at 11:25
  • @canton7 I want to ensure it is not null, so I have just left it as `= default!;`, but just seemed weird to me that I had to do it that way. Other option was to constrain with `: struct` – Bassie Mar 10 '22 at 11:26
  • @Bassie What do you mean, ensure that it is not null? You explicitly set it to null yourself! – canton7 Mar 10 '22 at 11:27
  • @canton7 well I'm setting it to `default`, which is not always null in fact it never will be in my use cases where it will almost always be `int` or `Guid` (so `0` or `00000-0000...`) – Bassie Mar 10 '22 at 11:30
  • @Bassie But what if someone creates an `IdValuePair`, where the Id is a `string`? They're allowed to do that, and you're seeing the `Id` property to `null` in that case. – canton7 Mar 10 '22 at 11:30
  • @canton7 ye, its a problem, which is why I wanted to add `notnull` constraint – Bassie Mar 10 '22 at 11:31
  • That's not what the `notnull` constraint does. And even so, you're missing the point: constraining `Id` to be a non-nullable *type* (such as `string`, rather than `string?`), but still assigning `null` to it in your constructor, still gives your problem. I think either: 1) constrain `TId` to be a value type, then it can't be null, or 2) make `Id` a `TId?` -- that means, if `TId` is a reference type then it can be null; if it's a value type the `?` has no effect – canton7 Mar 10 '22 at 11:33
  • Thanks canton, I will probably just include a `EnsureArg.HasValue(id, nameof(id));` in my top ctor – Bassie Mar 10 '22 at 11:35
  • @Bassie That still doesn't help, since your other constructor will create `IdValuePair` instances with a null `Id` property, and yet the signature of your `Id` promises that this can't happen – canton7 Mar 10 '22 at 11:42
  • @canton7 as long as the value is considered as `default` then it is fine for my case - whether that `default` is `null` or `0` or whatever – Bassie Mar 10 '22 at 11:45
  • @Bassie If your `Id` property can hold `default(TId)`, then it should be declared as `TId? Id { get; }`. That's what the question mark means (when applied to an unconstrained generic type). `TId?` means "can contain `default(TId)`" – canton7 Mar 10 '22 at 11:45

1 Answers1

2

Consider IdValuePair<string, int>, the default value for string is null. So assigning null to the Id would clearly be illegal, since you promised that it should not be null.

Adding the constraint where TId : notnull constraint would only prevent declarations like IdValuePair<string?, int>. So we would go from a "Possible null reference assignment" to a "Definite null reference assignment".

There is, as far as I know, no generic constraint that restricts a type to either a nullable reference or nullable struct. And this can cause problems in some generic code.

There are a few possible options:

  1. use a struct-constraint. Value types can never be null, so assigning default should be perfectly safe.
  2. Use a new-constraint, and create a new object rather than using default
  3. Create your own Maybe<T> / Option<T> / Nullable<T> type.
JonasH
  • 28,608
  • 2
  • 10
  • 23