Variable Capture in C#
-
I was browsing around to try to figure out how to make a c# analyzer detect a foreach, when suddenly I was sidetracked by this article:
I instinctively understood why variable capture has to work this way, but it really is surprising right at first.
The basic idea is that:
void Foo() { var actions = new List<Action>(); for (int i = 0; i < 10; i++) { actions.Add(() => Console.WriteLine(i)); } foreach(var a in actions) { a(); } }
Outputs:
10 10 10 10 10 10 10 10 10 10
-
This post is deleted!
-
@Magus OK, so "has to" seems wrong, especially since the code is so stupid and obviously wrong.
But what is the official reason why the 10 lambdas aren't distinct functions? They're apparently all defined with different return values.
-
@Captain the lambdas are all separate objects, but they all have a reference to the same thing that has a single i variable, which is 10 after the final iteration of the loop.
-
It's right in the name. It captures the variable. Not the value.
-
@Magus said in Variable Capture in C#:
I instinctively understood why variable capture has to work this way, but it really is surprising right at first.
There are two options: capturing the name/value mapping at the point that the lambda is created, and capturing the variable itself (the mutable entity). Obviously, C# must be doing the latter.
-
@pie_flavor wrong. It's a value type. It's only ever passed by value.
-
@dkf neither. The loop has its context preserved as an object, because the lambda references locals that need to exist in the lambdas, which are longer lived.
-
@dkf said in Variable Capture in C#:
@Magus said in Variable Capture in C#:
I instinctively understood why variable capture has to work this way, but it really is surprising right at first.
There are two options: capturing the name/value mapping at the point that the lambda is created, and capturing the variable itself (the mutable entity). Obviously, C# must be doing the latter.
For once (no really, how often does this happen) C++ is sane in that you explicitly specify if you capture by value or reference.
-
@Magus The difference really doesn't matter too much when there's only a single captured variable.
-
If "you" thought Haskell's binding semantics and evaluation are tricky... read all the C# corner cases for a laugh.
-
@Magus said in Variable Capture in C#:
I instinctively understood why variable capture has to work this way, but it really is surprising right at first.
Except you probably thought there were multiple closures all referring to the same variable by reference; when in fact the variable is passed by value, but there's only one closure and it's only initialized after the loop!
Edit:
@Magus said in Variable Capture in C#:
@Captain the lambdas are all separate objects, but they all have a reference to the same thing that has a single i variable, which is 10 after the final iteration of the loop.
You've just confirmed my assumption.Nope. My mistake.
-
@Zecc there are multiple closures, though.
-
@Magus Yeah, I read your post better.
-
@Zecc said in Variable Capture in C#:
and it's only initialized after the loop!
That'd be strange. Partially constructing the closure like that is a great deal of work…
(I prefer Java's semantics in this area, which only allow capture of single-assignment variables, and that prohibits this exact scenario. The fix for it untangles the lifetimes and makes the behaviour what everyone would intuitively expect.)
-
@dkf the article goes into a way to fix it, and touches on the fact that foreach loops also used to only show the last value of the iterator when used in lambdas, but were changed to redeclare it on every iteration, and while maybe it would make sense to do this for for loops as well, I think that's the only context where this can even happen. Generally speaking, you want your lifetimes to be extended by the lambda.
C# later added the truly weird stuff like ref returns, which could have some really strange effects in situations like this.
-
Serves me right for posting while I'm distracted with something else. Let's see if I get this right.
I'd personally expect a new closure to be instantiated each time through this line, and its
i
field to be initialized there as well:actions.Add(() => Console.WriteLine(i));
But according to the article, what happens is that a single instance of the class implementing the closure is produced, before the loop. Then instead of operating on a local variable
i
and assigning it to the closure field on that one line, the code is compiled to use thei
field directly as the iterator variable. The single instance of the closure then gets added repeatedly.
This is the unintuitive bit.
-
@Captain said in Variable Capture in C#:
If "you" thought Haskell's binding semantics and evaluation are tricky... read all the C# corner cases for a laugh.
Says the guy who was going full XY in C#, had the solution carefully explained to him, and then proceeded to do the exact same thing again.
-
@Zecc said in Variable Capture in C#:
Serves me right for posting while I'm distracted with something else. Let's see if I get this right.
I'd personally expect a new closure to be instantiated each time through this line, and its
i
field to be initialized there as well:actions.Add(() => Console.WriteLine(i));
But according to the article, what happens is that a single instance of the class implementing the closure is produced, before the loop. Then instead of operating on a local variable
i
and assigning it to the closure field on that one line, the code is compiled to use thei
field directly as the iterator variable. The single instance of the closure then gets added repeatedly.
This is the unintuitive bit.Not quite. There are two classes involved here: a single "display class" to hold the captured variables, and a lambda class with multiple instances that hold a reference to the single display class.
-
@Zecc the options really are to either create the context that needs an extended lifetime when the variables in that context are declared (when the loop is) or each iteration of the loop.
-
@pie_flavor said in Variable Capture in C#:
Says the guy who was going full XY in C#, had the solution carefully explained to him, and then proceeded to do the exact same thing again.
Huh? You mean that time I did a Linq select to try to iterate over an array or list or whatever? And then I forgot how that works and did it again a year later?
Yeah, that's another stupid C# binding and evaluation corner case.
-
-
@Captain said in Variable Capture in C#:
If "you" thought Haskell's binding semantics and evaluation are tricky... read all the C# corner cases for a laugh.
Meh. You just have to remember: C# variables are varioids in the category of endovars.
-
@boomzilla said in Variable Capture in C#:
Meh. You just have to remember: C# variables are varioids in the category of endovars.
If that was a thing I'd probably have a better grasp on its evaluation model. ;-)
-
@Captain said in Variable Capture in C#:
@pie_flavor said in Variable Capture in C#:
Says the guy who was going full XY in C#, had the solution carefully explained to him, and then proceeded to do the exact same thing again.
Huh? You mean that time I did a Linq select to try to iterate over an array or list or whatever? And then I forgot how that works and did it again a year later?
Yeah, that's another stupid C# binding and evaluation corner case.
No, it's a stupid user who doesn't read documentation.
-
@Applied-Mediocrity wow. It's such an isolated thing these days. You were really using your for iteration variable in a lambda?
-
lol you're so triggered you're making it personal.
Fine, so I didn't read the Linq documentation. I'm not a C# developer. I'm a SQL developer who used some C# to write a test harness. Because I wouldn't let some shitty asshole like you stop me from doing what I need to do. As stupid as I am, I do know C# is supposed to be strict and eager.
How many exceptions to the strict eager imperative model does C# have? Can you even name them all? Hell, do they even belong to a single type? If not, name the types. :-DDD
How many places do you need to read to learn them all? Good luck remembering them all. Hell, Haskell's entire spec is like 60 pages long. You can find 60 corner cases in C#.
How often will you be bitten in the ass while you discover them, like the one in this thread?
-
@Captain meh, he's always like that. As far as exceptions to the greedy imperative model go, it's kind of been a slow thing. Linq was a big jump, but they've been fitting in pattern matching and a really good asynchronous programming model, to the point that c# is a pretty interesting mix of things from different programming styles. That's basically what I like best about it: it's generally got whatever is appropriate to solve your problems.
Sure, you have your navel gazers like @Mason_Wheeler who want really weird features and are willing to make their own language with no tooling for their weird edge case, and I for one would like support for the tail call recursion optimization (the tooling team is holding them back or they'd do it).
But overall cases like this one are interesting in part because they're so rare.
-
@Magus Oh, I should not have, but did.
for (Foo i = 0; i <= (Foo)Byte.MaxValue; i++) { if (EnumEx<Foo>.IsDefined((Foo)i)) RadioMessageProcMap.Add( (MessageCategory.Counter, (short)i), (MessageMask.Other, (object Info, object Param) => FormatPlain(Info, Params), (object Info, object Param) => FormatForXml(Info, Params), (object Info, object Param) => FormatExtended(Info, Params, (Foo)i + " counter increased"))); }
Programming confessions is ...
I might add that VS goes ape very often with this sort of code. I posted a similar version a while ago when :@levicki: was ranting about the red squigglies.
-
@Applied-Mediocrity I dislike your code.
-
@boomzilla said in Variable Capture in C#:
@Captain said in Variable Capture in C#:
If "you" thought Haskell's binding semantics and evaluation are tricky... read all the C# corner cases for a laugh.
Meh. You just have to remember: C# variables are varioids in the category of endovars.
I feel like I just read something about cricket.
-
@Captain said in Variable Capture in C#:
How many exceptions to the strict eager imperative model does C# have?
Only one: function body doesn't get executed until it's called.
Sounds like you've had way too much Haskell for your own good and forgot how to do regular programming.
-
@Gąska That doesn't sound right, considering there's TWO examples IN THIS THREAD...
-
@Captain TWO examples of
i
not evaluating until lambda is called - is that what you mean? Oh, and a third example of what I assume was you doing LINQ magic and not enumerating the final result in the end, which prevents any functions inside from being called.
-
@Gąska dude even I can't follow what you're on about.
-
@Magus I'm on about @Captain not understanding that C# isn't some dark magic and the only thing that isn't eagerly evaluated is function bodies before being called.
-
@Gąska Don't know what your shoulder aliens are telling you now. Don't care either. :snark:
-
@Captain they're telling me that you said there are some circumstances in which C# is not always using strict eager evaluation. This is wrong and I'm triggered, because I'm triggered by people being wrong.
-
@Gąska said in Variable Capture in C#:
you've had way too much Haskell for your own good
I've never had any, and I still think I've had too much.
-
@HardwareGeek you need some negative Haskell. You need... PHP.
-
@Gąska I need a lot of things, but that is definitely not one of them.
-
@Gąska said in Variable Capture in C#:
you need some negative Haskell. You need... PHP.
That's a bit like trying to cure a headache by smashing your toes with a hammer.
-
@cvi I mean, it works.
-
@Captain That's a really dumb way of putting it. The C# language is strict and eager. Whatever you write, that's what gets done. That does not constrain function semantics. If you call a LINQ function, it will eagerly create an adapted iterator that has the applied transformation, and when you call MoveNext, it will eagerly apply that transformation on the current element. Any interpretation of these as "lazy" is semantic meaning, not lexical.
There is literally no imperative language that does not allow you to encode semantic laziness; it's reducible to having any kind of function pointer or virtual lookup. Even in ye olde BASIC you could do lazily evaluated design by storing the line number for a GOSUB, although you were probably touched in the head if you did. You're not finding a gotcha about C#, you're finding the world outside of Haskell.
Oh, and it wasn't just LINQ.
-
@pie_flavor said in Variable Capture in C#:
Oh, and it wasn't just LINQ.
Exactly. Any and all
IEnumerable
s are lazy evaluation built on top of eager evaluation. (Which is much, much easier to do than building eager evaluation on top of a lazy-eval default, which is why eager evaluation is objectively the better default.)
-
@pie_flavor Toby faire, C# is somewhat inextricably tied to .NET, with language features directly tied to framework features (e.g. basic types map to framework types,
using
referencesIDisposable
,lock
referencesMonitor.Enter
/Monitor.Exit
, LINQ has its own special syntax that ties toIEnumerable
andIQueryable
). I can understand someone being confused about the demarcation where language ends and framework begins.
-
@Mason_Wheeler said in Variable Capture in C#:
@pie_flavor said in Variable Capture in C#:
Oh, and it wasn't just LINQ.
Exactly. Any and all
IEnumerable
s are lazy evaluation built on top of eager evaluation. (Which is much, much easier to do than building eager evaluation on top of a lazy-eval default, which is why eager evaluation is objectively the better default.)No, I meant doing the same ask-question-and-ignore-the-answer thing for: exceptions, testing patterns, invariance of List<>
-
@error said in Variable Capture in C#:
LINQ has its own special syntax that ties to
IEnumerable
andIQueryable
Which is very rarely used IME. From what I've seen, it's far more common to do LINQ as extension method calls than as the SQL-esque "LINQ syntax".
-
@Mason_Wheeler said in Variable Capture in C#:
@error said in Variable Capture in C#:
LINQ has its own special syntax that ties to
IEnumerable
andIQueryable
Which is very rarely used IME. From what I've seen, it's far more common to do LINQ as extension method calls than as the SQL-esque "LINQ syntax".
True but irrelevant. The fact is that the language has first-class support for LINQ.
-
@error said in Variable Capture in C#:
@pie_flavor Toby faire, C# is somewhat inextricably tied to .NET, with language features directly tied to framework features (e.g. basic types map to framework types,
using
referencesIDisposable
,lock
referencesMonitor.Enter
/Monitor.Exit
, LINQ has its own special syntax that ties toIEnumerable
andIQueryable
). I can understand someone being confused about the demarcation where language ends and framework begins.Again, though, this is a universal property of programming languages. Even most functional languages work eagerly, with only a few being pure enough for it to not matter like Haskell.