title |
---|
Exploring C# 8 New Language Features |
- ☁️ Qualification
- ☁️ Qualification
- Originally released as part of Visual Studio .NET 2002
- Was very similar to Java in syntax
- Had the basic build blocks of an object-oriented programming language
- Introduced more advanced features
- Generics
- Anonymous members and types
- Nullable types
- Iterators
- Lambda Expressions
- Dynamic
- Interop
- Introduced language-native asynchronous keywords (
async
/await
) - Introduced Task-based asynchronous programming
- Added many quality-of-life features
- Advanced exception filter syntax
- Auto-initialized properties
- Members with expression bodies
- String interpolation
- Null conditionals
- Released side-by-side with Visual Studio 2015
- Roslyn meant that C# was now compiled using C#
- The rate of progress can now grow exponentially
- 👉 Many C# developers are here
- Out variables
- Tuples
- Discards
- Local functions
- Async Main methods
- Pattern matching
- Default interface implementations
- Using delcarations
- Async streams
- ...and more...
- Cover many of the enhancements in C# 7 & 8
- Use these new enhanced features side-by-side with language features from C# 6 and before
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
::: notes
Static, synchronous method with arguments
:::
static void Main(string[] args)
{
var task = new Library().DoSomething();
task.Wait();
var result = task.Result;
}
static void Main(string[] args)
{
var taskOne = new Library().DoSomething();
var taskTwo = new Library().DoSomethingElse();
Task.WaitAll(taskOne, taskTwo);
var resultOne = taskOne.Result;
var resultTwo = taskTwo.Result;
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
AsyncCalls().Wait();
}
static async Task AsyncCalls()
{
var resultOne = await new Library().DoSomething();
var resultTwo = await new Library().DoSomethingElse();
}
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var resultOne = await new Library().DoSomething();
var resultTwo = await new Library().DoSomethingElse();
}
- Many cloud SDKs/libraries are asynchronous by default
- Reduces the complexity of your Program.cs file
public static void Main(string[] args)
{
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build()
.Run();
}
public static async Task Main(string[] args)
{
await Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build()
.RunAsync();
}
- Typically, must implement all members of an interface
- What if you wanted to ship a default implementation with the interface?
- Useful if you need to add members to an in-use interface
public interface IPerson
{
string FirstName { get; }
string LastName { get; }
}
public interface IPerson
{
string FirstName { get; }
string LastName { get; }
string GetFullName() {
return $"{FirstName} {LastName}";
}
}
public class User : IPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
IPerson me = new User { FirstName = "Sidney", LastName = "Andrews" };
Console.WriteLine(me.GetFullName());
Methods declared within the context of another method Ideal for units of logic only called from within the current method Used commonly to separate parameter validation from method implementation
::: notes
https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-7#local-functions
:::
public string ConcatenateNumbers(params int[] values)
{
if (values.Count() == 0)
throw new ArgumentException(nameof(values));
return $"[{String.Join(", ", values)}]";
}
public string ConcatenateNumbers(params int[] values)
{
if (!CheckNumbers())
throw new ArgumentException(nameof(values));
return CombineNumbers();
string CombineNumbers()
{
return $"[{String.Join(", ", values)}]";
}
bool CheckNumbers()
{
return values.Count() > 0;
}
}
public string ConcatenateNumbers(params int[] values)
{
if (!CheckNumbers(values))
throw new ArgumentException(nameof(values));
return CombineNumbers(values);
static string CombineNumbers(int[] values)
{
return $"[{String.Join(", ", values)}]";
}
static bool CheckNumbers(int[] values)
{
return values.Count() > 0;
}
}
::: notes
The local function can be static since it doesn’t access any variables in the enclosing scope.
This example is functionally identical.
:::
static void Main(string[] args)
{
Console.WriteLine(GetGreeting(args.FirstOrDefault()));
static string GetGreeting(string name)
{
return $"Hello, {name ?? "Person"}!";
}
}
- Very common to need default values for our properties
- Many times, we may have read-only properties that are only set in the constructor of our class
public class User
{
private string _domain = "github.com";
string Domain {
get { return _domain; }
}
}
public class User
{
string Domain { get; }
public User()
{
Domain = "github.com";
}
}
- Declare an initial value for a property as part of the property declaration
- Removes extra code necessary to initialize properties in the constructor
::: notes
https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-6#auto-property-initializers
:::
public class User
{
string Domain { get; } = "github.com";
}
- Use lambda syntax to define the body of a member
- Methods
- Properties
- Constructors
- Get/Set Accessors
- Finalizers (Garbage Collection)
public class User
{
public string FirstName { get; set; }
public string Domain { get; set; }
public string Email => $"{FirstName}@{Domain}";
}
public class User
{
public string FirstName { get; set; }
public string Domain { get; set; }
public override string ToString() =>
$"{Domain}//{FirstName}";
}
public class User
{
public string FirstName { get; set; }
public string Domain = "SKILLMEUP.COM";
public User(string firstName) =>
FirstName = firstName;
}
public class User
{
private string _firstName;
string FirstName
{
get => _firstName;
set => _firstName = value ?? "STUDENT";
}
}
static void Main(string[] args) =>
Console.WriteLine("Hello, World!");
public static async Task Main(string[] args) =>
await Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build()
.RunAsync();
- Types defined in a lightweight syntax
- Supported for a long time but the syntax was recently improved
private Tuple<string, int> GetRoom()
{
return new Tuple<string, int>("Breakout", 234);
}
var room = GetRoom();
Console.WriteLine($"Room: {room.Item1}");
Console.WriteLine($"Number: {room.Item2}");
private (string, int) GetRoom()
{
return ("Breakout", 234);
}
(string name, int number) room = GetRoom();
Console.WriteLine($"Room: {room.name}");
Console.WriteLine($"Number: {room.number}");
string name = "Breakout";
int number = 234;
var room = (name, number);
Console.WriteLine($"Room: {room.name}");
Console.WriteLine($"Number: {room.number}");
(string first, string last) person = ("Sidney", "Andrews");
string fullname = $"{person.first} {person.last}";
var person = (first: "Sidney", last: "Andrews");
string fullname = $"{person.first} {person.last}";
private (string, int) GetRoom()
{
return ("Breakout", 234);
}
(string name, int number) = GetRoom();
Console.WriteLine($"Room: {name}");
Console.WriteLine($"Number: {number}");
- Dummy variables that are intentionally unused
- Equivalent to variables that were never assigned
- Uses a special character as its name
private (string, int) GetRoom()
{
return ("Breakout", 234);
}
(_, int number) = GetRoom();
Console.WriteLine($"Number: {number}");
::: notes
Here, we don't need the room name so the code discards it immediately.
:::
::: notes
TBD
:::
- Test that a value has a certain schema (or shape)
- If the value "matches", then extract information
- All about improving the syntax for common checks
if (shape is Rectangle)
{
var r = shape as Rectangle;
return r.HorizontalSide * r.VerticalSide;
}
else if (shape is Circle)
{
var c = shape as Circle;
return c.Radius * c.Radius * Math.PI;
}
else
{
throw new ArgumentException("Shape not found", nameof(shape));
}
if (shape is Rectangle r)
return r.HorizontalSide * r.VerticalSide;
else if (shape is Circle c)
return c.Radius * c.Radius * Math.PI;
else
throw new ArgumentException("Shape not found", nameof(shape));
- Pattern matching changes functionality of a statement based on characteristics of the data
- Use matching with switch statements to build more concise “filters”
switch (shape)
{
case Rectangle r:
return r.HorizontalSide * r.VerticalSide;
case Circle c:
return c.Radius * c.Radius * Math.PI;
default:
throw new ArgumentException("Shape not found", nameof(shape));
}
double area = shape switch
{
Rectangle r => r.HorizontalSide * r.VerticalSide,
Circle c => c.Radius * c.Radius * Math.PI,
_ => throw new ArgumentException("Shape not found", nameof(shape))
};
double area = shape switch
{
Rectangle r when r.HorizontalSide == 0 || r.VerticalSide == 0 => 0,
Circle c when c.Radius == 0 => 0,
Rectangle r => r.HorizontalSide * r.VerticalSide,
Circle c => c.Radius * c.Radius * Math.PI,
_ => throw new ArgumentException("Shape not found", nameof(shape))
};
public enum Location
{
FloorOne = 0,
FloorTwo,
FloorThree
}
Location loc = Location.FloorTwo;
string location = loc switch
{
Location.FloorOne => "Main Lobby",
Location.FloorTwo => "Guest Rooms",
Location.FloorThree => "Penthouse Suite",
_ => "Not Found"
};
Console.WriteLine($"Location: {location}");
public class Student
{
public string Name { get; set; }
public double GPA { get; set; }
}
Student student = new Student { Name = "Savannah" };
int grade = student switch
{
{ Name: "Jaiden" } => 90,
{ Name: "Jackson" } => 70,
{ Name: "Savannah" } => 85,
_ => throw new ArgumentNullException(nameof(student))
};
Console.WriteLine($"{student.Name}'s Math Grade: {grade}");
(string first, string last) = ("Jaiden", "Ashby");
string greeting = (first, last) switch
{
("Jaiden", _) => "Student of the year, Jaiden!",
(_, "Andrews") => "Mr. Andrews",
(_, _) => String.Empty
};
Console.WriteLine(greeting);
public enum Group
{
Unknown = 0,
UpperElementary,
LowerElementary
}
public class Student
{
public string FullName { get; set; }
public int GradeLevel { get; set; }
public void Deconstruct(out string name, out int grade)
{
name = FullName;
grade = GradeLevel;
}
}
public enum Group
{
Unknown = 0,
UpperElementary,
LowerElementary
}
public class Student
{
public string FullName { get; set; }
public int GradeLevel { get; set; }
public void Deconstruct(out string name, out int grade) =>
(name, grade) = (FullName, GradeLevel);
}
var student = new Student { FullName = "Jaiden Ashby", GradeLevel = 5 };
Group group = student switch
{
var (_, grade) when grade >= 3 => Group.UpperElementary,
var (_, grade) when grade <= 2 => Group.LowerElementary,
_ => Group.Unknown
};
::: notes
TBD
:::
- Using Declarations
- Using Static
- Variable declaration that is disposed at the end of the enclosing scope
- Removes some of the extra syntax required with a traditional using statement
::: notes
https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-8#using-declarations
:::
using System.IO;
public void ReadContentFromFile()
{
using (var stream = File.OpenRead(@"D:\sys.log"))
{
// Perform operations here
}
}
using System.IO;
public void ReadContentFromFile()
{
using var stream = File.OpenRead(@"D:\sys.log")
// Perform operations here
}
::: notes
Disposal occurs at the end of the enclosing scope
:::
- Allows access to static members and child (nested) types without strictly specifying the root type name
- Special directive
- Typically included at the top of the code file
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/keywords/using-static
:::
using static System.IO.Directory;
public class Program
{
public bool LogDirectoryAvailable()
{
return Exists("C:\Logs");
}
}
::: notes
string System.IO.Directory.Exists
:::
using static System.IO.File;
public class Program
{
public string GetLogContent()
{
return ReadAllText("C:\Logs\server.txt");
}
}
::: notes
string System.IO.File.ReadAllText(string filename)
:::
using static System.Math;
public class Program
{
public double GetSquare(double number)
{
return Pow(number, 2);
}
}
::: notes
double System.Math.Pow(double number, double power)
:::
foreach (var device in devices)
{
(double lat, double lon) = await device.GetReadingAsync();
Console.WriteLine($"Latitude: {lat} | Longitude: {lon}");
}
async IAsyncEnumerable<(double, double)> GetReadingsAsync(IEnumerable<Device> devices)
{
foreach (var device in devices)
{
yield return await device.GetReadingAsync();
}
}
await foreach ((double lat, double lon) in GetReadingsAsync(devices))
{
Console.WriteLine($"Latitude: {lat} | Longitude: {lon}");
}
var orders = context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.OrderByDescending(o => o.SubmittedDate);
await foreach(var order in orders.AsAsyncEnumerable())
{
// Perform logic
}
- Simple syntax to get a subset of an array
- Syntax can start from the beginning index or end index
- You can also use the range operator (..) to create open-ended ranges
- Can also be used on Span<> and ReadOnlySpan<>
string[] phonetic =
{
"Alfa", "Bravo", "Charlie", "Delta",
"Echo", "Foxtrot", "Golf", "Hotel",
"India", "Juliett", "Kilo", "Lima",
"November", "Oscar", "Papa", "Quebec",
"Romeo", "Sierra", "Tango", "Uniform",
"Victor", "Whisket", "X-Ray", "Yankee",
"Zulu"
};
string[] m_thru_s = phonetic[12..19];
string[] a_thru_e = phonetic[0..5];
string[] u_thru_z = phonetic[20..26];
string[] v_to_end = phonetic[21..];
string[] start_to_g = phonetic[..7];
string z_to_end =
string[] q_to_x = phonetic[^10..^2];
string[] t_to_end = phonetic[^7..];
Range start_to_m = ..13;
string[] frontHalfAlphabet = phonetic[start_to_m];
Range n_to_end = ^13..;
string[] backHalfAlphabet = phonetic[n_to_end];
- Performs a null check and returns an operand based on the result
- If the checked value is null, return the right-hand operand
- If the checked value is not null, return the left-hand operand
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/null-coalescing-operator
:::
int? result = null;
Console.WriteLine(result ?? "No Result");
string response = null;
Console.WriteLine(response ?? String.Empty);
- Evaluates a Boolean expression and evaluates one of two expressions based on the result
- If the checked value is true, evaluates the left-side expression (consequent)
- If the checked value is false, evaluates the right-side expression (alternative)
- Expressions must evaluate to the same type
- Can be nested multiple times
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/conditional-operator
is this condition true ? yes : no
:::
true ? "Returned Value" : "Not Returned"
bool isAvailable = false;
bool hasBackup = true;
isAvailable ? "☑☑" : hasBackup ? "❎☑" : "❎❎";
isAvailable ? "☑☑" : (hasBackup ? "❎☑" : "❎❎");
::: notes
The operator is right-associative. That means the last two expressions are equivalent
:::
- Tests the left-hand operand for null
- If null, short-circuits execution and returns null
- If not null, perform member access
- More concise syntax for null checks
- Can be chained
- Will short-circuit on first null value
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/null-conditional-operators
:::
string firstName = student?.FirstName;
string firstName = student?.Name?.First;
bool result = student?.Enroll("CSC 100", "Fall 2035");
- Similar to conditional member access
- Can be used in a chain with member access
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/null-conditional-operators
:::
var student = students?["demo-student-1345"];
var name = students?["demo-student-1345"]?.FirstName
var firstStudent = course?.Students[0]?.Name?.First;
var id = universities?[0].Courses?[0]?.Identifier;
- Used to assign the right-hand value to the left-hand conditionally
- Only if the left-hand currently evaluates to null
- If left-hand is non-null, the right-hand is not evaluated
::: notes
https://docs.microsoft.com/dotnet/csharp/language-reference/operators/null-coalescing-operator
:::
IEnumerable<int> numbers = null;
numbers ??= Enumerable.Empty<int>();
int count = numbers.Count();
::: notes
TBD
:::