How to detect whether a type can be nullable at ru

2020-06-23 07:27发布

问题:

I'm trying to detect whether a type can be nullable or not at runtime to convert that type to the corresponding GraphQL type so that, for example:

With nullable reference types enabled:

  • string is converted to String!
  • string? is converted to String

With nullable reference types disabled:

  • string is converted to String
  • NonNull<string> is converted to String! (NonNull is a custom library type)

I'm having trouble adapting the code that detected the nullability of a type:

bool isNullable = !typeInfo.IsValueType;

How can I change it so that it works with nullable reference types both enabled and disabled?

回答1:

Note that there are good ways to check for "the old" nullable types that applies to value types that is documented thoroughly here on Stack Overflow.

I will then instead only focus on nullable reference types, and provide the means to check if one of those are in effect.

Let me sum up my comments on the question first as they are rather important.

Contrary to the name of the new feature, nullable reference types is not about the types, but rather about the things these types are used for. These things are:

  • Fields
  • Properties
  • Method return values
  • Method parameters

Now, of course this also applies to local variables but you need a whole 'nother kind of introspection to deal with decoding instructions. I do not know how, or even if, this kind of information is encoded in the actual instructions for local variables.

OK, so with that out of the way, let's look at some code (btw, I'm using LINQPad with Roslyn experimental mode to test all this):

public string? Nullable;
public string NonNullable;

These are two public fields. Ignore whether this is a good idea or not. How would you check the type of these fields and detect the presence or lack of this question mark?

Well, let's try the simple route:

Type nullable = GetType().GetField("Nullable").FieldType;
Type nonNullable = GetType().GetField("NonNullable").FieldType;
Console.WriteLine(ReferenceEquals(nullable, nonNullable));

Running this gives me:

True

So clearly this doesn't work. The Type objects are the exact same instance. They don't just compare equal, I got the same thing back, no difference. Basically, FieldType is oblivious to the presence or lack of this question mark.

My comments up above has some of those details but the main reason for this is at all existing nuget packages and compiled code will still work with this new support because of this. There is no need for any code to be rewritten to handle something like NullableReferenceType<T> suddenly. This is a good thing, but also means that you will still be passing null-references around and getting them back from existing nuget packages.

OK, so then, how would we detect this? The answer is that the information about the nullability is not attached to the type, as I mentioned above, but rather to the thing that has the type, in this case the fields.

Let's show attributes on these fields (again I'm using LINQPad):

GetType().GetField("Nullable").GetCustomAttributes().Dump();
GetType().GetField("NonNullable").GetCustomAttributes().Dump();

This gives this output:

As you can see here, the Nullable field has an additional attribute, NullableAttribute. I must confess that I do not know what that other attribute is about, I will have to investigate more.

This NullableAttribute attribute is much more complex than this simple example shows, as it has a collection property with bool values. Let's look at a slightly more complex example:

public List<string>? Nullable1;
public List<string?>? Nullable2;

Here, both fields are nullable references to a list, the difference is that I've said that one of the lists contains nullable references to strings, the other doesn't.

Here's some reflection to look at these collections:

GetType().GetField("Nullable1").GetCustomAttributesData().Dump();
GetType().GetField("Nullable2").GetCustomAttributesData().Dump();

and their output:

Here you can see that there is a difference on the second element in this collection (I've "circled" them with red ... rectangles ...), I expect the first element applies to the list, the second one to the first generic type parameter. If you have generic lists containing generic types, the number of parameters will increase accordingly.

You can also find more information on this excellent blog post by Rico Suter.