C#: IEnumerable, yield return, and lazy evaluation
stackoverflow.blog
stackoverflow.blog
I'm biased as hell because I think it's a wonderful language, but even if you decide it's not for you, getting the basics down helped me better understand how to use such tools across languages.
I am asking this as someone who has dabbled in functional languages (F#/ Haskell) multiple times over the years but, despite liking the elegance of functional code, never reached a level of productivity that came close to what I am used to from imperative languages. Specifically for C#, given the trend of baking more and more of the cool functional stuff into the language (e.g. [1]), I am wondering whether F# still has enough unique features to justify the effort.
[1] see e.g. https://devblogs.microsoft.com/dotnet/early-peek-at-csharp-1...
- Computation expressions make it possible to do ergonomically use value based error and nil handling, with option and result (very awkward to do in C#).
- type providers are a way to generate types at compile time based on some input. They let you do amazing stuff, like compile-time checking SQL against a specific database and automatically generating the correct types for inputs and outputs, or automatically generating types for stuff like XML or JSON files, based on a sample.
- really good type inference and (in my opinion) a better type system than C#'s, mostly wrt generics. You reap all the benefits of static typing, while also typing very very few types, and you get to express some things that just aren't unfeasible to express safely in C#. For example, the compiler will infer things like:
let add a b = a + b * b
//has type
^a -> ^c -> ^d
when (^a or ^b) : (static member ( + ) : ^a * ^b -> ^d) and
(^a or ^c) : (static member ( * ) : ^a * ^c -> ^b)
//which means: a function from 'a' and 'c' to 'd', where either 'a' or 'b' has an add operator, and either 'a' or 'c' has a multiplication operator
you could also explicitly type it out as a type signature, by the way.there's probably more, but these are the things that came to my head. DISCLAIMER: I usually reach for F# anyway, as I feel a lot more comfortable programming functionally compared to imperatively. F# is the only real option for actual FP on the CLR - so I'm a bit biased (and for much of the same reason, I would rather use Scala compared to Java on the JVM, for example). My actual favourite programming language to use is Haskell, so take that as you will :)
(*) : ^c * ^c -> ^b ? ^x * ^y -> ^z
because you could technically define a static member (*) (a: int) (b: float): double = ...
and there's nothing saying that you can't. in that case: (*) : ^a * ^c -> ^b //^a = int, ^c = float, ^b = double
(*) : ^c * ^c -> ^b //^c = int, ^c = float, ^b = double.
and the second one won't work, because ^c =/= ^cAnd the main challenge is that C# is good enough and nobody wants to switch over. As soon as F# evangelist leaves the team, all F# code slowly becomes obsolete and at some time rewritten in C#.
Learn it, absolutely. A lot of comments here are arguing "maybe don't use it in Production", but I think it's a clear case that it is still worth learning even if you aren't "using".
Even though C# keeps adding a lot of F#-inspired features, there's still (and likely will always be) a large gap in the "native" idioms and the cultural best practices. There's still some big differences in encountering the "F#-like" stuff in the imperative wilds of C# than in the tame functional fields of F#. Learning those same tools in the "safer environment" where they feel more natural, learning the idioms that make them truly powerful in F# unlocks ways to think about these tools, that even if you aren't using them day to day in F# and are just applying them to C#'s relatives can still be very useful ways to think.
(A lot of what this article here talks about has a different perspective from F# and especially Haskell where "Lazy evaluation" is a much, more natural way of working than the imperative mindset of "every line does a thing immediately", which is what imperative means.)
Coincidentally, I've made several recommendations in recent PRs that developers take some time in F# lately. I really do strongly believe that learning it makes for better C# programmers in 2022, especially because of all the F# tools now in the language. It's too easy to fall into mental traps you don't even know might exist in LINQ and records and async/await and AsyncEnumerable and ReactiveX and so forth, with the list still growing, without the education of languages like F#. Even if the other comments can give you a lot of reasons why you maybe shouldn't push to use F# in Production, it's still an incredibly useful educational tool at least.
Not GP, but I'd do it even for just the interactive shell on the .net ecosystem.
That said, I think F#'s beauty comes from how much easier it is to model your code, find errors in compile, and easily restructure once you really get the hang of doing proper design and passing functions. The few runtime errors that occur are pretty trivial to debug as well thanks to immutable by default.
It DOES require a major shift in how you approach your problems if you've been coding with something like C# for a long time, and that can take awhile. The tutorials are not great (often assume you're coming from something else), and the unfortunate mix of library support with awkward issues (you can use specter console, but if you follow the docs you'll hate your life) is brutal on adoption.
I do sincerely believe it's basically this diamond being totally ignored though because "well i'll just use C#" is a fair thing to say when at the end of the day we've all got shit to get done and the library docs are for that.
Learning F# though has made me a vastly better coder and I think it's EXTREMELY elegant in how it enforces good practices and lays out its code. It doesn't have all the advantages of something like haskell which is a shame (Higher kinded types and generic lens syntax for records would be AMAZING), but it also doesn't force you to learn monads to print to the screen.
Being able to jump back and forth between styles as needed is VERY powerful, and you don't have to worry about having sideeffects in your first mockup. And once you REALLY get it, having super light and easy syntax for passing functions around lets you do some CRAZY abstraction on your program logic that makes down the line adjustments/additions trivial and intuitive.
This comes from someone who spent nearly a year being TERRIBLE at the language. In part because the onboarding was even worse when i started (worse tutorials, lots of stuff referencing no longer working framework libraries, weird VS Code bugs that made things harder than it should be, me just not being very good at this stuff).
I think that the biggest shame is that F# almost never makes libraries because "Oh there's a dotnet one already", and while you TECHNICALLY can just use it, you quickly can wind up missing out on the neat features that brought you to F#, until you really learn how to easily swap between styles so you can invoke whatever features you want, and then get them back in idiomatic F# and start shoving around your functions.
So in my experience I'd say it makes no sense to start with F# if you're new to the .NET ecosystem.
Other scenarios I've seen it fail :
- enthusiastic team members try to push it, leave the company and nobody wants to touch that anymore
- tooling support being garbage - worked on a project where you had to disable F# projects just to get intellisense working
- second class citizen everywhere - using rider right now and C# experience is > VS (IMO), meanwhile while working on this script found one or two "not supported yet" scenarios and code analysis died randomly through using it (probably type providers)
At the end of the day C# added records, switch expression gets me pretty far, global usings and file scoped namespaces - it cut down on boilerplate and noise, it gives me most of the F# benefits while being well supported and widespread.
The main draws are functions as a first class citizen (with a light syntax for that), low boiler plate, type inference with strong typing, and immutable by default. Oh and I also like the top down coding structure.
The amount of runtime errors i have in F# is minimal (generally because you're connecting to something wrong), because so much is caught in the compiler. Debugging is usually cake, and you're not afraid to really abstract out your code and start passing around functions because the syntax is light and easy.
Jetbrains is the new company practicing “embrace, extend, extinguish”. They royally screwed up the implementation of YARD macros in Rubymine. Their F# IDE launch was a technical failure as well.
Never had any of those problems with Visual Studio.
The best use i've seen of them is for quick, run once to get something, scripts where you're briefly connecting to a complicated dataset and want to rip some data out and do something with it.
As a long standing production tool.....i feel like you wind up running into enough issues with trying to adapt it to data changes that you're just better off not using it in the first place.
As for team members pushing it and leaving...well that's an issue with just about any language adoption, good or bad. We'd all still be on C++ if some people hadn't bothered to learn new toys.
Tooling...i dunno. I've never had something that severe but it's second classness shows. I use vs code/vs and its MOSTLY great, but there's some WEIRD shit that can happen for sure,
There's no doubt C# is more supported than F# and it's a shame because that'll always remain the case, but F# can work on top of C# very well (not so much the other way).
I really do see type providers as almost near inheritance though in the "oh this is cool until you use it and realize it's not". The main draws for me are that it's super easy to set up errorless models with types/records/etc and it's very low boilerplate while still actually having strong typing and immutable by default.
It usually takes people a while to wrap their heads around it and (also hinted at in the article) forgetting to add "yield return" in front of an IEnumerator is a common mistake, but it is an awesome tool to flatten out callback hell or complicated every-frame checks.
It's still a bit of a miss match though. Delay instructions and waiting for other coroutines are implemented as type unsafe yield return values. Still, a pretty good hack. It ends up with a lot less garbage than async/await.
Can you elaborate? I was under the impression that `YieldInstruction` is a well-defined type.
Yielding a final value that isn't a yield instruction is a technique to return a value from a coroutine. You can pull the final return value from the IEnumerator's Current property. Hacks on hacks but it gets the job done.
This is how ValueTask is implemented in newer .NET versions which is a struct, but you can roll your own.
Cancellation is another gotcha with Tasks. The main way to cancel tasks is to use exceptions as control flow. This causes cancellation to be very expensive. You can still use cancellation tokens and check for IsCancelled and finish your task cooperatively but then you're kind of doing thinks in an non-canononical way.
Unity also hasn't done the work to make cheap unity lifecycle delay calls such as Task wait for end of frame, (although they could). The work arounds are expensive.
They are not older or newer, they belong together. IEnumerable has only one method GetEnumerator() which returns an IEnumerator. If you have an IEnumerator, you can iterate over the sequence once [at a time], if you have an IEnumerable you can obtain as many IEnumerator as you need or want.
But seeing how async/await is a thing since like a decade, this is another display of Unity doing things their own way for no good reason.
Both are supported as return types with yield return, in fact if you use IEnumerable, the generated iterator's implementation will be:
IEnumerator GetEnumerator() => return this;This year I began another RTS prototype, but this time using coroutines instead of state machines for doing procedural animation, AI controllers etc.
It doesn't take that long to learn and once you get how it works it makes development a lot nicer. Just a side note, when using these in Unity make sure to look at More Effective Coroutines (free/paid versions available) on the asset store to make sure you're not unnecessarily allocating memory.
'co_yield' makes iterators/generators super easy to write as shown in this article. But also it enables great abstraction/encapsulation for where you want to provide an interface allowing callers to iterate over elements, without exposing the underlying container type.
(Note that 'std::generator' is not yet officially standardized, but with a little work we're using Microsoft's 'std::experimental::generator' across platforms, compiling with clang and gcc. Alternatively, I hear the cppcoro library is pretty great.)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
AFAICT, not really all that close in time:
Python 2.2 (2001)
.NET 1.1 (2003)
PHP (2012-ish, based on RFC date)
JS ES2015 (2015) [though nonstandardized impls in some engines prior]
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
> C# version 2.0 brought iterators. To put it succinctly, iterators let you examine all the items in a List (or other Enumerable types) with a foreach loop. Having iterators as a first-class part of the language dramatically enhanced readability of the language and people's ability to reason about the code.
The implicit assumption in that sentence is that enumerables existed before iterators.
But the non-generic IEnumerable and foreach syntax was in from 1.0 https://docs.microsoft.com/en-us/dotnet/api/system.collectio...
"Yield return" generators is also from 2.0
https://stackoverflow.com/questions/53190388/in-which-versio...
- Enumerator are documented as soon as 1.9.1[1] in 2009[2]
- Enumerable are already present in 1.8.6[3] released in 2006[4], and the matching source code is easily trackable on Github through enumerable.c[5] back to 2005. The Git repository won’t bring any meaningful information before that on this file, due to the import from SVN if I well understand. There is also an enum.c file, but I’m not versed enough in Ruby code base to tell what is it about.
[1] https://ruby-doc.org/core-1.9.1/Enumerator.html
[2] https://www.ruby-lang.org/en/news/2009/01/30/ruby-1-9-1-rele...
[3] https://ruby-doc.org/core-1.8.6/Enumerable.html
[4] https://www.ruby-lang.org/en/news/2007/03/12/ruby-1-8-6-rele...
[5] https://github.com/ruby/ruby/commits/master?after=78425d7e74...
And Generator is present in 1.8 (though I can't determine how early in the 1.8 series). Same basic function, slightly different API (and Generator used Continuations under the hood, while Enumerator uses Fibers, which improved performance and, in theory, implementability in alternative Rubies which didn't all support Continuation, which was one motivation for moving Continuation out of the core language in 1.9.)
After generators were introduced themselves, Generator expressions were added in 2.4, coroutine support with yield as an expression in 2.5, “yield from” to delegate to a subgenerator in 3.3.
So, yeah, that's plausible.
I love both languages, and sincerely hope that C# is able to compile to native at some point in the future; there are already some efforts to compile it to WebAssembly with LLVM [0].
[0]: https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-...
https://visualstudiomagazine.com/articles/2022/04/15/net-7-p...
Is asp.net not just another console app?
Hopefully they actually mean it this time!
In the end, we get a dylib that we can call like any other. A big pain point is debugging the generated native code, which isn't impossible but best to be avoided. I'd recommend having a good logging infrastructure in place and building abstractions that let you debug and test your .NET code without the UI layer.
NGEN has been part of .NET since day one, and it always used a JIT, the only version of .NET that has ever done interpretation was the compact framework and micronet.
In regards to NGEN as AOT solution, there was mono AOT, CosmOS, IL2CPP, Windows 8 MDIL (Based on Singularity Bartok), .NET Native (Based on Midori efforts, initially known as Project N).
And now Native AOT, coming on .NET 7.
Isn't that what Unity is doing?
algorithms weren't really composable unlike LINQ.
Ranges however are, see the examples here: https://mariusbancila.ro/blog/2019/01/20/cpp-code-samples-be...
And they also heavily benefit from coroutines, basically allowing to make ranges from anything with e.g. std::generator<T>
With a 1990s lens it's a revolution, now, not so much.
It looked weird first time but makes sense.
A lazy company would use some dynamic language and tons of cloud VMs and pay 100x for the hosting.
It's worse when the project is outsourced to vendors, since time used for optimization is a wasted cost for vendor, while business is unlikely to pay for optimization unless it's really severe.
We’re using it extensively and coupled with the syntactic sugar that enables its consumption in foreach, it makes the code look as elegant as a traditional synchronous enumeration.
"And to muddy the waters just a little, not all iterators are synchronous; there’s also an IAsyncEnumerable interface (you can loop through it with await foreach)."
By the way, its against the guidelins to tell people to read the article ...
Though I also think IAsyncEnumerable is even better when paired with Rx, too, as some things are easier to express as Observable "in the middle" as an AsyncEnumerable->Observable->AsyncEnumerable "sandwich". (You can sort of think of it as an pullable Event Source being spread into a push-based message bus/pipeline and then being collected back into a pullable Result feed.)
[0] https://www.nuget.org/packages/System.Interactive.Async/
Obviously you can never evaluate the whole thing, but because of the lazy evaluation you can get enumerators and pull out values all day long. Just don’t .ToList() the darn thing.
[0] https://gist.github.com/arlm/6647474#file-sieveoferatosthene...
But I have used the similar feature in Python for years. And I love it.
Is it just me, or are a lot of these languages starting to all feel a lot like each other?
Lest folks think this is a new feature, it was invented by Barbara Liskov in her CLU language, as explained in this 1977 paper: https://web.eecs.umich.edu/~weimerw/2011-6610/reading/liskov... (PDF, see section 4.2.)
The only problem, what if your language adopts an idea which prevents you from having a better API?
As far as I know, coroutine/generator doesn't prevent you from doing anything.
Usually when I look at a language One of the first things I do is look for familiarity: How to map, filter and reduce - and how to work with collections and lazy enumerables, whatever they may be called - IEnumerable, generator, Stream, etc.
It’s amazing how similar many popular languages are these days, at least in terms of developer experience. This doesn’t mean they are trivially interchangeable of course. You can’t write Elixir in an idiomatic C# style and you shouldn’t try to!
They never did, or did I miss it?
> An iterator could query a database, for example—including the unfortunate possibility that it might alter data, or that iterating through it twice might yield completely different results! Some tools (like ReSharper) will warn you against multiple enumeration for this reason.
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
https://stackoverflow.com/a/44505349/213246In fact, you can connect a .NET app to an Oracle Database, invoke a PL/SQL pipelined function and, while that function is emitting results via pipe row inside a PL/SQL for loop, your C# wrapper streams the results to its called via yield return inside a C# for loop.
This operation is technically called "to reify" the enumeration", or "Reification"
From the dictionary:
> reify: make (something abstract) more concrete or real.
That rule of thumb "are there calls to Add after this query" is the biggest one where "you might need List<T>" (and even then maybe you are taking a too imperative approach to something that could be reified differently).
Though often I find what projects really need are ToDictionary or ToLookup, especially ToLookup, a lot of C# developers sleep on that, because it doesn't have an Add and doesn't implement ICollection<T> and you have to search NuGet for a mutable ILookup<T> implementation that you can just `new` yourself, but ILookup<T> is often the best shape of reified results that have multiple iterations of subsets. Take your O(n^2) operations and make them more O(n log n) or whatever if you pick the right key(s) for your subsets.
It's not uncommon for me to start a performance optimization pass on a C# app simply by doing a Find All for ToList and removing every single use.
Interesting. I have recently replaced a List<int> with Hashset<int> This is a (IMHO, underused) part of the standard library https://docs.microsoft.com/en-us/dotnet/api/system.collectio...
C# Devs tend to know List<T>, Dictionary<K, V> but not HashSet<T>.
The reason for that replacement was simply: "we have a lot of 'business category ids' in this opt-in list, all we do is ask 'is the current id in the opt-in list or not?' so duplicates have no meaning, and ordering is possible but of secondary importance ... that's mathematically speaking, a 'Set' not a 'List'. "
So using Hashset reduces the check time from O(n) to O(1) and otherwise it's pretty much a drop-in replacement: Same initialiser, same 'Add' and 'Contains' methods, also found in 'System.Collections.Generic'.
I'm looking at ILookup<K, V> and it looks fairly similar to Dictionary<K, IEnumerable<V>> When would I go out of my way to use it instead?
ILookup<K, V> is basically the "in memory join and group by" tool. If a query sort of naturally ends in a GroupBy because you want things sorted/grouped by an enum field or a foreign key field, but you still want all of the items in the groupings rather than just aggregate stats on the groupings, that's often where you want ToLookup instead of GroupBy to reify your query.
In general, Enumerable.Join and other in memory "join" operations (as opposed to the IQueryable's Join which more often than not gets sent to a highly tuned query planner in an SQL server somewhere) are often O(n^2) even at best. A lot of developers do notice this, that joins are expensive when performed in memory, but simply rewrite them by hand as nested foreach loops which still has the same O(n^2) worst cases, but it's hand-written so developers feel more comfortable with how slow it is. On the other side, in C# a lot of developers tend to naturally write these kinds of "in memory join operations" as nested foreach loops anyway (or a foreach loop with a big Enumerable.Where query inside acting as a second foreach loop on the other data source), just because they've always done it that way, and might not even think of them as "an in memory join" much less think to replace hand-written loops with something more LINQ-like anyway. ToLookup is generally the handiest tool to replace both sorts of O(n^2) "joins" the Enumerable.Join operations that do their best but don't know your model domain and don't have the query planner and indexes of an SQL database to fallback on, and the hand-written nested loops: you "index" the "right side" of your join (the inner foreach/if or Enumerable.Where in hand-written loops) on your join key with ToLookup.
As an example, perhaps: you've got a list of items in a shopping cart, which is only in memory in session data (so "naturally reified"), and a table of coupon/discount "rules" that apply primarily based on "item category ID" (with a Category FK table in between Item and CouponRule in your normalized DB structure). The most naive way of writing that would be something like:
foreach (var item in shoppingCart) {
var couponRules = db.CouponRules.AsQueryable()
.Where(rule => rule.CategoryId == item.CategoryId);
foreach (var rule in couponRules) {
if (RulesService.DoesRuleApply(item, couponRule)) {
RulesService.Apply(item, couponRule);
}
}
}
Written that naively that's at least one database query for every item in the shopping cart, which is obviously not optimal so a developer's first instinct is going to be to reify that query somewhere. The simplest, equally naive reification is often this (though often by this point "hidden" as a cache inside "RulesService"): var allCouponRules = await db.CouponRules.AsQueryable()
.ToListAsync();
foreach (var item in shoppingCart) {
var couponRules = allCouponRules
.Where(rule => rule.CategoryId == item.CategoryId);
foreach (var rule in couponRules) {
if (RulesService.DoesRuleApply(item, couponRule)) {
RulesService.Apply(item, couponRule);
}
}
}
(Again, it's maybe more obvious written this way, but often at this point what I find is that allCouponRules cache moves inside RulesService and that Where and foreach get "hidden" inside RulesService at this point, making it even tougher to optimize this situation many times in practice because you don't see all the loops in one place by that point.)To find possibly applicable coupon rules this does an O(n) search over all rules for every single item, even though we already know we primarily only care about the subset that joins on Category ID. This is where handwritten O(n^2) worst cases often hide. If you grow to have a lot of item categories with a lot of coupon rules that might be a huge list in memory. This is where ToLookup most shines as your easily cacheable reification to speed up exactly these sorts of joins.
var couponRulesByCategoryId = await db.CouponRules.AsQueryable()
.ToLookupAsync(rule => rule.CategoryId);
foreach (var item in shoppingCart) {
var couponRules = couponRulesByCategoryId[item.CategoryId];
/* most implementations of ILookup return Enumerable.Empty<T>() for missing keys so you don't even need to guard this lookup with a ContainsKey check */
foreach (var rule in couponRules) {
if (RulesService.DoesRuleApply(item, couponRule)) {
RulesService.Apply(item, couponRule);
}
}
}
That couponRulesByCategoryId lookup is itself O(log n) like most Dictionary/Set lookups, bringing the overall worst case here more in line with O(n log n) worst cases than O(n^2).I find so many patterns like this where cached ToLookup "indexes" in memory/caches greatly speed up operations. There's still further things that might be optimized with ToLookup to explore even in this example: you could "index" the "left side" here too (shoppingCart.ToLookup(item => item.CategoryCode) and maybe the "RulesService" can change to take IEnumerable<Item> instead of item at-a-time. Maybe lurking in DoesRuleApply() filter is a secondary key to "index" on, perhaps as a ToLookup to a tuple key instead (in turn saving even more rules that don't need to be checked individually and can be ignored as a group). Once you start finding these sorts of patterns I feel like you start to see them everywhere in C# and that's a big part of why I think ToLookup is an unsung hero of C# optimization work.
I do wish some version of the mutable MultiValueDictionary<K, V> which implements ILookup<K, V> finally makes into System.Collections.Generic properly instead of being an "experimental" or "labs" nuget package away. You can fake it with Dictionary<K, List<V>>, but that's a lot more awkward to work with (including a lot of ContainsKey/Add(new List) dances) and doesn't naturally implement ILookup<K, V>. I think an obviously mutable version would make a lot of developers more used to using ToLookup even if there are very good reasons that the default implementations are all immutable.
I believe, the best implementation of `IEnumerable<T>` we have today are Rust's iterator types which provide the same functionality but with performance comparable or even better than manually written imperative loops.
.NET's object is very different from Box<T> from my understanding. My rust understanding is rusty (not sorry), but .NET's object is much closer to &mut than Box<T> as it is the top type in the type system. There's the reference type/value type split "below" object. Reference types like `List<T>` are always heap allocated (like Box<T>), but value types like T[] might not be and may be stack allocated.
Small enough arrays can reify entirely in stack space (function locals) rather than heap space and with some uses of ToArray() the JIT compiler is smart enough to make the right call, whereas List<T> always creates heap allocations and ToList() unavoidably churns at least Gen 0 of the garbage collector that tiny bit more.
IEnumerable<T> doesn't give you enough information of the implementation details to know if that interface is implemented as a reference type or value type. Some implementations of IEnumerable<T> are non-heap allocated value types (structs)! Often many implementations in performance critical areas of the framework are structs.
First and foremost, '&mut' in Rust is a mutable reference. The C# alternative is just 'ref'. Both point to specific addresses. 'Box<T>' OTOH is a handle for a struct allocated on a heap, just like C#'s one is implementation-wise (except in C# they are further managed by runtime but that's besides the point).
Now regarding the arrays - 'T[]' is strictly object withouts ifs and buts. There is no "short array" optimisation that would substitute array type with stack-allocated inline buffer. If you want this, you have to write it manually. See: https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6b...
The last but not least, if the method signature accepts an argument by its interface type e.g. 'HandleEnumerable<T>(IEnumerable<T> values)', then the IEnumerable-implementing struct will be boxed before method can be called. C#/.NET doesn't do full generic monomorphization like Rust. Instead, for class-type generics a common '_Canon' method body will be JITted which will also implicitly accept generic-type arguments to correctly dispatch on an actual <T> of the method. OTOH, for structs, all method calls will be fully monomorphized per each struct type. This means that you can avoid struct boxing when dispatching by interface by rewriting method signature to 'HandleEnumerable<T, TEnumerable>(TEnumerable values) where TEnumerable : IEnumerable<T>'. In this case, unless you cast your struct to an interface type explicitly beforehand, the struct will not be boxed.
For the sake of simplicity, this explanation scope doesn't include advanced optimisations done by Tier-1 JIT compilation such as guarded de-virtualization, more aggressive inlining that can potentially elide boxing, etc.
JIT never did proper escape analysis for object stack allocation except for the small proposal prototype implementation which the runtime team decided to put away for better times: https://github.com/dotnet/runtime/issues/11192 It is never enabled and will not work the way you expect it to even if you try.
Huh, now that I think about it, this account might be posting messages generated by GPT-3 and I'm just wasting time.
What you linked is an issue regarding storing Reference Types (classes) in stack space. Again, there is a difference in .NET between a Reference Type and Value Type, and this is a fundamental dichotomy going back to the earliest days of .NET.
I don't appreciate the ad hominem attack and suggest that you google any further follow ups yourself.
In addition, regarding Span<T> and other ref structs, they can hold any type and have no restriction to T : unmanaged. That's because objects by their nature of being on the heap are guaranteed to outlive the lifetime of any ref struct and the runtime supports tracking GC references held like this anyway.
Jon Skeet also has a (now old) blog series called edu-linq that basically implements linq just using generator blocks and it really makes you think about stuff like validation of arguments vs actually doing the operation. It’s very interesting. A couple of caveats in there.
They forget that 99% of C# developers use full fledged IDEs that gradually teach you new language features by suggesting useful refactorings.
The only important C# specific language features that takes effort to understand is LINQ that came out in 2007 and async/await that came out in 2012.
Any C# programmer who has been in coma since 2012 can get up and running with the more recent language features in just a few hours.
I've had trouble running this in Firefox recently, but Chromium seems fine
Most people just use the .ToListAsync() and .ToArrayAsync() "materialization" tools which I believe use IAsyncEnumerable<T> under the hood, but you can use IAsyncEnumerable<T> directly if you want.
Generally though, EF Core's support for IAsyncEnumerable<T> is mostly known for the confusion it causes with "Ix" (Interactive Extensions; the System.Interactive.Async nuget package that adds a bunch of LINQ extensions for IAsyncEnumerable<T> to the `using System.Linq.Async;` namespace) because if you install that package and have such using statements there's overload conflicts between IQueryable<T> and IAsyncEnumerable<T> for many LINQ operators and you have to explicitly add .AsQueryable() to almost all of your EF Core LINQ to make sure to avoid those operator conflicts and force a cast to just IQueryable<T>, not both IQueryable<T> and IAsyncEnumerable<T>.