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

Champion "Readonly ref" (C# 7.2) #38

Open
MadsTorgersen opened this Issue Feb 9, 2017 · 70 comments

Comments

Projects
None yet
@MadsTorgersen
Contributor

MadsTorgersen commented Feb 9, 2017

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Feb 10, 2017

Contributor

I'm not quite clear why this has to be a separate feature from readonly locals and paramerters?

Contributor

alrz commented Feb 10, 2017

I'm not quite clear why this has to be a separate feature from readonly locals and paramerters?

@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar Feb 10, 2017

Member

@alrz they are very different features.

  • readonly ref: restricts the ability to mutate the target of a ref variable.
  • readoonly locals: restricts the ability to retarget a variable to a new location.

These features do have some overlap with st. t values. But overall are different features.

Member

jaredpar commented Feb 10, 2017

@alrz they are very different features.

  • readonly ref: restricts the ability to mutate the target of a ref variable.
  • readoonly locals: restricts the ability to retarget a variable to a new location.

These features do have some overlap with st. t values. But overall are different features.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Feb 11, 2017

Contributor

So potentially, we could have readonly in parameters at some point?

Contributor

alrz commented Feb 11, 2017

So potentially, we could have readonly in parameters at some point?

@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar Feb 14, 2017

Member

@alrz

Possibly but that's a bit of a redundant notation. All ref identifiers are unassignable by default. Hence adding readonly is just making it more explicit for no real gain.

Member

jaredpar commented Feb 14, 2017

@alrz

Possibly but that's a bit of a redundant notation. All ref identifiers are unassignable by default. Hence adding readonly is just making it more explicit for no real gain.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Feb 14, 2017

Contributor

I'm under the impression that you are distinguishing something like const char* and char* const, does that analogy work here?

Contributor

alrz commented Feb 14, 2017

I'm under the impression that you are distinguishing something like const char* and char* const, does that analogy work here?

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Feb 14, 2017

Member

We might support reassignment of ref variables in the future. In order for that to be possible, we will need to distinguish between ref readonly (a ref to something you can't write), readonly ref (a ref that you can't reassign), and readonly ref readonly (a ref that you can't reassign to something you can't write).

Member

gafter commented Feb 14, 2017

We might support reassignment of ref variables in the future. In order for that to be possible, we will need to distinguish between ref readonly (a ref to something you can't write), readonly ref (a ref that you can't reassign), and readonly ref readonly (a ref that you can't reassign to something you can't write).

@Thaina

This comment has been minimized.

Show comment
Hide comment
@Thaina

Thaina Feb 17, 2017

readonly ref readonly (a ref that you can't reassign to something you can't write).

Should just use const. const local and const parameter

for ref readonly and readonly ref. I think there are sealed,fixed and lock. 3 keywords that currently cannot be local or param modifier. We should reuse these

Thaina commented Feb 17, 2017

readonly ref readonly (a ref that you can't reassign to something you can't write).

Should just use const. const local and const parameter

for ref readonly and readonly ref. I think there are sealed,fixed and lock. 3 keywords that currently cannot be local or param modifier. We should reuse these

@gafter gafter added this to the 7.2 candidate milestone Feb 22, 2017

@ufcpp

This comment has been minimized.

Show comment
Hide comment
@ufcpp

ufcpp Feb 27, 2017

Can in parameters be contravariant?

interface ISetter<in T>
{
    void SetValue(in T value);
}

ufcpp commented Feb 27, 2017

Can in parameters be contravariant?

interface ISetter<in T>
{
    void SetValue(in T value);
}
@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar Feb 27, 2017

Member

@ufcpp

Can in parameters be contravariant?

No. The CLI only truly recognizes ref parameters. The C# notions of in and out are a construct specific to C#. There is no underlying runtime support for enforcing these notions and hence it can't be a part of the core CLI type system.

Member

jaredpar commented Feb 27, 2017

@ufcpp

Can in parameters be contravariant?

No. The CLI only truly recognizes ref parameters. The C# notions of in and out are a construct specific to C#. There is no underlying runtime support for enforcing these notions and hence it can't be a part of the core CLI type system.

@Athari

This comment has been minimized.

Show comment
Hide comment
@Athari

Athari Mar 18, 2017

and readonly ref readonly

And here I thought that C++-style of consts will never be considered for C#... Are const readonly functions also something being considered?

Athari commented Mar 18, 2017

and readonly ref readonly

And here I thought that C++-style of consts will never be considered for C#... Are const readonly functions also something being considered?

@BreyerW

This comment has been minimized.

Show comment
Hide comment
@BreyerW

BreyerW Mar 24, 2017

Could this feature enable ref readonly property with both setter and getter (getter being ref readonly returning while setter being normal method)? Main use case is to be able to reliably call OnValueChanged events and the like while still being able to benefit from ref performance gain on struct

BreyerW commented Mar 24, 2017

Could this feature enable ref readonly property with both setter and getter (getter being ref readonly returning while setter being normal method)? Main use case is to be able to reliably call OnValueChanged events and the like while still being able to benefit from ref performance gain on struct

@soroshsabz

This comment has been minimized.

Show comment
Hide comment
@soroshsabz

soroshsabz Mar 28, 2017

ITNOA

I think #188 that describe about readonly parameters is more general than this issue and that proposal is superset of this proposal.

soroshsabz commented Mar 28, 2017

ITNOA

I think #188 that describe about readonly parameters is more general than this issue and that proposal is superset of this proposal.

@jnm2

This comment has been minimized.

Show comment
Hide comment
@jnm2

jnm2 Mar 28, 2017

Contributor

ITNOA

What does this mean?

Contributor

jnm2 commented Mar 28, 2017

ITNOA

What does this mean?

@Ultrahead

This comment has been minimized.

Show comment
Hide comment
@Ultrahead

Ultrahead May 3, 2017

I happened to find this champion as I was searching the repo for proposals of const/fixed refs.

In proposals/readonly-ref.md it is mentioned this example of XNA. Please due note that the proposed solution will not match perf of, in the example used, the ADD operation of XNA, since it's returning a copy of the resulting/new vector.

Ultrahead commented May 3, 2017

I happened to find this champion as I was searching the repo for proposals of const/fixed refs.

In proposals/readonly-ref.md it is mentioned this example of XNA. Please due note that the proposed solution will not match perf of, in the example used, the ADD operation of XNA, since it's returning a copy of the resulting/new vector.

@Ultrahead

This comment has been minimized.

Show comment
Hide comment
@Ultrahead

Ultrahead May 3, 2017

In terms of C# 7, it should be something like, instead:

static void Add (in Vector3 v1, in Vector3 v2, out var result)
{
    // OK
    result = new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

Ultrahead commented May 3, 2017

In terms of C# 7, it should be something like, instead:

static void Add (in Vector3 v1, in Vector3 v2, out var result)
{
    // OK
    result = new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
@JosephTremoulet

This comment has been minimized.

Show comment
Hide comment
@JosephTremoulet

JosephTremoulet May 16, 2017

Would there be a way to elide the ldelema-implied type check when taking the address of an array element as a readonly ref? It would be nice if e.g. Voltile.Read on an array element (as in ThreadPool.TrySteal) didn't require a type test...

JosephTremoulet commented May 16, 2017

Would there be a way to elide the ldelema-implied type check when taking the address of an array element as a readonly ref? It would be nice if e.g. Voltile.Read on an array element (as in ThreadPool.TrySteal) didn't require a type test...

@BreyerW

This comment has been minimized.

Show comment
Hide comment
@BreyerW

BreyerW May 19, 2017

What about out shorthand for ref readonly return?

BreyerW commented May 19, 2017

What about out shorthand for ref readonly return?

@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar May 19, 2017

Member

@JosephTremoulet

Would there be a way to elide the ldelema-implied type check when taking the address of an array element as a readonly ref? It would be nice if e.g. Voltile.Read on an array element (as in ThreadPool.TrySteal) didn't require a type test...

That's so tempting. Within the realm of C# it would be fine. We would never actually write to the location hence it should be fine to avoid the type check. But it's still possible for us to call out to other DLLS that ignore ref readonly (even though we have a mod req).

Hmm, then again, maybe the mod req could be enough to save us here. At that point the code is willfully ignoring the IL.

@VSadov

Member

jaredpar commented May 19, 2017

@JosephTremoulet

Would there be a way to elide the ldelema-implied type check when taking the address of an array element as a readonly ref? It would be nice if e.g. Voltile.Read on an array element (as in ThreadPool.TrySteal) didn't require a type test...

That's so tempting. Within the realm of C# it would be fine. We would never actually write to the location hence it should be fine to avoid the type check. But it's still possible for us to call out to other DLLS that ignore ref readonly (even though we have a mod req).

Hmm, then again, maybe the mod req could be enough to save us here. At that point the code is willfully ignoring the IL.

@VSadov

@VSadov

This comment has been minimized.

Show comment
Hide comment
@VSadov

VSadov May 19, 2017

Member

@JosephTremoulet @jaredpar - yes, in the world of C# it would be safe to emit .readonly ldelema when fetching an element ref to pass to a ref readonly parameter.

However if the calee misbehaves, we can end up not just with something that is readonly unexpectedly mutated (we are kind of ok with that), but with a type-system hole. That would seem a bigger problem.

Here is the scenario:

string[] sArr = ... get an array of strings
object[] oArr = sArr;    // allowed!!

// bravely suppress type check here via  .readonly
Evil(oArr[2]);

void Evil([IsReadOnly] ref object arg)
{
     // muhahaha
     arg = new Exception();
}
Member

VSadov commented May 19, 2017

@JosephTremoulet @jaredpar - yes, in the world of C# it would be safe to emit .readonly ldelema when fetching an element ref to pass to a ref readonly parameter.

However if the calee misbehaves, we can end up not just with something that is readonly unexpectedly mutated (we are kind of ok with that), but with a type-system hole. That would seem a bigger problem.

Here is the scenario:

string[] sArr = ... get an array of strings
object[] oArr = sArr;    // allowed!!

// bravely suppress type check here via  .readonly
Evil(oArr[2]);

void Evil([IsReadOnly] ref object arg)
{
     // muhahaha
     arg = new Exception();
}
@tannergooding

This comment has been minimized.

Show comment
Hide comment
@tannergooding

tannergooding May 23, 2017

Member

For interop scenarios, Is the compiler going to emit a warning for static extern Vector3 Normalize([In, Out] ref readonly Vector3 value)?

That is, when the user attempts to explicitly tell the marshaller that ref readonly has copy-back semantics?

I did log an issue (dotnet/coreclr#11830) requesting that the marshaller be updated to recognize ref readonly as [In] rather than [In, Out] (assuming that no attributes were specified).

Member

tannergooding commented May 23, 2017

For interop scenarios, Is the compiler going to emit a warning for static extern Vector3 Normalize([In, Out] ref readonly Vector3 value)?

That is, when the user attempts to explicitly tell the marshaller that ref readonly has copy-back semantics?

I did log an issue (dotnet/coreclr#11830) requesting that the marshaller be updated to recognize ref readonly as [In] rather than [In, Out] (assuming that no attributes were specified).

@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar May 23, 2017

Member

For interop scenarios, Is the compiler going to emit a warning for static extern Vector3 Normalize([In, Out] ref readonly Vector3 value)?

No. The compiler doesn't impart any semantic meaning to [In] or [Out]. It would be pretty inconsistent to warn on [Out] ref readonly but not warn on [In] out.

Member

jaredpar commented May 23, 2017

For interop scenarios, Is the compiler going to emit a warning for static extern Vector3 Normalize([In, Out] ref readonly Vector3 value)?

No. The compiler doesn't impart any semantic meaning to [In] or [Out]. It would be pretty inconsistent to warn on [Out] ref readonly but not warn on [In] out.

@yizhang82

This comment has been minimized.

Show comment
Hide comment
@yizhang82

yizhang82 May 23, 2017

Member

@jaredpar Can the compiler emit [In] by default with ref readonly? This way runtime don't have to make any special assumptions about in semantics. This also aligns with out keyword being [Out] by default.
If user specify anything other than the default, the runtime would do what exactly user specifies.

Member

yizhang82 commented May 23, 2017

@jaredpar Can the compiler emit [In] by default with ref readonly? This way runtime don't have to make any special assumptions about in semantics. This also aligns with out keyword being [Out] by default.
If user specify anything other than the default, the runtime would do what exactly user specifies.

@jcouv

This comment has been minimized.

Show comment
Hide comment
@CyrusNajmabadi

This comment has been minimized.

Show comment
Hide comment
@CyrusNajmabadi

CyrusNajmabadi Nov 29, 2017

Do in parameters generate implicit copies when calling property getters?

Not if you have a "readonly struct" i.e. a struct declared as readonly struct S { ... Props ... }.

This seems wasteful, since property getters in almost all cases do not logically mutate the state of the object.

Unfortunately, the language cannot optimizae around "almost all cases". That would break the structs that do mutate on access.

And, if you have a struct that won't mutate, best to declare it as "readonly struct", so you then get these sorts of benefits.

CyrusNajmabadi commented Nov 29, 2017

Do in parameters generate implicit copies when calling property getters?

Not if you have a "readonly struct" i.e. a struct declared as readonly struct S { ... Props ... }.

This seems wasteful, since property getters in almost all cases do not logically mutate the state of the object.

Unfortunately, the language cannot optimizae around "almost all cases". That would break the structs that do mutate on access.

And, if you have a struct that won't mutate, best to declare it as "readonly struct", so you then get these sorts of benefits.

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Nov 29, 2017

Contributor

What language version has readonly struct S { ... } (where is this proposal)?

Contributor

bbarry commented Nov 29, 2017

What language version has readonly struct S { ... } (where is this proposal)?

@jcouv

This comment has been minimized.

Show comment
Hide comment
@jcouv
Member

jcouv commented Nov 29, 2017

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Mar 2, 2018

The in/ref readonly creates a copy implicitly (which cancels the benefit of ref) whenever a method/property is access. The only way to avoid it is to declare the struct as readonly struct thus all instance members immutable. This does not seem to address the core scenario enough.

e.g. for a Matrix4x4, it is not practical to make it immutable. Then, the only way to benefit from ref while avoiding implicit copy is to avoid calling any of its properties/methods, which is only possible for the bottom layer of a math library.

Then, the next workaround is to avoid adding any property and instance-methods to the struct, but only static methods with in parameter. And extension methods, with this in, also seem not supported.

I remember there is discussion about marking a method pure/readonly, which will remove the implicit copy better, in my understanding. Why readonly struct is chosen over that?

qrli commented Mar 2, 2018

The in/ref readonly creates a copy implicitly (which cancels the benefit of ref) whenever a method/property is access. The only way to avoid it is to declare the struct as readonly struct thus all instance members immutable. This does not seem to address the core scenario enough.

e.g. for a Matrix4x4, it is not practical to make it immutable. Then, the only way to benefit from ref while avoiding implicit copy is to avoid calling any of its properties/methods, which is only possible for the bottom layer of a math library.

Then, the next workaround is to avoid adding any property and instance-methods to the struct, but only static methods with in parameter. And extension methods, with this in, also seem not supported.

I remember there is discussion about marking a method pure/readonly, which will remove the implicit copy better, in my understanding. Why readonly struct is chosen over that?

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Mar 23, 2018

I will love this more than any previously added C# feature (seriously whole heartedly), if it would allow to return a reference to a struct (in an array of structs) in an efficient way, so that the original struct can't be modified by this reference, while the array holding the struct remains modifiable (privatly for the owner class).
Most features added/confirmed so far make my daily life much simpler and are really great, but if such a readonly ref would be added, it would open a new class of problems we can solve within C# efficiently rather than using C++.
(I'm working in game dev and need to care about performance and low GC in quite many cases)

I am in particular looking for the ability to grant readonly access to structs for multiple threads without copying.
And have the ability to modify/replace their values at a safe synchronization step when no parallel execution happens.
I know I could work around with solutions like IReadonlyCollection, but then structs would be copied all over the place with additional virtual calling overhead.

So will readonly ref allow to solve such problems efficiently or did I just fantasize too much about how much low level control it will give in our hands and it does not work just like a const& reference in C/C++?

Thanks in advance for an answer.

Voidzer0 commented Mar 23, 2018

I will love this more than any previously added C# feature (seriously whole heartedly), if it would allow to return a reference to a struct (in an array of structs) in an efficient way, so that the original struct can't be modified by this reference, while the array holding the struct remains modifiable (privatly for the owner class).
Most features added/confirmed so far make my daily life much simpler and are really great, but if such a readonly ref would be added, it would open a new class of problems we can solve within C# efficiently rather than using C++.
(I'm working in game dev and need to care about performance and low GC in quite many cases)

I am in particular looking for the ability to grant readonly access to structs for multiple threads without copying.
And have the ability to modify/replace their values at a safe synchronization step when no parallel execution happens.
I know I could work around with solutions like IReadonlyCollection, but then structs would be copied all over the place with additional virtual calling overhead.

So will readonly ref allow to solve such problems efficiently or did I just fantasize too much about how much low level control it will give in our hands and it does not work just like a const& reference in C/C++?

Thanks in advance for an answer.

@Ultrahead

This comment has been minimized.

Show comment
Hide comment
@Ultrahead

Ultrahead Mar 23, 2018

@Voidzer0: I was requesting for something similar here:

#536

Ultrahead commented Mar 23, 2018

@Voidzer0: I was requesting for something similar here:

#536

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick Mar 23, 2018

Contributor

@Voidzer0

If I understand you correctly, then the already released C# 7.2 has exactly what you want.

For example, have a look at the following code:

using System;

class C
{
    private DateTime[] dates = new[] { new DateTime(2018, 1, 1) };
    
    public ref readonly DateTime GetDate() => ref dates[0];
    
    public void Increment() => dates[0] = dates[0].AddDays(1);
}

class Program
{
    static void Main()
    {
        var c = new C();
        
        ref readonly var date = ref c.GetDate();

        // 2018-01-01 00:00:00Z
        Console.WriteLine($"{date:u}");

        c.Increment();

        // 2018-01-02 00:00:00Z
        Console.WriteLine($"{date:u}");

        // error CS0131: The left-hand side of an assignment must be a variable, property or indexer
        // date = date.AddMonths(1);
    }
}
Contributor

svick commented Mar 23, 2018

@Voidzer0

If I understand you correctly, then the already released C# 7.2 has exactly what you want.

For example, have a look at the following code:

using System;

class C
{
    private DateTime[] dates = new[] { new DateTime(2018, 1, 1) };
    
    public ref readonly DateTime GetDate() => ref dates[0];
    
    public void Increment() => dates[0] = dates[0].AddDays(1);
}

class Program
{
    static void Main()
    {
        var c = new C();
        
        ref readonly var date = ref c.GetDate();

        // 2018-01-01 00:00:00Z
        Console.WriteLine($"{date:u}");

        c.Increment();

        // 2018-01-02 00:00:00Z
        Console.WriteLine($"{date:u}");

        // error CS0131: The left-hand side of an assignment must be a variable, property or indexer
        // date = date.AddMonths(1);
    }
}
@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Mar 23, 2018

@Ultrahead thanks for the pointer and I like your proposal, except for the modifiable state and I don't think it does match what a const& reference does fully.
As well in C++ you can neither call non-const member functions on const& references, since this could modify their internal state, nor modify any public fields.
Since C# has no concept of "const member functions" I am not sure if the compiler can actually know what is safe to call, but public vars at least should be doable, but already getters could have side effects.
For me it sounds like @qrli kind of suggested something like pure "const member functions" if I got it right, which would allow the compiler to determine such things easier.

For my scenario full immutability for the reference and its value contents is wanted (and needed, for thread safety) at least for all code which only has access to the reference, rather than the internal modifiable array which is only modifed when this is safe.
For this I could work around by using totally immutable structs as long I can replace them as a whole in my internal array.
I would still prefer to have the option to use normal modifiable structs here, but I am fine if these structs have to be immutable. (Since they are mostly small I can create new instances for every change)

Edit:
@svick Your post popped while I was in the middle of writing this answer.
Yes that looks exactly like what I want.
If I can use any type of custom struct and replace (or even modify) what is stored at dates[i],
while GetDate() returns just a reference and doesn't copy under any circumstances, just like with a const& ref in C++,
so that the values contents of the structs stored in the 'dates' array can't be modified using this ref,
then all wishes are fulfilled and I am a super happy C# programmer now! :)
Is it really true?

Maybe one day I can finally stop to use P/Invoke and do these thinks fast directly in C# and get a rid of some ongoing nightmares in some code base ;)

Voidzer0 commented Mar 23, 2018

@Ultrahead thanks for the pointer and I like your proposal, except for the modifiable state and I don't think it does match what a const& reference does fully.
As well in C++ you can neither call non-const member functions on const& references, since this could modify their internal state, nor modify any public fields.
Since C# has no concept of "const member functions" I am not sure if the compiler can actually know what is safe to call, but public vars at least should be doable, but already getters could have side effects.
For me it sounds like @qrli kind of suggested something like pure "const member functions" if I got it right, which would allow the compiler to determine such things easier.

For my scenario full immutability for the reference and its value contents is wanted (and needed, for thread safety) at least for all code which only has access to the reference, rather than the internal modifiable array which is only modifed when this is safe.
For this I could work around by using totally immutable structs as long I can replace them as a whole in my internal array.
I would still prefer to have the option to use normal modifiable structs here, but I am fine if these structs have to be immutable. (Since they are mostly small I can create new instances for every change)

Edit:
@svick Your post popped while I was in the middle of writing this answer.
Yes that looks exactly like what I want.
If I can use any type of custom struct and replace (or even modify) what is stored at dates[i],
while GetDate() returns just a reference and doesn't copy under any circumstances, just like with a const& ref in C++,
so that the values contents of the structs stored in the 'dates' array can't be modified using this ref,
then all wishes are fulfilled and I am a super happy C# programmer now! :)
Is it really true?

Maybe one day I can finally stop to use P/Invoke and do these thinks fast directly in C# and get a rid of some ongoing nightmares in some code base ;)

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jun 24, 2018

When I wrote the above I got side tracked by another project nearing a deadline.
But meanwhile I got to play with the new tools and I am impressed and very happy.
Thank you so much C# designers for getting these great features into the language!
C# got now a lot more useful for high performance programming.

I just got one question which made me wonder:
It is nice to be able return readonly refs, but what is if it can not be guaranteed that a requested reference exists.
Example:
ref readonly T GetRef<T>(int anyUniqueKey)
Is fine when the caller is sure that anyUniqueKey exists, but like done with Dictionary<K,V>,
it would be nice to be able to do a contains check and a retrival at the same time like TryGetValue, but for ref readonly.
But it seems this isn't supported:
bool TryGetRef<T>(int anyUniqueKey, out readonly T outReference)
If I'd use just out here, I'd need to copy the value, but all I want to do is giving a ref readonly if an element with given key exists.
Right now I need to work around the problem by separating Contains-check and retrieval into two calls which requires two lookups rather than one.

Does anyone know if this can be done?
(Sorry for asking this question here, but I do not know where such questions should be asked best. Please link me to a place to ask general questions about the new C# features if you know one. Thanks!)

Voidzer0 commented Jun 24, 2018

When I wrote the above I got side tracked by another project nearing a deadline.
But meanwhile I got to play with the new tools and I am impressed and very happy.
Thank you so much C# designers for getting these great features into the language!
C# got now a lot more useful for high performance programming.

I just got one question which made me wonder:
It is nice to be able return readonly refs, but what is if it can not be guaranteed that a requested reference exists.
Example:
ref readonly T GetRef<T>(int anyUniqueKey)
Is fine when the caller is sure that anyUniqueKey exists, but like done with Dictionary<K,V>,
it would be nice to be able to do a contains check and a retrival at the same time like TryGetValue, but for ref readonly.
But it seems this isn't supported:
bool TryGetRef<T>(int anyUniqueKey, out readonly T outReference)
If I'd use just out here, I'd need to copy the value, but all I want to do is giving a ref readonly if an element with given key exists.
Right now I need to work around the problem by separating Contains-check and retrieval into two calls which requires two lookups rather than one.

Does anyone know if this can be done?
(Sorry for asking this question here, but I do not know where such questions should be asked best. Please link me to a place to ask general questions about the new C# features if you know one. Thanks!)

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Jun 24, 2018

Contributor

@Voidzer0 you can do it like this:

public class C {
    int[] list =  {1,2,3};
    static int[] defaultvalue = {0};
    
    ref readonly int TryGetByKey(int key, out bool success) {
        success = false;
        if((uint)key < list.Length) {
            success = true;
            return ref list[key];
        }
        return ref defaultvalue[0];
    }
}

It is a little bit odd but c'est la vie. You'll have to assign it to a local anyway and cannot do that in an if statement.

Contributor

bbarry commented Jun 24, 2018

@Voidzer0 you can do it like this:

public class C {
    int[] list =  {1,2,3};
    static int[] defaultvalue = {0};
    
    ref readonly int TryGetByKey(int key, out bool success) {
        success = false;
        if((uint)key < list.Length) {
            success = true;
            return ref list[key];
        }
        return ref defaultvalue[0];
    }
}

It is a little bit odd but c'est la vie. You'll have to assign it to a local anyway and cannot do that in an if statement.

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jun 24, 2018

Thank you @bbarry,
I thought about this solution, but as you say it is a bit odd and also less elegant.
Because the power of returning a boolean is to do the retrival optional in an if.

You'll have to assign it to a local anyway and cannot do that in an if statement.

In C# 7 you can, eg.
if (int.TryParse( "169", out var userId )) { /* do something with userId */ }

See http://structuredsight.com/2016/08/30/c-7-additions-out-variables/

And thus it would be nice if out readonly would be a thing.

if (myLookup.TryGetRef(key, out readonly T value)) { /* do something with value */ }

versus less elegant:

`
ref readonly T value = ref myLookup.TryGetRef(key, out bool result);

if (result)
{
/* do something with value */
}
`

Does anyone know if such a feature is suggested or in the works, or knows why it wouldn't be possible to support?
It seems kind of obvious to me that this should be supported, but maybe I am wrong?

(Similar to bbarry's example I would assign the out variable with a ref to a default initialized struct variable inside the lookup if the element doesn't exist)

Cheers

Voidzer0 commented Jun 24, 2018

Thank you @bbarry,
I thought about this solution, but as you say it is a bit odd and also less elegant.
Because the power of returning a boolean is to do the retrival optional in an if.

You'll have to assign it to a local anyway and cannot do that in an if statement.

In C# 7 you can, eg.
if (int.TryParse( "169", out var userId )) { /* do something with userId */ }

See http://structuredsight.com/2016/08/30/c-7-additions-out-variables/

And thus it would be nice if out readonly would be a thing.

if (myLookup.TryGetRef(key, out readonly T value)) { /* do something with value */ }

versus less elegant:

`
ref readonly T value = ref myLookup.TryGetRef(key, out bool result);

if (result)
{
/* do something with value */
}
`

Does anyone know if such a feature is suggested or in the works, or knows why it wouldn't be possible to support?
It seems kind of obvious to me that this should be supported, but maybe I am wrong?

(Similar to bbarry's example I would assign the out variable with a ref to a default initialized struct variable inside the lookup if the element doesn't exist)

Cheers

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Jun 25, 2018

Contributor

I cannot find a suggestion for that in the issue list and cannot say why it would have been overlooked.

Contributor

bbarry commented Jun 25, 2018

I cannot find a suggestion for that in the issue list and cannot say why it would have been overlooked.

@Thaina

This comment has been minimized.

Show comment
Hide comment
@Thaina

Thaina Jun 25, 2018

@Voidzer0 I was proposed #521

There was also #497 and I was mentioned in #497 (comment)

Thaina commented Jun 25, 2018

@Voidzer0 I was proposed #521

There was also #497 and I was mentioned in #497 (comment)

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Jun 25, 2018

Contributor

How does the ability to represent an undefined value with a ref become the ability to state an out variable is a readonly reference?

There is some overlap in this particular use case, but there could be other motivating examples, for example in some interop (this is not a real api, but is inspired by one available in shell32.dll):

[DllImport("shell32.dll")]
public static extern IntPtr GetFileInfo(string path, out readonly FileInfo fi);
Contributor

bbarry commented Jun 25, 2018

How does the ability to represent an undefined value with a ref become the ability to state an out variable is a readonly reference?

There is some overlap in this particular use case, but there could be other motivating examples, for example in some interop (this is not a real api, but is inspired by one available in shell32.dll):

[DllImport("shell32.dll")]
public static extern IntPtr GetFileInfo(string path, out readonly FileInfo fi);
@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jun 25, 2018

Hey @Thaina
similar to @bbarry, I can't follow exactly and do not find this particular suggestion in the linked issues.
We do not need to put an undefined value into a ref, since we can make it point readonly to a default initialized variable/array element, in case no value with given key exists.

I see a similar syntax mentioned in "#497 (comment)" but there it is shortly discussed and then the conversation seems to switch focus to that null problem which we don't have, because we can always make the ref point to something "valid".

But you are right, I should have used "out ref readonly" rather than "out readonly" in my example, since I want to initialize the reference and not the value it is pointing to as out parameter:
if (myLookup.TryGetRef(key, out ref readonly T value)) { /* do something with value */ }

One thing said there seems wrong:

The out keyword already implies ref.

Is not exactly true, because with out the caller provides a reference to a piece of memory on the caller side, where then a value from the callee (Collection) is copied into.
What we would like here instead is to provide just a reference variable (no memory on caller side except for the reference/address itself) as out variable to the callee, which then is made pointing to a piece of memory (value) from the callee (Collection) and thus does not involve copying the value into a different memory location.
Is this suggested in your links?

Voidzer0 commented Jun 25, 2018

Hey @Thaina
similar to @bbarry, I can't follow exactly and do not find this particular suggestion in the linked issues.
We do not need to put an undefined value into a ref, since we can make it point readonly to a default initialized variable/array element, in case no value with given key exists.

I see a similar syntax mentioned in "#497 (comment)" but there it is shortly discussed and then the conversation seems to switch focus to that null problem which we don't have, because we can always make the ref point to something "valid".

But you are right, I should have used "out ref readonly" rather than "out readonly" in my example, since I want to initialize the reference and not the value it is pointing to as out parameter:
if (myLookup.TryGetRef(key, out ref readonly T value)) { /* do something with value */ }

One thing said there seems wrong:

The out keyword already implies ref.

Is not exactly true, because with out the caller provides a reference to a piece of memory on the caller side, where then a value from the callee (Collection) is copied into.
What we would like here instead is to provide just a reference variable (no memory on caller side except for the reference/address itself) as out variable to the callee, which then is made pointing to a piece of memory (value) from the callee (Collection) and thus does not involve copying the value into a different memory location.
Is this suggested in your links?

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Jun 25, 2018

Contributor

@Voidzer0 out does in fact mean ref in C#, specifically it means ref that is not yet assigned to and that the method must assign to before the method completes (in order to allow the definite assignment language contract).

In this particular proposal, a ref return value was permitted, which is similar to a ref parameter, and a readonly attribute was permitted on ref parameters (becoming in parameters) to state that the callee has readonly semantics, and a readonly attribute was permitted on a ref return value to state that the caller has readonly semantics on the return value.

Since this proposal doesn't deal with output parameters, I think you should enter a new issue for out readonly.

Contributor

bbarry commented Jun 25, 2018

@Voidzer0 out does in fact mean ref in C#, specifically it means ref that is not yet assigned to and that the method must assign to before the method completes (in order to allow the definite assignment language contract).

In this particular proposal, a ref return value was permitted, which is similar to a ref parameter, and a readonly attribute was permitted on ref parameters (becoming in parameters) to state that the callee has readonly semantics, and a readonly attribute was permitted on a ref return value to state that the caller has readonly semantics on the return value.

Since this proposal doesn't deal with output parameters, I think you should enter a new issue for out readonly.

@mikedn

This comment has been minimized.

Show comment
Hide comment
@mikedn

mikedn Jun 25, 2018

What we would like here instead is to provide just a reference variable (no memory on caller side except for the reference/address itself) as out variable to the callee, which then is made pointing to a piece of memory (value) from the callee (Collection) and thus does not involve copying the value into a different memory location.

This is basically a reference to a reference, that's not something that the runtime supports.

Besides, it's not clear how that would address your problem - out arguments are required to always be assigned a value in the callee. If the collection doesn't contain the element you're looking for, what value are you going to assign to value in TryGetRef?

mikedn commented Jun 25, 2018

What we would like here instead is to provide just a reference variable (no memory on caller side except for the reference/address itself) as out variable to the callee, which then is made pointing to a piece of memory (value) from the callee (Collection) and thus does not involve copying the value into a different memory location.

This is basically a reference to a reference, that's not something that the runtime supports.

Besides, it's not clear how that would address your problem - out arguments are required to always be assigned a value in the callee. If the collection doesn't contain the element you're looking for, what value are you going to assign to value in TryGetRef?

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jun 25, 2018

@bbarry
Yes out is a reference but to memory on the caller side in which a value from the callee side is copied into.
And this is fine for small value types and reference types.
"out ReferenceType variable" gives a reference to the memory address of "variable" to copy a value into, in case of a reference type the value is the memory address of the pointer the callee is assigning to.
Why shouldn't it be possible to do the same with references to value types?
Just think about a large struct "LargeValueType", then "out LargeValueType variable" gives a reference to the memory of "variable" to copy a value of LargeValueType into, but this value is large and thus costly to copy.
Now imagine we could instead use reference variable as out parameter rather than a value variable via "out ref LargeValueType variable" (which is more or less the same as if we'd have a normal reference type variable).
This would give now a reference to the value of the reference variable (which is just a memory address) to the callee.
Then the callee puts a reference (address) to an internal value (eg. Array element) of LargeValueType into that reference.
Now the caller can operate directly on the large struct without any need of copying it, while doing contains check and retrival in one lookup.
In C++ at least this is doable, which is why I hope it could be done in C# as well.

@mikedn

Besides, it's not clear how that would address your problem - out arguments are required to always be assigned a value in the callee. If the collection doesn't contain the element you're looking for, what value are you going to assign to value in TryGetRef?

We assign a reference to a default initialized value or array element.
See @bbarry's code example which does that.
I would have used a default initialized member variable:
[code]
class MyTookup<T> // where T is a large struct
{
T defaultValue = default; // to refer to in case the element doesn't exist
T[] array;
...
public bool TryGetRef(int key, out ref readonly T referenceVariable)
{
if (key < 0 || key >= array.Length)
{
referenceVariable = ref defaultValue; // element not found assign ref to default value
return false;
}
referenceVariable = ref array[key];
return true;
}
}
[/code] (sorry I don't know how to make nicely indented code snipplets using this forum)
If this makes sense?

Voidzer0 commented Jun 25, 2018

@bbarry
Yes out is a reference but to memory on the caller side in which a value from the callee side is copied into.
And this is fine for small value types and reference types.
"out ReferenceType variable" gives a reference to the memory address of "variable" to copy a value into, in case of a reference type the value is the memory address of the pointer the callee is assigning to.
Why shouldn't it be possible to do the same with references to value types?
Just think about a large struct "LargeValueType", then "out LargeValueType variable" gives a reference to the memory of "variable" to copy a value of LargeValueType into, but this value is large and thus costly to copy.
Now imagine we could instead use reference variable as out parameter rather than a value variable via "out ref LargeValueType variable" (which is more or less the same as if we'd have a normal reference type variable).
This would give now a reference to the value of the reference variable (which is just a memory address) to the callee.
Then the callee puts a reference (address) to an internal value (eg. Array element) of LargeValueType into that reference.
Now the caller can operate directly on the large struct without any need of copying it, while doing contains check and retrival in one lookup.
In C++ at least this is doable, which is why I hope it could be done in C# as well.

@mikedn

Besides, it's not clear how that would address your problem - out arguments are required to always be assigned a value in the callee. If the collection doesn't contain the element you're looking for, what value are you going to assign to value in TryGetRef?

We assign a reference to a default initialized value or array element.
See @bbarry's code example which does that.
I would have used a default initialized member variable:
[code]
class MyTookup<T> // where T is a large struct
{
T defaultValue = default; // to refer to in case the element doesn't exist
T[] array;
...
public bool TryGetRef(int key, out ref readonly T referenceVariable)
{
if (key < 0 || key >= array.Length)
{
referenceVariable = ref defaultValue; // element not found assign ref to default value
return false;
}
referenceVariable = ref array[key];
return true;
}
}
[/code] (sorry I don't know how to make nicely indented code snipplets using this forum)
If this makes sense?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Jun 25, 2018

Contributor

@Voidzer0

(sorry I don't know how to make nicely indented code snipplets using this forum)

You can use fenced code blocks:

https://help.github.com/articles/creating-and-highlighting-code-blocks/

The suffix for C# formatting is cs

Contributor

HaloFour commented Jun 25, 2018

@Voidzer0

(sorry I don't know how to make nicely indented code snipplets using this forum)

You can use fenced code blocks:

https://help.github.com/articles/creating-and-highlighting-code-blocks/

The suffix for C# formatting is cs

@mikedn

This comment has been minimized.

Show comment
Hide comment
@mikedn

mikedn Jun 25, 2018

In C++ at least this is doable, which is why I hope it could be done in C# as well.

Not quite. C++ does not allow references to references, pointers to references, null references. It does allow references to pointers which is kind of equivalent to something like ref string x. And while its standard container classes make heavy use of references they handle cases like TryGetRef rather differently.

I would have used a default initialized member variable:

This would work but it has the disadvantage that your class is larger due to that extra defaultValue value member. There are ways to minimize the impact of this default value (e.g. make it static) but it probably cannot be completely eliminated.

It also seems rather risky - it's a form of "null" that's hard to detect. If someone forgets to check the return of TryGetRef you'll end up with some funny silent bugs.

You really should open a separate issue if you want to follow up, it's a very different situation than readonly ref.

mikedn commented Jun 25, 2018

In C++ at least this is doable, which is why I hope it could be done in C# as well.

Not quite. C++ does not allow references to references, pointers to references, null references. It does allow references to pointers which is kind of equivalent to something like ref string x. And while its standard container classes make heavy use of references they handle cases like TryGetRef rather differently.

I would have used a default initialized member variable:

This would work but it has the disadvantage that your class is larger due to that extra defaultValue value member. There are ways to minimize the impact of this default value (e.g. make it static) but it probably cannot be completely eliminated.

It also seems rather risky - it's a form of "null" that's hard to detect. If someone forgets to check the return of TryGetRef you'll end up with some funny silent bugs.

You really should open a separate issue if you want to follow up, it's a very different situation than readonly ref.

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jun 25, 2018

@mikedn

Not quite. C++ does not allow references to references, pointers to references, null references. It does allow references to pointers which is kind of equivalent to something like ref string x. And while its standard container classes make heavy use of references they handle cases like TryGetRef rather differently.

While I get that, I wonder if this can not be treated like a normal reference assignment, since an out parameter must be assigned to a valid value anyway.
Together with the new way of declaring a var while being out parameter, the reference does not have to exist before it is assigned (and thus is not pointing to nirvana).
Under these circumstances
ref readonly T r = ref lookup.GetRef(key);
is equivalent to:
lookup.GetRef(key, out ref readonly T r);
because assignment of r on creation is assured.
This makes it a normal reference assignment, rather than needing references to references here.
If we would use an unassigned ref local declared before, then this would be required indeed.

This would work but it has the disadvantage that your class is larger due to that extra defaultValue value member. There are ways to minimize the impact of this default value (e.g. make it static) but it probably cannot be completely eliminated.

I wouldn't do a lookup/dictionary if there wouldn't be many structs, so the one struct extra memory is neglectable.
This practice is quite common when using emplacement new in C++.
If I would use classes for all these structs the total overhead is likely much bigger.

It also seems rather risky - it's a form of "null" that's hard to detect. If someone forgets to check the return of TryGetRef you'll end up with some funny silent bugs.

I would never do that, if I wouldn't hand out a readonly ref of an immutable struct. Everything else would be borderline insane I guess xD
The reference is not supposed to be used if the TryGet-result is false anyway.

You really should open a separate issue if you want to follow up, it's a very different situation than readonly ref.

You are probably right, but given that there is a (less nice looking) work around (as @bbarry pointed out) and that reference semantic is only valid for declaring the out parameter in the call where it is assigned,
I guess the likelyhood that support for this is added is rather low I guess.
Thanks to all your comments I realize how niche the requirements are to use this optimization.
Still I did run into a case where this would have been quite useful, given that it is supposed to be exposed as an API for other devs.
I will see if I can make the workaround look a bit nicer for now.

Voidzer0 commented Jun 25, 2018

@mikedn

Not quite. C++ does not allow references to references, pointers to references, null references. It does allow references to pointers which is kind of equivalent to something like ref string x. And while its standard container classes make heavy use of references they handle cases like TryGetRef rather differently.

While I get that, I wonder if this can not be treated like a normal reference assignment, since an out parameter must be assigned to a valid value anyway.
Together with the new way of declaring a var while being out parameter, the reference does not have to exist before it is assigned (and thus is not pointing to nirvana).
Under these circumstances
ref readonly T r = ref lookup.GetRef(key);
is equivalent to:
lookup.GetRef(key, out ref readonly T r);
because assignment of r on creation is assured.
This makes it a normal reference assignment, rather than needing references to references here.
If we would use an unassigned ref local declared before, then this would be required indeed.

This would work but it has the disadvantage that your class is larger due to that extra defaultValue value member. There are ways to minimize the impact of this default value (e.g. make it static) but it probably cannot be completely eliminated.

I wouldn't do a lookup/dictionary if there wouldn't be many structs, so the one struct extra memory is neglectable.
This practice is quite common when using emplacement new in C++.
If I would use classes for all these structs the total overhead is likely much bigger.

It also seems rather risky - it's a form of "null" that's hard to detect. If someone forgets to check the return of TryGetRef you'll end up with some funny silent bugs.

I would never do that, if I wouldn't hand out a readonly ref of an immutable struct. Everything else would be borderline insane I guess xD
The reference is not supposed to be used if the TryGet-result is false anyway.

You really should open a separate issue if you want to follow up, it's a very different situation than readonly ref.

You are probably right, but given that there is a (less nice looking) work around (as @bbarry pointed out) and that reference semantic is only valid for declaring the out parameter in the call where it is assigned,
I guess the likelyhood that support for this is added is rather low I guess.
Thanks to all your comments I realize how niche the requirements are to use this optimization.
Still I did run into a case where this would have been quite useful, given that it is supposed to be exposed as an API for other devs.
I will see if I can make the workaround look a bit nicer for now.

@mikedn

This comment has been minimized.

Show comment
Hide comment
@mikedn

mikedn Jun 25, 2018

This makes it a normal reference assignment, rather than needing references to references here.

That's how it looks in C# but in IL this is still a reference (aka byref aka managed pointer) to a reference. I suppose it may be possible to cheat to an extent - byrefs can only live on the stack so a byref to a byref can actually be passed as an unmanaged pointer. But that's probably a can of worms as you end up with a method signature that's basically lying.

Still I did run into a case where this would have been quite useful, given that it is supposed to be exposed as an API for other devs.

For completeness it may be worth detailing how C++ containers handle this:

  • std::map's "indexer" automatically inserts a default value if the key does not exist and returns a reference to the newly added value. This would work in C# if you're ok with this automatic insertion behavior.
  • If automatically inserting a value is not desirable std::map offers find that returns an "iterator". If it's not the "end" iterator then it can be dereferenced and that gives you a reference to the stored value. This would too work in C# but probably it would be less efficient than in C++ as the "iterator" would likely need to store multiple values (e.g. an array reference and an index).
  • std::vector's "indexer" returns a reference that upon use produces undefined behavior if the index is not valid. In C# you'd probably throw an exception in this case, exactly like arrays do.
  • It's probably worth mentioning std::stack as well - its pop returns void. It can't a return a reference because popping destroys the referenced object. .NET's Stack.Pop shot itself in the foot here as it will generate a copy, unless you're lucky and the JIT inlines it and removes the useless copy.

I'm not aware of a case where standard C++ classes return references to some sort of hidden, null/default-like objects.

mikedn commented Jun 25, 2018

This makes it a normal reference assignment, rather than needing references to references here.

That's how it looks in C# but in IL this is still a reference (aka byref aka managed pointer) to a reference. I suppose it may be possible to cheat to an extent - byrefs can only live on the stack so a byref to a byref can actually be passed as an unmanaged pointer. But that's probably a can of worms as you end up with a method signature that's basically lying.

Still I did run into a case where this would have been quite useful, given that it is supposed to be exposed as an API for other devs.

For completeness it may be worth detailing how C++ containers handle this:

  • std::map's "indexer" automatically inserts a default value if the key does not exist and returns a reference to the newly added value. This would work in C# if you're ok with this automatic insertion behavior.
  • If automatically inserting a value is not desirable std::map offers find that returns an "iterator". If it's not the "end" iterator then it can be dereferenced and that gives you a reference to the stored value. This would too work in C# but probably it would be less efficient than in C++ as the "iterator" would likely need to store multiple values (e.g. an array reference and an index).
  • std::vector's "indexer" returns a reference that upon use produces undefined behavior if the index is not valid. In C# you'd probably throw an exception in this case, exactly like arrays do.
  • It's probably worth mentioning std::stack as well - its pop returns void. It can't a return a reference because popping destroys the referenced object. .NET's Stack.Pop shot itself in the foot here as it will generate a copy, unless you're lucky and the JIT inlines it and removes the useless copy.

I'm not aware of a case where standard C++ classes return references to some sort of hidden, null/default-like objects.

@Thaina

This comment has been minimized.

Show comment
Hide comment
@Thaina

Thaina Jun 26, 2018

I also support reference of reference. It also solve the return of multiple ref or Tuple of ref

Thaina commented Jun 26, 2018

I also support reference of reference. It also solve the return of multiple ref or Tuple of ref

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jul 25, 2018

I use the new tools now on a daily basis and I got now stuck on a compiler behaviour I do not understand.
Now I tried to use the pattern @bbarry suggested to get a "TryGetValue with return references", but I encountered a case where even this workaround doesn't work.
How is this a compile error: (I simplified it as much as I could)

class CompileError
{
bool _someInternalValidCheck = false;
int _resultIfValid = 7;
int _resultIfInvalid = 0;
ref readonly int TryGetResult(out bool valid)
{
    if (_someInternalValidCheck)
    {
        valid = true;
        return ref _resultIfValid;
    }
    else
    {
        valid = false;
        return ref _resultIfInvalid;
    }
}

int _error = -1;
public ref readonly int Test()
{
    ref readonly int result = ref TryGetResult(out bool valid);
    if (valid)
        return ref result;
    return ref _error;
}
}

It says "result" was initialized in a way it can not be returned by reference, but it is a reference already, so why not?
What ever I try to change to make it work, it shows a different error.
But funnily when I simply get a rid of the "out bool valid" parameter it becomes valid, while this out boolean param should have absolutely nothing to do with the issue.
Now I think I am either borderline blind to not see the obvious or might I've run for real into a little bug with the new compiler?
Any help or insightful answer would be much appreciated.
(Btw. I am using VS 2017 (Version 15.7.5 newest), Console Application, targeting .NET Framework 4.6)

Just another thing:
Also I know return tuples are structs (ValueTuple) and can not have refs as members, but it would be so nice if the compiler could forget that limitation in certain moments and allow us to do:

(ref AnyType name1, ref AnotherType name2, YetAnotherType name3) = FunctionReturningTwoRefsAndAnotherValue(); 
// That would make things simpler, there is nothing unsafe if the result tuple is not stored anywhere, but I know that with current ValueTuple implementation it cannot be supported.

Voidzer0 commented Jul 25, 2018

I use the new tools now on a daily basis and I got now stuck on a compiler behaviour I do not understand.
Now I tried to use the pattern @bbarry suggested to get a "TryGetValue with return references", but I encountered a case where even this workaround doesn't work.
How is this a compile error: (I simplified it as much as I could)

class CompileError
{
bool _someInternalValidCheck = false;
int _resultIfValid = 7;
int _resultIfInvalid = 0;
ref readonly int TryGetResult(out bool valid)
{
    if (_someInternalValidCheck)
    {
        valid = true;
        return ref _resultIfValid;
    }
    else
    {
        valid = false;
        return ref _resultIfInvalid;
    }
}

int _error = -1;
public ref readonly int Test()
{
    ref readonly int result = ref TryGetResult(out bool valid);
    if (valid)
        return ref result;
    return ref _error;
}
}

It says "result" was initialized in a way it can not be returned by reference, but it is a reference already, so why not?
What ever I try to change to make it work, it shows a different error.
But funnily when I simply get a rid of the "out bool valid" parameter it becomes valid, while this out boolean param should have absolutely nothing to do with the issue.
Now I think I am either borderline blind to not see the obvious or might I've run for real into a little bug with the new compiler?
Any help or insightful answer would be much appreciated.
(Btw. I am using VS 2017 (Version 15.7.5 newest), Console Application, targeting .NET Framework 4.6)

Just another thing:
Also I know return tuples are structs (ValueTuple) and can not have refs as members, but it would be so nice if the compiler could forget that limitation in certain moments and allow us to do:

(ref AnyType name1, ref AnotherType name2, YetAnotherType name3) = FunctionReturningTwoRefsAndAnotherValue(); 
// That would make things simpler, there is nothing unsafe if the result tuple is not stored anywhere, but I know that with current ValueTuple implementation it cannot be supported.
@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick Jul 25, 2018

Contributor

@Voidzer0 Your issue is not really about readonly ref, it's a general limitation of ref returns. The best source for this I could find is this blog post:

In a general case, compiler has no knowledge of what is going on inside Callee. Conservatively, compiler must assume that any byref parameter or its field may be returned back by reference, so as long as any of the ref arguments are not safe to return, the result of the call is not safe to return either.
Note that in some cases, knowing the types of the ref parameters and the return type, it could be proven that the return can not possibly be referencing data from one of the parameters. However, it was decided to be conservative here for the sake of simplicity and consistency. (considering structs, interfaces and generics, the additional rules could get really complicated).

Here are the actual “safe to return” rules as enforced by the language:

[…]

  1. a ref, returned from another method is safe to return if all refs/outs passed to that method as formal parameters were safe to return.

In your case, the valid variable is not safe to return, which makes the ref returned from TryGetResult not safe to return.

Contributor

svick commented Jul 25, 2018

@Voidzer0 Your issue is not really about readonly ref, it's a general limitation of ref returns. The best source for this I could find is this blog post:

In a general case, compiler has no knowledge of what is going on inside Callee. Conservatively, compiler must assume that any byref parameter or its field may be returned back by reference, so as long as any of the ref arguments are not safe to return, the result of the call is not safe to return either.
Note that in some cases, knowing the types of the ref parameters and the return type, it could be proven that the return can not possibly be referencing data from one of the parameters. However, it was decided to be conservative here for the sake of simplicity and consistency. (considering structs, interfaces and generics, the additional rules could get really complicated).

Here are the actual “safe to return” rules as enforced by the language:

[…]

  1. a ref, returned from another method is safe to return if all refs/outs passed to that method as formal parameters were safe to return.

In your case, the valid variable is not safe to return, which makes the ref returned from TryGetResult not safe to return.

@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jul 25, 2018

Oh this is interesting, I was totally aware that locals are not safe to return, but this is quite around the corner.
However in this case the programmer would need to be plain evil to make it not safe to return.

  1. I mean we are passing an out param not a ref. Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?
  2. We are passing an boolean out parameter to the Callee, which returns an int ref parameter. How could a programmer ever make Callee refer to this boolean with an int ref? Casting would cause an exception and there is no reinterpret_cast in C#. How could that ever be not safe to return?

So I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.
I really hope the future C# versions make the compiler less defensive and more powerful in supporting safe cases.
But in this way I can not make use of @bbarry's workaround to solve my issue and need to do two lookups anyway.

My actual problem looks like this:
I have two hashtables (hashtable1 and hashtable2) containing int-struct-pairs and lookups return readonly refs to the structs.
If a key isn't in hashtable1, it must be in hashtable2 and most of the time keys are present in hashtable1.
Thus I could safe one lookup to hashtable1, if I could have a "TryGetRef" (a by ref equivalent of Dictionary<K,V>.TryGetValue(key, out value)).
Here some code showing the problem:

public ref readonly T GetCorrect(int id) // where T : struct
{ 
  // Compiling fine, but wasteful:
  return ref _hastable1.ContainsKey(id) ? ref _hastable1.GetRef(id) : ref _hastable2.GetRef(id);
  //                     ^  lookup                        ^ lookup                    ^ lookup
  // vs
  // Doesn't compile, but correct and more efficient:
  ref readonly var current = ref _hastable1.TryGetRef(id, out bool exists);
  //                                         ^ lookup
  return ref exists ? ref current : ref _hastable2.GetRef(id);
  //                       ^ NO LOOKUP              ^ lookup
  // => If key exists in _hastable1, only one lookup is needed, but two are required due to compiler limitations
}

I really hoped I could safe this additional lookup in C#, but it seems like I can't.

Voidzer0 commented Jul 25, 2018

Oh this is interesting, I was totally aware that locals are not safe to return, but this is quite around the corner.
However in this case the programmer would need to be plain evil to make it not safe to return.

  1. I mean we are passing an out param not a ref. Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?
  2. We are passing an boolean out parameter to the Callee, which returns an int ref parameter. How could a programmer ever make Callee refer to this boolean with an int ref? Casting would cause an exception and there is no reinterpret_cast in C#. How could that ever be not safe to return?

So I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.
I really hope the future C# versions make the compiler less defensive and more powerful in supporting safe cases.
But in this way I can not make use of @bbarry's workaround to solve my issue and need to do two lookups anyway.

My actual problem looks like this:
I have two hashtables (hashtable1 and hashtable2) containing int-struct-pairs and lookups return readonly refs to the structs.
If a key isn't in hashtable1, it must be in hashtable2 and most of the time keys are present in hashtable1.
Thus I could safe one lookup to hashtable1, if I could have a "TryGetRef" (a by ref equivalent of Dictionary<K,V>.TryGetValue(key, out value)).
Here some code showing the problem:

public ref readonly T GetCorrect(int id) // where T : struct
{ 
  // Compiling fine, but wasteful:
  return ref _hastable1.ContainsKey(id) ? ref _hastable1.GetRef(id) : ref _hastable2.GetRef(id);
  //                     ^  lookup                        ^ lookup                    ^ lookup
  // vs
  // Doesn't compile, but correct and more efficient:
  ref readonly var current = ref _hastable1.TryGetRef(id, out bool exists);
  //                                         ^ lookup
  return ref exists ? ref current : ref _hastable2.GetRef(id);
  //                       ^ NO LOOKUP              ^ lookup
  // => If key exists in _hastable1, only one lookup is needed, but two are required due to compiler limitations
}

I really hoped I could safe this additional lookup in C#, but it seems like I can't.

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick Jul 25, 2018

Contributor

@Voidzer0

Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?

Maybe, but I think it's too late for that. You can return an out parameter by reference, and changing that now would be a breaking change, so it's very unlikely to happen.

I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.

As I understand it, it doesn't have to. It was a design choice made at that point in time and it's one that could be relaxed now.

If this is something you want to see, I think you should open a new issue on this repo specifically about that. And it would probably help if you also had a set of rules for what exactly should be allowed. (Consider e.g. that a if a method takes a ref ValueTuple<int, int> parameter and returns ref int, its result can't be safe to return if the parameter wasn't.)

I really hoped I could safe this additional lookup in C#, but it seems like I can't.

I think you can, assuming you control the hashtable type. Though it would require more complex design: TryGetRef will return a struct that has two properties: bool Exists and ref readonly T Value. Internally, it stores index into your hashtable and uses that to retrieve the value.

Contributor

svick commented Jul 25, 2018

@Voidzer0

Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?

Maybe, but I think it's too late for that. You can return an out parameter by reference, and changing that now would be a breaking change, so it's very unlikely to happen.

I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.

As I understand it, it doesn't have to. It was a design choice made at that point in time and it's one that could be relaxed now.

If this is something you want to see, I think you should open a new issue on this repo specifically about that. And it would probably help if you also had a set of rules for what exactly should be allowed. (Consider e.g. that a if a method takes a ref ValueTuple<int, int> parameter and returns ref int, its result can't be safe to return if the parameter wasn't.)

I really hoped I could safe this additional lookup in C#, but it seems like I can't.

I think you can, assuming you control the hashtable type. Though it would require more complex design: TryGetRef will return a struct that has two properties: bool Exists and ref readonly T Value. Internally, it stores index into your hashtable and uses that to retrieve the value.

@jaredpar

This comment has been minimized.

Show comment
Hide comment
@jaredpar

jaredpar Jul 25, 2018

Member

@Voidzer0

Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?

From the perspective of correctness out is no different than ref. This is just a reality we have to deal with as other languages see out as ref. This is definitely unsafe for ref given our safety rules hence it's also unsafe for out.

I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.

Not sure how to convince you of this. But I can assure you that this is how we view out parameters in terms of ref safety.

Member

jaredpar commented Jul 25, 2018

@Voidzer0

Shouldn't it rather be prohibitted to return an out param (by reference) rather than preventing this very valid code?

From the perspective of correctness out is no different than ref. This is just a reality we have to deal with as other languages see out as ref. This is definitely unsafe for ref given our safety rules hence it's also unsafe for out.

I still don't buy that the compiler has to be this conservative (defensive) in such cases. There must be a way to relax at least some of these limitations to support more valid code.

Not sure how to convince you of this. But I can assure you that this is how we view out parameters in terms of ref safety.

@VSadov

This comment has been minimized.

Show comment
Hide comment
@VSadov

VSadov Jul 25, 2018

Member

It was possible to pass out by reference since version 1.0
As long as it is assigned, it is a normal l-value. Not different from other l-values like ref arguments or fields.
Why would it be illegal to return it by reference?
Especially since you can lose track of out-ness very easily.

ref int M1(out int arg)
{
   arg = 42;
   return ref ReturnsItsArg(ref arg); // <- which part of this can be made illegal and not break things?
}
Member

VSadov commented Jul 25, 2018

It was possible to pass out by reference since version 1.0
As long as it is assigned, it is a normal l-value. Not different from other l-values like ref arguments or fields.
Why would it be illegal to return it by reference?
Especially since you can lose track of out-ness very easily.

ref int M1(out int arg)
{
   arg = 42;
   return ref ReturnsItsArg(ref arg); // <- which part of this can be made illegal and not break things?
}
@Voidzer0

This comment has been minimized.

Show comment
Hide comment
@Voidzer0

Voidzer0 Jul 25, 2018

Thanks a lot for these answers guys.

Don't get me wrong, I am utterly happy to have these features in C# and I am very positive about the direction that has been taken with C# 7+ and what is coming in version 8.
I am raising critics on a high level here, just because I want to maximize the utility of the new language features to get the most out of C# and maybe at some point I can use it just for everything what I am doing.

@jaredpar @VSadov
I am not fimiliar with CLR internals, but when out is just ref from the perspective of other .NET languages then this point is essentially lost.
Still let me elaborate my thoughts.

@VSadov:
Why would it be illegal to return it by reference?

I was just thinking from C# perspective, which could have considered "out" as a higher level construct which is bound to some rules, rules which would prevent it from being referenced in any other way than as "out", which in turn would allow the compiler to make my example code and generally more code a valid and safe option.
Because for me it makes sense, as "out" is something which is meant to be "filled in by the callee" and in this regard there is very limited use of returning out params per reference.
And if returning a reference to it would be prohibitted, then more code could be assured to be safe to return.
At least I hope everyone agrees, that it is far from obvious for a programmer why my code sample wouldn't be valid and this is just because someone could do something which has likely almost no use case, but forces the compiler to assume it to happen eventually.
But I get the technical issue that out is seen as a normal ref and thus makes certain language use and compiler optimizations impossible.

However I still see potential, despite this limitation:

Solution 1)

- If a reference in question to be not safe to return has a different type than (and is not a nested type of) the ref parameter passed to a Callee, then the reference returned is still safe to return (unless other reasons are found).
Agreed? Can a compiler see this as well?
I mentioned nested type to take this problem into account:

@svick:
(Consider e.g. that a if a method takes a ref ValueTuple<int, int> parameter and returns ref int, its result can't be safe to return if the parameter wasn't.)

Even if someone could have bad luck with types matching, in my case I would be sorted by this improvment.

Solution 2)

Furthermore especially now that with C# 7 out variables can be declared right in the place they are initialized (Thank you so much for this devs!), even my original and more elegant suggestion:
bool TryGetRef<T>(int key, out (ref) readonly T outReference)
could be made work, if the compiler would simply enforce that out references are always declared in-place (and thus no dangling reference can exist, since the callee is forced to initialize it).
An out reference could be treated with the same rules as a return reference.
This would avoid the other problem above on its base, because no returned reference need to be returned.
This should be safe and valid either, agreed?

Solution 3)

Well there is yet another idea which seems valid to me and would maybe be the most flexible of all
(bool keyExists, ref readonly var value) = ref _hastable1.TryGetRef(id);
I know this is not possible because ValueTuple<...> is a struct and can not have a ref member, but if it could, it would perfectly solve my problem.
And the only reason why it can't be done, is because ValueTuple is a normal mutable struct, since it was invented before (ReadOnly)Span.
But what would be if ValueTuple would be a ref struct instead?
Then many of the issues could be avoided, since no one can store a value tuple and it could easily hold plain (readonly) references like Span essentially does.
And I would have no need to use an out or ref parameter, because the only reason out parameters were needed, is to compensate the lack of mutiple return values.

Now refs can neither be returned in tuples nor passed by out parameters. Any method returning a ref is thus limited to one ref only and given the limitations for out parameters with respect to return refs, it is basically limited to only return the ref and nothing else and I think this is a rough limitation in quite some cases which would be worth to relax if anyhow possible.

No matter which of the three ideas would be considered, they all should be safe (correct me if I'm wrong) and would fix my current issue.

Which brings me to
@svick
First of all: Thanks a lot for always taking the time to answer questions precisely. It made me always understand quickly.

@svick:
I think you can, assuming you control the hashtable type. Though it would require more complex design: TryGetRef will return a struct that has two properties: bool Exists and ref readonly T Value. Internally, it stores index into your hashtable and uses that to retrieve the value.

I didn't think about this possibility and that would work for me.
But I hope you agree this is quite hacky, has additional overhead and is far from elegant, but a rather smart way to work around the problem indeed.
In the end, if no better solution can be found I might consider doing this, even if it might make my coding hands a bit bloody. ;)

@svick:
As I understand it, it doesn't have to. It was a design choice made at that point in time and it's one that could be relaxed now.
If this is something you want to see, I think you should open a new issue on this repo specifically about that.

If you still think this is the case after reading the other answers and if you think it is likely that things could change in this way, I would be clearly up for that.
My issue is I do not even know what to suggest in a new issue, since as I showed there is an entire spectrum of possible solutions that all would work for me, but unfortunately none of them seems to work currently.
Since I am new to this forum, I would appreciate any help to find the right wording and format for such an issue thread.

Cheers everyone for patiently reading and answering all the questions.

Voidzer0 commented Jul 25, 2018

Thanks a lot for these answers guys.

Don't get me wrong, I am utterly happy to have these features in C# and I am very positive about the direction that has been taken with C# 7+ and what is coming in version 8.
I am raising critics on a high level here, just because I want to maximize the utility of the new language features to get the most out of C# and maybe at some point I can use it just for everything what I am doing.

@jaredpar @VSadov
I am not fimiliar with CLR internals, but when out is just ref from the perspective of other .NET languages then this point is essentially lost.
Still let me elaborate my thoughts.

@VSadov:
Why would it be illegal to return it by reference?

I was just thinking from C# perspective, which could have considered "out" as a higher level construct which is bound to some rules, rules which would prevent it from being referenced in any other way than as "out", which in turn would allow the compiler to make my example code and generally more code a valid and safe option.
Because for me it makes sense, as "out" is something which is meant to be "filled in by the callee" and in this regard there is very limited use of returning out params per reference.
And if returning a reference to it would be prohibitted, then more code could be assured to be safe to return.
At least I hope everyone agrees, that it is far from obvious for a programmer why my code sample wouldn't be valid and this is just because someone could do something which has likely almost no use case, but forces the compiler to assume it to happen eventually.
But I get the technical issue that out is seen as a normal ref and thus makes certain language use and compiler optimizations impossible.

However I still see potential, despite this limitation:

Solution 1)

- If a reference in question to be not safe to return has a different type than (and is not a nested type of) the ref parameter passed to a Callee, then the reference returned is still safe to return (unless other reasons are found).
Agreed? Can a compiler see this as well?
I mentioned nested type to take this problem into account:

@svick:
(Consider e.g. that a if a method takes a ref ValueTuple<int, int> parameter and returns ref int, its result can't be safe to return if the parameter wasn't.)

Even if someone could have bad luck with types matching, in my case I would be sorted by this improvment.

Solution 2)

Furthermore especially now that with C# 7 out variables can be declared right in the place they are initialized (Thank you so much for this devs!), even my original and more elegant suggestion:
bool TryGetRef<T>(int key, out (ref) readonly T outReference)
could be made work, if the compiler would simply enforce that out references are always declared in-place (and thus no dangling reference can exist, since the callee is forced to initialize it).
An out reference could be treated with the same rules as a return reference.
This would avoid the other problem above on its base, because no returned reference need to be returned.
This should be safe and valid either, agreed?

Solution 3)

Well there is yet another idea which seems valid to me and would maybe be the most flexible of all
(bool keyExists, ref readonly var value) = ref _hastable1.TryGetRef(id);
I know this is not possible because ValueTuple<...> is a struct and can not have a ref member, but if it could, it would perfectly solve my problem.
And the only reason why it can't be done, is because ValueTuple is a normal mutable struct, since it was invented before (ReadOnly)Span.
But what would be if ValueTuple would be a ref struct instead?
Then many of the issues could be avoided, since no one can store a value tuple and it could easily hold plain (readonly) references like Span essentially does.
And I would have no need to use an out or ref parameter, because the only reason out parameters were needed, is to compensate the lack of mutiple return values.

Now refs can neither be returned in tuples nor passed by out parameters. Any method returning a ref is thus limited to one ref only and given the limitations for out parameters with respect to return refs, it is basically limited to only return the ref and nothing else and I think this is a rough limitation in quite some cases which would be worth to relax if anyhow possible.

No matter which of the three ideas would be considered, they all should be safe (correct me if I'm wrong) and would fix my current issue.

Which brings me to
@svick
First of all: Thanks a lot for always taking the time to answer questions precisely. It made me always understand quickly.

@svick:
I think you can, assuming you control the hashtable type. Though it would require more complex design: TryGetRef will return a struct that has two properties: bool Exists and ref readonly T Value. Internally, it stores index into your hashtable and uses that to retrieve the value.

I didn't think about this possibility and that would work for me.
But I hope you agree this is quite hacky, has additional overhead and is far from elegant, but a rather smart way to work around the problem indeed.
In the end, if no better solution can be found I might consider doing this, even if it might make my coding hands a bit bloody. ;)

@svick:
As I understand it, it doesn't have to. It was a design choice made at that point in time and it's one that could be relaxed now.
If this is something you want to see, I think you should open a new issue on this repo specifically about that.

If you still think this is the case after reading the other answers and if you think it is likely that things could change in this way, I would be clearly up for that.
My issue is I do not even know what to suggest in a new issue, since as I showed there is an entire spectrum of possible solutions that all would work for me, but unfortunately none of them seems to work currently.
Since I am new to this forum, I would appreciate any help to find the right wording and format for such an issue thread.

Cheers everyone for patiently reading and answering all the questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment