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: Allow null-coalescing ?? operator on value types #328

Closed
lachbaer opened this issue Mar 24, 2017 · 19 comments
Closed

Proposal: Allow null-coalescing ?? operator on value types #328

lachbaer opened this issue Mar 24, 2017 · 19 comments

Comments

@lachbaer
Copy link
Contributor

lachbaer commented Mar 24, 2017

Redesignating the null-coalescing ?? operator to default-coalescing operator would allow to use it on value types as well.

For build in standard value types this would work like a charm:

public struct Fraction {
    public double Counter { get; set; }
    public double Denominator {
        double field;
        get => field ?? 1;
        set => field = value ?? throw new ArgumentException("Denominator must not be zero (0.0)");
    }
}

In this example a fraction is always valid, even if not initialized.

It is a shortcut for

public struct Fraction {
    public double Counter { get; set; }
    public double Denominator {
        double field;
        get => field != 0 ?  field : 1;
        set => field = value !=0 ? value
                              : throw new ArgumentException("Denominator must not be zero (0.0)");
    }
}

By comparing with default(T), the null-calescing character stays intact but also allows for value types. Nullable value types have a default of null, if one wants to compare the value to 0 or false, she must use ValueType?.GetValueOrDefault() ?? ....

Known issue

Though it would work fine with Nullables, there is the ambiguity that one wants to check against the value, but accidentially checks again null.

int x? = 0;
var y = x ?? 1;

Is it by purpose to check against x != null or is the actual intention to check against x != 0? Latter would have been correct with

int x? = 0;
var y = x.GetValueOrDefault() ?? 1;

(This issue is a bit related to #196).

@YaakovDavis
Copy link

YaakovDavis commented Mar 24, 2017

This will break the following existing code:

int? x = 0;
var y = x ?? 1;

With the existing compiler, y == 0; with this proposal y == 1.

@lachbaer
Copy link
Contributor Author

lachbaer commented Mar 24, 2017

@YaakovDavis
No, it won't. If x == 0 then also x != null, so x will be assigned to y, resulting in being y == 0.

To provoke y == 1 do...

int? x = 0;
var y = x.GetValueOrDefault() ?? 1;

PS: The default(Nullable<T>) is also null and not 0

@YaakovDavis
Copy link

YaakovDavis commented Mar 24, 2017

@lachbaer

No, it won't. If x == 0 then also x != null

Well, you were the one to suggest treating default values as nulls. It follows directly from that 0 (the default value of int) will be treated as null.

Here's another example, more reminiscent of the one you provided:

double? x = 0.0;
var y = x ?? throw new Exception();

With the existing compiler, no exception will be thrown. With the new proposal, an exception will occur.

@lachbaer
Copy link
Contributor Author

@YaakovDavis
Again, no it won't.

I think there is a misunderstanding here.

By now v = x ?? y is equivalent to
if (x != null) v=x else v=y.
When extending ?? to be a default-coalescing operator then it would be equivalent to
if (x != default({TypeOfX}) v=x else v=y.

@YaakovDavis
Copy link

YaakovDavis commented Mar 24, 2017

@lachbaer

You seem to suggest that
(int?)0 ?? 1
and
(int)0 ?? 1

will yield default results (0 in the former vs. 1 in the latter).

I'll dislike this idea very much. This will lead IMO to subtle bugs which are hard to track down.
Nullable value types are a strong safety feature. Making their use confusing & ambiguous will destroy their usefulness.

@lachbaer
Copy link
Contributor Author

@YaakovDavis Just in the case of Nullables.
You already can use ?? on Nullables now and it will always be a check agains null and not 0.

For (build-in) standard value types there is currently no such an coalescing operator. ?? will just fit in here as it is absolutely comparable to its initial purpose.

Though for Nullables there might indeed be the ambiguity that you want to check against 0, but you accidentially check against null and do not get the desired result.

@YaakovDavis
Copy link

YaakovDavis commented Mar 24, 2017

?? checks angainst nulls, not against default values; nulls a are a (strict) subset of the default values possible in C#.

Extending ?? to defaults changes its meaning in an unexpected way, as demonstrated.

@lachbaer
Copy link
Contributor Author

@YaakovDavis @jnm2
And what is the practical difference...? None...

For reference types the default is null.
For nullable types the default is null.

See... no difference!

It would just extend to:
For number value types the default is 0 or 0.0.
For boolean value types the default is false.
For struct value types the default is the default or its members - it can't be null anyhow

I don't see any conflicts, only benefits (except for the ambiguity with Nullables).

Extended first post for clarification

@jnm2
Copy link
Contributor

jnm2 commented Mar 24, 2017

@lachbaer If it was overwhelmingly common for people to write x != 0 ? x : y then I would think that a default-coalescing operator x ?? y would make sense. However, I really can't think of any place where I would use this or where any code I've seen could take advantage of this.

@CyrusNajmabadi
Copy link
Member

I'm with @jnm2 on this. This doesn't seem like a common pattern at all to be trying to specialize.

@lachbaer
Copy link
Contributor Author

I just experienced a conflict 😢

object obj = new object();
var z = obj?.GetHashCode() ?? 12345;

On first sight I would say, that it should give a compiler error, because GetHashCode returns an int and ?? does not work on value types. But it actually returns the hash code.
I thinks that's a bit of a violation of C#'s type safety, but on the other hand it is probably the expected behaviour.

With this proposal it is arbitrary whether ?? should check only agains obj != null, obj != null && obj.GetHashCode() != 0 or only obj.GetHashCode() != 0.

Restricting it to obj != null, because it has the null-coalescing member operator, can be ambiguous to the case where you purposely want to check against GetHashCode() != 0.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 24, 2017

On first sight I would say, that it should give a compiler error

That is not how the language works. You used the ?. operator. As such, the type of obj?.GetHashCode() is int? not int. ?? works as expected on an int? on the left and an int on the right.

I thinks that's a bit of a violation of C#'s type safety

There is no type-safety violation here.

@lachbaer
Copy link
Contributor Author

lachbaer commented Mar 24, 2017 via email

@jnm2
Copy link
Contributor

jnm2 commented Mar 24, 2017

@lachbaer

Yes, but that is not "obvious", just by seeing the GetHashCode signature.

The int? strong-typing is obvious if you understand what the ?. signifies. ?.GetHashCode() is strongly-typed as int? every bit as much as (int?)x.GetHashCode() is.

@lachbaer
Copy link
Contributor Author

@jnm2 @CyrusNajmabadi
I know that now ;-) And I had a look on the first infos the documentation gives me on that. It does not clearly state the way it works. And I doubt that all programmers learn the specifications! I've already used that construct and it worked like expected, but didn't know about the internals.

Also I have inspected the Roslyn code for the Null-Coalescing operator. Changing it doesn't seem to be too hard.

  • Reference types will still be compared to not equal null
  • Nullable types will also still be compared to not equal null
  • Primitive types will be compared to not equal their constant default value
  • struct value types will be compared by calling Equals<T>(T other) or Equals(object other)
  • there seems to special treatment of decimal for the default value - must inspect that
  • don't know what to do with dynamics yet

I'm on it, whether it will be incorporated or not :-)

@lachbaer
Copy link
Contributor Author

I have completed the adaptation and tested with all current kind of types.
The MSIL output is as expected and the operator works fine.

@CyrusNajmabadi
There are no XUnit tests yet. I will try to compose some. May I submit a PR on dotnet/roslyn when done?

@lachbaer
Copy link
Contributor Author

lachbaer commented Apr 7, 2017

After I have implemented the new behaviour for ?? i just encountered a slight problem. In case of

class C1
{
    public int x {get; set;}
    public int? y {get; set;}
}

var x1 = c?.x ?? 42;
var x2 = c?.y ?? 42;

the behaviour of a default-coalescing-operator is completely arbitrary.

Let's assume C1.x and C1.y are set to their default, 0 and (int?) null respectively.

Shall x1 be set to (int) 0 or (int) 42? Currently it is 0.
But in case of a def-co-op the expression would see the 0 [default(int)] and return 42 instead.
That would break existing code!

For x2 it still is conclusively.

Update:
Actually even x1 is absolutely conclusive, because C1.x becomes type int?. I always fall for that 😠

@lachbaer
Copy link
Contributor Author

After playing a bit with my implementation of this (dotnet/roslyn#18526) I must admit that I don't like it that much anymore.

  • the real use cases that came to my mind and code are indeed rather rare
  • The fact that it is used on every value type, which can also be nullable anytime, and the very similar look between the two can - no, will - probably lead to semantic errors where you wanted to use x.GetValueOrDefault ?? ... instead of x ?? ...
  • It will change the primary null meaning of the ? token in C# too much towards a 0 meaning. That is a big shift in an established construct, even if it had no consequences on existing code.
  • Meanwhile I have a better idea on how to achieve the wanted behaviour without the breaking change from this proposal. [Reminder: put link here after that proposal is online]

Nevertheless it was good to have it implemented. It helped me by analyzing the usefullness of this proposal. And it was fun learning about the compiler internals during the work :-)

@lachbaer
Copy link
Contributor Author

lachbaer commented May 8, 2017

I still have the idea in mind to make the null-conditional and null-coalescing operator available to custom value types or by extension-methoding existing ones, e.g. by introducing an bool operator null or by letting the compiler respecting an existing HasValue or IsValid method or interface. But that shall be a seperate proposal.

@lachbaer lachbaer closed this as completed May 8, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants