Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
GrabYourPitchforks opened this issue Mar 14, 2019 · 13 comments
Open

Comments

@GrabYourPitchforks
Copy link
Member

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
Copy link
Member

Have you considered using if (myString is null)?

@GrabYourPitchforks
Copy link
Member Author

@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
Copy link

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
Copy link

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

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

@GrabYourPitchforks
Copy link
Member Author

@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
Copy link
Member

benaadams commented Mar 14, 2019

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
Copy link
Member

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

@benaadams
Copy link
Member

@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
Copy link
Member

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
Copy link

@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
Copy link
Member

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

@juliusfriedman
Copy link

juliusfriedman commented Mar 31, 2019

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
Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants