Stop Using == null: The Safer C# Pattern You Need Today!
C# inequality operators and `is not` pattern matching behave differently around constants, boxing, type checks, nulls, and overloaded operators.
Transcript 6 topics
Equality versus pattern matching
0:00These two may look the same, but not knowing the difference can cause you days of tracking down bugs. Let's demystify when to use each, why is not exists, and how using the wrong one can sneak bugs into your code, especially around boxing, null checking, and even operator overloads. The inequality operator has been around since C sharp one and is basically just syntactic sugar for this.
The not pattern
0:23Pattern matching hasn't been around as long and C sharp nine brought us the not pattern. That means is not is pattern matching, not using the old equality pipeline, which can cause some side effects that we'll check out later. But let's try out just basic equality and see how that works. Like if we had some code that said var left equals a 100, var right equals 42. And then we said if left does not equal right, then let's console dot right line.
They don't match yo. And then if we run that, we'll get they don't match yo, which is right. We compared their equality. Now with pattern matching, we have to use constants on the right side of the expression.
So if we change right over into a constant int, we can then say is not. And if we run that, we'll get they don't match you, but that's using pattern matching instead of that equality.
Constant patterns and enums
1:30But when I say a constant, I don't just mean literals. This could also be a compile time constant expression. For instance, let's look at some more code. If we were to create an enum and we'll call it viewer location, and we'll say YouTube equals one. We'll say TikTok equals two.
Enum, enum, how do you say that? Let me know in the comments. And then let's create a record, and we'll call it a viewer. And we'll say it takes in a string of name and then a viewer location that we'll call location. And then let's say we're gonna have a var user equals new viewer, and we'll say not viewer location, you weirdo.
What's the name? Joe commenter. There you go. Viewer location, viewer location dot YouTube. Perfect.
So then we've got that user. So if we were then to come through and say, if user dot viewer location is not viewer location dot TikTok console writeline, I'm not from TikTok. Right? Oh, it's not called viewer location there. It's just location.
Right? Remember what you name things. Pro tip. And if we run this, then we should get I'm not from TikTok. So that was still a constant expression that was there at compile time even though it wasn't a constant literal.
Boxing and null checks
3:18But there are subtle differences, especially around boxing because in the case of boxing is and is not pattern matching usually wins the day. And I'll show you why. What if we were to box viewers? Now, normally, if we're gonna compare that, let's have a constant int of max viewers equals one twenty three as well. And we can say if viewers does not equal max viewers, do something.
Compiler again is gonna throw up at us. So we could either unbox that object or cast them. There are ways around this, but with the magic of pattern matching, we don't have to. And this will actually compile and run. So if we dot net run it, there we go.
We didn't console log anything, but it executed. That's because the pattern matching says, well, let me see if the runtime value in that, I can do anything with it. And in this case, yeah, I can. So all's well. But one place that pattern matching really shines is with checking our types because unlike our operators that are checking equality, pattern matching can actually look at the pattern and understand inheritance.
So what happens if we go back to our code with our record viewer and we said, Hey, I want an interface that has, let's call it ILocation. And it's gonna have a viewer location called location, get setter on there. And then in our interface, let's say you implement ilocation. Makes sense. Now it's gonna tell me that it doesn't because that's an init, right?
So let me just admit that. There we go. Now I've got user. I can say if user dot get type equals type of viewer, then console writeline. I'm a viewer.
Perfect. But if we wrote the same thing and said if user is viewer, Why does it keep doing that? We can say console dot writeline pattern. I'm a viewer. Let's see what that does.
I would expect both of those to say they are a viewer. Works out great. But what if I said type of eye location? And then down here, if I said is an eye location, I'm an eye location, and I'll do the same here. We're gonna get what you would expect that I'll run the right command.
We're gonna get what you would expect. The pattern says, yeah. Yeah. I know that you can implement that iLocation. So I'm gonna say you are one, but that operator says, no, no, your runtime type is completely different than that.
You are not the same. So in this case, pattern matching is solid.
Operator overload pitfalls
6:48But one of the things that make operators really powerful is the fact that we can overload them. But with the great power comes great responsibility and that can get you into trouble. Let's show why overriding those overloads could cause us problems when we're checking. So if we've got this class subscriber, I've got the name and social on it. Now let's assume that if we see somebody with the same social, we want them to be equal.
So let's overload public static, bool operator equal equal. And we'll say, you're gonna take in a subscriber A and a subscriber B. And I want you to return, let's just say s, no, excuse me, a dot SSN equals b dot SSN. And then we'll say public static. Actually, we can just copy and paste this.
You have to have the inequality if you have the inequality and we'll just do that. Okay, great. So now it's gonna report whether quality is there based on that SSN. So if I said subscriber could be no, we'll call it subscriber, but it's not no, we're gonna new it up. And then we say subscriber other equals new as well.
They'll get the same social security number. What happens when we run this? Well, nothing because we didn't tell it to write anything out. But if we console dot write line, pro pro here, equals other. Let's record that cleared dot net run.
What happens? True. It says they're equal because their Social Security numbers social social security social security numbers match. Right? But what happens if we come in and said, you know what, that subscriber is no.
And tell me if subscriber does not equal no. Now, the way I've written this, it's giving me a warning here, but not stopping me from running. So if I clear and DNR, it blows up because we passed a null into that operator and we didn't expect null down here. But what if what if we were smart enough to think about that? What happens?
I'll show you what happens. You might say, Hey, well, know, we would see that warning and fix it, but we don't know all the logic within. It's just an example of you don't know what people are putting in their overloaded operators. So you can't necessarily trust them. So if I do that now and run that, .net run, what happens?
I get a false subscriber does not equal null. But subscriber does equal null. What the heck? It told me a lie. That's because I overrode those operators and didn't put in the correct logic to check for null so that it could come out.
Whereas if I were to say is null, that disregards equality checkers altogether, you can't overload that. And if I clear it and dot net run, I'm gonna get a true every time because even though I've overridden or overloaded those operators, it doesn't care. It's just doing a pattern match. So does this mean you shouldn't use operators at all? Not in the least.
They're very powerful as we mentioned, and they provide some opportunity for equality checking that we can't get with pattern matching. But if we're talking about null checking or working with boxed items, those pattern matching are gonna be better suited for that.
Choosing the safer check
10:39That said, when should you use equality operator overloads and when should you use pattern matching? It's a great question. If you are writing code that is gonna be reused by others and others might extend your classes or interfaces, then in that instance, I would suggest always using pattern matching when possible in your code base. That way, if anyone overrides or derives some class from one of your classes and overloads that operator, they can't get you in a sticky situation because of something they did. Now, if this code is gonna be like in your own code, reused by others, it's in your own API or backend, and you and your team have decided, no, we're not gonna overload operators.
There's no risk of that sort of thing. Then operators are a great way to go. But I think in the end, we're trying to say here is write code that says what you mean and can't be misinterpreted. Your future self and your on call team will thank you in the future. Until next time.
Sign in to join in. Reading needs nothing.