C# 8.0 New Features
Hi,
Today I thought lets go deeply all the new C# 8.0 feature practically and usages concepts. Here are all the details which you will like.
1. Readonly members
You can apply the readonly modifier to members of a struct. It indicates that the member doesn't modify state. It's more granular than applying the readonly modifier to a struct declaration. Consider the following mutable struct:
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Distance => Math.Sqrt(X * X + Y * Y);
public override string ToString() => $"({X}, {Y}) is {Distance} from the origin";
}
Like most structs, the ToString() method doesn't modify state. You could indicate that by adding the readonly modifier to the declaration of ToString():
public readonly override string ToString() => $"({X}, {Y}) is {Distance} from the origin";
2. Default interface methods
You can now add members to interfaces and provide an implementation for those members. Existing implementations inherit the default implementation.
namespace TestConsoleCore3._1
{
class Program
{
static void Main(string[] args)
{
ICustomer obj = new ChildClass();
obj.ComputeLoyaltyDiscount();
obj.Run();
Console.WriteLine("Execution completed....");
}
}
public interface ICustomer
{
void Run();
public void ComputeLoyaltyDiscount()
{
Console.WriteLine("Interface method implementation called");
}
}
public class ChildClass : ICustomer
{
public void Run()
{
Console.WriteLine("Child calss run method called");
}
// public void ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);
// protected static void DefaultLoyaltyDiscount(ICustomer c)
// {
// Console.WriteLine("Override interface implemented method into child class called");
// }
}
}
//Output
Interface method implementation called
Child calss run method called
Execution completed....
Extend the default above implemented method : So the method "ComputeLoyaltyDiscount" default implemented body get available with child class. But if you want to override or change implementation in child/inherited class then use protect and static keyword in child class to override the method. So in above code uncomment the commented code for this example and output will be -
//Output
Override interface implemented method into child class called
Child calss run method called
Execution completed....
3. More Patterns expressions
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 expression and the switch statement. 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.
Switch Expression Pattern-
Often, a switch statement produces a value in each of its case blocks. Switch expressions enable you to use more concise expression syntax. There are fewer repetitive case and break keywords, and fewer curly braces. As an example
public enum Rainbow { Red, Orange }
public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
_ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand))
};
The equivalent code using the classic switch statement with old version:
public static RGBColor FromRainbowClassic(Rainbow colorBand)
{
switch (colorBand)
{
case Rainbow.Red:
return new RGBColor(0xFF, 0x00, 0x00);
case Rainbow.Orange:
return new RGBColor(0xFF, 0x7F, 0x00);
default:
throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
};
}
There are several syntax improvements here:
- The variable comes before the switch keyword. The different order makes it visually easy to distinguish the switch expression from the switch statement.
- The case and : elements are replaced with =>. It's more concise and intuitive.
- The default case is replaced with a _ discard.
- The bodies are expressions, not statements.
Similarly Property Pattern, example
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "MN" } => salePrice * 0.75M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};
Similarly Tuple Pattern
Some algorithms depend on multiple inputs. Tuple patterns allow you to switch based on multiple values expressed as a tuple.
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
(_, _) => "tie" };
}
Positional Pattern-
You can use positional patterns to inspect properties of the object and use those properties for a pattern. Consider the following Point class that includes a Deconstruct method to create discrete variables for X and Y:
public class Point
{
public int X { get; }
public int Y { get; }
//Here the left capes (X, Y) is where value getting assigned from passed value via constructor and right section i.e. small (x, y).
public Point(int x, int y) => (X, Y) = (x, y);
//Here the left small (x, y) is where value getting assigned from class property capes (X, Y) on object creation/via-constructor.
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
4. 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. For example,
static void WriteLinesToFile(IEnumerable<string> lines)
{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
// do some coding here to create/write file....
// file is disposed here
}
The preceding code is equivalent to the following code,
static void WriteLinesToFile(IEnumerable<string> lines)
{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
// do some coding here to create/write file....
// file is disposed here
}
}
5. 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, "A static local function can't contain a reference to local variable." Below is the example,
public class X
{
int y = 5;
//Can not make static as local non-static variable y is being use
int LocalFunction(int moreValue) => y + moreValue;
//static can be made as no local variabble is being use
static int Add(int left, int right) => left + right;
}
6. Disposable ref structs
A struct declared with the ref modifier may not implement any interfaces and so can't implement IDisposable. Therefore, to enable a ref struct to be disposed, it must have an accessible void Dispose() method. This feature also applies to readonly ref struct declarations.
ref struct Test
{
..........
public void Dispose() {...}
}
7. Nullable reference types
Inside a nullable annotation context, any variable of a reference type is considered to be a nonnullable reference type. If you want to indicate that a variable may be null, you must append the type name with the ? to declare the variable as a nullable reference type.
string? name;
The compiler uses static analysis to determine if a nullable reference is known to be non-null. The compiler warns you if you dereference a nullable reference when it may be null. You can override this behavior by using the null-forgiving operator ! following a variable name. For example, if you know the name variable isn't null but the compiler issues a warning, you can write the following code to override the compiler's analysis:
name!.Length;
8. Indices and ranges
Indices and ranges provide a succinct syntax for accessing single elements or ranges in a sequence.
This language support relies on two new types, and two new operators:
System.Index represents an index into a sequence. The index from end operator ^, which specifies that an index is relative to the end of the sequence.
System.Range represents a sub range of a sequence. The range operator .., which specifies the start and end of a range as its operands.
Let's start with the rules for indexes. Consider an array sequence. The 0 index is the same as sequence[0]. The ^0 index is the same as sequence[sequence.Length]. Note that sequence[^0] does throw an exception, just as sequence[sequence.Length] does.
A range specifies the start and end of a range. The start of the range is inclusive, but the end of the range is exclusive, meaning the start is included in the range but the end isn't included in the range. The range [0..^0] represents the entire range.
Let's look at a few examples. Consider the following array, annotated with its index from the start and from the end:
var 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
}; // 9 (or words.Length) ^0
You can retrieve the last word with the ^1 index:
Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"
The following code creates a subrange with the words "quick", "brown", and "fox". It includes words[1] through words[3]. The element words[4] isn't in the range.
var quickBrownFox = words[1..4];
The following code creates a subrange with "lazy" and "dog". It includes words[^2] and words[^1]. The end index words[^0] isn't included:
var lazyDog = words[^2..^0];
The following examples create ranges that are open ended for the start, end, or both:
var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
You can also declare ranges as variables:
Range phrase = 1..4;
Not only arrays support indices and ranges. You also can use indices and ranges with string, Span<T>, or ReadOnlySpan<T>.
9. Null-coalescing assignment
C# 8.0 introduces the null-coalescing assignment operator ??=. You can use the ??= operator to assign the value of its right-hand operand to its left-hand operand only if the left-hand operand evaluates to null. So this can be used for null value check for the same object/variabble and assign some value if its null.
List<int> numbers = null;
int? i = null;
numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);
Console.WriteLine(string.Join(" ", numbers)); // output: 17 17
Console.WriteLine(i); // output: 17
operator ??
This being use to check null for some condition before assigning value and if null then instead of original object/variable value assign the right side value.
int? a = null;
int b = a ?? -1;
Console.WriteLine(b); // output: -1
If I want to use ??= operator in same above example then instead of assigning to b variable it will be check for a variable itself, like below-
int? a = null;
a ??= -1;
Console.WriteLine(a); // output: -1
10. Enhancement of interpolated verbatim strings
Order of the $ and @ tokens in interpolated verbatim strings can be any: both $@"..." and @$"..." are valid interpolated verbatim strings. In earlier C# versions, the $ token must appear before the @ token
Categories/Tags: c# 8.0~c# 8