Adding a BETTER way to loop in C#
Вставка
- Опубліковано 16 жов 2022
- Check out my courses: dometrain.com
Become a Patreon and get source code access: / nickchapsas
Hello everybody I'm Nick and in this video I will show you the secret extensibility points of C#, which will allow us to bring an AWESOME feature from Kotlin into the language. Not only we will make it work as if it was an official C# feature but we will also make sure it is really performant and fast.
Workshops
dotnetdays | 20 - 22 Oct | bit.ly/dotnetdays2022workshop
NDC Minnesota | 15 - 18 Nov | bit.ly/ndcminnesota2022workshop
NDC London | 23-27 January 2023 | bit.ly/ndclondon2023
Don't forget to comment, like and subscribe :)
Social Media:
Follow me on GitHub: bit.ly/ChapsasGitHub
Follow me on Twitter: bit.ly/ChapsasTwitter
Connect on LinkedIn: bit.ly/ChapsasLinkedIn
Keep coding merch: keepcoding.shop
#csharp #dotnet
A couple of things I didn't cover in the video. Firsly if you are actually going to use this, make sure you also add an analyzer to warn or error on inappropriate usecases. Also, if you wanna support descending ranges for example foreach(var i in 10..5) you'd need to add a different check in the constructor and MoveNext() to support descending iteration.
You also need to implement Reset() method to make an iteration variable reusable
how do you implement for loop with custom step like:
for ( int i=0; i
@@nanvlad You are theorically right. Unfortunately Microsoft gave up on this a while ago, as the Reset method of enumerator generated with yield throw NotSupportedException. Therefore you cannot rely on Reset unless you known which enumerator you are dealing with and how it is implemented.
More importantly ref struct won't work in async code. I wonder how slow would be class with pooling.
#Edit just change it to plain struct. Even .net List uses non-ref struct Enumerator so shouldn't matter for performance.
@@maunguyenvan9296 You can make an extension method on ValueTuple. So you can say: "foreach (var i in (0..50, 2))".
Or you can extend ValueTuple, based on how ranges in Haskell work: they are of the form "[first, second .. last]". So that would be: "foreach (var i in (0, 2..50))"
This is why coding is a form of art… I knew about all these features but never considered putting them together in this way
In your defense the usability of a range with for loops is not something you should have to hack in yourself. It should come with the language.
It sure can be such a form of art, especially when you get into clean coding and trying to come up with ways to make the simplest syntax
If we all coded in our own forms of "Art" we would be back to the 90's/00's with terrible code rampant everywhere that noone understands and doesn't want to touch.
@@kylekeenan3485 or you could end up with a github community with guidelines and standards that everyone follows like we have now. A form of art that people are influenced by and mimic.
Art is influenced by each other, not every single person will want uniqueness on every single thing we create. I mean is it not true that while poetry and drawings are limitless, there's really only a select number of ways to create things that make for a better product than other techniques that give the same result. So as with coding, and as it is today, everyone can create code in whatever way they choose, but because it's so artsy, you can write several ways to make the same hello world application, but do we? Nope, we always use the same examples everywhere with the same standards, etc, and we naturally, and typically, want to use the simplest path with the cleanest looking code. So it's just a matter of exposing the art for others to be influenced by.
@@EddieVillamor Your talking about a dream. We see people moaning about how agile doesn't help and that is clearly documented and has standards, guidelines, training courses galore. If people under those circumstances can't get it right, then following some unique (art) coding style will be a nightmare. Coding in your own unique style that's unusual and different to standard practices will just cause time delays and tech debt. It could in theory with proper guidance and teaching become useful as you stated but it never does. If people want to deviate from standard practices then it's probably best to keep it in their own personal projects. If you do discover something generally better then write a book 😁.
As much as I love the simplicity of the syntax, unless it's implemented in the language itself, I wouldn't use it because it is unexpected. Anyone else coming to the codebase would be confused seeing that and not finding anything about it in the official docs.
In a similar vein, I love playing with the implicit operators but they have the downside of hiding what's really happening underneath. You've got to weigh up the benefit with the cost of abstraction as with anything.
It never ceases to amaze me how much effort people will spend to save a tiny bit of effort.
hahahahahhaahaa
well this is just an example, you could also provide enumerators for your own types and make them behave this way if appropriate in that situation
This is what we call a programmer move :^)
true
Its a cool feature that could make DSL even more represented in code. But "unexpected" C# code should be maintained, documented and is not junior-friendly.
Agreed. It is a bit too clever and the benefits are IMHO not that big.
This is a feature that should be baked into C#, I don't really agree with add extension methods for this.
I don't get why ranges don't just return an enumerator
@@fredriksjoborg7021 Let's just get back to C# 5 then it is even more simple.
@@neociber24 re: why ranges doesn't return an enumerator
ranges can't always produce a start/end value unless it's provided with a length. It's Start and End properties are Index, not int.
E.g. "0..", anything with the hat symbol.. on my phone so I don't know how to type it... the one above the a: â
@@NoGamer256 The difference is that official language features are extensively documented and can be found in any codebase. Custom language extensions are a new thing that needs to be learned. If it provides enough value, it can be worth it. Is changing "Enumerable.Range" into ".." worth it? That's up to you.
Having come from Python to C#, I could see myself really liking this near the beginning of my C# path. But now I have gotten used to how C# works and looks.
This is nevertheless interesting from an educational point. And it is a good reminder that one can build your own things to meet your needs or just to make things behave more to your taste.
power of extensions - love it
I've used enumerators before, but never had it implemented as a feature of the code itself. Genius!
I definitely want this language feature in future version of C# natively! For me it's really fast to write and clear to read
I expect this to enter the language. It also feels like its exclusion is an oversight (which, of course, it's not). The .. format is just so elegant and self-explanatory.
very interesting. Hopefully will be built in. Having this custom classes all around and having to add them as a reference to each project for Syntactic sugar purposes is a tremendously big deal. Thanks for sharing though!
If/when the Roles & Extensions proposal is implement into C#, you'll be able to not only make anything enumerable, but also implement IEnumerable for any type, so you'd literally be able to use Linq on ranges. You're also able to do the same for GetAwaiter which is what makes the await operator function, and you can even make types besides Task/ and ValueTask/ into acceptable return types for async methods. Extension methods (and hopefully soon Roles & Extensions) is truly one of the best features of C#.
They started exploring in 2017. I just don't have much hope. There is crowd of people saying that C# is getting too complex and people don't know what to use anymore. I think they listen to them.
Omg yesss! This is a feature I very much miss from Rust when using C#. Thanks!
You continue to blow my mind with these little pearls. Thanks Nick!
I love your content. I am 100% self-trought and learned on the job where I work. So I don't get exposed to things as much I as I would like. But your small bite-sized videos keep me thinking about new ways to do things. I am able to take these back to some of our older projects and be a fresh set of eyes on them. Thanks again! I am in MN but due to budget cuts, I lost the funds set aside for training and events this year. Maybe I will be able to check you out next time you are in the citites. Keep up the amazing work!
Best way is to start coding at home, and to just try new things and read.
Eventually you come across stuff like this.
A super interesting way of using extensions! Thanks a lot for this
I would use it, very elegant, congrats!
Not a fan of this feature/syntax, but still super cool to see how you'd go about implementing it. Not something I had thought of but can definitely see using in other ways in the future
Worked , thanks a lot!
I didn't even think about creating useful enumerators based on native types. Great video, thanks.
That is really a clever idea Nick! Well done. 👏
Awesome, Nick. I really love your videos!
Fun side note. C# actually has quite a few of these "magic methods" that you can implement yourself. The LINQ query syntax is full of them, like being able to use the "from x in y select..." syntax on any type if you add a Select method with the correct signature to that type.
I don't normally recommend it because its quite obscure and confusing to most people but its nice to know how the internals of some of these language features work.
Your specific example is pretty useful for implementing monadic code with an imperative syntax. I do like it, and it has its merits.
This is the real beauty of C#. Its not coding mate...it's Art. 👏
It's a nice idea if it was implemented into C# for all to use, but not as a random thing added to a project. Last thing we need is rewriting the wheel because it looks slightly nicer. This sort of behaviour is what leads to technical debt over a long period of time.
Neat truck! I will absolutely use this.
Is there any specific advantage on not implementing IEnumerator in the example? I understand the advantages of duck typing for types that we don't own (like the methods extending Range and Index) but whenever possible I'd still prefer using interfaces when available.
Also, this is something I'd like MS added to the BCL as it makes very much sense to iterate on a range.
Wow thank you so much that really helped
Supporting descending without affecting performance is tough. You don't want an if statement in the middle of MoveNext. Branch Prediction might sort it out, but the generated ASM contains the cmp every time so there's no compiler optimisation breaking the method into two and preselecting which to use. I didn't run a performance test, though, so it might be OK.
I tried swapping behaviours in with a Func behind MoveNext() which pointed to named methods (lambdas in structs can't reference instance members), but while the enumerator worked in that it iterated the right number of times, the value in the foreach loop remained 0 which is pretty odd. As if either the value was cached, or copied and different copies used inside and outside the enumerator.
What did work was creating an interface with Current and MoveNext, and a static method (yes, interfaces can have static methods!) to get one of two specific implementations (as structs, not ref structs). Structs implementing interfaces can have performance implications with boxing if the structs are cast to and from the interface, but the fact that foreach loops only require Current and MoveNext to exist, and don't depend on IEnumerator, indicates that it's unlikely to be an issue.
OMG, Nick this is awesome. Thank you.
Probably i will not use this technique, but i liked the way how you elegantly implemented it.
thanks 🙏 Nick!
Great trick! I think we should open an issue in c# repo to support this internally, would be great feature
Yes I like it and would love to see it in C# itself
This is actually super cool. Greetings from kotlin, every language has its nice feature we miss elsewhere.
I learned a trick in the '80s that I still use today: instead of using 'i'/'j'/etc. for your loops, use 'ii'/'jj'/etc. That way, any time you come across a double letter you *know* that it is a loop variable.
Cool, though I wouldn't put a GetEnumerator on int. That seems a recipe for subtle bugs if you intend to iterate through an IEnumerable but accidentally grab a single int first. Normally you'd get a compiler error. So definitely should be careful with implied casts and similar tools like this.
Plus if someone else looks at this code... for (var i in 10) ... they're not going to know wtf that is or why it even works. It is difficult to find the extension powering that. At least with the Range they can search the code for uses of the Range type to find it.
Pretty cool but I find with foreach loops I don't tend to use normal for loops much. Even when I need the index or to put that index into a separate array, I can use LINQ .Select or .Zip to do that in a foreach (yes probably not efficient, my philosophy is to write nice code and only come back later if there's a performance need). That said I do avoid for because of the verboseness as you said so this does look tempting. Iterating over objects in a collection is usually what I am really wanting in most loops though.
Great!!! I will use it.
I even haven’t known about syntax sugar. Nice videos, Nick! Great performance and work you are doing on your channel, thanks!
This is the type of thing that really makes me interested in C#
This looks like something Jon Skeet would have done, I love it :D
Oh! that is just the best! Yes, let's see some numbers. Very cool
Beautiful powerful C# that make it feel - almost - like Python! Great video Nick thankyou!!
Very interesting! I'm sure there is more to do with this!
Thats pretty sweet!
This is so cool
I loved this kind of syntax in Python. So I would use this without a second thought in C#.
I didnt knew x..y was an actual operator in C#. Well done.
Tnx!
Amazing.. mind blowing ❤👌🎉
Most of your us are still learning the crust of .NET, figuring out the ins and outs and the basics. But here u r extending the entire framework and diving deep into its internal source code and showing us how to rewrite parts of .NET to perform better. This is just a no-go zone for most of us
6:16 I don't see any problem in going to infinity since it's an Enumerator, the user code can break out of it using some other condition, that's the user-code responsibility, it does make sense.
Now if start is from start then maybe count from the end down would make more sense
It actually won't go into infinity the way its implemented. It will just instantly exit the loop
@@nickchapsas I think what @Felype Rennan means is that it could make sense to make it loop infinity if no end is defined. You'd just have to write something like `foreach (var i in (5..).Where(x => x < 100))` so that a linq extension eventually breaks the looping
@@ronsijm Oh I see. Yeah it might make sense, butthere is no infinity technically since ints have a max value, so you'd need special handling for a feature that doesn't really make sense to me from a language design standpoint. It's why Kotlin itself doesn't allow it too
@@nickchapsas An infinite iterator itself makes sense, so here you /could/ interpret `10..` as `10 forever`: 10,10,10...
@@nickchapsas true. I would implement " is from end" as "Int32.MaxValue" and call it a day, because in practice no indexed loop realistically goes to infinity
If the programmer needs something infinite then they'd be using while(true) instead, it's even faster to write and easier to read than foreach(var i in x..)
At the same time I think it's useful for if you need (0..).Where(Predicate).Take(200)
Where limiting the range to 200 first then applying a filter would produce an incorrect result
This is more convenient than requiring the user code to explicitly say (0..Int32.MaxValue).Where....
IMO
I used this feature to be able to await a cancellation token. Awesome stuff, feels like magic :D
Looks interesting!
You're the most prominent C# instructor on UA-cam.
Nice!!!
If ref structs are used for enumerators foreach loops can't be used in async methods.
Loop like foreach (var i in 0..) actually may make sense in combination with break and yield. That's actually a cool way to write a loop from zero to "infinity" with a counter
I'd suggest "infinity" or "from end" would be int.MaxValue then.
Nice, literally anything that's puts c# closer to f# is a win in my book.
@@MiningForPies oh I agree, wholeheartedly, but the more c# looks and acts f#-ish, the more people will see that f# isn't intimidating, and maybe they will switch to the best dotnet language after that..
@@WillEhrendreich they already switched to C#....
Please add a PR for this be included on C# 12!
I've wanted more duck typing for numerics for ages - this is an awesome capability. Replacing Enumerable.Range(...) will go a long way to having functional code look a lot more functional, and quite competitive with Python, Matlab and Ruby for expressiveness. And with the new static abstract interfaces, I'm actually looking forward to building custom Span iterators over large multi-dimensional datasets (across different axes). Thank you Nick!!
A similarly cool "language hack" that's been possible for ages is using the "Add" extension method to implement the JavaScript and Kotlin "spread" behavior for lists, to support syntax like: new List { "a", "b", nestedListOfString };
That was fun.
I'm not a c# programmer, have been mostly working with perl, python, js and java. It feels like home for me.
I would use it in a personal project for sure. I'm not sure about work. Probably be fine for work because we create extension methods for things from time to time so it's really not any different than that.
I like the terse syntax and would use this. However, most of the looping in my applications is iterating over a collection, so 'foreach(var item in items)' is my typical pattern.
Reminds me of overflowing c++ code with macros where everything looks cool but you don't understand how it works anymore without reverse engineering them back to normal code
I’m always in two minds about macros. Easy refactoring, terrible readability
So in Kotlin it is cool cause you can't find out how it works easily. In C# it is bad cause you have to write it yourself. Makes sense.
@@NoGamer256 Its cool for Kotlin because its Kotlin syntax, its bad for C# because its Kotlin syntax.
Wow. This is damn awesome.
Hey Nick, I would be interested to see if you know of any use cases for the other duck typings in c# (await for example).
bruh, this is magic
Another slight drawback is Range does not support negative numbers, so you can't do -5..5 as you can in Kotlin. It should have been called IndexRange as it's designed to support index ranges rather than value ranges.
Still a cool hack if you're ok with non-negative ranges.
You actually can.
e.g. foreach(var i in ^10..0) { } will go from -10 to 0.
You'll need this version of the ref struct though.
public ref struct RangeEnumerator
{
private int _current;
private readonly int _end;
private readonly bool _forward;
public RangeEnumerator(Range range)
{
_current = range.Start.IsFromEnd ? -range.Start.Value : range.Start.Value;
_end = range.End.IsFromEnd ? -range.End.Value : range.End.Value;
_forward = _end >= _current;
if (_forward) _current--;
else _current++;
}
public readonly int Current => _current;
public bool MoveNext() => _forward ? ((++_current) = _end);
}
@@billy65bob Thanks! I didn't think about the ^ operator. And that's some tight code, very impressive.
I am a stickler for having the rigid syntax of a for loop usually.But i am in love with this extension,seriously thinking of adding it to my projects.But the only concern would be for other people to understand or even run my code if they dont have this extension method.
I really like this syntax. I had seen for some time that it was possible to make conventional arrays support destructuring through extension methods as well (to support a more functional way of dealing with arrays, without having to use Spans [which work very well for that too]), it's interesting see the breadth that you can take this.
Note that in func programing you can see even x.. (to infinity lazy) and its valid use case where you can stop the loop in the inner implementation.
Although if its lazy, you're not really stopping a loop, you're just finished grabbing values from the iterator :)
Hi. Is there a coupon code for your courses? Thanks.
Big fan of extension methods in general and this is really nice!
I think testing would need to be done with multiple types and scenarios, to see if and when it makes sense to use it.
If it is actually overall performant on large collections, it could at least be used to write faster and more readable code at the same time.
Though, if a beginner adopts this, I imagine it may be difficult to receive help debugging.
If readability is the primary goal, is there a way to make a Loop() method for all types of collections?
I can imagine making extension methods for tuples of specific types.
ex: Tuple contains my repository class (which implements a generic "get all" method) and the total number of records to return
ex: Tuple contains my API endpoint (Uri) and JSON data (Object) which returns an array of JSON objects serialized to type T
That said, it's probably best to just create custom classes for each of these and implement IEnumerable, but the possibilities are interesting to think about.
It's very cool, but I think that the normal loop seems better.
Actually, this is the first time when I see, that the duck typing in foreach loops can be really useful. I've never thought about ranges in such a context. This is really an elegant approach👍
I never knew this was possible! I always thought a class needs to implement IEnumerable to be used in a foreach loop. as for the feature itself, I think I feel about it similar to Unity's coroutine System: I'm not sure if it is a perversion of existing features or just brilliantly elegant.
Thank you for this great tips.
Could you please tell me how about to return as IEnumerable instead of int?
Because I wanna use this extension for converting range of integer to List
var listLong = Enumerable.Range(0, 1000).Select(x => (long)x);
How I can do that with your range method?
I did a benchmark and this is the result:
test1: Enumerable.Range(0, 1000).Select(x => (long)x)
test2:
var l = new List();
foreach(var i in ..1000)
l.Add(Convert.ToInt64(i));
test1:
mean: 68.50 ns
allocated: 88 B
test2:
mean: 6,937.60 ns
allocated: 16600 B
Nice, I just like to know in which version it's supported ! Thanks
By necessity, I work on python for the last few years, and I'm kinda used to python loops and find them elegant. But I see languages converge on something that is just easy to write and read, and it's lovely to see C#, my most loved language, to go yet one more step in this direction.
Hey Nick thanks for the video. You also coding in kotlin or just interested in other languages 😁
doing things like this is one of the funnest things to do in programming, but all my coworkers hate me when I do :D
I do really like the syntax but would you have any concern about a fresh developer coming onto a project that implements this? If the extensions do have custom implementations that may not be obvious, it would be very easy for a new developer to miss that for something that should just be a basic loop.
The same concern I have when senior developers get exposed to new C# features
I think my concern is more that the implementation is in the extension. Depending on who has written it, it could be inclusive or exclusive. I still consider myself a relatively new developer and while this syntax is easily readable, hiding the actual mechanics of it away in an extension class could cause confusion down the line if they then try to use it elsewhere. I guess it's making sure there is decent onboarding to new devs to show them, this ISN'T standard functionality, this is how it works in OUR project.
Is the WHILE loop still used/useful? Between the FOR and FOREACH it feels kind of dead.
I saw a benchmark where Exists was faster than Any. Do you know the reason? I can't find any material on the subject so might make for a good video.
I would love this usage to be available, even as a C# feature. HOWEVER a big caveat for C#: Implementing anything other than what the current Range operator allows will just confuse code intent. That is start..end must be inclusive of start and exclusive of end. So 0..3 has indexes 0,1,2. This is just an implicit unchangeable fact of life for C#.
It could make sense to use till the end, the user can always exit the loop on whatever condition
Yeah you can do this type of duck typing to create custom Task classes too, to mimic how Java/Android used to do them. Same way middleware works in asp.
Is it possible to implement "=>" for foreach loop?
F.e.
foreach(var element in elements) => element.doSomething();
To avoid the conventional for loop, we are creating an extensions class & getenumerator method. Thus we are making our for loop more consize and readable/presentable. Please let me know what other benefits can we get through this method. I am learning, please correct me if I got this wrong
how does that work with JIT optimization and bounds checking? - would the excess complexity stop some of the optimization potential?
As you can see in the benchmarks, which include JIT optimisation, there isn’t any meaningful difference
Why MS didn't make Range enumerable in the first place ?
Thats an creative way of implementing it. I somewhere saw how you can implement an extension that makes it possible to write "await TimeSpan.FromSeconds(3)" or even "await 3" by making an extension method on int like this
public static TaskAwaiter GetAwaiter(this int millis)
=> return Task.Delay(millis*1000).GetAwaiter();
What library do you use for the Benchmarks please buddy?
BenchmarkDotNet
F# has had this functionality in the core language for many years.
Elegant? Yes, and a very cool experiment. But would I use it? Probably not. It doesn't really save anything except a few keystrokes and unless your loops routinely deal with larger ranges then there is a performance penalty, albeit a very small one which may or may not add up to something more significant (YMMV of course). And even if you use larger ranges all the time, it's performance neutral and still confers no tangible benefit. Someone else made the point about it being 'unexpected' code and needing maintaining and documenting, and not being junior friendly and I think those are very valid and underrated points. If you are prepared to invest that time and effort and don't mind the extra cognitive load for juniors, albeit a small one but again these things add up, then I guess you could go for it for the sake of elegance but it feels like this is better held up as a practical explanation/example of duck typing than something to use generally.
I'd perhaps use it in code I owned if I had a lot of for() loops I was dealing with. But I'd be cautious about using it in a team setting. The team would have to agree that it was an extension with a value that exceeded the potential cost of requiring the knowledge that this extension was present, how it worked, and any limitations the implementation may have. For example are the ranges inclusive or exclusive, are descending ranges supported, does it just work with ints or can you use enums, etc. Is usage of this required, recommended, or developer preference? Who maintains that code and how is the team notified if there are any changes made to it that affect behavior? So another words, the same as any other component you consider taking on as a dependency 🙂