dotnet / csharplang

The official repo for the design of the C# programming language
11.53k stars 1.03k forks source link

Proposal: Special equality operator when one argument is null #2340

Open GrabYourPitchforks opened 5 years ago

GrabYourPitchforks commented 5 years ago

Problem

In the Utf8String prototype I've defined two operator == methods:

public static bool operator ==(Utf8String left, Utf8String right);
public static bool operator ==(Utf8String left, string right);

However, now simple equality / inequality comparisons against null will not properly compile, as the statement if (myUtf8String == null) is an ambiguous call between the two methods, since null matches both signatures.

It would be nice if I could define an operator == overload which would explicitly match null, so the compiler would no longer see an ambiguity given the code snippet above.

// similar to accepting nullptr_t as an argument to a C++ method
public static bool operator ==(Utf8String left, null) => (object)left == null;

There's an additional benefit to this proposal. Say that I didn't have the string-based overload of operator == and there was no ambiguity to begin with. Even if one side of the operator were a literal null, the compiler still emits a call to the normal operator == method, and it would still go through unoptimized code paths before determining that it can early exit. By having a null-specific operator == overload, the type author can write the simplest possible implementation of the method. It'll give behavior similar to the special support the C# compiler has today for if (myString == null), but without requiring the compiler to special-case particular types.

yaakov-h commented 5 years ago

Have you considered using if (myString is null)?

GrabYourPitchforks commented 5 years ago

@yaakov-h This isn't for my code; I can always jump through whatever hoops are necessary within my own library. It's for .NET consumers at large, most of whom are used to writing if (x == null) and having it just work.

theunrepentantgeek commented 5 years ago

The == operator is supposed to be symmetric.

Writing foo == bar should always give exactly the same result as bar == foo.

I'd therefore suggest that you keep this operator:

public static bool operator ==(Utf8String left, Utf8String right);

and ditch this one:

public static bool operator ==(Utf8String left, string right);

Instead, implement IEquatable<string> with bool Equals(string value) and you get to have your 🍰 cake and eat it too.

sonergonul commented 5 years ago

Writing foo == bar should always give exactly the same result as bar == foo.

@theunrepentantgeek Except one case. If baris a subclass of foo, they won't give you the same result.

GrabYourPitchforks commented 5 years ago

@theunrepentantgeek There was a symmetric operator == and corresponding operator != that I didn't list for brevity, so foo == bar and bar == foo should still give the same result. Recall: the purpose of this isn't for the code that I will be writing internal to the library. I'll perform whatever gyrations are necessary internally. The purpose is so that consumers of the library can write natural syntax and get the results they expect.

benaadams commented 5 years ago

Overload resolution could prefer the matching type to resolve the ambiguity?

e.g.

// preferred for Utf8String == null or null == Utf8String 
public static bool operator ==(Utf8String left, Utf8String right);

public static bool operator ==(Utf8String left, string right);

// preferred for string == null or null == string
public static bool operator ==(string left, string right);

public static bool operator ==(string left, Utf8String right);
yaakov-h commented 5 years ago

And if there is no overload for ==(ThisType a, ThisType b) then the ambiguity remains?

benaadams commented 5 years ago

@yaakov-h not sure I understand; the ambiguity exists between the matching types and the alternative type. If there was only one there wouldn't be any ambiguity

benaadams commented 5 years ago

If you had 2 equality operators of different types; then yes the ambiguity remains e.g.

public static bool operator ==(Utf8String left, string right);
public static bool operator ==(Utf8String left, ThisType right);

//  Utf8String == null ambiguous

But then you could add a matching type and it would win the tie break

public static bool operator ==(Utf8String left, string right);
public static bool operator ==(Utf8String left, ThisType right);
public static bool operator ==(Utf8String left, Utf8String right);

//  Utf8String == null; matches Utf8String == Utf8String 
PathogenDavid commented 5 years ago

@benaadams's suggestion to prefer the Utf8String == Utf8String overload was my natural reaction to this problem.

@yaakov-h I highly doubt that there are many types which overload T == U and T == V, but not T == T.

yaakov-h commented 5 years ago

@PathogenDavid possibly not, but I do love edge cases.

juliusfriedman commented 5 years ago

Also consider the Null obejct pattern and have a Utf8String.Null which can be leveraged although that won't stop the ambiguity which is caused by having two overloads and preventing generic null from being used without a cast.

You could drop the 2nd overload as that is what causes it in general but that won't get you to the overload you need...

Instead perhaps an implicit cast to string could be provided but I can't see how that wouldn't need to alloc.

Perhaps an implicit cast to ReadOnlySpan byte would be more suited and then combining that with the null object pattern works but I've jumped through hoops.

Tldr;

I really wish I could special case null in my operators explicitly rather than checking for them as it would lend itself to defaults I could define as well as solve issues like this..

public static explicit operator null(Type type) { return Type.Null; }

But I do this today with object.ReferenceEquals

Even without the Type overloads just the null explicitly would be useful there imho with type support being an enhancement later.

Would also be nice to have a way to override is and default as well but that's another story?

public static explicit operator default T(T t){ }

public static explicit operator is boolean(T t){ }

You get around the generics by using the Type syntax above but I think it needs refinement for sure since you would want to control what your returning and when to do different things in different scenarios.... but then again... leveraging contravariance there could also be useful...

GrabYourPitchforks commented 5 years ago

Another possibly simpler way to solve the problem originally described here would be to introduce an attribute [ReferentialNullEquality] (did I mention I'm terrible at naming?) in the System.Runtime.CompilerServices namespace. Essentially, if the compiler sees a call to operator == on a type that is annotated with such an attribute, and if either argument to the equality operator is known to be null, then the compiler would emit a referential equality check instead of calling the operator == or operator != methods.