This is a rather contrived tongue-in-cheek edge case, and super nerdy... but I have some code that does this:
Console.WriteLine(liar == true); // True
Console.WriteLine(liar != true); // True
Console.WriteLine(!liar); // False
WHY!?!
Partly because I'm a contrarian, but also to highlight something that we might take for granted in C#.
@leekelleher wrote a great article about his take on coding standards where he compares two different ways of doing the same thing, specifically this:
if(!string.IsNullOrWhiteSpace(input)) {
//...
}
// or
if(string.IsNullOrWhiteSpace(input) == false) {
//...
}
Lee points out that functionally (and even in IL) these two lines of code are exactly the same (and for string.IsNullOrWhiteSpace()
that will almost certainly always be the case), but they don't have to be the same...
Logically these lines of code are doing something different, which shouldn't be a surprise because they are different.
If we do away with the method being invoked, which returns a bool
, and the if
then we can focus a bit on the logic.
var x = !a; // x IS NOT a.
var x = a == false; // x IS (a EQUALS false).
That first line is a logical negation. The second line is an equality comparison against a (static) false
value. These are using different operators, and in C# operators are implemented explicitly and independently and can be overridden.
What do I mean by that? let's take a look at some code from my (very naughty) Liar struct.
Jason's Evil Liar Struct
BTW, don't do this 🙈.
public struct Liar(bool isTrue)
{
private readonly bool _true = isTrue;
public static bool operator ==(Liar left, bool right)
{
return left._true == right;
}
public static bool operator !=(Liar left, bool right)
{
return left._true == right; // 😈
}
}
Here I'm defining what should happen when someone compares my Liar struct to a bool
. You can, of course, spot the deliberate mistake.
The point is, !=
and ==
invoke different methods, i.e. !=
is not simply a logical negation of ==
. This means that a != b
is not doing the same as !(a == b)
and vice versa.
So what about the logical negation operator? You'd be forgiven for thinking that a
has to be a bool for !a
to work, but that's not the case. It could be anything that has the implicit operator for a bool defined (or the true/false operators). That's right JavaScript fans, you can make truthy/falsey types in C# 😬.
Here's the code for implicit operator that lets me use my Liar struct as if it's a bool:
public static implicit operator bool(Liar liar)
{
return liar._true;
}
Now we can do this:
Liar liar = new(true);
if (liar)
{
//
}
We can also add an implicit operator so that we don't even need to create an instance of Liar
, and can just assign true to it:
public static implicit operator Liar(bool isTrue)
{
return new(isTrue);
}
Now we can do this:
Liar liar = true;
Yeah, but this never happens in the real world
Unless someone does something silly... and people never do silly things...
Though that's not really my point, there are a few other things I'd like you to take away from this:
1. Code that looks like it's doing something different, is doing something different
Two lines of code that look like they're doing something different almost always are doing something different, even if the outcome is functionally the same.
Sure, in the real world the difference between !string.IsNullOrWhiteSpace(input)
and string.IsNullOrWhiteSpace(input) == false
is largely academic, so you can choose what works for you. But that's not necessarily going to be the case all the time, and being aware of the difference here might just save you a few hours of debugging one day.
2. In C# we can choose how operators work
At least with our own code, it's called operator overloading. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading
I've known some C# devs get confused by equality in JavaScript with ==
, ===
, !=
,!==
and truthy/falsely logic like !someString
. In many ways this is actually a lot simpler than C# because it is what it is, and we can't change it.
In C# all bets are off - you have complete control in defining how operators work with the classes/structs etc. that you define.
This is actually a super powerful feature of C# and I love it when library authors implement it to improve the developer experience, like this example:
var ratio = new(16,9);
if(ratio == "16:9")) //😙👌
3. You can write operators to compare pretty much anything!
My Liar
is just a wrapped bool
to make a silly example, but we can have any logic inside these operators and use them for more complicated types.
There's also no limit on what types you can add operators for, here I've just added code to compare Liar
with bool
but we can write code to compare our types to anything.
Thanks for nerding out with me...
If you've not had a go at overloading operators in your own types before, why not give it a try?
I'm sure you've got classes that would be useful if they could be compared against a string
value, like the ratio example above, or an int
or whatever.
Here's the full example Liar struct
public readonly struct Liar(bool isTrue) : IEquatable<bool>
{
private readonly bool _true = isTrue;
public static bool operator ==(Liar left, bool right)
{
return left._true == right;
}
public static bool operator !=(Liar left, bool right)
{
return left._true == right; // 😈
}
public static implicit operator bool(Liar liar)
{
return liar._true;
}
public static implicit operator Liar(bool isTrue)
{
return new(isTrue);
}
public override bool Equals(object obj)
{
return obj is Liar liar && liar._true == _true;
}
public override int GetHashCode()
{
return _true.GetHashCode();
}
public bool Equals(bool other)
{
return _true == other;
}
}
Top comments (0)