From 1844919dde41c3e53ce61e745a9b394a3e326e1c Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Fri, 21 Jun 2019 16:09:08 -0400 Subject: [PATCH] Migrate explore C# 8 to try-sampless repo A straight copy from the dotnet/try#samples branch. --- csharp8/ExploreCsharpEight/AsyncStreams.cs | 35 ++ .../ExploreCsharpEight.csproj | 14 + csharp8/ExploreCsharpEight/ExternalSystems.cs | 29 ++ .../ExploreCsharpEight/IndicesAndRanges.cs | 171 ++++++++++ .../ExploreCsharpEight/NullableReferences.cs | 52 +++ csharp8/ExploreCsharpEight/Patterns.cs | 323 ++++++++++++++++++ csharp8/ExploreCsharpEight/Program.cs | 129 +++++++ .../StaticLocalFunctions.cs | 61 ++++ .../UsingDeclarationsRefStruct.cs | 62 ++++ csharp8/asynchronous-streams.md | 24 ++ csharp8/indices-and-ranges-examples.md | 33 ++ csharp8/indices-and-ranges-scenario.md | 10 + csharp8/indices-and-ranges.md | 56 +++ csharp8/nullable-fix-class.md | 40 +++ csharp8/nullable-reference-types.md | 28 ++ csharp8/patterns-occupancy.md | 35 ++ csharp8/patterns-peakpricing.md | 154 +++++++++ csharp8/patterns-types.md | 28 ++ csharp8/patterns.md | 63 ++++ csharp8/readme.md | 22 ++ csharp8/static-local-functions.md | 24 ++ csharp8/using-declarations-ref-structs.md | 21 ++ 22 files changed, 1414 insertions(+) create mode 100644 csharp8/ExploreCsharpEight/AsyncStreams.cs create mode 100644 csharp8/ExploreCsharpEight/ExploreCsharpEight.csproj create mode 100644 csharp8/ExploreCsharpEight/ExternalSystems.cs create mode 100644 csharp8/ExploreCsharpEight/IndicesAndRanges.cs create mode 100644 csharp8/ExploreCsharpEight/NullableReferences.cs create mode 100644 csharp8/ExploreCsharpEight/Patterns.cs create mode 100644 csharp8/ExploreCsharpEight/Program.cs create mode 100644 csharp8/ExploreCsharpEight/StaticLocalFunctions.cs create mode 100644 csharp8/ExploreCsharpEight/UsingDeclarationsRefStruct.cs create mode 100644 csharp8/asynchronous-streams.md create mode 100644 csharp8/indices-and-ranges-examples.md create mode 100644 csharp8/indices-and-ranges-scenario.md create mode 100644 csharp8/indices-and-ranges.md create mode 100644 csharp8/nullable-fix-class.md create mode 100644 csharp8/nullable-reference-types.md create mode 100644 csharp8/patterns-occupancy.md create mode 100644 csharp8/patterns-peakpricing.md create mode 100644 csharp8/patterns-types.md create mode 100644 csharp8/patterns.md create mode 100644 csharp8/readme.md create mode 100644 csharp8/static-local-functions.md create mode 100644 csharp8/using-declarations-ref-structs.md diff --git a/csharp8/ExploreCsharpEight/AsyncStreams.cs b/csharp8/ExploreCsharpEight/AsyncStreams.cs new file mode 100644 index 0000000..91e4abe --- /dev/null +++ b/csharp8/ExploreCsharpEight/AsyncStreams.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ExploreCsharpEight +{ + class AsyncStreams + { + #region AsyncStreams_Declare + internal async IAsyncEnumerable GenerateSequence() + { + for (int i = 0; i < 20; i++) + { + // every 3 elements, wait 2 seconds: + if (i % 3 == 0) + await Task.Delay(3000); + yield return i; + } + } + #endregion + + internal async Task ConsumeStream() + { + #region AsyncStreams_Consume + await foreach (var number in GenerateSequence()) + { + Console.WriteLine($"The time is {DateTime.Now:hh:mm:ss}. Retrieved {number}"); + } + #endregion + + return 0; + } + + } +} diff --git a/csharp8/ExploreCsharpEight/ExploreCsharpEight.csproj b/csharp8/ExploreCsharpEight/ExploreCsharpEight.csproj new file mode 100644 index 0000000..ad8f5c9 --- /dev/null +++ b/csharp8/ExploreCsharpEight/ExploreCsharpEight.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp3.0 + 8.0 + + + + + + + + diff --git a/csharp8/ExploreCsharpEight/ExternalSystems.cs b/csharp8/ExploreCsharpEight/ExternalSystems.cs new file mode 100644 index 0000000..ca71e16 --- /dev/null +++ b/csharp8/ExploreCsharpEight/ExternalSystems.cs @@ -0,0 +1,29 @@ +namespace ConsumerVehicleRegistration +{ + public class Car + { + public int Passengers { get; set; } + } +} + +namespace CommercialRegistration +{ + public class DeliveryTruck + { + public int GrossWeightClass { get; set; } + } +} + +namespace LiveryRegistration +{ + public class Taxi + { + public int Fares { get; set; } + } + + public class Bus + { + public int Capacity { get; set; } + public int Riders { get; set; } + } +} diff --git a/csharp8/ExploreCsharpEight/IndicesAndRanges.cs b/csharp8/ExploreCsharpEight/IndicesAndRanges.cs new file mode 100644 index 0000000..72667e3 --- /dev/null +++ b/csharp8/ExploreCsharpEight/IndicesAndRanges.cs @@ -0,0 +1,171 @@ +using System; +using System.Linq; + +namespace ExploreCsharpEight +{ + class IndicesAndRanges + { + // TODO: replace in IndicesAndRanges.md when non-editable blocks are supported. + #region IndicesAndRanges_Initialization + private string[] words = new string[] + { + // index from start index from end + "The", // 0 ^9 + "quick", // 1 ^8 + "brown", // 2 ^7 + "fox", // 3 ^6 + "jumped", // 4 ^5 + "over", // 5 ^4 + "the", // 6 ^3 + "lazy", // 7 ^2 + "dog" // 8 ^1 + }; + #endregion + + internal int Syntax_LastIndex() + { + #region IndicesAndRanges_LastIndex + Console.WriteLine($"The last word is {words[^1]}"); + #endregion + return 0; + } + + internal int Syntax_Range() + { + #region IndicesAndRanges_Range + var quickBrownFox = words[1..4]; + foreach (var word in quickBrownFox) + Console.Write($"< {word} >"); + Console.WriteLine(); + #endregion + return 0; + } + + internal int Syntax_LastRange() + { + #region IndicesAndRanges_LastRange + var lazyDog = words[^2..^0]; + foreach (var word in lazyDog) + Console.Write($"< {word} >"); + Console.WriteLine(); + #endregion + return 0; + } + + internal int Syntax_PartialRange() + { + #region IndicesAndRanges_PartialRanges + var allWords = words[..]; // contains "The" through "dog". + var firstPhrase = words[..4]; // contains "The" through "fox" + var lastPhrase = words[6..]; // contains "the, "lazy" and "dog" + foreach (var word in allWords) + Console.Write($"< {word} >"); + Console.WriteLine(); + foreach (var word in firstPhrase) + Console.Write($"< {word} >"); + Console.WriteLine(); + foreach (var word in lastPhrase) + Console.Write($"< {word} >"); + Console.WriteLine(); + #endregion + return 0; + } + + internal int Syntax_IndexRangeType() + { + #region IndicesAndRanges_RangeIndexTypes + Index the = ^3; + Console.WriteLine(words[the]); + Range phrase = 1..4; + var text = words[phrase]; + foreach (var word in text) + Console.Write($"< {word} >"); + Console.WriteLine(); + #endregion + return 0; + } + + internal int Syntax_WhyChosenSemantics() + { + #region IndicesAndRanges_CreateRange + var numbers = Enumerable.Range(0, 100).ToArray(); + int x = 12; + int y = 25; + int z = 36; + #endregion + + #region IndicesAndRanges_MathWithLength + Console.WriteLine("===================== ^0 is the same as Length. =>"); + Console.WriteLine($"{numbers[^x]} is the same as {numbers[numbers.Length - x]}"); + Console.WriteLine($"{numbers[x..y].Length} is the same as {y - x}"); + Console.WriteLine(); + #endregion + + #region IndicesAndRanges_Disjoint + Console.WriteLine("===================== Consecutive disjoint sequences. =>"); + Console.WriteLine("numbers[x..y] and numbers[y..z] are consecutive and disjoint:"); + Span x_y = numbers[x..y]; + Span y_z = numbers[y..z]; + Console.WriteLine($"\tnumbers[x..y] is {x_y[0]} through {x_y[^1]}, numbers[y..z] is {y_z[0]} through {y_z[^1]}"); + Console.WriteLine(); + #endregion + + #region IndicesAndRanges_RemoveFromEnds + Console.WriteLine("===================== Remove elements from both ends. =>"); + Console.WriteLine("numbers[x..^x] removes x elements at each end:"); + Span x_x = numbers[x..^x]; + Console.WriteLine($"\tnumbers[x..^x] starts with {x_x[0]} and ends with {x_x[^1]}"); + Console.WriteLine(); + #endregion + + #region IndicesAndRanges_IncompleteRanges + Console.WriteLine("===================== Incomplete sequences imply 0, ^0 =>"); + Console.WriteLine("numbers[..x] means numbers[0..x] and numbers[x..] means numbers[x..^0]"); + Span start_x = numbers[..x]; + Span zero_x = numbers[0..x]; + Console.WriteLine($"\t{start_x[0]}..{start_x[^1]} is the same as {zero_x[0]}..{zero_x[^1]}"); + Span z_end = numbers[z..]; + Span z_zero = numbers[z..]; + Console.WriteLine($"\t{z_end[0]}..{z_end[^1]} is the same as {z_zero[0]}..{z_zero[^1]}"); + Console.WriteLine(); + #endregion + return 0; + } + + internal int ComputeMovingAverages() + { + #region IndicesAndRanges_MovingAverage + int[] sequence = Sequence(1000); + + + for(int start = 0; start < sequence.Length; start += 100) + { + Range r = start..start+10; + var (min, max, average) = MovingAverage(sequence, r); + Console.WriteLine($"From {r.Start} to {r.End}: \tMin: {min},\tMax: {max},\tAverage: {average}"); + } + + for (int start = 0; start < sequence.Length; start += 100) + { + Range r = ^(start + 10)..^start; + var (min, max, average) = MovingAverage(sequence, r); + Console.WriteLine($"From {r.Start} to {r.End}: \tMin: {min},\tMax: {max},\tAverage: {average}"); + } + + (int min, int max, double average) MovingAverage(int[] subSequence, Range range) => + ( + subSequence[range].Min(), + subSequence[range].Max(), + subSequence[range].Average() + ); + #endregion + return 0; + } + + + private int[] Sequence(int count) + { + return Enumerable.Range(0, count).Select(x => (int)(Math.Sqrt(x) * 100)).ToArray(); + } + } +} diff --git a/csharp8/ExploreCsharpEight/NullableReferences.cs b/csharp8/ExploreCsharpEight/NullableReferences.cs new file mode 100644 index 0000000..9e8f96c --- /dev/null +++ b/csharp8/ExploreCsharpEight/NullableReferences.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExploreCsharpEight +{ + #region Nullable_PersonDefinition + #nullable enable + internal class Person + { + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + + public Person(string first, string last) => + (FirstName, LastName) = (first, last); + + public Person(string first, string middle, string last) => + (FirstName, MiddleName, LastName) = (first, middle, last); + + public override string ToString() => $"{FirstName} {MiddleName} {LastName}"; + } + #nullable restore + #endregion + + class NullableReferences + { + + #region Nullable_GetLengthMethod + #nullable enable + private static int GetLengthOfMiddleName(Person p) + { + string middleName = p.MiddleName; + return middleName.Length; + } + #nullable restore + #endregion + + internal int NullableTestBed() + { + #region Nullable_Usage + #nullable enable + Person miguel = new Person("Miguel", "de Icaza"); + var length = GetLengthOfMiddleName(miguel); + Console.WriteLine(length); + #nullable restore + //Was this tested on a person who doesn't have a middle name? + #endregion + return 0; + } + } +} diff --git a/csharp8/ExploreCsharpEight/Patterns.cs b/csharp8/ExploreCsharpEight/Patterns.cs new file mode 100644 index 0000000..e123890 --- /dev/null +++ b/csharp8/ExploreCsharpEight/Patterns.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CommercialRegistration; +using ConsumerVehicleRegistration; +using LiveryRegistration; + + +namespace ExploreCsharpEight +{ + // I made this interface so I can usethe same test routine + // with each iteration of the toll calculator + interface ITollCalculator + { + decimal CalculateToll(object vehicle); + } + + class TollCalculator_V1 : ITollCalculator + { + #region Pattern_CalculateToll + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car c => 2.00m, + Taxi t => 3.50m, + Bus b => 5.00m, + DeliveryTruck t => 10.00m, + {} => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; + #endregion + } + + class TollCalculator_V2 : ITollCalculator + { + #region Pattern_CarTaxiOccupancy + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car { Passengers: 0 } => 2.00m + 0.50m, + Car { Passengers: 1 } => 2.0m, + Car { Passengers: 2 } => 2.0m - 0.50m, + Car _ => 2.00m - 1.0m, + + Taxi { Fares: 0 } => 3.50m + 1.00m, + Taxi { Fares: 1 } => 3.50m, + Taxi { Fares: 2 } => 3.50m - 0.50m, + Taxi _ => 3.50m - 1.00m, + + Bus b => 5.00m, + DeliveryTruck t => 10.00m, + {} => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; + #endregion + } + + class TollCalculator_V3 : ITollCalculator + { + #region Pattern_BusOccupancy + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car { Passengers: 0 } => 2.00m + 0.50m, + Car { Passengers: 1 } => 2.0m, + Car { Passengers: 2 } => 2.0m - 0.50m, + Car _ => 2.00m - 1.0m, + + Taxi { Fares: 0 } => 3.50m + 1.00m, + Taxi { Fares: 1 } => 3.50m, + Taxi { Fares: 2 } => 3.50m - 0.50m, + Taxi _ => 3.50m - 1.00m, + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus _ => 5.00m, + + DeliveryTruck t => 10.00m, + {} => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; + #endregion + } + + class TollCalculator_V4 : ITollCalculator + { + #region Pattern_DeliveryTruckWeight + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car { Passengers: 0 } => 2.00m + 0.50m, + Car { Passengers: 1 } => 2.0m, + Car { Passengers: 2 } => 2.0m - 0.50m, + Car _ => 2.00m - 1.0m, + + Taxi { Fares: 0 } => 3.50m + 1.00m, + Taxi { Fares: 1 } => 3.50m, + Taxi { Fares: 2 } => 3.50m - 0.50m, + Taxi _ => 3.50m - 1.00m, + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus _ => 5.00m, + + DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, + DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, + DeliveryTruck _ => 10.00m, + + {} => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; + #endregion + } + + class TollCalculator_V5 : ITollCalculator + { + #region Pattern_ChainedPatterns + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car c => c.Passengers switch + { + 0 => 2.00m + 0.5m, + 1 => 2.0m, + 2 => 2.0m - 0.5m, + _ => 2.00m - 1.0m + }, + + Taxi t => t.Fares switch + { + 0 => 3.50m + 1.00m, + 1 => 3.50m, + 2 => 3.50m - 0.50m, + _ => 3.50m - 1.00m + }, + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus b => 5.00m, + + DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, + DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, + DeliveryTruck t => 10.00m, + + {} => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; + #endregion + + private enum TimeBand + { + MorningRush, + Daytime, + EveningRush, + Overnight + } + + private static TimeBand GetTimeBand(DateTime timeOfToll) + { + int hour = timeOfToll.Hour; + if (hour < 6) + return TimeBand.Overnight; + else if (hour < 10) + return TimeBand.MorningRush; + else if (hour < 16) + return TimeBand.Daytime; + else if (hour < 20) + return TimeBand.EveningRush; + else + return TimeBand.Overnight; + } + + private static bool IsWeekDay(DateTime timeOfToll) => + timeOfToll.DayOfWeek switch + { + DayOfWeek.Saturday => false, + DayOfWeek.Sunday => false, + _ => true + }; + + #region Pattern_PeakTime + public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) => + (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch + { + (true, TimeBand.Overnight, _) => 0.75m, + (true, TimeBand.Daytime, _) => 1.5m, + (true, TimeBand.MorningRush, true) => 2.0m, + (true, TimeBand.EveningRush, false) => 2.0m, + (_, _, _) => 1.0m, + }; + #endregion + + } + + class Patterns + { + internal int VehicleType() + { + #region Patterns_VehicleType + var tollCalc = new TollCalculator_V1(); + + var car = new Car(); + var taxi = new Taxi(); + var bus = new Bus(); + var truck = new DeliveryTruck(); + + Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}"); + Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}"); + Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}"); + Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}"); + + try + { + tollCalc.CalculateToll("this will fail"); + } + catch (ArgumentException e) + { + Console.WriteLine("Caught an argument exception when using the wrong type"); + } + try + { + tollCalc.CalculateToll(null); + } + catch (ArgumentNullException e) + { + Console.WriteLine("Caught an argument exception when using null"); + } + #endregion + return 0; + } + + internal int TestOccupancy(string regionName) + { + + ITollCalculator tollCalc = regionName switch + { + "Pattern_CarTaxiOccupancy" => new TollCalculator_V2() as ITollCalculator, + "Pattern_BusOccupancy" => new TollCalculator_V3() as ITollCalculator, + "Pattern_DeliveryTruckWeight" => new TollCalculator_V4() as ITollCalculator, + "Pattern_ChainedPatterns" => new TollCalculator_V5() as ITollCalculator, + _ => new TollCalculator_V1() as ITollCalculator, + }; + + var soloDriver = new Car(); + var twoRideShare = new Car { Passengers = 1 }; + var threeRideShare = new Car { Passengers = 2 }; + var fullVan = new Car { Passengers = 5 }; + var emptyTaxi = new Taxi(); + var singleFare = new Taxi { Fares = 1 }; + var doubleFare = new Taxi { Fares = 2 }; + var fullVanPool = new Taxi { Fares = 5 }; + var lowOccupantBus = new Bus { Capacity = 90, Riders = 15 }; + var normalBus = new Bus { Capacity = 90, Riders = 75 }; + var fullBus = new Bus { Capacity = 90, Riders = 85 }; + + var heavyTruck = new DeliveryTruck { GrossWeightClass = 7500 }; + var truck = new DeliveryTruck { GrossWeightClass = 4000 }; + var lightTruck = new DeliveryTruck { GrossWeightClass = 2500 }; + + Console.WriteLine($"The toll for a solo driver is {tollCalc.CalculateToll(soloDriver)}"); + Console.WriteLine($"The toll for a two ride share is {tollCalc.CalculateToll(twoRideShare)}"); + Console.WriteLine($"The toll for a three ride share is {tollCalc.CalculateToll(threeRideShare)}"); + Console.WriteLine($"The toll for a fullVan is {tollCalc.CalculateToll(fullVan)}"); + + Console.WriteLine($"The toll for an empty taxi is {tollCalc.CalculateToll(emptyTaxi)}"); + Console.WriteLine($"The toll for a single fare taxi is {tollCalc.CalculateToll(singleFare)}"); + Console.WriteLine($"The toll for a double fare taxi is {tollCalc.CalculateToll(doubleFare)}"); + Console.WriteLine($"The toll for a full van taxi is {tollCalc.CalculateToll(fullVanPool)}"); + + Console.WriteLine($"The toll for a low-occupant bus is {tollCalc.CalculateToll(lowOccupantBus)}"); + Console.WriteLine($"The toll for a regular bus is {tollCalc.CalculateToll(normalBus)}"); + Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(fullBus)}"); + + Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(heavyTruck)}"); + Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}"); + Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(lightTruck)}"); + + try + { + tollCalc.CalculateToll("this will fail"); + } + catch (ArgumentException e) + { + Console.WriteLine("Caught an argument exception when using the wrong type"); + } + try + { + tollCalc.CalculateToll(null!); + } + catch (ArgumentNullException e) + { + Console.WriteLine("Caught an argument exception when using null"); + } + return 1; + } + + internal int PeakPricing() + { + var tollCalc = new TollCalculator_V5(); + + var testTimes = new DateTime[] + { + new DateTime(2019, 3, 4, 8, 0, 0), // morning rush + new DateTime(2019, 3, 6, 11, 30, 0), // daytime + new DateTime(2019, 3, 7, 17, 15, 0), // evening rush + new DateTime(2019, 3, 14, 03, 30, 0), // overnight + + new DateTime(2019, 3, 16, 8, 30, 0), // weekend morning rush + new DateTime(2019, 3, 17, 14, 30, 0), // weekend daytime + new DateTime(2019, 3, 17, 18, 05, 0), // weekend evening rush + new DateTime(2019, 3, 16, 01, 30, 0), // weekend overnight + }; + + foreach (var time in testTimes) + { + Console.WriteLine($"Inbound premium at {time} is {tollCalc.PeakTimePremium(time, true)}"); + Console.WriteLine($"Outbound premium at {time} is {tollCalc.PeakTimePremium(time, false)}"); + } + return 0; + } + + } +} diff --git a/csharp8/ExploreCsharpEight/Program.cs b/csharp8/ExploreCsharpEight/Program.cs new file mode 100644 index 0000000..aff32be --- /dev/null +++ b/csharp8/ExploreCsharpEight/Program.cs @@ -0,0 +1,129 @@ +using System; +using System.Threading.Tasks; + +namespace ExploreCsharpEight +{ + // These are my sample creation notes, with the goal + // of making a set of recommendations for upcoming tutorials (online or local) + + // Rule 1: Make a new class for each section. + // Rule 2: Each section should have two parts: + // a. Basic syntax for the new feature. + // b. A scenario for using the feature. + + // Questions: + // What is "package" used for? + // How to include non-editable blocks + class Program + { +#nullable enable + static async Task Main(string? region = null, + string? session = null, + string? package = null, + string? project = null, + string[]? args = null) + { + return region switch + { + "Pattern_CalculateToll" => new Patterns().VehicleType(), + "Pattern_CarTaxiOccupancy" => new Patterns().TestOccupancy("Pattern_CarTaxiOccupancy"), + "Pattern_BusOccupancy" => new Patterns().TestOccupancy("Pattern_BusOccupancy"), + "Pattern_DeliveryTruckWeight" => new Patterns().TestOccupancy("Pattern_DeliveryTruckWeight"), + "Pattern_ChainedPatterns" => new Patterns().TestOccupancy("Pattern_ChainedPatterns"), + "Pattern_PeakTime" => new Patterns().PeakPricing(), + + "LocalFunction_Counting" => new StaticLocalFunctions().LocalFunctionWithCapture(), + + "Using_Block" => new UsingDeclarationsRefStruct().OldStyle(), + "Using_Declaration" => new UsingDeclarationsRefStruct().NewStyle(), + + "Nullable_PersonDefinition" => new NullableReferences().NullableTestBed(), + "Nullable_GetLengthMethod" => new NullableReferences().NullableTestBed(), + "Nullable_Usage" => new NullableReferences().NullableTestBed(), + + "AsyncStreams_Declare" => await new AsyncStreams().ConsumeStream(), + "AsyncStreams_Consume" => await new AsyncStreams().ConsumeStream(), + + "IndicesAndRanges_LastIndex" => new IndicesAndRanges().Syntax_LastIndex(), + "IndicesAndRanges_Range" => new IndicesAndRanges().Syntax_Range(), + "IndicesAndRanges_LastRange" => new IndicesAndRanges().Syntax_LastRange(), + "IndicesAndRanges_PartialRanges" => new IndicesAndRanges().Syntax_PartialRange(), + "IndicesAndRanges_RangeIndexTypes" => new IndicesAndRanges().Syntax_IndexRangeType(), + "IndicesAndRanges_CreateRange" => new IndicesAndRanges().Syntax_WhyChosenSemantics(), + "IndicesAndRanges_MathWithLength" => new IndicesAndRanges().Syntax_WhyChosenSemantics(), + "IndicesAndRanges_Disjoint" => new IndicesAndRanges().Syntax_WhyChosenSemantics(), + "IndicesAndRanges_RemoveFromEnds" => new IndicesAndRanges().Syntax_WhyChosenSemantics(), + "IndicesAndRanges_IncompleteRanges" => new IndicesAndRanges().ComputeMovingAverages(), + "IndicesAndRanges_MovingAverage" => new IndicesAndRanges().ComputeMovingAverages(), + null => await RunAll(), + _ => await RunAll() + }; + } +#nullable restore + + private static async Task RunAll() + { + Console.WriteLine("========== Starting pattern matching samples. =========="); + var patterns = new Patterns(); + Console.WriteLine(" ========== Run vehicle type. =========="); + patterns.VehicleType(); + Console.WriteLine(" ========== Run car taxi occupancy. =========="); + patterns.TestOccupancy("Pattern_CarTaxiOccupancy"); + Console.WriteLine(" ========== Run bus occupancy. =========="); + patterns.TestOccupancy("Pattern_BusOccupancy"); + Console.WriteLine(" ========== Run delivery truck. =========="); + patterns.TestOccupancy("Pattern_DeliveryTruckWeight"); + Console.WriteLine(" ========== Run chained patterns. =========="); + patterns.TestOccupancy("Pattern_ChainedPatterns"); + Console.WriteLine(" ========== Run peak pricing. =========="); + patterns.PeakPricing(); + + Console.WriteLine("========== Starting static local functions. =========="); + var localFuncs = new StaticLocalFunctions(); + Console.WriteLine(" ========== Run local func capture. =========="); + localFuncs.LocalFunctionWithCapture(); + Console.WriteLine(" ========== Run local static func. =========="); + localFuncs.LocalFunctionWithNoCapture(); + + Console.WriteLine("========== Starting using declaration / ref struct. =========="); + var usings = new UsingDeclarationsRefStruct(); + Console.WriteLine(" ========== Run using block style. =========="); + usings.OldStyle(); + Console.WriteLine(" ========== Run declaration style. =========="); + usings.NewStyle(); + + Console.WriteLine("========== Starting nullable reference samples. =========="); + var nullableSamples = new NullableReferences(); + Console.WriteLine(" ========== Run nullable. =========="); + try + { + nullableSamples.NullableTestBed(); + }catch (NullReferenceException) + { + Console.WriteLine("Initial nullable sample threw the expected NullReferenceException"); + } + Console.WriteLine("========== Starting Async Stream Samples. =========="); + var asyncSamples = new AsyncStreams(); + Console.WriteLine(" ========== Generate sequence. =========="); + await asyncSamples.ConsumeStream(); + + Console.WriteLine("========== Starting Index and Range Samples. =========="); + var indexSamples = new IndicesAndRanges(); + Console.WriteLine(" ========== Last Index. =========="); + indexSamples.Syntax_LastIndex(); + Console.WriteLine(" ========== Range. =========="); + indexSamples.Syntax_Range(); + Console.WriteLine(" ========== Last Range. =========="); + indexSamples.Syntax_LastRange(); + Console.WriteLine(" ========== Partial Range. =========="); + indexSamples.Syntax_PartialRange(); + Console.WriteLine(" ========== Index and Range types. =========="); + indexSamples.Syntax_IndexRangeType(); + Console.WriteLine(" ========== Why this syntax. =========="); + indexSamples.Syntax_WhyChosenSemantics(); + Console.WriteLine(" ========== Scenario. =========="); + indexSamples.ComputeMovingAverages(); + return 0; + } + } +} diff --git a/csharp8/ExploreCsharpEight/StaticLocalFunctions.cs b/csharp8/ExploreCsharpEight/StaticLocalFunctions.cs new file mode 100644 index 0000000..9405a93 --- /dev/null +++ b/csharp8/ExploreCsharpEight/StaticLocalFunctions.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExploreCsharpEight +{ + class StaticLocalFunctions + { + #region LocalFunction_Counting + public IEnumerable Counter(int start, int end) + { + if (start >= end) throw new ArgumentOutOfRangeException("start must be less than end"); + + return localCounter(); + + IEnumerable localCounter() + { + for (int i = start; i < end; i++) + yield return i; + } + } + #endregion + + internal int LocalFunctionWithCapture() + { + var items = Counter(1, 10); + foreach(var item in items) + { + Console.WriteLine(item); + } + return 1; + } + + internal int LocalFunctionWithNoCapture() + { + var items = StaticCounter(1, 10); + foreach (var item in items) + { + Console.WriteLine(item); + } + return 1; + } + + + #region LocalFunction_Static + public IEnumerable StaticCounter(int start, int end) + { + if (start >= end) throw new ArgumentOutOfRangeException("start must be less than end"); + + return localCounter(start, end); + + static IEnumerable localCounter(int first, int endLocation) + { + for (int i = first; i < endLocation; i++) + yield return i; + } + } + #endregion + + } +} diff --git a/csharp8/ExploreCsharpEight/UsingDeclarationsRefStruct.cs b/csharp8/ExploreCsharpEight/UsingDeclarationsRefStruct.cs new file mode 100644 index 0000000..6598ac7 --- /dev/null +++ b/csharp8/ExploreCsharpEight/UsingDeclarationsRefStruct.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace ExploreCsharpEight +{ + internal class ResourceHog : IDisposable + { + private string name; + private bool beenDisposed; + + public ResourceHog(string name) => this.name = name; + + public void Dispose() + { + beenDisposed = true; + Console.WriteLine($"Disposing {name}"); + } + + internal void CopyFrom(ResourceHog src) + { + switch (beenDisposed, src.beenDisposed) + { + case (true, true): throw new ObjectDisposedException($"Resource {name} has already been disposed"); + case (true, false): throw new ObjectDisposedException($"Resource {name} has already been disposed"); + case (false, true): throw new ObjectDisposedException($"Resource {name} has already been disposed"); + default: Console.WriteLine($"Copying from {src.name} to {name}"); return; + }; + + } + } + + internal class UsingDeclarationsRefStruct + { + internal int OldStyle() + { + #region Using_Block + using (var src = new ResourceHog("source")) + { + using (var dest = new ResourceHog("destination")) + { + dest.CopyFrom(src); + } + Console.WriteLine("After closing destination block"); + } + Console.WriteLine("After closing source block"); + #endregion + return 0; + } + internal int NewStyle() + { + #region Using_Declaration + using var src = new ResourceHog("source"); + using var dest = new ResourceHog("destination"); + dest.CopyFrom(src); + Console.WriteLine("Exiting block"); + #endregion + return 0; + } + } +} diff --git a/csharp8/asynchronous-streams.md b/csharp8/asynchronous-streams.md new file mode 100644 index 0000000..151d130 --- /dev/null +++ b/csharp8/asynchronous-streams.md @@ -0,0 +1,24 @@ +## Asynchronous streams + +Starting with C# 8.0, you can create and consume streams asynchronously. A method that returns an asynchronous stream has three properties: + +1. It's declared with the `async` modifier. +1. It returns an `System.Collections.Generic.IAsyncEnumerable` +1. The method contains `yield return` statements to return successive elements in the asynchronous stream. + +Consuming an asynchronous stream requires you to add the `await` keyword before the `foreach` keyword when you enumerate the elements of the stream. Adding the `await` keyword requires the method that enumerates the asynchronous stream to be declared with the `async` modifier and to return a type allowed for an `async` method. Typically that means returning a `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task`. It can also be a `System.Threading.Tasks.ValueTask` or `System.Threading.Tasks.ValueTask`. A method can both consume and produce an asynchronous stream, which means it would return an `System.Collections.Generic.IAsyncEnumerable`. The following code generates a sequence from 0 to 19. Every 3 elements, it pauses for 2 seconds to simulate retrieving the next set from a device: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/AsyncStreams.cs --region AsyncStreams_Declare --session async-stream +``` + +You would enumerate the sequence using the `await foreach` statement: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/AsyncStreams.cs --region AsyncStreams_Consume --session async-stream +``` + +Click the run button to experiment. Look at the times when elements are retrieved. Change some of the constants to experiment with different values, or time delays. + +> *Note*: +> This sandbox environment will timeout and halt programs that appear to be stuck. Avoid very long delays or very large sequences. + +#### Next: [Indices and ranges »](./indices-and-ranges.md) Previous: [Nullable reference types «](./nullable-fix-class.md) Home: [Home](readme.md) diff --git a/csharp8/indices-and-ranges-examples.md b/csharp8/indices-and-ranges-examples.md new file mode 100644 index 0000000..0768ee5 --- /dev/null +++ b/csharp8/indices-and-ranges-examples.md @@ -0,0 +1,33 @@ +# Indices and ranges + +The previous examples show two design decisions that caused much debate: + +- Ranges are *exclusive*, meaning the element at the last index is not in the range. +- The index `^0` is *the end* of the collection, not *the last element* in the collection. + +You can explore several examples that highlight reasons for these decisions on this page. Start by defining an array of elements, and three variables to use as indices into that array. All these samples are tied together. You can edit the values of `x`, `y` and `z`, or the expressions in the other windows, click the *indices-ranges* button and see the results. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_CreateRange --session indices-ranges +``` + +The choice that `^0` matches *the end* means math using the `Length` property is reasonable: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_MathWithLength --session indices-ranges +``` + +Ranges are *exclusive*, making consecutive, disjoint sequences clear: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_Disjoint --session indices-ranges +``` + +The choice of end means removing the same number of elements from each end of a sequence is obvious: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_RemoveFromEnds --session indices-ranges +``` + +Incomplete ranges can assume `0` or `^0` for the missing index. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_IncompleteRanges --session indices-ranges +``` + +#### Next: [Indices and ranges scenario »](./indices-and-ranges-scenario.md) Previous: [Indices and ranges «](./nullable-fix-class.md) Home: [Home](readme.md) diff --git a/csharp8/indices-and-ranges-scenario.md b/csharp8/indices-and-ranges-scenario.md new file mode 100644 index 0000000..9fefde8 --- /dev/null +++ b/csharp8/indices-and-ranges-scenario.md @@ -0,0 +1,10 @@ +# Indices and ranges + +This final example shows taking a moving average on a sequence of numbers that approaches a maximum value. As you move through the range looking at small samples of data, you can get a clear picture of general shape of the data. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_MovingAverage +``` + +Consider using this new syntax whenever you are working with subsets of data. + +#### Previous: [Indices and ranges examples «](./indices-and-ranges-examples.md) Home: [Home](readme.md) diff --git a/csharp8/indices-and-ranges.md b/csharp8/indices-and-ranges.md new file mode 100644 index 0000000..7c8ea19 --- /dev/null +++ b/csharp8/indices-and-ranges.md @@ -0,0 +1,56 @@ +# Indices and ranges + +Ranges and indices provide a succinct syntax for specifying subranges in an array, `Span`, or `ReadOnlySpan`. + +## Language support for indices and ranges + +You can specify an index **from the end**. You specify **from the end** using the `^` operator. You are familiar with `array[2]` meaning the element "2 from the start". Now, `array[^2]` means the element "2 from the end". The index `^0` means "the end", or the index that follows the last element. + +You can specify a **range** with the **range operator**: `..`. For example, `0..^0` specifies the entire range of the array: 0 from the start up to, but not including 0 from the end. Either operand may use "from the start" or "from the end". Furthermore, either operand may be omitted. The defaults are `0` for the start index, and `^0` for the end index. + +Let's look at a few examples. Consider the following array, annotated with its index from the start and from the end: + +```csharp +private string[] words = new string[] +{ + // index from start index from end + "The", // 0 ^9 + "quick", // 1 ^8 + "brown", // 2 ^7 + "fox", // 3 ^6 + "jumped", // 4 ^5 + "over", // 5 ^4 + "the", // 6 ^3 + "lazy", // 7 ^2 + "dog" // 8 ^1 +}; +``` + +The index of each element reinforces the concept of "from the start", and "from the end", and that ranges are exclusive of the end of the range. The "start" of the entire array is the first element. The "end" of the entire array is *past* the last element. + +You can retrieve the last word with the `^1` index: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_LastIndex +``` + +The following code creates a subrange with the words "quick", "brown", and "fox". It includes `words[1]` through `words[3]`. The element `words[4]` is not in the range. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_Range +``` + +The following code creates a subrange with "lazy" and "dog". It includes `words[^2]` and `words[^1]`. The end index `words[^0]` is not included: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_LastRange +``` + +The following examples create ranges that are open ended for the start, end, or both: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_PartialRanges +``` + +You can also declare ranges or indices as variables. The variable can then be used inside the `[` and `]` characters: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/IndicesAndRanges.cs --region IndicesAndRanges_RangeIndexTypes +``` + +#### Next: [Indices and ranges examples »](./indices-and-ranges-examples.md) Previous: [Asynchronous streams «](./asynchronous-streams.md) Home: [Home](readme.md) diff --git a/csharp8/nullable-fix-class.md b/csharp8/nullable-fix-class.md new file mode 100644 index 0000000..21ad574 --- /dev/null +++ b/csharp8/nullable-fix-class.md @@ -0,0 +1,40 @@ +# Declare nullable property + +The following code shows the existing definition for the `Person` class, displayed in a **nullable enabled** context. You should see the warning in the constructor that takes `firstName` and `lastName` arguments, but does not set the `MiddleName` property. + +On this page, you're going to modify the `Person` class in different ways to remove the warning. Farther down this page, you see the code that uses the `Person` class. Different changes to the `Person` class will generate different warnings in those later samples. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/NullableReferences.cs --region Nullable_PersonDefinition --session nullable +``` + +Think about your design intent. Should `MiddleName` always be non-null? Or does it better reflect your design for `MiddleName` to be `null` for some people? You can either initialize the `MiddleName` property, or change `string` to `string?` in the declaration: + +```csharp +public string? MiddleName { get; set; } +``` + +After you've made that change, look at the two code snippets that follow. Once the environment analyzes your code, you'll see warnings. Examine each and adjust the code to fix the warnings. Start with the `GetLengthOfMiddleName` method: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/NullableReferences.cs --region Nullable_GetLengthMethod --session nullable +``` + +If needed, fix the code that calls it: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/NullableReferences.cs --region Nullable_Usage --session nullable +``` + +One possible solution for `GetLengthOfName` would be the following code: + +```csharp + private static int GetLengthOfMiddleName(Person p) + { + string? middleName = p.MiddleName; + return middleName?.Length ?? 0; + } +``` + +You can let the *may be null* type flow among declarations. You can use the `?.` operator to safely dereference a *may be null* variable, combined with the `??` operator to provide a default value. + +Nullable reference types enable you to declare your intent around reference types: some should never be `null`, while for others `null` indicates a missing value. By declaring your intent, the compiler can help you enforce that intent. + +#### Next: [Asynchronous streams »](./asynchronous-streams.md) Previous: [Nullable reference types «](./nullable-reference-types.md) Home: [Home](readme.md) diff --git a/csharp8/nullable-reference-types.md b/csharp8/nullable-reference-types.md new file mode 100644 index 0000000..daa680e --- /dev/null +++ b/csharp8/nullable-reference-types.md @@ -0,0 +1,28 @@ +# Nullable reference types + +C# 8/0 defines new syntax and static analysis rules so that you can declare your design intent regarding the nullability of any reference variable. + +Earlier, C# compiled all code in a **nullable oblivious** context. The compiler had no information about your design intent: Did you expect every variable to be initialized and assigned to non-null values? Or was `null` a valid value that you should check before dereferencing the variable? In a **nullable oblivious** context, the compiler does not issue any nullability warnings, and assumes you understand your design intent throughout your codebase. + +Beginning with C# 8.0, the compiler defaults to a **nullable disabled** context. You can also opt in to a **nullable enabled** context. A **nullable disabled** context behaves the same as the earlier **nullable oblivious** behavior. You can opt in to a **nullable enabled** context. In a **nullable enabled** context, any variable of a reference type is a **non-nullable reference**. You can declare reference type variables with the `?` suffix to indicate that a variable's value may be null. + +In a **nullable enabled** context, the compiler uses static analysis to enforce these rules: + +- A non-nullable reference must be initialized to a non-null value. +- A non-nullable reference cannot be assigned to a variable that may be null. +- A nullable reference may only be dereferenced safely when static analysis determines its value cannot be null. + +There may be situations where you know a variable is not null, even though static analysis cannot prove it. For those situations, you can use the null forgiveness operator, `!`, to declare that the variable is not null. + +The nullable enabled context provides more safety against null reference exceptions. You can declare your intent, and the compiler issues warnings when your code isn't consistent with that declared intent. + +## Explore a basic scenario + +Try running the following code block: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/NullableReferences.cs --region Nullable_Usage +``` + +**Note:** That block throws a `NullReferenceException` because the `MiddleName` property is null for this person. Someone coded this error and the compiler didn't provide any helpful information to inform them that this code wasn't safe. In the next pages, you'll explore how nullable reference types would have avoided this bug. + +#### Next: [Declare design intent »](./nullable-fix-class.md) Previous: [Using declarations «](./using-declarations-ref-structs.md) Home: [Home](readme.md) diff --git a/csharp8/patterns-occupancy.md b/csharp8/patterns-occupancy.md new file mode 100644 index 0000000..f4b4361 --- /dev/null +++ b/csharp8/patterns-occupancy.md @@ -0,0 +1,35 @@ +# Add occupancy pricing + +The toll authority wants to encourage vehicles to travel at maximum capacity. They've decided to charge more when vehicles have fewer passengers, and encourage full vehicles by offering lower pricing: + +- Cars and taxis with no passengers pay an extra $0.50. +- Cars and taxis with two passengers get a 0.50 discount. +- Cars and taxis with three or more passengers get a $1.00 discount. +- Buses that are less than 50% full pay an extra $2.00. +- Buses that are more than 90% full get a $1.00 discount. + +These rules can be implemented using the **property pattern** in the same switch expression. The property pattern examines properties of the object once the type has been determined. The single case for a `Car` expands to four different cases, as does the single case for `Taxi`: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_CarTaxiOccupancy +``` + +The first three cases test the type as a `Car`, then check the value of the `Passengers` property. If both match, that expression is evaluated and returned. Next, implement the occupancy rules by expanding the cases for buses, as shown in the following example: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_BusOccupancy +``` + +The toll authority isn't concerned with the number of passengers in the delivery trucks. Instead, they charge more based on the weight class of the trucks. Trucks over 5000 lbs are charged an extra $5.00. Light trucks under 3000 lbs are given a $2.00 discount. That rule is implemented with the following code: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_DeliveryTruckWeight +``` + +Many of these switch arms are examples of **recursive patterns**. For example, `Car { Passengers: 1}` shows a constant pattern inside a property pattern. + +You can make this code less repetitive by using nested switches. The `Car` and `Taxi` both have four different arms in the preceding examples. In both cases, you can create a type pattern that feeds into a property pattern. This technique is shown in the following code: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_ChainedPatterns +``` + +In the preceding sample, using a recursive expression means you don't repeat the `Car` and `Taxi` arms containing child arms that test the property value. This technique isn't used for the `Bus` and `DeliveryTruck` arms because those arms are testing ranges for the property, not discrete values. + +#### Next: [Add peak time pricing »](./patterns-peakpricing.md) Previous: [Type pattern «](./patterns-types.md) Home: [Home](readme.md) diff --git a/csharp8/patterns-peakpricing.md b/csharp8/patterns-peakpricing.md new file mode 100644 index 0000000..e0bbb78 --- /dev/null +++ b/csharp8/patterns-peakpricing.md @@ -0,0 +1,154 @@ +# Add peak pricing + +For the final feature, the toll authority wants to add time sensitive peak pricing. During the morning and evening rush hours, the tolls are doubled. That rule only affects traffic in one direction: inbound to the city in the morning, and outbound in the evening rush hour. During other times during the workday, tolls increase by 50%. Late night and early morning, tolls are reduced by 25%. During the weekend, it's the normal rate, regardless of the time. + +You'll use pattern matching for this feature, but you'll integrate it with other techniques. You could build a single pattern match expression that would account for all the combinations of direction, day of the week, and time. The result would be a complicated expression. It would be hard to read and difficult to understand. That makes it hard to ensure correctness. Instead, combine those methods to build a tuple of values that concisely describes all those states. Then use pattern matching to calculate a multiplier for the toll. The tuple contains three discrete conditions: + +- The day is either a weekday or a weekend. +- The band of time when the toll is collected. +- The direction is into the city or out of the city + +The following table shows the combinations of input values and the peak pricing multiplier: + +| Day | Time | Direction | Premium | +| ---------- | ------------ | --------- |--------:| +| Weekday | morning rush | inbound | x 2.00 | +| Weekday | morning rush | outbound | x 1.00 | +| Weekday | daytime | inbound | x 1.50 | +| Weekday | daytime | outbound | x 1.50 | +| Weekday | evening rush | inbound | x 1.00 | +| Weekday | evening rush | outbound | x 2.00 | +| Weekday | overnight | inbound | x 0.75 | +| Weekday | overnight | outbound | x 0.75 | +| Weekend | morning rush | inbound | x 1.00 | +| Weekend | morning rush | outbound | x 1.00 | +| Weekend | daytime | inbound | x 1.00 | +| Weekend | daytime | outbound | x 1.00 | +| Weekend | evening rush | inbound | x 1.00 | +| Weekend | evening rush | outbound | x 1.00 | +| Weekend | overnight | inbound | x 1.00 | +| Weekend | overnight | outbound | x 1.00 | + +There are 16 different combinations of the three variables. By combining some of the conditions, you'll simplify the final switch expression. + +The system that collects the tolls uses a structure for the time when the toll was collected. Build member methods that create the variables from the preceding table. The following function uses a pattern matching switch expression to express whether a represents a weekend or a weekday: + +```csharp +private static bool IsWeekDay(DateTime timeOfToll) => + timeOfToll.DayOfWeek switch + { + DayOfWeek.Monday => true, + DayOfWeek.Tuesday => true, + DayOfWeek.Wednesday => true, + DayOfWeek.Thursday => true, + DayOfWeek.Friday => true, + DayOfWeek.Saturday => false, + DayOfWeek.Sunday => false + }; +``` + +That method works, but it's repetitious. You can simplify it, as shown in the following code: + +```csharp +private static bool IsWeekDay(DateTime timeOfToll) => + timeOfToll.DayOfWeek switch + { + DayOfWeek.Saturday => false, + DayOfWeek.Sunday => false, + _ => true + }; +``` + +Next, add a similar function to categorize the time into the blocks: + +```csharp +private enum TimeBand +{ + MorningRush, + Daytime, + EveningRush, + Overnight +} + +private static GetTimeBand GetTimeBand(DateTime timeOfToll) +{ + int hour = timeOfToll.Hour; + if (hour < 6) + return TimeBand.Overnight; + else if (hour < 10) + return TimeBand.MorningRush; + else if (hour < 16) + return TimeBand.Daytime; + else if (hour < 20) + return TimeBand.EveningRush; + else + return TimeBand.Overnight; +} +``` + +The previous method doesn't use pattern matching. It's clearer using a familiar cascade of `if` statements. You do add a private `enum` to convert each range of time to a discrete value. + +After you create those methods, you can use another `switch` expression with the **tuple pattern** to calculate the pricing premium. You could build a `switch` expression with all 16 arms: + +```csharp +public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) => + (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch + { + (true, TimeBand.MorningRush, true) => 2.00m, + (true, TimeBand.MorningRush, false) => 1.00m, + (true, TimeBand.Daytime, true) => 1.50m, + (true, TimeBand.Daytime, false) => 1.50m, + (true, TimeBand.EveningRush, true) => 1.00m, + (true, TimeBand.EveningRush, false) => 2.00m, + (true, TimeBand.Overnight, true) => 0.75m, + (true, TimeBand.Overnight, false) => 0.75m, + (false, TimeBand.MorningRush, true) => 1.00m, + (false, TimeBand.MorningRush, false) => 1.00m, + (false, TimeBand.Daytime, true) => 1.00m, + (false, TimeBand.Daytime, false) => 1.00m, + (false, TimeBand.EveningRush, true) => 1.00m, + (false, TimeBand.EveningRush, false) => 1.00m, + (false, TimeBand.Overnight, true) => 1.00m, + (false, TimeBand.Overnight, false) => 1.00m, + }; +``` + +The above code works, but it can be simplified. All eight combinations for the weekend have the same toll. You can replace all eight with the following line: + +```csharp +(false, _, _) => 1.0m, +``` + +Both inbound and outbound traffic have the same multiplier during the weekday daytime and overnight hours. Those four switch arms can be replaced with the following two lines: + +```csharp +(true, TimeBand.Overnight, _) => 0.75m, +(true, TimeBand.Daytime, _) => 1.5m, +``` + +The code should look like the following code after those two changes: + +```csharp +public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) => + (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch + { + (true, TimeBand.MorningRush, true) => 2.00m, + (true, TimeBand.MorningRush, false) => 1.00m, + (true, TimeBand.Daytime, _) => 1.50m, + (true, TimeBand.EveningRush, true) => 1.00m, + (true, TimeBand.EveningRush, false) => 2.00m, + (true, TimeBand.Overnight, _) => 0.75m, + (false, _, _) => 1.00m, + }; +``` + +Finally, you can remove the two rush hour times that pay the regular price. Once you remove those arms, you can replace the `false` with a discard (`_`) in the final switch arm. You'll have the following finished method: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_PeakTime +``` + +This example highlights one of the advantages of pattern matching: the pattern branches are evaluated in order. If you rearrange them so that an earlier branch handles one of your later cases, the compiler warns you about the unreachable code. Those language rules made it easier to do the preceding simplifications with confidence that the code didn't change. + +Pattern matching makes some types of code more readable and offers an alternative to object-oriented techniques when you can't add code to your classes. The cloud is causing data and functionality to live apart. The *shape* of the data and the *operations* on it aren't necessarily described together. In this tutorial, you consumed existing data in entirely different ways from its original function. Pattern matching gave you the ability to write functionality that overrode those types, even though you couldn't extend them. + +#### Next: [static local functions »](./static-local-functions.md) Previous: [Add occupancy pricing «](./patterns-occupancy.md) Home: [Home](readme.md) diff --git a/csharp8/patterns-types.md b/csharp8/patterns-types.md new file mode 100644 index 0000000..a32b52a --- /dev/null +++ b/csharp8/patterns-types.md @@ -0,0 +1,28 @@ +# Pattern matching designs + +This scenario highlights the kinds of problems that pattern matching is well-suited to solve: + +- The objects you need to work with aren't in an object hierarchy that matches your goals. You may be working with classes that are part of unrelated systems. +- The functionality you're adding isn't part of the core abstraction for these classes. The toll paid by a vehicle *changes* for different types of vehicles, but the toll isn't a core function of the vehicle. + +When the *shape* of the data and the *operations* on that data are not described together, the pattern matching features in C# make it easier to work with. + +## Implement the basic toll calculations + +The most basic toll calculation relies only on the vehicle type: + +- A `Car` is $2.00. +- A `Taxi` is $3.50. +- A `Bus` is $5.00. +- A `DeliveryTruck` is $10.00 + +Create a new `TollCalculator` class, and implement pattern matching on the vehicle type to get the toll amount. The following code shows the initial implementation of the `TollCalculator` class's `CalculateToll`. + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/Patterns.cs --region Pattern_CalculateToll +``` + +The preceding code uses a **switch expression** (not the same as a [`switch`](../language-reference/keywords/switch.md) statement) that tests the **type pattern**. A **switch expression** begins with the variable, `vehicle` in the preceding code, followed by the `switch` keyword. Next comes all the **switch arms** inside curly braces. The `switch` expression makes other refinements to the syntax that surrounds the `switch` statement. The `case` keyword is omitted, and the result of each arm is an expression. The last two arms show a new language feature. The `{ }` case matches any non-null object that didn't match an earlier arm. This arm catches any incorrect types passed to this method. The `{ }` case must follow the cases for each vehicle type. If the order were reversed, the `{ }` case would take precedence. Finally, the `null` pattern detects when a `null` is passed to this method. The `null` pattern can be last because the other type patterns match only a non-null object of the correct type. + +You're starting to see how patterns can help you create algorithms where the code and the data are separate. The `switch` expression tests the type and produces different values based on the results. That's only the beginning. + +#### Next: [Add occupancy pricing »](./patterns-occupancy.md) Previous: [Pattern Matching «](./patterns.md) Home: [Home](readme.md) diff --git a/csharp8/patterns.md b/csharp8/patterns.md new file mode 100644 index 0000000..baf4532 --- /dev/null +++ b/csharp8/patterns.md @@ -0,0 +1,63 @@ +# More patterns in more places + +**Pattern matching** gives tools to provide shape-dependent functionality across related but different kinds of data. C# 7.0 introduced syntax for type patterns and constant patterns by using the [`is`](../language-reference/keywords/is.md) expression and the [`switch`](../language-reference/keywords/switch.md) statement. These features represented the first tentative steps toward supporting programming paradigms where data and functionality live apart. As the industry moves toward more microservices and other cloud-based architectures, other language tools are needed. + +C# 8.0 expands this vocabulary so you can use more pattern expressions in more places in your code. Consider these features when your data and functionality are separate. Consider pattern matching when your algorithms depend on a fact other than the runtime type of an object. These techniques provide another way to express designs. + +## Scenarios for pattern matching + +Modern development often includes integrating data from multiple sources and presenting information and insights from that data in a single cohesive application. You and your team won't have control or access for all the types that represent the incoming data. + +The classic object-oriented design would call for creating types in your application that represent each data type from those multiple data sources. Then, your application would work with those new types, build inheritance hierarchies, create virtual methods, and implement abstractions. Those techniques work, and sometimes they are the best tools. Other times you can write less code. You can write more clear code using techniques that separate the data from the operations that manipulate that data. + +In this tutorial, you'll create and explore an application that takes incoming data from several external sources for a single scenario. You'll see how **pattern matching** provides an efficient way to consume and process that data in ways that weren't part of the original system. + +Consider a major metro area that is using tolls and peak time pricing to manage traffic. You write an application that calculates tolls for a vehicle based on its type. Later enhancements incorporate pricing based on the number of occupants in the vehicle. Further enhancements add pricing based on the time and the day of the week. + +From that brief description, you may have quickly sketched out an object hierarchy to model this system. However, your data is coming from multiple sources like other vehicle registration management systems. These systems provide different classes to model that data and you don't have a single object model you can use. In this tutorial, you'll use these simplified classes to model for the vehicle data from these external systems, as shown in the following three short code snippets. + +The `Car` class comes from the `ConsumerVehicleRegistration` system: + +```csharp +namespace ConsumerVehicleRegistration +{ + public class Car + { + public int Passengers { get; set; } + } +} +``` + +The `DeliveryTruck` class comes from the `CommercialRegistration` system: + +```csharp +namespace CommercialRegistration +{ + public class DeliveryTruck + { + public int GrossWeightClass { get; set; } + } +} +``` + +And the `LiveryRegistration` system manages the `Taxi` and `Bus` classes: + +```csharp +namespace LiveryRegistration +{ + public class Taxi + { + public int Fares { get; set; } + } + + public class Bus + { + public int Capacity { get; set; } + public int Riders { get; set; } + } +} +``` + +These classes are from different systems, and are in different namespaces. No common base class, other than `System.Object` can be leveraged. Object Oriented techniques will not serve for this problem. + +#### Next: [Explore type patterns »](./patterns-types.md) Home: [Home](readme.md) diff --git a/csharp8/readme.md b/csharp8/readme.md new file mode 100644 index 0000000..149be75 --- /dev/null +++ b/csharp8/readme.md @@ -0,0 +1,22 @@ +# What's new in C# 8.0 - exploration + +This exploration enables you to experiment with the features that have been released in C# 8.0, preview 2. You can try the code in the samples, modify it, and see the effects of using the new features in a variety of scenarios. + +You can try the following features that first appeared in preview 2: + +- [Pattern matching enhancements](./patterns.md): +- [Static local functions](static-local-functions.md) +- [Using declarations](using-declarations-ref-structs.md) + +The following language features first appeared in C# 8.0 preview 1: + +- [Nullable reference types](nullable-reference-types.md) +- [Asynchronous streams](asynchronous-streams.md) +- [Indices and ranges](indices-and-ranges.md) + +> *Note*: +> This exploration was last updated for C# 8.0 preview 2. + +You can try all of the features in the upcoming version of C# by stepping through the different sections of this tutorial. Or, you can select any of the preceding links to go to the feature you want to explore first. + +#### Next: [More patterns in more places »](./patterns.md) diff --git a/csharp8/static-local-functions.md b/csharp8/static-local-functions.md new file mode 100644 index 0000000..5cc4c51 --- /dev/null +++ b/csharp8/static-local-functions.md @@ -0,0 +1,24 @@ +# Static local functions + +You can now add the `static` modifier to local functions to ensure that local function doesn't capture (reference) any variables from the enclosing scope. Doing so generates `CS8421`, "A static local function can't contain a reference to \." + +Typically, any resources declared or used in a function are released when the function exits. However, when a function *captures* a variable that is declared in an enclosing scope, that resource has a lifetime that extends beyond the function itself. In many scenarios, this isn't an issue. In others, capturing and preventing the release of resources can impact performance. Declaring local functions as `static` ensures this doesn't happen. + +Consider the following code. The local function `LocalFunction` accesses the variable `y`, declared in the enclosing scope (the method `M`). Therefore, `LocalFunction` can't be declared with the `static` modifier: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/StaticLocalFunctions.cs --region LocalFunction_Counting +``` + +The local iterator method *captures* the parameters `start` and `end`. Add the `static` modifier to see the compiler generated warning. You'll need to declare arguments to the local function so that those values aren't captured. Make the changes shown below to get the warning removed: + +```csharp +static IEnumerable localCounter(int first, int endLocation) +{ + for (int i = first; i < endLocation; i++) + yield return i; +} +``` + +The sample should compile and run correctly. + +#### Next: [disposable ref structs and using declarations »](using-declarations-ref-structs.md) Previous: [Add peak pricing «](./patterns-peakpricing.md) Home: [Home](readme.md) diff --git a/csharp8/using-declarations-ref-structs.md b/csharp8/using-declarations-ref-structs.md new file mode 100644 index 0000000..3fdd294 --- /dev/null +++ b/csharp8/using-declarations-ref-structs.md @@ -0,0 +1,21 @@ +# using declarations + +A **using declaration** is a variable declaration preceded by the `using` keyword. It tells the compiler that the variable being declared should be disposed at the end of the enclosing scope. The `using` block has always been recommended for local variables when the type implements `IDisposable`. The following example shows a hypothetical use of two objects that implement `IDisposable`. Run the code and you'll see the `ResourceHog` writes a message when it is being disposed: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/UsingDeclarationsRefStruct.cs --region Using_Block +``` + +In the preceding example, each object is disposed when the closing brace for its `using` statement is reached. The new `using` declaration generates code that automatically disposes an object at the end of the enclosing block. That results in cleaner code that is easier to understand: + +```cs --project ./ExploreCsharpEight/ExploreCsharpEight.csproj --source-file ./ExploreCsharpEight/UsingDeclarationsRefStruct.cs --region Using_Declaration +``` + +In the preceding example, the resources are disposed at the end of the block. Run it to see the results, which should model the previous sample. + +In both cases, the compiler generates the call to `Dispose()`. The compiler generates an error if the expression in the using statement is not disposable. + +# Disposable ref structs + +A `struct` declared with the `ref` modifier may not implement any interfaces and so cannot implement `System.IDisposable`. Therefore, to enable a `ref struct` to be disposed, it must have an accessible `void Dispose()` method. This also applies to `readonly ref struct` declarations. These types can now release resources in `using` declarations or `using` blocks. + +#### Next: [nullable reference types »](./nullable-reference-types.md) Previous: [Static local functions «](./static-local-functions.md) Home: [Home](readme.md)