Roslyn: Proposal: Add support for ternary ref expression

Created on 12 Jan 2017  路  17Comments  路  Source: dotnet/roslyn

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

Area-Language Design Feature Request Language-C# New Language Feature - Ref Locals and Returns

All 17 comments

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

@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.

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.

@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:
```C#
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");
    }
}

}
```

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

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.

@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;

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

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

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

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...

You are missing a ref before the b

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

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

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?

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

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.

Was this page helpful?
0 / 5 - 0 ratings