- "params Span. This is an easy one, we should be out in 10 minutes."
- "For some value of 8"
- "Stack goes up. Stack goes down. You can't explain that."
- "That killed my monologue." "Did you mean 'evil villain monologue'?" "No one killed me during the monologue therefore I can't be an evil villain." "No one kills the evil villain during the monologue, they just learn the evil plan and then escape to thwart it when the villain leaves the implementation to his minions."
- "Solved the mystery: cat sitting on the spacebar"
In C# 8, we relaxed the name shadowing requirements for lambdas and local functions, but we wanted to revisit the specific implementation strategy and whether we went too far. In particular, we allowed a local function to both capture a variable from an outer scope and shadow that variable in a nested scope inside the local function. This and in the previous sentence is the part we're concerned with: several members of the LDT didn't realize that we were agreeing to this in the original discussions on shadowing.
There are two general models of shadowing in C#:
- Locals shadowing another local within the same function. This is expressly disallowed, and has been since C# 1.
- Locals shadowing a field. This is allowed, but the user can always get to the field version by using
this
. In the same method, it is possible to use the field without thethis
qualifier and shadow it in a nested scope.
Shadowing between local functions while also capturing that local from the outer scope has similarities to both of these models: on the one hand, it's a local variable, even if that variable has been potentially lifted to a field in an implicit closure. On the other hand, it's a variable from a scope outside the current function, just like a field. This duality leads to conflicting resolution strategies, but we ultimately think this has more in common with locals shadowing other locals in the same method than it does with locals shadowing fields.
The rules we wish we had implemented are: if a lambda or local function (or a nested lambda or local function) captures a variable from an outer method, it is an error to shadow that variable. However, these rules have been out in the wild for nearly 3 years at this point, and we don't feel that the level of concern warrants a language change or a warning wave to cover it. If an analyzer wants to implement such a suggestion (in dotnet/roslyn-analyzers, for example), they are free to, but the compiler itself will not do so.
We regret that simultaneous capture/shadowing was allowed, and now it's too late to really fix it. Future features should keep this regret in mind and not use the way shadowing in local functions/lambdas works as precedent.
params Span<T>
is a feature that has been requested since we first implemented Span<T>
in C# 7, and has a number of benefits. Today, users interested in performance with
params
methods need to maintain overloads for common scenarios, such as passing 1-4 arguments (as we do for Console.WriteLine
). These APIs are often not straightforward
to implement: if there was a simple common method they could all delegate to, that API would have been exposed in the first place.
Because of this focus, params Span<T>
is an interesting first for the language: a Span<T>
-based API that is explicitly targetted at the average C# developer, rather than
at someone who already knows what a Span<T>
and why they would want to use it. There are also strong conflicting desires in the space that makes it very difficult to design
a single lowering strategy. We want to avoid allocations where possible, but we also want to avoid blowing out the stack when large numbers of parameters are passed to a
params
method. This is particularly important in recursive code: Roslyn regularly runs into issues when inlining decisions can affect whether we are able to compile some
customer's code, as the compiler is an extremely recursive codebase.
At the same time, we also want to avoid being overly specific in the language specification on how this feature works. We would like to view the details as more an implementation concern, much like we do with how lambdas are emitted, and avoid making strong guarantees about what will or won't stackalloc for what scenarios. As this is more targetted to perf than lambdas are, it's possible that we can't be as glib in this space as we can for lambdas, but it's an aspiration for the feature.
Despite our desire to be less specific, we do think it's important to the initial design, and therefore the conception of who the feature is for, to have an implementation strategy in mind. There are a number of different strategies that have been brought, with various pros and cons:
- Using a custom struct with a specific layout, and then making a span from the start of that struct.
- A shadow-stack that exists solely for the purpose of being able to give out and return stackalloc'd chunks of memory (think RAII stack space).
- Pushing variables onto the stack, then making a span from the start of those pushes (similar to the first approach, but without a dedicated type to be wrapped).
stackalloc
space that can be popped after a function is called.- Not doing any kind of optimized IL in C#, and relying on the JIT to perform the escape analysis and translation from array to stackalloc when viable.
We'd like to see a group from the compiler and the runtime work through these proposed strategies and come up with "the way" or combination of ways that this will work, so we can start to see how the user experience for the feature would actually work.