 All righty, thank you very much, Kendra. Thank you, Mads. So Mads talked primarily about nullable reference types, which we devoted an entire half hour to, because that's the one that is going to change how you code the most. It's going to affect those warnings that you talked about. It's going to affect the libraries that you use in terms of how we work with the code that you have. I get to talk about all the fun stuff. It's just things that you can adopt that will just make your life better. But they're not going to introduce warnings as soon as you turn them on. So there's a couple of different themes that are really true about what was added in C-Sharp 8 outside of nullable reference types. We're going to talk about it as being more modern and more productive. We're going to increase your productivity, and we're going to add features that make it easier for you to work with modern development scenarios. Primarily those are cloud-based, device-based programs that run across multiple machines. So let's look quickly at that catalog of features, and then we're going to dive right into code. So the ones that are going to support these modern cloud scenarios, we're going to be talking about async and numerables. We're going to talk about more patterns in more places and how you can use pattern matching, which will do a lot more different things as we're combining applications and combining code from other places. We're going to talk about default interface members, ways to add versions onto an interface, ways to provide implementation in an interface, and to be able to update those as part of a distributed program. And we're going to talk about indices and ranges, which is just a nicer, cleaner syntax for working with large sequences of data, whether that be arrays, large data sets that come from an ML.NET program, and so on. We're also going to breeze through quickly a lot of different smaller features that are really nice ways to increase your productivity, using statement, static local functions, read-only members, null-coalescing assignment, unmanaged constraints, and interpolated verbatim strengths. These are different features that you can just add into your daily habits and start writing a little bit more productive, a little bit more concise code, and make the experience a little bit better. So now from that, that's everything that we're going to be talking about, and that's more than enough talking. Now let's look at the code and let's actually look at those features in action. So what I have here is I'm using Visual Studio for Mac. As part of that.NET Core story of cross-platform, open-source, and free. Make this just a little bit bigger. So my main program here, we're using one of the features that was in one of the previous versions. So we're going to have async iterators, so my main method is also asynchronous. Let's look at the idea behind what an async iterator does. If you think about everything we had to write in order to do asynchronous, grab data, and then work with that data, and then consume it, you would do an asynchronous call, and you would have to wait until all the data was available. And then you'd get that entire large data set, and then you would work with that data set. Using async enumerators, when you write the code that retrieves data asynchronously, now you can put that into an async enumerator method, such as this one here, where as the data is available, you immediately yield return it. When the data is not available and you need to say, get the next page in a GraphQL query or make another web service call to get more data, well, now it's going to await and do something asynchronous, and then the next block of data comes back. So let's run this application. And I want you to watch how the screen scrolls as we do this. And that went just a little bit too quick. So let's run that one a little bit. We'll add a little bit more of a delay here. Let's make sure we have the change. Make sure I'm running the right application. Let's build, and then we'll run this one. So what we're going to see is that, as the first data packet is available, we'll immediately see those first 10 elements. And then we'll see a pause while I'm simulating doing a web request to go get some more data. And then we'll see the next honk go by. So we'll see we'll get some data after the first pause for five seconds to get the first block. And now it's pausing to do that web request to get some more data. It's going to come back. And there's the next block. And you notice that even as we're consuming that iterator, it's going to just await for each. And the calling code doesn't need to know exactly anything about the page size or how that data is going to get returned or when it will pause. It can simply continue to ask for the next data element. If the data element's already there, it's going to immediately return. If the data element is not there, and it needs to instead go get the next data block, then we'll go ahead and it will be asynchronous. It will await. And then when the next data block is ready, it can resume processing. So that's async iterators. And we find that that's an incredibly useful feature when we're talking about doing anything with working with distributed programs where you get some data, have to go get the process some of it, get some more data, and have to go process some of it again. So I'm going to go ahead and just remove task and async here, because otherwise I'm going to continue to get warnings, because the rest of the code in this demo is not asynchronous. And let's look at our patterns matching demo. And this is one that we've covered in docs. But the scenario behind this is imagine that you're building an application that is composed of a number of different applications that are delivering data to you. So in this instance, it's an application that calculates tolls for a busy highway going into and out of a city. Well, as luck would have it, and as a lot of real life scenarios that we have to work with are constructed, the companies that put together the software that registers cars or registers delivery trucks or registers taxis and buses, they build totally separate software systems that you now have to integrate. So what that means is even though in a classic model where we'd look at object orientation and we'd say, we can just do an object oriented thing and have car and trucks and taxis and buses derived from a vehicle class and have a virtual method to calculate the toll and provide different implementations, we really don't have that here, because we're constrained to use these data types that we've been given from someone else. And that's where pattern matching comes in. So if we look at how we're calculating the tolls, if we were to take a vehicle and we switch on what type it is, we can set a cost for a car, set a cost for the taxi, set a cost for the delivery truck, and we're looking at the shape of the data and the kind it is. And we're not relying on a virtual hierarchy that we've imposed upon those types, but we're just putting them together in terms of knowing some things about it. But we can expand on that more. And then as more business rules come in, we may have some advanced rules that say, well, once we put more people in the car, cars should get a little cheaper. So if I'm going to say my car and if a car has got passengers with, if it's got two or more, so it's got two. And I'm going to say then it's only 1.5. And now if you look, I'm not going to get a little bit of squiggles coming in here, because I'm not covering every case with the car. So I would have to add another car with a default of 2.0. And now if I were to say, well, if there's no passengers in there, we can put these in. And if it's zero, well, now you're going to owe a little bit more. You have to pay, I guess I didn't have that one. But if it's got three or more passengers, then you only have to pay a dollar, and so on. So for each different case, what we can now do is we leap into, instead of just looking at the type, once we know the type, we're going to look at some of the properties of that type in a recursive pattern. So if it's a car and one of the properties has a certain value, then we can go through and learn a little bit more. And then we can also learn about, if it's some other type completely, we can catch those where somebody is probably a programmer error, someone passed in the wrong type. Or if someone passes in null, that would be a different case that we could work with. And then we could do something where we could get even more advanced, and we could say, depending on the days of the week and depending on the time of the day, well, then we have some other business rules. If it's a weekday, well, then we're going to spend a little bit more. And depending on the time band, we're going to add a little bit more other code to say morning rush coming into the city should cost more, evening rush out of the city should cost more. Now, if we look here at this one for this weekday, you can see that this is pretty repetitive right now. Well, I can change that by I'm going to delete all these because they're all the same. And then Saturday and Sunday, I should return false. But if it's not Saturday or Sunday from this enum, I should be able to just return true. So now I can go, well, I can look at the exceptions because there's only two of them. And then I can treat those that are not exceptions as just a general rule to say that must be a weekday. And we can do some of the same things here in terms of the time band, where we can look at depending on the time, if else is here, we could refactor that if we wanted to as well. And then if you look at our peak time premium, now here what we're doing is we're also using that new switch statement, but we're going to construct a tuple of the three variables we care about. We care about whether or not it's a weekday. We care about if it's overnight, if it's daytime, or if it's morning rush hour, or evening rush hour. And we may care about whether or not it's inbound or outbound. And now if I were to build an entire table of all those, I would get, weekday is either true or false, times the time band of which there are four. So that would be eight rows. And then whether or not it's inbound is another Boolean of two different ways. So I would get 16 different rows here. And then we could start to simplify this. And by simplifying it, I can come into these rules where if it's overnight or daytime, really doesn't matter if it's inbound or not. So I'll just leave that as an underscore so any value would be fine. And I have those rules. And if it's morning or evening rush and it's a weekday, that's covered under the next two rules here where I'm going to check all three variables. And on those instances, because peak time inbound is more expensive and peak time outbound in the evening is more expensive, well then we're going to add a bit more of a premium. And the other cases, there's no particular premium and there's no discount. So we'll just leave all the others, underscores, and discards. And that will fall through as a default case. So this is one of the questions we get on this syntax is why did we pick underscore rather than using the default, which is what's been on a switch statement for a long time? And a big reason is there are a lot of cases here where we would start to use multiple variables or multiple properties. And what we'll end up with is by simplifying it, we'll get those discards or default, we don't care what the value is, in more than one place. And it just looked kind of weird if this last one was going to be default, default, default, that seems kind of odd and doesn't really seem to fit the syntax. So this switch expression is now doing some different things in this switch statement that we're all familiar with in its construction. The two big changes that are in the syntax when you start to adopt this is that what you'll see is we start by giving the variable that we're going to switch on, followed by the switch keyword. And then we're going to have the cases in terms of what are the types, the properties, property constants that we're going to be switching on. And the reason we did it that way is this composes very well in more complicated scenarios. I can go from here and then I can take the output of this one and then I can also multiply it by this peak time premium. Or if you look here, I'm composing inside of, by calling that is weekday or get time band, which are also switch based, and then composing directly into that. So if we run this, I have some test code up here that's going to just create a bunch of different solo writers, rideshare, full vans and empty vans and so on and so forth, and try to walk through all of those cases, including some that are invalid, where we don't pass a vehicle type in at all, or pass a null in and see what happens. So we can go ahead and just build and run this one, which will, since it's not asynchronous, since it doesn't have those pauses in, we'll start a little bit smaller. We'll make our font noticeably bigger on this one. If we find that quickly, finding that one right as quick as I would like, sorry about that, unless we, so we'll pop out of this one. And the next one that we want to discuss is the other big one that kind of changes and really helps with these programs that work across multiple machines. We're going to talk about adding default interface members, which is one of these that has caused a fair amount of a little bit of concern, especially with people who have been familiar with C++ and multiple inheritance. So there are a lot of rules in terms of how you can use default interface implementations, which we'll really try to do a lot to make sure that you really can't get into that default diamond of death scenario that was possible in C++ with multiple inheritance. So if we look at what we're trying to do here with a default interface member, we can look at the idea of a logger. So we're going to write some kind of a string to some form of a log. That log may take different implementations. In this contrived example, I've got a console logger that writes to the console. I have a database logger that also writes to the console because didn't want to add a database and everything for this demo. So it's going to write to the console, but it's going to say that it wrote something to a database. And if you note that what we did here is we added an implementation to this. So now this logger method can now be called on any type that implements this console logger or database logger interface. So we can implement the derived interface and that will just inherit and accept that implementation. So now if we look, I added a logger factory that we can take the constant of an enum of which kind of logger you want to create, and I'm just going to return one of those. If one doesn't exist, well, then we're going to throw an exception. And note that that's using that same pattern matching again. So we can use that in other scenarios such as this, where we're doing a form of a dependency injection. And now if we look, I can just create a logger. And now if I want to log that, it's going to log the hello world or it's going to log it into my database type. Based on which one was created, either of those factories will work and it will come into play and it will do the correct thing in terms of writing to that particular log. So if we rebuild, then we'll go ahead and run. And we'll see the two different messages that come out hello or hello inserted into the database. And where I want to talk a little bit about that deadly diamond of death and multiple inheritance is if we look here and if I took my database logger, I can change this and I'm going to just implement from this class. I'm going to now make this implement, change this to an interface with that implementation. And now we just need to create a class. And let's change the name here and make this an I database logger. And that's going to implement I database logger. And I don't need to provide any implementation. I could have whatever else I wanted in there. And now if I wanted to try to get into this weird state where I said, you know, I want to do both. So now let's add this to the console logger as well. We'll make this an interface. And now just to make this compile below, I'm going to make a class that's a console logger, which implements I console logger. It doesn't have any implementation. And that now works. And if I wanted to take the database logger and I wanted to also implement the I console logger, now we're going to see instead of that deadly diamond of death that would throw some exceptions or do some strange behavior at runtime, you can see I'm getting red squiggles. I can't do this. What the compiler is going to tell me is that both of these have an implementation for that Ilogger.log method. And none of them is better than the other. None of them is closer in the hierarchy. So it won't compile. So what you're going to find, and we've got some docs that go through and walk through creating default interface implementations and adding new features to these, what you'll find is that any of the scenarios where the compiler can't absolutely figure out which one should be running, which one should be correct, you're going to get a compiler error and you're going to have to explicitly say which one you wanted or provide a class implementation and come back from that class into creating in that class which one of the interface implementations you wanted to call and be explicit about it. And then once it compiles, what the compiler will guarantee is there is exactly one better method that is the correct one to call. And the other thing to note is that notice that any of these implementations now have to use default, or excuse me, explicit interface implementations. So if you rev an interface and add a method that some class has already used, what you'll end up with in your default interface implementation is that new method you added to the interface will only be called if the object is called through the interface reference, just like an explicit interface implementation. Otherwise, the method that was already added in the class, that one will win with overload resolution. So a lot of work went into this. And where that went is mostly to try to make sure that whatever method you pick are going to end up being the correct method for your scenario. Now the last one that really has a lot to do with working on these distributed programs is indices and ranges. I'm going to just show a lot of the code. And down I'll run this one because once you see the code, the code pretty much shows what goes on. So what we did with default or indices and ranges is it's a better way and a much more concise way to use bracket notation to look at a single element, either from the start or from the end of an array, or to look at a range anywhere inside an array or a span or some sequence of elements. So if you look at here, I've got an array where we've got the quick brown fox jumped over the lazy dog. And you can see the index from the start starts at 0 and goes up through 8. One of the controversial things on this feature is if you look at the index on the end, it's going to start from hat 1 and then move up to hat 9. Now the reason for that is hat 0 is the length of the array. So it is actually one past the end and it points to the very end of the array. So that's where you can think of hat 0 is sequence that length, hat 1 is sequence that length minus 1, which matches up with the last element. Now where this feature works fairly well, we can write out things either from either end. We can also take a slice of it. If you look in the second demo, it would take the last two words from bang 2 or from hat 2 up to hat 0. And if you notice here, ranges are exclusive. So the last element in a range is not included in that range. And where that gets important is as we look at some of the other ones here where we really look at a lot of the rules, which is why we chose an exclusive range. As if you look, numbers from x through y is the same as the range of y minus x. It has that many elements. And these other rules once again go x to y, y to z. They should be sequential and not overlapping. X to hat x should remove exactly the same number of elements from both sides of the sequence. So 0 to hat 0 is the entire sequence. 1 to hat 1 is peeling 1 element off each end. And one of the real scenarios where we use this a lot is if you're working through large data sets, you might take a moving average over any number of elements. So we start with how many do we have? And we take the next 10, and we just keep moving through. And we start to see how the sequence moves, but kind of balance it a little bit. And then from there, now we're getting into just these things where they're really just small little features, but they just make your life more productive. So let's start with our using statement. So if we look at the using statement, I've just got these nested usings. And I'm just going to start removing some extra characters, remove some brackets. And we can do the same on the next line. And then when we reformat this, it starts to look a little bit cleaner. And of course, not typing this morning. And then everything would line up, when we just have these different usings all in one area. And because it's now a using statement, I need the semicolons. And there, instead of just all that, fireman's ladder of brackets using multiple items, we get just one. How much more do we got? Can I keep going? You can keep going if you want to. We've got time. There's a few more we want to keep going. Next one would be static local functions. We saw local functions in C-sharp 7. And what we've added with static local functions is now we can look at something and say, I know this local function does not use any of the arguments that are in its outer scope. And the compiler and the JIT can make some optimizations on those. So if I want to take something like this and say, I really wanted this to be something so that I know it never uses any of the outer scope variables, I should be able to declare it a static. If you know that when I do this now, I'm seeing red squigglies under start and end. So now I have to update that to say, I'm going to want to pass those in and make new copies of those variables so that I'm not capturing something from the outside. So what the compiler will do here is it helps make your code more efficient. If you don't think you're supposed to be using some of the outer variables and capturing those and making a closure, which can be a little expensive, especially in hot pads, something like this will let you then say, I know I don't want to do that. I'll turn on that static feature, and then what I'm going to do is make sure that I can't write code that would violate that. And we walk on to read-only members, where now in the Z-sharp seven-time frame, we added read-only structs. So I could create a struct and guarantee that it would never change. So what we want to do now is to be able to give you a little bit more better ways to do that. And if I look at two string, I should be able to say that should be read-only because two string should never change anything. Well, it's annoyed because it's a static is going to do that. So I'm going to have to change distance to now say that that's also read-only. And now what that does is it tells the compiler, this method is not supposed to change the state of the struct. And because of that, then if it was passed by ref, the compiler could then go, well, this isn't supposed to. And it won't change the state of that struct. So then we can move forward from that, and it can make some optimizations. So if you know something isn't supposed to change state, it can add that modifier. And the compiler will enforce your intent. We're going to do just one more because the last two are really quite small. And we will look at the null-coalescing assignment. And this is one of these that really will help as you start to adopt nullable reference types, which Matt's talked about in part one, where a lot of what we have is we want to be able to assign something and ensure that it's not null. So by using this null-coalescing assignment, what I'm saying is, for instance, here, I'm going to say that this numbers collection is either going to be the value it already had, or if its value was null, go create a new one. And then I'm going to add some elements to it. And I would try to add i. If i is null, well, then it will actually add 17. If i is not null, it would add whatever the value of i is. So that's a way to, instead of having to start sprinkling your code with all this, if something isn't null, just make it a fault assignment and put the right thing in place. And with that, we'll go back into finishing up and talk with a lot of the questions. These were the features that we had. The ones I didn't talk about was the unmanaged constraint, primarily it's going to be used by libraries for very high performance scenarios and interpolated verbatim strings, which means now the dollar sign and the at sign can be in either order. And with that, let's go to Kendra and get some questions. Great, thanks so much, Bill.