C#New Language Features (6.0-8.0)

Read-only Auto Properties

Make this property read-only by declaring an automatic property that has only a get accessor

public string FirstName { get; }
public string LastName { get;  }

Auto-read-only properties can be assigned in constructors, and assignments elsewhere will result in compilation errors.

Automatic Attribute Initializer

When declaring an automatic property, you can also assign it an initial value.The initial value is part of the entire declaration.

public ICollection<double> Grades { get; } = new List<double>();

String Insertion

Allows you to embed expressions in strings.The string begins with $and wraps the expression to be embedded in the appropriate position with {and}.

public string FullName => $"{FirstName} {LastName}";

You can also format expressions

public string GetGradePointPercentage() =>
    $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";

Exception filter

public static async Task<string> MakeRequest()
{
    WebRequestHandler webRequestHandler = new WebRequestHandler();
    webRequestHandler.AllowAutoRedirect = false;
    using (HttpClient client = new HttpClient(webRequestHandler))
    {
        var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/");
        try
        {
            var responseText = await stringTask;
            return responseText;
        }
        catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
        {
            return "Site Moved";
        }
    }
}

nameof expression

Gets the name of a variable, property, or member field

if (IsNullOrWhiteSpace(lastName))
    throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));

await applies to catch and finally blocks of code

That's all. It's easy. Just look at the code

public static async Task<string> MakeRequestAndLogFailures()
{ 
    await logMethodEntrance();
    var client = new System.Net.Http.HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try {
        var responseText = await streamTask;
        return responseText;
    } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
    {
        await logError("Recovered from redirect", e);
        return "Site Moved";
    }
    finally
    {
        await logMethodExit();
        client.Dispose();
    }
}

Initialize collection through indexer

The index initializer makes initialization of collection elements consistent with index access.Previously, braces were used for the initialization of Dictionary as follows:

private Dictionary<int, string> messages = new Dictionary<int, string>
{
    { 404, "Page not Found"},
    { 302, "Page moved, but left a forwarding address."},
    { 500, "The web server can't come out to play today."}
};

Now you can initialize it in a way similar to index access, and the code above can be changed to:

private Dictionary<int, string> webErrors = new Dictionary<int, string>
{
    [404] = "Page not Found",
    [302] = "Page moved, but left a forwarding address.",
    [500] = "The web server can't come out to play today."
};

out variable declaration

Before calling a method with an out parameter, you need to declare a variable and assign an initial value before calling the method

int result=0;
if (int.TryParse(input, out result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

You can now declare the out variable while calling the method

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

Implicit types are also supported, and you can proxy the actual parameter types with var

if (int.TryParse(input, out var answer))
    Console.WriteLine(answer);
else
    Console.WriteLine("Could not parse input");

Enhanced Tuple

Prior to 7.0, tuples had to be used in the form of new Tuple <T1, T2.... >(), and each element in a tuple could only be accessed by the attribute names Item1, Item2..., which was laborious and unreadable.

Now you can declare tuples, assign values to them, and assign a name to each property in the tuple

(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

The tuple namedLetters contains two fields, Alpha and Beta.Field names are only valid at compile time and become Item1, Item2, etc. at run time.So don't use these names when reflecting.

You can also specify the name of the field on the right side when assigning values, see the code below

var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

In addition, the compiler can infer field names from variables, such as the following code

int count = 5;
string label = "Colors used in the map";
var pair = (count: count, label: label);
//The above line can be written in a different way, and the field name is automatically inferred from the variable name
var pair = (count, label);

You can also unpack tuples returned from methods by declaring separate variables for each member of the tuple to extract its members.This operation is called deconstruction.View the following code

(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

You can provide similar deconstruction for any.NET type.Provide a Deconstruct method for this class that requires a set of out parameters, one for each property to be extracted.

    public class User
    {
        public User(string fullName)
        {
            var arr = fullName.Split(' ');
            (FirstName, LastName) = (arr[0], arr[1]);
        }

        public string FirstName { get; }
        public string LastName { get; }

        public void Deconstruct(out string firstName, out string lastName) =>
            (firstName, lastName) = (this.FirstName, this.LastName);
    }

By assigning User to a tuple, you can extract fields

            var user = new User("Rock Wang");
            (string first, string last) = user;
            Console.WriteLine($"First Name is: {first}, Last Name is: {last}");

Discard

It is common to encounter situations where you do not care about the value of some variables when you deconstruct tuples or call methods with out parameters, or you do not intend to use them in subsequent code, but you still have to define a variable to receive its value.C#introduces the concept of discards to handle this situation.

A discard is a read-only variable named (underscore). You can assign all the values you want to discard to the same discard variable, and the discard variable is cost to an unassigned variable.Discard variables can only be used in statements that assign values to them, not elsewhere.

Discards can be used in the following scenarios:

  • When a tuple or user-defined type is deconstructed
using System;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }
   
    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;
      
        if (name == "New York City")
        {
            area = 468.48; 
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149
  • When calling a method with an out parameter
using System;

public class Example
{
   public static void Main()
   {
      string[] dateStrings = {"05/01/2018 14:57:32.8", "2018-05-01 14:57:32.8",
                              "2018-05-01T14:57:32.8375298-04:00", "5/01/2018",
                              "5/01/2018 14:57:32.80 -07:00", 
                              "1 May 2018 2:57:32.8 PM", "16-05-2018 1:00:32 PM", 
                              "Fri, 15 May 2018 20:10:57 GMT" };
      foreach (string dateString in dateStrings)
      {
         if (DateTime.TryParse(dateString, out _)) 
            Console.WriteLine($"'{dateString}': valid");
         else
            Console.WriteLine($"'{dateString}': invalid");
      }
   }
}
// The example displays output like the following:
//       '05/01/2018 14:57:32.8': valid
//       '2018-05-01 14:57:32.8': valid
//       '2018-05-01T14:57:32.8375298-04:00': valid
//       '5/01/2018': valid
//       '5/01/2018 14:57:32.80 -07:00': valid
//       '1 May 2018 2:57:32.8 PM': valid
//       '16-05-2018 1:00:32 PM': invalid
//       'Fri, 15 May 2018 20:10:57 GMT': invalid
  • When doing pattern matching with is and switch statements (described below)
using System;
using System.Globalization;

public class Example
{
   public static void Main()
   {
      object[] objects = { CultureInfo.CurrentCulture, 
                           CultureInfo.CurrentCulture.DateTimeFormat, 
                           CultureInfo.CurrentCulture.NumberFormat,
                           new ArgumentException(), null };
      foreach (var obj in objects)
         ProvidesFormatInfo(obj);
   }

   private static void ProvidesFormatInfo(object obj)         
   {
      switch (obj)
      {
         case IFormatProvider fmt:
            Console.WriteLine($"{fmt} object");
            break;
         case null:
            Console.Write("A null object reference: ");
            Console.WriteLine("Its use could result in a NullReferenceException");
            break;
         case object _:
            Console.WriteLine("Some object type without format information");
            break;
      }
   }
}
// The example displays the following output:
//    en-US object
//    System.Globalization.DateTimeFormatInfo object
//    System.Globalization.NumberFormatInfo object
//    Some object type without format information
//    A null object reference: Its use could result in a NullReferenceException
  • It can be used as an identifier whenever you want to ignore a variable
using System;
using System.Threading.Tasks;

public class Example
{
   public static async Task Main(string[] args)
   {
      await ExecuteAsyncMethods();
   }

   private static async Task ExecuteAsyncMethods()
   {    
      Console.WriteLine("About to launch a task...");
      _ = Task.Run(() => { var iterations = 0;  
                           for (int ctr = 0; ctr < int.MaxValue; ctr++)
                              iterations++;
                           Console.WriteLine("Completed looping operation...");
                           throw new InvalidOperationException();
                         });
      await Task.Delay(5000);                        
      Console.WriteLine("Exiting after 5 second delay");
   }
}
// The example displays output like the following:
//       About to launch a task...
//       Completed looping operation...
//       Exiting after 5 second delay

ref localization and return values

This feature allows you to reference a variable defined elsewhere and return it as a reference to the caller.The following example is used to manipulate a matrix, find an element at a location with a certain characteristic, and return a reference to that element.

public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

You can declare the return value as ref and modify the value stored in the original matrix.

ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

To prevent misuse, C#requires that the following rules be followed when using ref localization and return values:

  • When defining a method, the ref keyword must be added to the method signature and to all return statements
  • The ref return value can be assigned to a value variable or to a reference variable
  • The return value of a common method cannot be assigned to a ref local variable, and statements such as ref int i = sequence.Count() are not allowed.
  • The ref variable to be returned cannot be scoped less than the method itself.If it is a local variable of a method, its domain disappears after execution, and such a variable cannot be returned by ref
  • Cannot be used in async methods

Several code improvements to improve performance

When manipulating some value types by reference, there are several ways to reduce memory allocation and improve performance.

  1. Add in modifier to the parameter.In is a complement to the existing ref and out.It indicates that the parameter is passed by reference, but its value will not be modified within the method.In passing a value type parameter to a method, if no modifier is specified for out, ref, and in, the value is copied in memory.These three modifiers indicate that parameter values are passed by reference to avoid being duplicated.Performance improvements are evident when the parameter type passed is a larger structure (through batches larger than IntPtr.Size); for some small value types, the effect is not obvious, or even degrades performance, such as sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, enum, etc.These modifier lines have their own roles, as follows:
    • out: The value of the parameter must be modified within the method
    • ref: The value of the parameter may be modified within the method
    • in: The value of a parameter cannot be modified within a method
  2. For ref return values (see attribute ref localization and return values), if you do not want the caller to modify the returned values, you can add ref readonly on the return, and the caller will also use the ref readonly variable to receive the returned values, so the previous code can be modified as follows:
    public static ref readonly int Find(int[,] matrix, Func<int, bool> predicate)
    {
        for (int i = 0; i < matrix.GetLength(0); i++)
            for (int j = 0; j < matrix.GetLength(1); j++)
                if (predicate(matrix[i, j]))
                    return ref matrix[i, j];
        throw new InvalidOperationException("Not found");
    }
    ref readonly var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
    Console.WriteLine(item);
    item = 24;
    Console.WriteLine(matrix[4, 2]);
  3. Declare the structure with a readonly modifier to indicate that the struct is not modifiable and should be passed to the method as an in parameter

Non-explicit named parameters

Named parameters refer to methods that can be passed as parameter names: parameter values regardless of their position in the method signature.

    static void PrintOrderDetails(string sellerName, int orderNum, string productName)
    {
        if (string.IsNullOrWhiteSpace(sellerName))
        {
            throw new ArgumentException(message: "Seller name cannot be null or empty.", paramName: nameof(sellerName));
        }

        Console.WriteLine($"Seller: {sellerName}, Order #: {orderNum}, Product: {productName}");
    }

 

PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");

PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

If the above call is a call to the same method, not an overloaded method, the position of the visible parameter may not follow the position in the method signature.If a parameter appears in the same location as it does in the method signature, the parameter name can be omitted and only the parameter value can be passed.As follows:

PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");

In the example above, orderNum is in the right place, simply passing in the parameter value without specifying the parameter name; however, if the parameter does not appear in the right place, you must specify the parameter name, and the following statement compiler throws an exception

// This generates CS1738: Named argument specifications must appear after all fixed arguments have been specified.
PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");

 

Expression Body Member

Some functions or attributes have only one statement and may be just an expression, which can be replaced by an expression body member

// Using in constructors
public ExpressionMembersExample(string label) => this.Label = label;

// Use in finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// stay get, set Use in Accessors
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

//Use in methods
public override string ToString() => $"{LastName}, {FirstName}";

//Use in read-only properties
public string FullName => $"{FirstName} {LastName}";

throw expression

Prior to 7.0, throw could only be used as a statement.This makes throwing exceptions not supported in some scenarios, including:

  • Conditional operator.For example, if the parameter passed in is an empty string array, an exception will be thrown. If you needed an if / else statement before 7.0, you don't need it now
private static void DisplayFirstNumber(string[] args)
{
   string arg = args.Length >= 1 ? args[0] : 
                              throw new ArgumentException("You must supply an argument");
   if (Int64.TryParse(arg, out var number))
      Console.WriteLine($"You entered {number:F0}");
   else
      Console.WriteLine($"{arg} is not a number.");                            
}
  • In the empty join operator.In the following example, a throw expression is used with an empty join operator.When assigning a value to the Name property, throw an exception if the value passed in is null
public string Name
{
    get => name;
    set => name = value ?? 
        throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}
  • In lambda expressions or methods with body of expression
DateTime ToDateTime(IFormatProvider provider) => 
         throw new InvalidCastException("Conversion to a DateTime is not supported.");

Improvement of Numeric Writing

Numeric constants are often miswritten or misread.c#introduces easier-to-read writing

public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

The 0b at the beginning indicates that this is a binary number and the (underscore) indicates the number separator.Delimiters can appear anywhere in this constant as long as they help you read it.For example, when writing decimal numbers, you can write them as follows

public const long BillionsAndBillions = 100_000_000_000;

Separators can also be used for decimal, float, double types

public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

Beginning with 7.2, binary and hexadecimal arrays can also begin with

int binaryValue = 0b_0101_0101;
int hexValue = 0x_ffee_eeff;

private protected access modifier

private protected indicates that a member can only be accessed by a containing class (as opposed to an internal class) or a derived class under the same assembly

Note: protected internal indicates that a member can only be accessed by derived classes or other classes within the same assembly

ref expression for condition

Now the conditional expression can return the result of a ref as follows:

ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);

 

 

Asynchronous Main Method

The async main method allows you to use await in the Main method.You might have written this before:

static int Main()
{
    return DoAsyncWork().GetAwaiter().GetResult();
}

Now you can write as follows:

static async Task<int> Main()
{
    // This could also be replaced with the body
    // DoAsyncWork, including its await expressions:
    return await DoAsyncWork();
}

If your program does not need to return any exit codes, you can have the Main method return a Task:

static async Task Main()
{
    await SomeAsyncMethod();
}

default Literal Expression

The default literal expression is an improvement on the defalut value expression, which assigns a default value to a variable.You wrote this before:

Func<string, bool> whereClause = default(Func<string, bool>);

Now you can omit the type on the right

Func<string, bool> whereClause = default;

 

using static

The using static statement allows you to export static methods from a class, which can be used directly in the current file without the class name

using static System.Math
//Old Writing
System.Math.Abs(1, 2, 3);

//neographism
Abs(1, 2, 3);

Null-conditional operator

Empty conditional operators make it easier and smoother to determine voids.Replace member access operator with?.

var first = person?.FirstName;

In the above code, if person is null, assigning null to first does not throw a NullReferenceException; otherwise, assigning person.FirstName to first.You can also apply empty conditional operators to read-only automatic properties

Make this property read-only by declaring an automatic property that has only a get accessor

 

public string FirstName { get; }

public string LastName { get; }

 

Auto-read-only properties can be assigned in constructors, and assignments elsewhere will result in compilation errors.

 

Automatic Attribute Initializer

When declaring an automatic property, you can also assign it an initial value.The initial value is part of the entire declaration.

public ICollection<double> Grades { get; } = new List<double>();

Local functions

Some methods are called only in one place, which is usually small, single-function, and without complex logic.Local functions allow you to declare another method within one method.Local functions allow others to see at a glance that this method is only used within the method in which it is declared.The code is as follows:

int M()
{
    int y;
    AddOne();
    return y;

    void AddOne() => y += 1;
}

In the code above, AddOne is a local function that adds 1 to y.Sometimes you may want these local functions to be more "independent" and not to use variables directly in the context. You can declare the local functions as static methods. If you use variables in the context in static methods, the compiler will error CS8421.The following code shows:

int M()
{
    int y;
    AddOne();
    return y;

    static void AddOne() => y += 1;
}

Your code needs to be modified accordingly

int M()
{
    int y;
    y=AddOne(y);
    return y;

    static intAddOne(int toAdd) =>{ toAdd += 1; return toAdd;}
}

Subscripts and ranges of sequences

When an element of a sequence is indexed, adding ^ before the bottom indicates that the count starts at the end.Operator.. The number on either side represents the start and end subscripts, assuming the following array

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 use the ^1 subscript to take the last element (note: ^0 equals words.Length and throws an exception)

Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"

The code below takes out a subset of words [1],'brown'and'fox', which corresponds to words [1],'words[2],'words[3], which are not included in words[4]

var quickBrownFox = words[1..4];

The code below takes out the subcollections of "lazy" and "dog", corresponding to words[^2] and words[^1], respectively.wrods[^0] is not included.

var lazyDog = words[^2..^0];

Empty Joint Assignment

Recall the empty union operator first?

It means that if the left side of the operator is not null, it is returned.Otherwise, return the result of calculation to the right of the operator

int? a = null;
int b = a ?? -1;
Console.WriteLine(b);  // output: -1

Empty Joint Assignment: When?? is null on the left, assign the result of calculation on the right to the left

List<int> numbers = null;
int? a = null;

(numbers ??= new List<int>()).Add(5);
Console.WriteLine(string.Join(" ", numbers));  // output: 5

numbers.Add(a ??= 0);
Console.WriteLine(string.Join(" ", numbers));  // output: 5 0
Console.WriteLine(a);  // output: 0

Enhancement of string for Interpolation Replacement

$@ is now equivalent to @$

var text1 = $@"{a}_{b}_{c}";
var text2 = @$"{a}_{b}_{c}";

Readonly Members

The readonly modifier can be applied to a member of a struct, indicating that the member will not modify the state.Readonly display is more refined than using struct.

Consider the front of this variable structure:

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";
}

Usually the ToString() method does not and should not modify the state, so you can indicate this by adding a readonly modifier to it.The code is as follows:

public readonly override string ToString() =>
    $"({X}, {Y}) is {Distance} from the origin";

Since the Distance property is used in the ToString() method and Distance is not read-only, you will receive the following warnings when compiling:

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'

To eliminate this warning, add a readonly modifier to DIstance

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

Since getter s for X and Y attributes are implemented automatically, the compiler defaults to them being readonly, so no warning is given.

Members with readonly do not necessarily have to be able to modify the state. To put it in perspective, it only serves as a hint to the programmer, not as a force. The following code can still be compiled and passed:

public readonly void Translate(int xOffset, int yOffset)
{
    X += xOffset;
    Y += yOffset;
}

Default interface method

You can add a default implementation to the member in the interface definition, and if the member is not overridden in the implementation class, the implementation class inherits the default implementation.The member is not publicly visible at this time.Consider the following code:

public interface IControl
{
    void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
    // Paint() is inherited from IControl.
}

In the code above, SampleClass inherits IConrol's Paint () method by default, but does not expose that you cannot access it through SampleClass.Paint(), you need to convert SampleClass to IControl before accessing it.The code is as follows:

var sample = new SampleClass();
//sample.Paint();// "Paint" isn't accessible.
var control = sample as IControl;
control.Paint();

pattern matching

Pattern matching is an extension and enhancement of existing is and switch statements.It includes two functions: check value and extract value.

Suppose we have the following graphic classes, Square, Circle, Rectangle, Triangle:

public class Square
{
    public double Side { get; }

    public Square(double side)
    {
        Side = side;
    }
}
public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }
}
public struct Rectangle
{
    public double Length { get; }
    public double Height { get; }

    public Rectangle(double length, double height)
    {
        Length = length;
        Height = height;
    }
}
public class Triangle
{
    public double Base { get; }
    public double Height { get; }

    public Triangle(double @base, double height)
    {
        Base = @base;
        Height = height;
    }
}

For these graphics, we write a method to calculate their area. The traditional way to write these graphics is as follows:

public static double ComputeArea(object shape)
{
    if (shape is Square)
    {
        var s = (Square)shape;
        return s.Side * s.Side;
    } 
    else if (shape is Circle)
    {
        var c = (Circle)shape;
        return c.Radius * c.Radius * Math.PI;
    }
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

Now extend the is expression so that it can be used not only for checking, but also for assigning a variable if the check passes.This makes our code very simple, as follows:

public static double ComputeAreaModernIs(object shape)
{
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

In this updated version, the is expression not only checks the type of variable, but also assigns a new variable with the appropriate type.In addition, this version includes the Rectangel type, which is a struct, that is, the is expression acts not only on reference types, but also on value types.The above pattern matching is called type pattern.

The syntax is as follows:

expr is type varname

If expr is of type or a derived class from it, convert expr to type and assign it to variable varname.

Constant mode

string aaa="abc";
if(aaa.Length is 3)
{
   //Processing logic when length is 3  
}

if(aaa is null)
{
  //by null Logic of time
}

From the code above, you can see that is can also tell if it is null.

var mode

The syntax is as follows:

expr is var varname

The var pattern is always successful, and the main purpose of the above code is to assign expr to the variable varname, considering the following code

int[] testSet = { 100271, 234335, 342439, 999683 };

var primes = testSet.Where(n => Factor(n).ToList() is var factors
                                    && factors.Count == 2
                                    && factors.Contains(1)
                                    && factors.Contains(n));

 

The variables s, c, r in the above code follow the following rules:

  • Only if the if condition is met will it be assigned
  • Only available in the corresponding if branch, not visible elsewhere

The if in the above code can be replaced with a switch statement, as shown below

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

This is different from traditional switch statements, where case can only be followed by constants, so swich can only be used to detect variables of numeric and string types. In the new syntax, switch is no longer followed by constants, and case expressions are no longer restricted to constants.This means that only one case matched successfully before, and now multiple cases match, so that the order of cases is different and the results of the program run differently.

Next, unlike a switch statement, which is a block of code, a switch expression is an expression that, strictly speaking, is expressed as a value.Change the above code to a swich expression, and the code is as follows

public static double ComputeAreaModernSwitch(object shape) => shape switch
{
    Square s    => s.Side * s.Side,
    Circle c    => c.Radius * c.Radius * Math.PI,
    Rectangle r => r.Height * r.Length,
    _           => throw new ArgumentException(
            message: "shape is not a recognized shape",
            paramName: nameof(shape))
};

This is different from the swich statement in that:

  • The variable precedes the switch and you can tell from this order whether it is a switch statement or a switch expression
  • case and: (colon) are replaced by => for simplicity and intuition
  • default is replaced by (ignorer)
  • Each case's body is an expression, not a statement

In the following examples, two types of writing are usually written for comparison.

Use when statement in case expression

When the square side length is 0, its area is 0; when either side length of the rectangle is 0, its area is 0; when the radius of the circle is 0, its area is 0; when the bottom or height of the triangle is 0, its area is 0; in order to detect these cases, we need to make additional conditional judgments as follows:

//switch Sentence
public static double ComputeArea_Version4(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height / 2;
        case Rectangle r:
            return r.Length * r.Height;
        case null:
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

 

//switch Expression
public static double ComputeArea_Version4(object shape) => shape switch
{
     Square s when s.Side == 0                       => 0,
     Circle c when c.Radius == 0                     => 0,
     Triangle t when t.Base == 0 || t.Height == 0    => 0,
     Rectangle r when r.Length == 0 || r.Height == 0 => 0,
     Square s                                        => s.Side * s.Side,
     Circle c                                        => c.Radius * c.Radius * Math.PI,
     Triangle t                                      => t.Base * t.Height / 2,
     Rectangle r                                     => r.Length * r.Height,
     null                                            => throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null"),
     _                                               => throw new ArgumentException( message=> "shape is not a recognized shape", paramName=> nameof(shape)) 
}

 

Additional conditional judgments can be made by using the when statement in a case expression.

Use var in case expressions

With var, the compiler infers a type from the variable before switch ing, which is the type at compile time rather than the real type at run time.That is, if there are interface instances or inheritance relationships, the variable after var will not be the actual type of the parameter.

 

Recursive pattern matching

Recursive pattern matching refers to an expression that can be output as another expression, so that it can be nested indefinitely over and over again.

A switch expression is equivalent to a value that can be used as the output of another expression and, of course, another switch expression, so it can be used recursively

Consider the following scenarios: China's local provinces, cities, and counties all implement the IArea interface, requiring an IArea to be given and the name of the province in which it resides. If it is a city, consider municipalities directly under the Central Government and municipalities at the local level.The code is as follows:

public interface IArea
{
    string Name { get; }
    IArea Parent { get; }
}

public class County: IArea
{
    
}

public class City: IArea
{
}

public class Province: IArea
{
}

public string GetProvinceName(IArea area) => area switch
{
    Province p => p.Name,
    City c => c switch
    {
        var c when c.Parent == null => c.Name,//Municipality directly under the Central Government
        var c                       => c.Parent.Name,
    },
    County ct => ct switch
    {
        var ct when ct.Parent.Parent ==null => ct.Parent.Name,//Counties under the Central Government
        var ct                          => ct.Parent.Parent.Name
    }
};

This code is intended to demonstrate recursive patterns only and is not the best way to write a solution to this problem.(

Attribute pattern matching

Is to match some properties of the object being detected.For example, an e-commerce website should implement different tax rates according to the customer's region, and code directly, which is easy to understand.

public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
    location switch
    {
        { State: "WA" } => salePrice * 0.06M,
        { State: "MN" } => salePrice * 0.75M,
        { State: "MI" } => salePrice * 0.05M,
        // other cases removed for brevity...
        _ => 0M
    };

 

Tuple mode

Some algorithms require more than one input parameter for detection, at which point a tuple may be used as the detection object for the switch expression, such as the code below, showing the scissors, stones, cloth games, entering what comes out of two, based on the input and output results

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.",
        ("paper", "rock") => "paper covers rock. Paper wins.",
        ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
        ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
        ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
        (_, _) => "tie"
    };

Location mode

Some types have Deconstruction methods that deconstruct attributes into multiple variables.Based on this feature, a location pattern can be used to apply a matching pattern to multiple attributes of an object.

For example, the Point class below contains a Deconstruct method that decomposes its X and Y properties into variables.

public class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) =>
        (x, y) = (X, Y);
}

The following enumeration represents the different regions in the coordinate system

public enum Quadrant
{
    Unknown,
    Origin,
    One,
    Two,
    Three,
    Four,
    OnBorder
}

The following method uses the location pattern to extract the values of x, y, and uses the when statement to determine the region of a point in the coordinate system

static Quadrant GetQuadrant(Point point) => point switch
{
    (0, 0) => Quadrant.Origin,
    var (x, y) when x > 0 && y > 0 => Quadrant.One,
    var (x, y) when x < 0 && y > 0 => Quadrant.Two,
    var (x, y) when x < 0 && y < 0 => Quadrant.Three,
    var (x, y) when x > 0 && y < 0 => Quadrant.Four,
    var (_, _) => Quadrant.OnBorder,
    _ => Quadrant.Unknown
};

using declaration

The use declaration is to declare a variable and destroy it when it exceeds its scope, as follows:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    // Notice how we declare skippedLines after the using statement.
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
        else
        {
            skippedLines++;
        }
    }
    // Notice how skippedLines is in scope here.
    return skippedLines;
    // file is disposed here
}

Previous grammars require braces, and when an end brace is encountered, the object is destroyed with the following code:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    // We must declare the variable outside of the using block
    // so that it is in scope to be returned.
    int skippedLines = 0;
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
    } // file is disposed here
    return skippedLines;
}

Writing is simpler than before, and you do not need to use using nested writing when there are multiple objects in the method that need to be destroyed immediately.Consider the following code:

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using (var file1 = new System.IO.StreamWriter("WriteLines1.txt"))
    {
        using (var file2 = new System.IO.StreamWriter("WriteLines1.txt"))
        {
            foreach (string line in lines)
            {
                if (!line.Contains("Second"))
                {
                    file1.WriteLine(line);
                }
                else
                {
                    file2.WriteLine(line);
                }
            }            
        }// file2 is disposed here
    } // file1 is disposed here
    
    //
    // some other statements
    //
}

Use the new syntax code as follows:

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file1 = new System.IO.StreamWriter("WriteLines1.txt");
    using (var file2 = new System.IO.StreamWriter("WriteLines1.txt");
    
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file1.WriteLine(line);
        }
        else
        {
            file2.WriteLine(line);
        }
    }            
    
    //
    // some other statements
    //
    
    
    // file2 is disposed here
    // file1 is disposed here
}

 

  

ref localization and return values

Tags: C# Attribute Web Server less Lambda

Posted on Thu, 16 Apr 2020 00:50:22 -0700 by harmor