-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Ref Returns and Locals #118
Comments
If I recall, Eric Lippert blogged about this some years back and the response in the comments was largely negative. I do not like this feature for C#. The resulting code is like an uglier version of C++, and code written with it takes longer to reason about and understand. The use-cases are not particularly compelling, and I have never run into a situation where I wished I had |
Yes, I know very well that mutable structs should be avoided. Still, one interesting use case would be lists of mutable structs. Consider: struct MutableStruct { public int X { get; set; } }
MutableStruct[] a = ...
List<MutableStruct> l = ..
a[3].X = 5; // changes the value of X of the struct in the array
l[3].X = 5; // compile time error If the indexer of the Unfortunately, I doubt that the return type of |
Disclaimer: I work on game engine, so I am probably not the typical user. One use case this could really help us is this one: MyHugeStruct[] data; // we use a struct to improve data locality and reduce GC pressure
// Ideally, we would like to be able to use List<T>, but we can't take ref then
for (int i = 0; i < data.Length; ++i)
{
// Option 1: make a local copy (slow)
var item = data[i];
// Option2: To avoid making a stack copy of MyHugeStruct,
// we have to defer to a inner loop function
MyLoopBody(ref data[i]);
// Option3: using new proposal, that would be much better:
ref MyHugeStruct = data[i];
} We end up making separate function for loop body, and in case of tight loop this can end up being quite bad:
Nice to have:
Extra (probably impossible without changing BCL):
|
What happens with this?
Does the value still exist after exiting |
@paulomorgado You would not be allowed to return a ref to a local variable or parameter. |
@gafter, the only difference between my |
@paulomorgado, the compiler would only let you return a ref to something that it knew was either on the heap or that came from the caller. In my example, the ref inputs to the Choose method were all from ref parameters (or ref locals to ref parameters), so the compiler would conclude that the result of the Choose method met the criteria and would allow its returned ref to be returned. But in your example, the refs passed to Choose were not from the caller nor from the heap, such that the compiler couldn't be sure that the result of Choose was allowed to be returned, and it would error out. |
@stephentoub, forget my What you're saying is that publicly exposed methods can't return |
@paulomorgado, I understand the confusion, but that's not what I'm saying. There would be some rules about what it would be safe to return, e.g.
Forget the implementation of Choose here. Assuming Choose abides by these rules (which the compilation of Choose would enforce), in my example all of the inputs to Choose were valid to be returned, therefore the result of Choose could be returned. In your example, at least one of the inputs to Choose wasn't valid to be returned, therefore the result of Choose could not be returned. The compiler can validate that. |
@stephentoub, what I'm having trouble with is understanding how those rules can be effectively enforced. And a proposal should have an example that works under the proposal. |
@paulomorgado, how does my example not work under the proposal? And why do you believe the rules can't be enforced? |
@stephentoub, either that or I totally missed everything. My understanding is that there's no way the caller can take the result of your |
@paulomorgado, in this example:
left and right are both safe to return because they came from the caller. In this example:
first, second, and third are all safe to return because they all came from the caller. max is safe to return because the only refs it's possibly assigned to are those which are safe to return. If I as a caller wanted to use Choose, e.g.
Both left and right are safe to return because they came from the caller. Therefore all of the ref inputs to Choose are safe to return. Therefore the resulting ref from Choose is also safe to return. I don't need to worry about the implementation of Choose, because the compiler is enforcing all of these same rules on the implementation of Choose. |
But ChooseByTime isn't returning neither left nor right. It's returning the return value of Choose. Noting but the implementation details of Choose is saying its return value is the same as one of its parameters. What if Choose is an implementation of an interface? You're restricting the use of Choose to cases where it works without any safeguards or proof that it's safe. My example shows the opposite. |
@paulomorgado, your example wouldn't compile... the compiler would error out exactly because it doesn't abide by the rules: your call to Choose is passed ref values that are not safe to return, therefore the result of your call to Choose is not safe to return. I'm sorry if I'm not explaining this well; not sure how to convey it differently. |
Ah, maybe this is the point of confusion. The implementation doesn't matter because the compiler assumes the worst: regardless of how a parameter is actually used, if any argument isn't safe to return, then the result of the call isn't safe to return. The compiler is conservative in that regard. |
A conservative compiler that assumes the worst cannot assume the return value of Choose is safe to return. Is this what you're proposing?
|
Why do you say that? What specifically about this example do you believe is problematic? |
Let's try something else: can you construct an implementation of Choose that will compile based on the aforementioned rules/explanations but where the caller of the method could not assume its return value was safe to return? |
No I can't. Because I haven't been able to understand how this would work. I can understand how, in your implementation of Choose, it is safe to return that reference. What I can't understand is why its callers can safely return the same reference without intimately knowing its internals.. |
Because it wouldn't be allowed to return anything that's not safe in the case where the caller assumes it is safe. If the only thing the caller passes in are refs that are safe to return, then what could this method return?
Etc. |
So, this wouldn't be safe, right?
|
No scenario, I don't propose this (as I said above, the pointer |
In C# world ref of struct is not pointer. It is the object itself. It can only be immutable pointer for mutable object And by the standard of |
Reading your comment I think there may be a bit of a terminology difference. Let me elaborate a bit on the operations for a
Attaching When I say At this time though the language doesn't allow for re-pointing of My skepticism aside though, assume we did desire both re-pointing and the ability to guard against it. That would be in addition to guarding against mutating the target (a very good case can be made for this feature). That means logically variables can now be defined as But if we did go with this feature I'm sure we'll spend plenty of time debating |
@jaredpar Ah, sorry, may be I have not been enough clear. I'm not proposing the idea to re-pointer the ref (though, I have never had a need for this, but hey, the idea could grow on me 😄 ) , but to disallow the variable (and the struct behind of course) to be re-assigned entirely. Let me take an example for a struct MyStruct
{
public readonly int X;
public int Y;
}
public void Process(readonly ref MyStruct val)
{
// This would not compile
// In this case, we also disallow the field X to be modified
// while with a regular ref, we could modify it indirectly with the following code
val = new MyStruct();
// We cannot do this
val.X++;
// But we can do this:
val.Y++;
....
} It allows typically to protect the variable + protect readonly fields behind, which is a nice behavior as It allows partial immutability of a ref struct. If the caller of the method is passing this struct, It can ensure that the callee will not be able to modify its readonly fields (or even private ones). On the other hand class MyClass
{
public static readonly MyStruct MyField;
}
public static void Process(ref readonly MyStruct val)
{
// We cannot do this:
val = new MyStruct();
// And also we cannot do this:
val.Y++;
}
Process(ref MyClass.MyField); // It would be possible Hope it makes more sense 😅 |
It'll be difficult to make a solid case for why we need "immutable references to mutable structs", "mutable references to immutable structs", "immutable references to immutable structs", and "mutable references to mutable structs". Seems to be IMO |
@whoisj, I have been abusing structs for years in C#, because they are lightweight objects, interop nicely with native code and allow to lower substantially pressure on the GC. And while using them a lot, I have been facing many problems, not only related to performance but also about their safety-ness. Being a strong users of structs makes me looking forward to more powerful abilities (e.g ref locals/returns... but I have so many other stuffs that would probably roll your eyes 😋 ) and stronger options for safety (readonly, more control on immutability). So yes, the cases you are listing like they are small side things (e.g who cares about safety or immutability?), are for me primordial. I'm not talking from a "nice to have place" but from a "real-world usage" place, as yours, but with a different "is all we need" world if you prefer... 😉 |
@whoisj I see two variants which @xoofx covers Pass byval semantics with pass byref cost which I think was the public static void Process(ref readonly MyStruct val)
{
// We cannot do this:
val = new MyStruct();
// And also we cannot do this:
val.Y++;
// However we can do this as it creates a copy; though introduces a byval cost
var newVal = val;
newVal.Y++;
} For byref where you want to allow modifications to the original but not allow overriding of properties which is the public void Process(readonly ref MyStruct val)
{
// This would not compile
// In this case, we also disallow the field X to be modified
// while with a regular ref, we could modify it indirectly with the following code
val = new MyStruct();
// We cannot do this
val.X++;
// But we can do this:
val.Y++;
} As if you can do the |
+1 to When you deal with large struct and want to avoid copies (think The problem is we can't use any of the |
Well, I have remember there is an argument about Even get only property can modified struct too Maybe we also need |
Gotcha. That is absolutely the intent of |
That's a |
I feel like you're trying to draw a distinction that doesn't really exist though. Their is no real difference between mutating the This example is clearer if you consider method calls. Take for example the following, completely legal, method: struct S
{
public readonly int X;
public int Y;
private int Z;
public void M()
{
this = new S();
}
} In your design would a |
@xoofx for params I could see the semi-muatable working as an // passed by val (or register)
void Process(MyStruct val)
// fully mutable including assignment
void Process(ref MyStruct val)
// readonly struct; no assignment, no method calls (get props allowed?)
void Process(ref readonly MyStruct val)
// must be assigned in function
void Process(out MyStruct val)
// semi-mutable struct, no assignment, but method calls & non readonly assignment fields allowed
void Process(in MyStruct val) However the // value/register struct
MyStruct val0 = val;
// fully mutable including assignment
ref MyStruct val0 = val;
// readonly struct; no assignment, no method calls (get props allowed?)
ref readonly MyStruct val = val;
// semi-mutable struct, no assignment, but method calls & non readonly assignment fields allowed
// Not sure what would match for local |
@jaredpar Yes. The struct itself know its state and is the owner of the implementation details. It disallows the callee to break anything that is not exposed by the public API on the struct, but the implementation in the struct can choose whatever is needed. Again, the As @benaadams is suggesting another keyword would be something like |
The ability to assign to a struct location and call methods without a copy are equivalent operations. Adding protection for one without protection for the other is just lulling developers into a false sense of confidence about their code. This all has to do with how |
Well, If a library A provides a struct (that can be created in a valid state only by lib A using some internal constructors) and and interface with a |
I'm testing ref locals and I've encountered following limitation. I declare variable
I thought I'll try to compare performance of this approach in my tree-like collection implemented on array. Currently when looking for element to add/remove, I have locals like |
@OndrejPetrzilka: AFAIK, that's unsupported. You can't reassign references in C++ either. |
@axel-habermaier: That makes sense, otherwise it would be probably much harder if not impossible for compiler to detect invalid use. I'm not happy about it though. Is it possible to reassign reference in IL? |
It is possible to assign managed pointer in IL, but it is not possible to reset ref local or parameter in C#. Not in C#7. Safety of use is indeed an issue to solve here. |
(Note: this proposal was briefly discussed in #98, the C# design notes for Jan 21, 2015. It has not been updated based on the discussion that's already occurred on that thread.)
Background
Since the first release of C#, the language has supported passing parameters by reference using the 'ref' keyword, This is built on top of direct support in the runtime for passing parameters by reference.
Problem
Interestingly, that support in the CLR is actually a more general mechanism for passing around safe references to heap memory and stack locations; that could be used to implement support for ref return values and ref locals, but C# historically has not provided any mechanism for doing this in safe code. Instead, developers that want to pass around structured blocks of memory are often forced to do so with pointers to pinned memory, which is both unsafe and often inefficient.
Solution: ref returns
The language should support the ability to declare ref locals and ref return values. We could, for example, now declare a function like the following, which not only accepts 'ref' parameters but which also has a ref return value:
With a method like that, one can now write code that passes two values by reference, with one of them being returned based on some condition:
Based on the function that gets passed in here, a reference to either 'left' or 'right' will be returned, and the M20 field of it will be set. Since we’re trading in references, the value contained in either 'left' or 'right' is updated, rather than a temporary copy being updated, and rather than needing to pass around big structures, necessitating big copies.
If we don't want the returned reference to be writable, we could apply 'readonly' just as we were able to do earlier with ‘ref’ on parameters (extending the proposal mentioned in #115 to also support return refs):
Note that when referencing the 'left' and 'right' ref arguments in the Choose method’s implementation, we used the 'ref' keyword. This would be required by the language, just as it’s required to use the ‘ref’ keyword when passing a value to a 'ref' parameter.
Solution: ref locals
Once you have the ability to receive 'ref' parameters and to return ‘ref’ return values, it’s very handy to be able to define 'ref' locals as well. A 'ref' local can be set to anything that’s safe to return as a 'ref' return, which includes references to variables on the heap, 'ref' parameters, 'ref' values returned from a call to another method where all 'ref' arguments to that method were safe to return, and other 'ref' locals.
We could also use ‘readonly’ with ref on locals (again, see #115), to ensure that the ref variables don’t change. This would work not only with ref parameters, but also with ref locals and ref returns:
The text was updated successfully, but these errors were encountered: