Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
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.

FAQs

What is the purpose of the as operator in C# and when should it be used? The as operator is designed for situations where you do not know for certain what type to expect. It tests whether an expression refers to an object of the target type, returning a suitably-typed reference if it does, or null otherwise. It is meant to be used in conjunction with a null test to ensure you only attempt to use the result when the conversion succeeds.
Why should you use a cast instead of as when you expect a conversion to always succeed? A cast expresses your intent and expectation more clearly. If the conversion fails, a cast throws an InvalidCastException with a descriptive message explaining exactly what went wrong. Using as without a null test produces a less useful NullReferenceException later when you try to use the null result, making debugging harder because the error occurs on a different line from the actual mistake.
What exception messages do you get when a type conversion fails with a cast versus as? A cast produces an InvalidCastException with a clear message like 'Unable to cast object of type JValue to type JObject', making the nature of the mistake plain. Using as produces a NullReferenceException with the unhelpful message 'Object reference not set to an instance of an object', giving no clues about what actually went wrong.
How does misusing the as operator affect C# 8.0 nullable reference type analysis? When you use as without testing for null, and you have nullable references enabled, the compiler generates warning CS8602 about dereferencing a possibly null reference. By using as, you have told the compiler the conversion might fail, so it correctly concludes the result may be null. Using a cast instead eliminates this warning because it expresses confidence that the conversion will succeed.
What is the modern alternative to using the as operator in C# 7 and later? Pattern matching with the is keyword provides a more succinct alternative that combines the type test with the conversion. For example, instead of using as followed by a null check, you can write 'if (expression is JObject o)' to test and convert in a single statement, making the code clearer and eliminating the possibility of forgetting the null test.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and 17 times Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 12.0, and has written Pluralsight courses on WPF fundamentals (WPF advanced topics WPF v4) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Ian has given over 20 talks while at endjin. Technology brings him joy.