简体   繁体   中英

Can you define a generic that takes *any* nullable type, value or reference?

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 Int and a class called 'Foo' as an example:

public class LabeledInt : LabeledValue<Int>{}

var myLabeledIntA = new LabeledInt(){
    label = "Forty Four",
    value = 44
}

var myLabeledIntB = new LabeledInt(){
    label = "Not Set",
    value = null
}

public class LabeledFoo : LabeledValue<Foo>{}

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

var myLabeledFooB = new LabeledFoo(){
    label = "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 (ie Int?) and nullable reference types (ie 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?>{}

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM