C# 8.0 nullable references: prepare today by not misusing 'as'
If you're not ready to enable nullable references in your C# projects today, you can still develop habits that will make adoption easier if you eventually decide to tackle it. In this post I'll look at one such technique: avoiding a popular misuse of C#'s as
operator.
In fact, getting this right is a good habit in itself, because this widespread misuse causes problems even if you never step into the world of nullable references.
The as operator
The as
operator is one of several mechanisms C# offers for when you have reason to believe that a reference refers to something more specialized than its static type. Take this example:
var o = JToken.Parse(text) as JObject;
bool hasItem = o.ContainsKey("item");
This uses the Json.NET library's JToken.Parse
method, which has a return type of JToken
. That is an abstract class, so in practice it must return some type derived from JToken
. The exact type you get depends on the input. If you pass an argument of "null"
, "42"
, or "\"Hello, world\""
, for example, JToken.Parse
returns a JValue
. If you pass "[]"
, "[1, 2, 3]"
, or "[1, "two", {"item":3}]"
, you'll get a JArray
. And if you pass JSON representing an object, such as "{\"item\":3}"
, you'll get a JObject
.
The as
operator is designed for when you don't know for certain what type to expect: if we write someExpression as JObject
, we are asking C# to test whether someExpression
refers to a JObject
. If it does, we get a suitably-typed reference back, but we get null
otherwise. So it is really meant to be used in conjunction with some sort of test for null
, to ensure that you only attempt to use the result when the conversion succeeds, as in these examples:
var o = JToken.Parse(text) as JObject;
if (o != null)
{
Console.WriteLine("item is " + (o.ContainsKey("item") ? "present" : "absent"));
}
bool hasItem = o != null ? o.ContainsKey("item") : false;
bool? itemPresence = o?.ContainsKey("item");
Since the introduction of pattern matching in C# 7, it rarely makes sense to use as
at all, because you can now use is
to combine the test with the conversion:
if (JToken.Parse(text) is JObject o)
{
bool hasItem = o.ContainsKey("item");
Console.WriteLine(hasItem);
}
What's wrong with not testing for null?
But why am I saying that as
needs to be combined with a test? If you are absolutely certain that the conversion is going to succeed—maybe in this JSON case you happen to know that the string will invariably represent an object, and so JToken.Parse
will always return a JObject
—what's wrong with the initial example? After all, it's widely used, and appears to work.
I'll start with a philosophical objection, and then move onto practical ones.
A significant problem with the first example is that it doesn't state our intent clearly. The as
operator is expressly designed for situations where the input expression might refer to an object that is not of the target type. If that's not your scenario, there's a better way: C# provides another mechanism intended for use when you expect the conversion always to succeed: a cast.
var o = (JObject)JToken.Parse(text);
bool hasItem = o.ContainsKey("item");
I believe that we should strive for our code to express our intents and expectations as clearly and precisely as possible. If we expect JToken.Parse
to return a JObject
we should use the language mechanism that says exactly that: a cast. Using as
here is a less good fit for what we want the code to say.
But what about practical consequences? The obvious one is that if our expectation turns out to be incorrect, we get a less useful exception when we use as
than we do if we use a cast. With a cast we've made it clear that we expect the result to be a JObject
, and if it turns out not to be, we'll get an exception that makes the nature of our mistake plain. The cast will throw an InvalidCastException
to let us know that the cast failed, and the exception's Message
will contain the following text (or a localised equivalent):
Unable to cast object of type 'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'.
That makes it pretty clear what the mistake is. We tried to cast something to JObject
, but a JValue
is not a JObject
. The presumption embodied in our cast turned out to be wrong.
Compare that with what happens if we're wrong in exactly the same way when relying on as
—how does the first example fail? The as
doesn't throw an exception, because by definition it doesn't treat a conversion failure as an error—it just produces null
. So the error we actually see is a NullReferenceException
when we first try to use the result of the as
operator. So the exception won't even be thrown from the line that made the mistake—it will occur slightly later—and its message gives us no useful clues as to the nature of the problem:
Object reference not set to an instance of an object.
With the nullable reference types feature added in C# 8, there's an additional practical problem with abusing the as
operator in this way.
Untested use of as, and nullability
In a project where you have enabled C# 8's nullable references feature, suppose you try to use the first snippet of code shown in this post, repeated here for convenience:
var o = JToken.Parse(text) as JObject;
bool hasItem = o.ContainsKey("item");
You will get a warning on the 2nd line of code:
warning CS8602: Dereference of a possibly null reference.
This is a direct result of this misuse of as
. By using as
, we've told the compiler quite clearly that we think the conversion to JObject
might fail. (That might not have been what the developer meant, but it is what the code says.) The compiler's nullability analysis correctly concludes from this that by the time we reach the second line, o
may well be null. (After all, if we were confident that it was definitely going to be a JObject
, we could have said so by using a cast.) So the compiler warns us that we've got a potential error here—the attempt to use o
might fail because it might be null.
If we use any of the other snippets shown above to test that we really do have a JObject
before using it, that warning goes away. And if we don't want to write any such test because we're confident that we'll definitely have a JObject
here, we can avoid this warning by using a cast instead of as
.
Stop (mis)using as today
Even if you've got no plans to start using nullable references, you should not be using as
in cases where a cast better expresses your intent and expectation. (In fact there's rarely any reason to use as
at all—patterns now generally provide a more succinct alternative.) And if you ever do enable nullable references for your code, this will be one less class of warnings needing attention.