9

Playing with the new nullable reference types in C#. Glad to see they poached this from Swift! It's such a great feature! BUT... since it's essentially 'bolted-on' to the language, I'm struggling to create a generic that can take any nullable type whether value or reference, which is trivial in Swift.

Consider this class:

public abstract class LabeledValue<TValue> {
    public string  label { get; set; }
    public TValue? value { get; set; }
}

Here's what I'm trying to achieve, using the types Int (value-type) and Foo (reference type) as examples:

public class LabeledInt : LabeledValue<Int>{}

var myLabeledIntA = new LabeledInt(){
    label = "Int is set",
    value = 44
}

var myLabeledIntB = new LabeledInt(){
    label = "Int is not set",
    value = null
}

public class LabeledFoo : LabeledValue<Foo>{}

var myLabeledFooA = new LabeledFoo(){
    label = "Foo is set",
    value = new Foo()
}

var myLabeledFooB = new LabeledFoo(){
    label = "Foo is not set",
    value = null
}

This complains that I have to define TValue as nullable. However I can't find a constraint that solves both nullable value types (i.e. Int?) and nullable reference types (i.e. Foo?). How would one write such a constraint?

These don't work...

public abstract class LabeledValue<TValue>
where TValue : Nullable {
    public string  label { get; set; }
    public TValue? value { get; set; }
}

public abstract class LabeledValue<TValue>
where TValue : struct {
    public string  label { get; set; }
    public TValue? value { get; set; }
}

public abstract class LabeledValue<TValue> {
    public string           label { get; set; }
    public Nullable<TValue> value { get; set; }
}

Note, I also tried this thinking the nullability could just be passed in as the actual type parameter, but then it complains that 'value' isn't set.

public abstract class LabeledValue<TValue> {
    public string label { get; set; }
    public TValue value { get; set; }
}

public class LabeledInt : LabeledValue<Int?>{}
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • 1
    Have you tried to search for existing answers here? `Nullable` is struct and value type, nullable references is for reference types obviously. You should use either `class` or `struct` generic constraint, like it's explained it `issue with T?` section in this [article](https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types/) – Pavel Anikhouski Jan 14 '20 at 18:46
  • If you look above, I tried that (last answer in 2nd to last code block.) And yes, I have searched here. When I didn't find anything that answered this, that's why I posted this question. If you know of a solution, please feel free to post it as an answer here and if it works, I'll mark it as such. – Mark A. Donohoe Jan 14 '20 at 18:56
  • 1
    You can have a look at this [thread](https://stackoverflow.com/questions/55975211/nullable-reference-types-how-to-specify-t-type-without-constraining-to-class) at least – Pavel Anikhouski Jan 14 '20 at 18:59
  • `TValue?` when `TValue: class` and when `TValue: struct` are completely different things from CLR point of view, nullable reference type are annotated using attributes, nullable value types is `Nullable` type. You should consider for yourself, what you want in this case. Or decorate the type with pre and post condition attributes – Pavel Anikhouski Jan 14 '20 at 19:19
  • Yeah, I had just posted an answer around that. Shame, but I understand since they were tacked on to C# whereas they were a fundamental part of Swift from the beginning. In Swift, regardless of if T is a class or a struct, the nullable variant is the concrete `Optional` so they're the same type, hence it's allowed. – Mark A. Donohoe Jan 14 '20 at 19:25

2 Answers2

7

Ok, found it. You have to use two new explicit attributes, AllowNull and MaybeNull.

Here's the revised code...

public abstract class LabeledValue<TValue> {

    public string? label { get; set; }

    [AllowNull, MaybeNull]
    public TValue value { get; set; }
}

With that change, I can now do all of the following...

public class LabeledInt  : LabeledValue<int>{}
public class LabeledNInt : LabeledValue<int?>{}
public class LabeledFoo  : LabeledValue<Foo>{}
public class LabeledNFoo : LabeledValue<Foo?>{}

And use them like this...

var a = new LabeledInt();
a.Value = 4;
a.value = null // This won't compile

var b = new LabeledNInt();
b.Value = 4;
b.Value = null; // This compiles just fine

var c = new LabeledFoo();
c.Value = new Foo();
c.Value = null; // This won't compile

var d = new LabeledNFoo();
d.Value = new Foo();
d.Value = null; // This compiles just fine

Note: There is still a warning about Value being uninitialized, but it's only a warning, not an error. You have to make sure to explicitly set Value for non-null types before accessing it. Kind of defeats the purpose of using nullable/non-nullable types, but this is more a hack than a true solution which isn't actually possible since nullable value types are really the concrete Nullable<T> whereas nullable reference types are regular reference types just adorned with an attribute to let the compiler know not to accept nulls.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Any ideas how to workaround cases when it won't compile? I have an `interface IHierarchicalEntity { T Id { get; } [CanBeNull] T ParentId { get; } }` but I can't find a way to instantiate any class with `ParentId = null` when `T` is `int`. It seems that duplicating definitions for structs and classes will introduce a hell, because it is relatively base interface for other types. – Artem Balianytsia May 03 '22 at 21:06
  • 1
    @ArtemBalianytsia There is no attribute named `CanBeNull`: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis?view=net-6.0 – Dai Aug 08 '22 at 11:33
0

Just adding another way to handle this. You basically write two versions of whatever you're trying to do... one for the reference-based version, one for the struct-based version.

For instance, here's a map command for C# that emulates a function I use regularly in Swift. Since it relies on two generic types that both have to take nulls, I have to create four 'versions' of the function.

// Class-Class version
public static U? Map<T,U>(this T? item, Func<T, U?> formatClosure)
where T : class
where U : class
    => (item != null)
        ? formatClosure(item)
        : null;

// Struct-Struct version
public static U? Map<T,U>(this T? item, Func<T, U?> formatClosure)
where T : struct
where U : struct
    => item.HasValue
        ? formatClosure(item.Value)
        : null;

// Class-Struct version
public static U? Map<T,U>(this T? item, Func<T, U?> formatClosure)
where T : class
where U : struct
    => (item != null)
        ? formatClosure(item)
        : null;

// Struct-Class version
public static U? Map<T,U>(this T? item, Func<T, U?> formatClosure)
where T : struct
where U : class
    => item.HasValue
        ? formatClosure(item.Value)
        : null;

Yes, it's verbose, but again, it's needed because a nullable reference type is not the same as a nullable value type. This makes the language transparent since it handles all variants.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286