-
Notifications
You must be signed in to change notification settings - Fork 1k
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: modifier (perhaps "static") for local functions, to explicitly disallow captured state semantics #1277
Comments
This was brought up during a PR review by @stephentoub It is way to easy to make that mistake. |
side thought: should the same modifier also be allowed for anonymous methods and lambdas, in the same way that
which would again mean that no implicit state capture is permitted inside the lambda expression or expression body. (again, not sure that |
See also: dotnet/roslyn#23970, #302 |
@mgravell This can also be solved using attributes and analyzers with #794, however, we still can't apply attributes in the context of anonymous methods and lambdas but with this it's going to be possible. Alternatively, if a language feature is introduced to support this I'd love it to be extended further and have C++ style capture list due to the flexibility it provides as described here. However, it seems like they already thinking about lambdas and |
In my opinion it shouldn't, this approach should be all or nothing for two reasons:
|
@eyalsk interesting related ideas, thanks; the "capture list" approach looks nice, but is complicated; IMO C# is already bloating fast enough, and an "all or nothing" is probably a very pragmatic reasonable approach - especially since "all" is already the implicit default. In particular, the "capture list" approach is complicated by the fact that capture closures are already scoped and nested by declaration location. It may be nearly impossible to implement such that you're not capturing something extra by accident (where A captures x+y and B captures y+z - either A may also be silently capturing z, or B may be silently capturing x - in terms of fields on the state machine and lifetime) Likewise, personally I'd be just fine with disallowing |
Related issue: #275. Though the scope of this proposal is far narrower, and likely far easier to implement, than that other one. |
Capture lists were suggested before and the team has rejected it. |
@Joe4evr Doesn't mean it can't be reevaluated but I understand the likelihood of it to be rejected again. |
The use of |
Can static local function access instance members of current instance? or will it be in static context? my suggest is that local function (whether static or not) in instance method can access instance members. but local function in static method cant access instance members (pretty obvious)
The reason to allow this is because we don't confuse fields with local variables (if you follow good old naming rules), thus there is nothing to worry about. |
The main problem is with avoiding accidental capture of a variable in the parent method thus causing allocations. I am not sure the above helps with that. Being able to make a local or annon method static means in the annon case the delegate can be single instance and cached. That the compilier can detect the capturing of variables and you can save a register as you don't need to pass a ref to "this". It seems like the solution that adds no keywords, uses preexieting language semantics that are well known (static methods) and covers 90% of the situations. Also will allow for a better perf profile... Seems like a win, win, win |
I really don't like the idea of a local function having the |
Static local methods wouldn't have the ability to access instance fields or properties and methods would they? If they did then they would need the "this" ref passed in and I agree that would be nasty. |
Local functions don't incur the same penalty for capture as delegates do. They don't cause any allocations, the enclosed variables are lifted to a struct and the function itself passed that struct with a public int M(int x, int y) {
int add() => x + y;
return add();
}
// translated into:
private struct DisplayClass {
public int x;
public int y;
}
private static int add(ref DisplayClass ptr) {
return ptr.x + ptr.y;
}
public int M(int x, int y) {
DisplayClass ptr = default;
ptr.x = x;
ptr.y = y;
return add(ref ptr);
} |
They do if they are async, even the presence of an async local function causes an allocation even if its not called; if it does any capturing dotnet/roslyn#18946 |
Sorry my fault I should have specified that async was my major use case for them. |
Why would you pass the same exact value instead of capturing it though? Isn't this what the compiler already does for you? You can be consistent and use the "obvious" name without declaring it twice. And if the parameter has a slightly different meaning than the original one, then it might as well have a slightly different name. If capturing the int is slower than passing it again then I think the right approach would be to try to optimize this rather than let us duplicate all the parameters with the exact same names. |
Because it causes allocations. |
I see. Yeah, there are several scenarios that cause local functions to use the delegate-style capturing rather than a struct, presumably because they all involve the potential lifting of that state into a value that must be able to persist beyond the lifetime of that method invocation. Another example is when a method happens to also create a delegate that encloses over part of the method state, even if that enclosed state doesn't overlap with the state of the local function. |
I am currently investigating ways to leverage the RoslynClrHeapAllocationAnalyzer for practical performance purposes. For example, an analyzer that recognized |
I get that an analyzer is nice but why not just stop it happening at all with the static word? It seems a little like the new get out of jail free card is "add an analyzer". I tend to agree that sometimes that makes sense to stop language bloat but in this case a well known and understood concept like "static" is really not an addition to the library. Infact I would go so far as to say without static it's odd, any other method can be static (getters/setters, constructors, methods) so why not these two types of methods? |
I think the idea there is that an analyzer can be done today whereas any language feature proposal has to endure the LDM process which is infinitely more protracted. It also fits pretty well in this case since the expected behavior of the language feature is a warning/error if you try to capture in one of these local functions. Regarding |
The definition would surely be as is today... There is no ref to this passed in and the method has access to it's stack as per any static? |
That doesn't imply anything about the locals, though. Static methods can freely capture their own locals and that doesn't involve any |
Method is just like a regular static method; its just scoped to the parent method; so only has access to static variables and its own parameters (no access the the locals of the parent method) |
@benaadams Agreed, I just don't think that the current definition per the spec implies that relationship. |
but this is not the well known meaning of "static"
|
I agree that |
what about |
Sorry, I'm slow. Where's the ambiguity in the term |
Perhaps I am slow to understand, but no, I read the proposal differently. It suggests that So for example: static int staticField;
static void Outer()
{
var local = 7;
staticField = 42;
Inner();
static void Inner()
{
local = 8; // disallowed?
staticField = 43; // disallowed?
}
} My understanding of present-day However, according to the proposal, at least one if not both assignments become illegal because they access outer state. That's a rather different outcome. I've seen the proposed re-phrasing of the definition of (It would also mean that C# diverges even further from the underlying VM's terminology, which isn't forbidden but will become painfully visible e.g. when doing reflection). If we're talking about "divorcing all state" then I'd rather see a new, dedicated keyword |
public class C {
static int staticField;
int instanceField;
public void Outer() {
int local = 0;
static void Inner() {
local = 123; // fail, can't capture locals
instanceField = 456; // fail, can't capture this
staticField = 789; // fine, doesn't require capturing anything
}
}
} |
I am not convinced you shouldn't be able to capture this. As that isn't a "capture" in the normal sense of the word and very unlikely to be the perf hit most worry about and is driving the feature? I could also be wrong ;) |
To add to the above, there is a case to be made that static in this context would mean static to the enclosing function but not to the instance. However, I'm not going to be the one making that case. ;) |
@HaloFour, @jnm2: I'd actually be fine with that if that were the end result BUT please note that this doesn't require a redefinition of what
The meaning of the modifier (
Again, By all means, go ahead with the proposal 👍, but please don't specify this new feature in a way that gives |
By disallowing captured state, you disallow capture of |
@jnm2: I agree with what you're saying. What I am objecting against are alternate definitions such as "divorce all state" (which would include static state btw.) or "disallow capture" being affixed to If you want a keyword that divorces all (captured) state, call it Note I've come to agree that |
I follow, though I'm not sure if you're right. To get to the point: how would you define the current meaning of |
@jnm2: See above. I don't have the spec at hand but my understanding of (What this does not answer is whether a local method is logically a member of the enclosing type at all. If not, my definition would no longer apply precisely.) |
@stakx You could expand that definition to, "this member does not capture instance or local state." Then Like variables being added to Newtonian physics that had been negligible in previous contexts, and so were not talked about until relativity came around. |
@jnm2: In my eyes, that wouldn't be an expansion of the existing definition, but a replacement, because I simply don't perceive Sure, I can get used to a new, different, but also more general meaning for I guess I've made my argument sufficiently clear, if it's at all taken into account in any form during the design of this new feature, then I'm more than happy. Either way, I won't raise any further objections. :) |
Adding that it "doesn't capture instance or local state"; means it still doesn't have anything to do with capture :) |
@benaadams: No. But you've now associated it with that term, with previously was an altogether separate thing. If I added to the definition of PS: That being said, I don't object to that small addition. (As long as you don't define static as "[...] static [...] hides its enclosing scope" which I'd find rather inaccurate. 😄) |
A static method is a method which can only access by name:
If you consider |
@jnm2: 👍 Totally happy with that except for one tiny nitpick: "other static members defined |
@stakx Is that necessary though? Local functions aren't considered members, are they? Sounds like the adjective |
It's not like we don't have keywords that are used in different contexts in the language, e.g., Just my 2c. 😄 p.s. I tend to miss things at times so if I do please enlighten me. 😉 |
@eyalsk My gut feeling is that it's not necessarily a good thing when that happens, if that's happening. But you could be right. |
So if we're not agreeing on |
Honestly... the more i use c++ the more i just like capture lists. I think it's good that C# defaulted to 'allow capture'. But i think it would be fine to be able to just say The fact that it's only two chars to prevent any capturing at all is really nice for me... |
See #1565 |
Woohoo, this appears to be in the 8.0 beta in VS 16 preview 2, and it works great. |
I'll go ahead this issue since it is a duplicate (and already implemented too ;-) ). |
A recurring problem I've seen a lot since the addition of local methods - and in particular in
async
optimizations (and not just me) is that it is very easy to accidentally introduce shared state into a local function when you really didn't want to. For example:This is a common intent when using local functions to enclose a method, without actually intending to allow shared capture state.
Currently, the only "fix" here is to move the method out:
This works, but it pollutes the space and loses code locality and expressiveness. I actually find myself routinely copy/pasting local methods out and back in again to check that they still compile - to see if I've leaked state.
Proposal: allow the
static
modifier on local functions, to express the intent to divorce all state. I'm not 100% sure thatstatic
is the right word, but it seems close. The effect would be simply to enforce that variables (parameters and locals) declared outside thestatic
local method are not in scope inside the local method, essentially as though the local method had been declared outside the method. The "local method", then, is simply here for code cleanliness reasons.This has the effect of avoiding unintended captured state machinery.
Example:
There is an outstanding question of whether it is acceptable to share the
this
instance but nothing else; this ambiguity is perhaps the biggest thing against thestatic
keyword.Additional possibility: because of the lack of state, it allows re-use of the most obvious/correct variable names. I've seen lots of examples essentially like:
but this proposal allows the "obvious" names to be used consistently:
The text was updated successfully, but these errors were encountered: