Your two lower examples are almost equal. But the second block
_odbcConnection?.Close();
_odbcConnection?.Dispose();
_odbcConnection = null;
will be translated by the compiler to something like
var tmp1 = _odbcConnection;
if (tmp1 != null) tmp1.Close();
var tmp2 = _odbcConnection;
if (tmp2 != null) tmp2.Dispose();
_odbcConnection = null;
This means that this version is thread-safe, while the first (with the outer if
clause) is not. If some mysterious thread would set _odbcConnection
to null
after the if
but before Close()
or Dispose()
, a NullReferenceException
would be thrown.
By using the null-conditional-operator you avoid this problem, because the reference is first stored in a compiler generated variable and then checked and used.
The above translation only applies to fields and properties. For local variables (only in scope of a single method, e.g. method parameters), this translation is not necessary and the code ends up like
if (_odbcConnection != null) _odbcConnection.Dispose();
That is because local variables cannot be changed by different threads.
And of course this is only the generated C#. In IL you may not see this anymore as it is either optimized away or obsolete, because in IL the reference value is loaded into a register and then compared. Again, another thread can no longer change that value in the register. So on IL level this discussion is somewhat pointless.