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: Add support for ternary ref expression #16473

Closed
VSadov opened this issue Jan 12, 2017 · 17 comments
Closed

Proposal: Add support for ternary ref expression #16473

VSadov opened this issue Jan 12, 2017 · 17 comments

Comments

@VSadov
Copy link
Member

VSadov commented Jan 12, 2017

The pattern of binding a ref variable to one or another expression conditionally is not currently expressible in C#.

The typical workaround is to introduce a method like:

ref T Choice(bool condition, ref T consequence, ref alternative)
{
    if (condition)
    {
         return ref consequence;
    }
    else
    {
         return ref alternative;
    }
}

Note that this is not an exact replacement of a ternary since all arguments must be evaluated at the call site.

The following will not work as expected:

       // will crash with NRE because 'arr[0]' will be executed unconditionally
      ref var r = ref Choice(arr != null, ref arr[0], ref otherArr[0]);

The proposed syntax would look like:

     <condition> ? ref <consequence> : ref <alternative>;

The above attempt with "Choice" can be correctly written using ref ternary as:

     ref var r = ref (arr != null ? ref arr[0]: ref otherArr[0]);

The difference from Choice is that consequence and alternative expressions are accessed in a truly conditional manner, so we do not see a crash if arr == null

The ternary ref is just a ternary where both alternative and consequence are refs. It will naturally require that consequence/alternative operands are LValues.
It will also require that consequence and alternative have types that are identity convertible to each other.

The type of the expression will be computed similarly to the one for the regular ternary. I.E. in a case if consequence and alternative have identity convertible, but different types, the existing type-merging rules will apply.

Safe-to-return will be assumed conservatively from the conditional operands. If either is unsafe to return the whole thing is unsafe to return.

Ref ternary is an LValue and as such it can be passed/assigned/returned by reference;

     // pass by reference
     foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));

     // return by reference
     return ref (arr != null ? ref arr[0]: ref otherArr[0]);

Being an LValue, it can also be assigned to.

    // assign to
    (arr != null ? ref arr[0]: ref otherArr[0]) = 1;

Ref ternary can be used in a regular (not ref) context as well. Although it would not be common since you could as well just use a regular ternary.

     int x = (arr != null ? ref arr[0]: ref otherArr[0]);

=======================
Implementation notes:

The complexity of the implementation would seem to be the size of a moderate-to-large bug fix. - I.E not very expensive.
I do not think we need any changes to the syntax or parsing.
There is no effect on metadata or interop. The feature is completely expression based.
No effect on debugging/PDB either

@gafter
Copy link
Member

gafter commented Jan 12, 2017

I like it. I wonder whether the ref outside the ternary expression is really needed.

@VSadov
Copy link
Member Author

VSadov commented Jan 12, 2017

@gafter the ref is a part of the consuming expression. We always use ref when operand is in a ref context. - to be explicit that we are binding to the variable itself, not reading its value.

foo(ref <ternary here>)    // I am passing by reference
foo(      ternary here    )    // I am passing by value

If we infer ref from the operands, then in a nested case (operands of the ternary are ternary expressions themselves) we would need to do the inference recursively. Seems too complicated.

Also there could be cases where ref ternary could be used in ambiguous ref/val contexts - argument of a dynamic/overloaded call, assignments to ref variable (potential future feature).

I think requiring outer ref, when used in ref context, is more consistent with the rest of the language.

@alrz
Copy link
Member

alrz commented Jan 12, 2017

It looks like a ref to a ref, You take a ref to a variable not expression, I think ref ( condition ? ref id : ref id ) is more confusing than just cond ? ref id : ref id . As long as both parts are either ref or val, there would be no ambiguity for the compiler to resolve. I believe requiring ref before args came from the fact that C# did not support ref locals before, and only pass-by-ref were possible. You can compare this to C++ ampersand operator. It is not part of the argument, it is itself a reference that passed to the function as-is. However, we still require ref on method invocation, which makes the first ref meaningless.

    void M(ref int i) {
        ref int a = ref i;
        M(ref a);
    }

You might argue that this is because it's more readable. But C# already chose ref over & for the sake of readability, I think it should at least keep it meaningful.

@VSadov
Copy link
Member Author

VSadov commented Jan 13, 2017

@alrz - I am not sure I completely understand your comment.

C# uses ref when an operand is passed as a reference/alias (as opposed to as a value/copy).
In addition to being explicit, and clearly requiring that the operand is an LValue, there are situations where both ref and val uses are possible and ref acts as disambiguator.

Example:

class Program
{
    static void Main()
    {
        int i = 42;

        var o = new Derived();
        // pass by value
        o.Test(i);
        // pass by ref
        o.Test(ref i);

        dynamic d = new Derived();
        // pass by value
        d.Test(i);
        // pass by ref
        d.Test(ref i);
    }

    class Base
    {
        public void Test(ref int x)
        {
            System.Console.WriteLine("ref");
        }
    }

    class Derived: Base
    {
        public void Test(int x)
        {
            System.Console.WriteLine("val");
        }
    }
}

@alrz
Copy link
Member

alrz commented Jan 13, 2017

Ok, how is this ambigious?

int i = 42;
var o = new Derived();

// pass by value
o.Test(i);

// pass by ref
ref int r = ref i;
o.Test(r);

dynamic d = new Derived();
// pass by value
d.Test(i);

// pass by ref
ref int r = ref i;
d.Test(r);

I understand C# uses ref when an operand is passed by reference, that was when we didn't have ref locals and it just imitated the syntax from C++ but with a ref instead of &, right?

f(&i); // take and pass address

Now that we have ref locals, we could follow the same model,

int* r = &i; // take address
f(r); // pass address

@alrz
Copy link
Member

alrz commented Jan 13, 2017

That's a pointer though. C# ref doesn't even behave like C++ references, because you should write ref on both sides of the assignment. I'm not sure I understand the reasoning behind this. What actually ref means is not clear. perhaps it means something different in every context e.g. rhs of assignment, method/variable type, method arguments, return ref and plus, ternary as proposed here. I know that clr restrict the context, but in terms of language constructs that doesn't appear to be coherent.

@gafter
Copy link
Member

gafter commented Jan 13, 2017

@alrz

When you declare

int z = 0;
ref int r = ref z;

and you call a method

public void M(ref int i);

we require you write

M(ref r);

so that we can distinguish it from a call to the method

public void M(int i);

which you would write

M(r);

For language uniformity, we also require ref when binding a ref local (as opposed to a ref parameter).

int ref r = ref z;

This enables us to distinguish assigning through the reference

r = 3;

from (a possible future feature) re-assigning the reference itself

ref r = ref q;

@alrz
Copy link
Member

alrz commented Jan 13, 2017

So I think "alias" is more appropriate for that. I've got confused with a reference not being a reference.

@CyrusNajmabadi
Copy link
Member

IDE does not have cost-concerns here. This seems nice and a totally natural continuation of what we've done with 'ref' so far.

VSadov added a commit to VSadov/csharplang that referenced this issue Mar 3, 2017
Proposal for the   "conditional ref expression" feature.
moved from dotnet/roslyn#16473
gafter pushed a commit to dotnet/csharplang that referenced this issue Mar 7, 2017
Proposal for the   "conditional ref expression" feature.
moved from dotnet/roslyn#16473
@jcouv jcouv added this to the 15.5 milestone Oct 22, 2017
@jcouv
Copy link
Member

jcouv commented Oct 22, 2017

I'll go ahead and close this issue, since this was done in C# 7.2.

@jcouv jcouv closed this as completed Oct 22, 2017
@thomaslevesque
Copy link
Member

I'll go ahead and close this issue, since this was done in C# 7.2.

Was it? Isn't this code supposed to work then? (it doesn't)

    bool b = false;
    int x = 1;
    int y = 2;
    ref int z = b ? ref x : ref y;

Or maybe I misunderstood what this issue is about...

@benaadams
Copy link
Member

You are missing a ref before the b

ref int z = ref b ? ref x : ref y;

@thomaslevesque
Copy link
Member

Ah, thanks @benaadams. That seems a bit redundant, but in a way, it makes sense...

@thomaslevesque
Copy link
Member

Doesn't seem to work with a switch expression, though...

    int i = 1;
    int x = 123;
    int y = 456;
    int z = 789;
    ref int r = ref i switch
    {
        1 => ref x,
        2 => ref y,
        3 => ref z
        _ => throw new Exception("oops");
    };

is this supported?

@gafter
Copy link
Member

gafter commented Jan 13, 2020

No, there is no ref version of the switch expression.

@timcassell
Copy link

No, there is no ref version of the switch expression.

Is there already a feature request for that? I was just thinking about using the new switch expression to replace a clunky nested ref ternary expression like that.

@gafter
Copy link
Member

gafter commented May 27, 2020

See dotnet/csharplang#3326

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

8 participants