Updated on

x != y compares two values and runs any != overload the type defines. x is not ... matches a pattern instead: the right side must be a constant, a type or another pattern, and our type’s overloads never run.

For an int compared with a constant, both compile to the same IL. They part ways on null checks, overloads, boxing, inheritance and NaN, and each case below comes with code we built and ran on .NET 10.

We use is not null for null checks, and != whenever we compare two values held in variables, which a pattern can’t do. != also wins when the overload is the whole point, as with Unity’s UnityEngine.Object.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

What Does the != Operator Do in C#?

The != operator is the inequality operator. It has been in C# since version 1.0, and it returns true when the value on its left is different from the value on its right.

We write it as x != y. Both sides can be variables or whole expressions, as long as their types match or the compiler can convert one side to the other. 1001 != 1000L compiles, while 1001 != "1000" does not.

What “different” means depends on the type. Numbers, bool, char and enums compare their values, and strings compare their characters. Other classes compare references unless they overload the operator, so two separate objects with the same properties still count as different.

Any class or struct can overload != and decide for itself. For built-in types, x != y gives the same result as !(x == y). With an overload, != and == are two separate methods, and nothing forces them to agree.

Now, let’s prepare a few classes that we can use to see the inequality operator in action.

We are going to start by defining the Vehicle class, having two properties SerialNumber and VehicleBrand:

public class Vehicle
{
    public Brand VehicleBrand { get; set; }
    public int SerialNumber { get; set; }
}

Since we need the Brand enumeration, let’s add it as well:

public enum Brand 
{
    Ford = 1, 
    Toyota = 2 
}

To continue, we are going to create an instance of Vehicle in the Program class:

var vehicle = new Vehicle()
{
    SerialNumber = 1001,
    VehicleBrand = Brand.Toyota
};

Finally, let’s define the Car class, derived from Vehicle:

public class Car : Vehicle
{
    public string? Model { get; set; }
}

To show  a simple use case for != operator, we can create the InequalityVsIsNotComparer class with the SerialNumberComparerUsingNotEqual method :

public class InequalityVsIsNotComparer
{
    public const int _constantSerialNumber = 1000;

    public void SerialNumberComparerUsingNotEqual(Vehicle vehicle)
    {
        if (vehicle.SerialNumber != _constantSerialNumber)
        {
            Console.WriteLine("serial number != 1000");
        }
    }
}

This method takes a Vehicle as input and it compares SerialNumber property with a constant using the != operator. The two numbers are different, so invoking the method in our Program.cs will print our message.

What Does is not Do in C#?

is not is the is operator followed by the not pattern, which arrived in C# 9. It checks a value against a pattern and returns true when the value doesn’t match.

Its syntax has the form <expression> is not <pattern>. The pattern can be a constant such as 1000 or null, a type such as Car, a range such as (>= 1000 and <= 1999), or a combination of them.

When the pattern is a value, the right term must be a constant. A variable on the right doesn’t compile, and that’s the first difference from !=.

How the match runs is up to the compiler. Numbers and enums compare their values, strings compare their characters, and null is a plain reference check. An == or != that our own type overloads is never called.

That’s why is not null is the null check no class can change, and why it’s the form we reach for whenever a value might be null.

Let’s use the same Vehicle example to show how it works. This method, like the ones after it, goes into our InequalityVsIsNotComparer class:

public void SerialNumberComparerUsingIsNot(Vehicle vehicle)
{
    if (vehicle.SerialNumber is not _constantSerialNumber)
    {
        Console.WriteLine("serial number is not 1000");
    }
}

In this method, we are using the same vehicle instance. So invoking this method in our Program.cs will print:

serial number is not 1000

Where Do != and is not Give Different Results?

Now, let’s try to explain the differences between these two operators. We can notice that, while inequality has its own != operator, is not combines the is operator with the not pattern.

For a string, that makes is not closer to !(x == y) than to x != y, and the compiled code shows it. car.Model is not "Focus" calls string‘s == and negates the result, while car.Model != "Focus" calls string‘s !=. For an int compared with a constant, both compile to the same instructions.

Let’s show some situations where differences between != and is not arise.

Constant Expressions

We have already seen that for the inequality operator we can have any expression both on the left and right sides. But when we use is not, the right-side expression needs to be constant. This means that we will have a compilation error if we try to write:

public void SerialNumberComparerUsingIsNot(Vehicle vehicle)
{
    var serialNumber = 10;
    if (vehicle.SerialNumber is not serialNumber) // error CS9135: A constant value of type 'int' is expected
    {
        Console.WriteLine("serial number is not 10");
    }
}

This is because serialNumber is not a constant.

We have to pay attention that a “constant expression” is not a “literal expression”. In fact:

public void BrandComparerUsingIsNot(Vehicle vehicle)
{
    if (vehicle.VehicleBrand is not Brand.Ford)
    {
        Console.WriteLine("brand is not Ford");
    }
}

public void ModelComparerUsingIsNot(Car car)
{
    if (car.Model is not "Focus")
    {
        Console.WriteLine("model is not Focus");
    }
}

public void AnotherModelComparerUsingIsNot(Car car)
{
    const string a = "Fo";
    const string b = "cus";

    if (car.Model is not $"{a}{b}") 
    { 
        Console.WriteLine("model is not Focus"); 
    } 
}

For all these methods we are using is not correctly. The first method checks two enums, the second one checks two strings, and the third one compares car.Model with a combination of two constant strings.

Impossible Comparisons

!= and is not have different ways to manage impossible comparisons:

public void ImpossibleComparer(Vehicle vehicle)
{
    if (vehicle.SerialNumber != "test string") //Error CS0019 Operator '!=' cannot be applied to operands of type 'int' and 'string'
    {

    }
    if (vehicle.SerialNumber is not "test string") //Error CS0029 Cannot implicitly convert type 'string' to 'int'
    {

    }
}

The SerialNumber is of type int, and this causes two different compilation errors. We can see that != needs an operator that accepts both types. Built-in types work when one side converts to the other, so an int and a long are fine, and developers may declare operator overloads that compare different types. Whether one should or not is another matter.

On the other hand, is not needs the constant on the right to convert to the type on the left. That’s why vehicle.SerialNumber is not 1000L fails with CS0266 (a long constant doesn’t convert to int), while a long variable compared with is not 5 compiles fine.

Comparison with Boxing

In this section, we are going to see how these two operators behave with boxing.

Let’s start by using the != operator:

public void BoxedSerialNumberComparerUsingNotEqual(Vehicle vehicle)
{
    object boxedSerialNumber = vehicle.SerialNumber;
    if (boxedSerialNumber != _constantSerialNumber) // error CS0019: Operator '!=' cannot be applied to operands of type 'object' and 'int'
    {
        Console.WriteLine("boxed serial number != 1000");
    }
}

We have a compilation error since we are comparing an object and an int. We can make it compile by casting the constant to object, but don’t copy this one, because the cast changes what != compares:

public void BoxedSerialNumberComparerUsingNotEqual(Vehicle vehicle)
{
     object boxedSerialNumber = vehicle.SerialNumber;
     if (boxedSerialNumber != (object)_constantSerialNumber)
     {
         Console.WriteLine("boxed serial number != 1000");
     }
}

Now both sides are references to two separate boxes, and != on two objects compares references. Two boxes are never the same object, so the condition is always true. With a serial number of 1000, the method still prints:

boxed serial number != 1000

This doesn’t happen when we deal with is not:

public void BoxedSerialNumberComparerUsingIsNot(Vehicle vehicle)
{
    object boxedSerialNumber = vehicle.SerialNumber;
    if (boxedSerialNumber is not _constantSerialNumber)
    {
        Console.WriteLine("boxed serial number is not 1000");
    }
}

Here, the compiler checks that the object holds an int, unboxes it and compares the value, so a boxed 1000 matches and nothing is printed. The type has to match too: a boxed 1000L is a long, so it doesn’t match the int constant 1000.

NaN Comparisons

Floating-point NaN is the one value where the two give different answers on a plain number. NaN isn’t equal to anything, itself included, so != returns true. The constant pattern double.NaN does match NaN, so is not gives the answer we wanted:

public void MeasurementComparer(double measurement)
{
    Console.WriteLine(measurement != double.NaN);
    Console.WriteLine(measurement is not double.NaN);
}

Invoking it with double.NaN prints:

True
False

So measurement != double.NaN is a bug that compiles without a warning. To test for NaN, we use double.IsNaN(measurement) or the pattern.

Inheritance Management

As we know from the inheritance paradigm, a Vehicle is not a Car, while a Car is a Vehicle. That said, if we compare these two types:

public void CarTypeComparerUsingNotEqual(Car car)
{
    if (car.GetType() != typeof(Vehicle))
    {
        Console.WriteLine("car != Vehicle"); 
    }
}

We are going to get an expected result:

car != Vehicle

We expected this result since the types are effectively different.

Now, let’s see what is going to happen if we create a comparison using the is not operator:

public void CarTypeComparerUsingIsNot(Car car)
{
    if (car is not Vehicle)
    {
        Console.WriteLine("car is not Vehicle");
    }
    else
    {
        Console.WriteLine("car is Vehicle");
    }
}

In this case, is not operator takes into consideration the inheritance between classes, and we get a different output:

car is Vehicle

That’s the gap between comparing exact types and type checking: GetType() returns the exact run-time type, while is not asks whether the object can be treated as a Vehicle.

One more case to remember: when car is null, car is not Vehicle returns true, because null matches no type pattern. car.GetType() on a null reference throws a NullReferenceException instead.

Operator Overload

Another important difference concerns the possibility to overload operators == and !=. In fact, we know that in C# we can provide a different implementation of these operators in order to use them. Since we can implement that custom implementation, it can happen that the result is different than we expect. This is something we can’t do with the is not operator.

For example, in our class Vehicle, we can add:

public static bool operator ==(Vehicle? vehicle, Vehicle? otherVehicle)
{
    if (otherVehicle is not null)
    {
        return otherVehicle.VehicleBrand == vehicle?.VehicleBrand;
    }

    return false;
}

public static bool operator !=(Vehicle? vehicle, Vehicle? otherVehicle)
{
    if (otherVehicle is not null)
    {
        return otherVehicle.VehicleBrand != vehicle?.VehicleBrand;
    }

    return false;
}

The overload of == operator returns true only if the compared vehicles have the same brand, while the overload of != operator returns true only if they have a different brand. In all the other cases they return false.

For the sake of simplicity, we didn’t override Equals and GetHashCode, but for the full operator overload implementation, you can read Operator Overloading in C# article.

To check the behavior, we can create two objects, or only the second one if our vehicle from earlier is already in Program.cs:

var vehicle = new Vehicle()
{
     SerialNumber = 1001,
     VehicleBrand = Brand.Toyota
};

var otherVehicle = new Vehicle()
{
    SerialNumber = 1002,
    VehicleBrand = Brand.Toyota
};

Since they have the same Brand property, we can compare these objects with !=:

Console.WriteLine(vehicle != otherVehicle);

And we get:

False

The same happens even if they are effectively different.

Equally, if we compare to null:

Console.WriteLine(vehicle != null);

We will get false as output, since the overload of != checks the VehicleBrand property, only if the right-hand element is not null. 

In this case, we’ve modified the standard behavior of the != operator, and it no longer even agrees with ==. Our == overload also returns false when the right side is null, so !(vehicle == null) says the opposite of vehicle != null. is not null ignores both overloads:

Console.WriteLine(vehicle != null);
Console.WriteLine(!(vehicle == null));
Console.WriteLine(vehicle is not null);

As you can see, only the first line gets it wrong:

False
True
True

Should We Use is not null or != null?

For a null check, we prefer is not null. It compiles to a plain reference comparison with null, and no class can change what it means.

!= null calls the type’s != operator when the type has one. On our Vehicle, that overload returns false for vehicle != null, so a valid object looks like null. On a type with no overload, the compiler emits the same instructions for both, and the choice is style.

is { } and is not { } belong here too. The empty property pattern { } matches any value that isn’t null, so vehicle is { } means “not null” and vehicle is not { } means “is null”. The double negative trips people up, so we stick to is null and is not null.

!= null wins when the overload is the point. Unity’s UnityEngine.Object overloads == to also check the native object behind it, so in Unity code != null is the check we want.

We can see the difference in what the compiler emits for the two checks on our Vehicle:

// vehicle != null
ldarg.0
ldnull
call Vehicle::op_Inequality
ret

// vehicle is not null
ldarg.0
ldnull
cgt.un
ret

Release build, .NET 10

The first loads null and calls our overload. The second compares the reference with null directly and never touches it. The Microsoft Learn page on the is operator says that when we match against null, “the compiler guarantees that no user-overloaded == or != operator is invoked.”

The positive form works the same way, and we compare those two in our article on is null versus == null.

How Do We Write NOT IN With the not Pattern?

C# has no NOT IN keyword. For a fixed set of constants, the not pattern with or does the same job:

public void SerialNumberNotInUsingIsNot(Vehicle vehicle)
{
    if (vehicle.SerialNumber is not (1000 or 2000 or 3000))
    {
        Console.WriteLine("serial number is not 1000, 2000 or 3000");
    }
}

Our vehicle’s serial number is 1001, so this prints the message. A vehicle with 2000 prints nothing.

Don’t drop the parentheses, though. Without them, not applies only to the first constant, so this reads as (not 1000) or 2000 or 3000, and a serial number of 2000 passes the check:

if (vehicle.SerialNumber is not 1000 or 2000 or 3000) // warning CS9336: The pattern is redundant.

The compiler does warn us, and that warning is worth treating as an error.

When the values live in a collection or only arrive at run time, a pattern can’t help, because patterns need constants. There we fall back on the ! operator, C#’s logical NOT, in front of a method call:

public void SerialNumberNotInListUsingNot(Vehicle vehicle, int[] recalledSerialNumbers)
{
    if (!recalledSerialNumbers.Contains(vehicle.SerialNumber))
    {
        Console.WriteLine("serial number is not recalled");
    }
}

! negates any bool expression, while not only exists inside a pattern, after is, in a switch expression arm or in a case label. Patterns can do a lot more than this, and our pattern matching article covers the rest.

Conclusions

Here are the differences side by side:

Questionx != yx is not ...
What can go on the right?Any expression of a compatible type, variables includedA constant, a type, a range or a combination, never a variable
Does it call an operator our type overloads?YesNo
Null check on our Vehiclevehicle != null returns False (the overload decides)vehicle is not null returns True
Boxed 1000 against 1000Needs a cast, then compares two references: "different"Unboxes and compares values: "same"
Car against Vehiclecar.GetType() != typeof(Vehicle) is true (exact type)car is not Vehicle is false (inheritance counts)
double.NaN against NaNtruefalse
int against an int constantSame IL as is notSame IL as !=
Available sinceC# 1.0C# 9

In this article, we’ve learned about the differences between != and is not operators. != compares two values of compatible types and runs whatever overload the type defines. is not matches a pattern, never takes a variable on the right, follows inheritance, unboxes values, gets NaN right and never calls our overloads.

For null checks, that makes is not null our default. For comparing two variables, != is still the only one of the two that can do it.

Tested with .NET 10.0.10 and xUnit 2.9.3.