La principal diferencia entre e != null y is not null es la forma en que el compilador ejecuta la comparación.
El compilador garantiza que no se invoque ningún operador de igualdad == sobrecargado por el usuario cuando se evalúa la expresión x es nula.
Es decir si sobre escriben mal == estamos muertos, por eso esta bueno siempre usar "is not null" o "is null"
Veamos un ejemplo:
public class TestObject
{
public string Test { get; set; }
// attempt to allow TestObject to be testable against a string
public static bool operator ==(TestObject a, object b)
{
if(b == null)
return false;
if(b is string)
return a.Test == (string)b;
if(b is TestObject)
return a.Test == ((TestObject)b).Test;
return false;
}
public static bool operator !=(TestObject a, object b)
{
if(b == null)
return false;
if(b is string)
return a.Test != (string)b;
if(b is TestObject)
return a.Test != ((TestObject)b).Test;
return false;
}
}
Como vemos la sobrecarga de == y != cuando b es nulo, retorna false y eso esta mal. Por lo tanto, si ejecutamos el siguiente código:
TestObject e = null;
if(e == null)
Console.WriteLine("e == null");
if(e is null)
Console.WriteLine("e is null");
El Output va ser : e is null
Y si hacemos :
TestObject e = new TestObject();
if(e != null)
Console.WriteLine("e != null");
if(e is not null)
Console.WriteLine("e is not null");
El Output va ser: e is not null
Ninguno de los operadores sobrecargados se implementa "correctamente", por lo que la consola nunca genera e == null o e != null.