C#语言新特性(6.0-8.0)

只读的自动属性

通过声明只有get访问器的自动属性,实现该属性只读

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

自动只读属性在能在构造函数中赋值,任何其他地方的赋值都会报编译错误。

自动属性初始化器

在声明自动属性时,还可以给它指定一个初始值。初始值作为整个声明的一部分。

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

字符串插入

允许你在字符串中嵌入表达式。字符串以$开头,把要嵌入的表达式在相应的位置用{和}包起来。

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

你还可以对表达式进行格式化

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

异常过滤器

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表达式

获取变量、属性或者成员字段的名称

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

await应用于catch和finally代码块

这个就不多说了,很简单,看代码吧

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();
    }
}

通过索引器初始化集合

索引初始化器使得对集合元素的初始化与通过索引访问保持一致。之前对Dictionary的初始化使用大括号的方式,如下:

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

现在你可以通过类似索引访问的方式进行初始化,上面的代码可以改为:

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变量声明

前要调用一个还有out参数的方法前,你需要先声明一个变量并赋一个初始值,然后才能调用这个方法

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

现在可以在调用方法的同时声明out变量

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

同时这种方式还支持隐式类型,你可以用var代理实际的参数类型

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

加强型元组(Tuple)

在7.0之前,要使用元组必须通过new Tuple<T1, T2....>()这种方式,并且元组中的各元素只能通过属性名Item1, Item2...的方式访问,费力且可读性不强。

现在你可以通过如下方式声明元组,给元组赋值,且给元组中的每个属性指定一个名称

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

元组namedLetters包含两个字段,Alpha和Beta。字段名只在编译时有效,在运行时又会变成Item1, Item2...的形式。所以在反射时不要用这些名字。

你还可以在赋值时,在右侧指定字段的名字,查看下面的代码

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

此外,编译器还可以从变量中推断出字段的名称,例如下面的代码

int count = 5;
string label = "Colors used in the map";
var pair = (count: count, label: label);
//上面一行,可以换个写法,字段名自动从变量名中推断出来了
var pair = (count, label);

你还可以对从方法返回的元组进行拆包操作,为元组中的每个成员声明独立的变量,以提取其中的成员。这个操作称为解构。查看如下代码

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

你可以为任意.NET类型提供类似的解构操作。为这个类提供一个Deconstruct方法,此方法需要一组out参数,每个要提取的属性对应一个out参数。

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

通过把User赋值给一个元组,就可以提取各个字段了

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

舍弃物

经常会遇到这样的情况:在解构元组或者调用有out参数的方法时,有些变量的值你根本不关心,或者在后续的代码也不打算用到它,但你还是必须定义一个变量来接收它的值。C#引入了舍弃物的概念来处理这种情况。

舍弃物是一个名称为_(下划线)的只读变量,你可以把所有想舍弃的值赋值给同一个舍弃物变量,舍弃物变量造价于一个未赋值的变量。舍弃物变量只能在给它赋值的语句中使用,在其它地方不能使用。

舍弃物可以使用在以下场景中:

  • 对元组或者用户定义的类型进行解构操作时
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
  • 调用带有out参数的方法时
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
  • 在进行带有is和switch语句的模式匹配时(模式匹配下面会讲到)
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
  • 在任何你想忽略一个变量的时,它可以作为一个标识符使用
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局部化和返回值

此特性允许你对一个在别的地方定义的变量进行引用,并可以把它以引用的形式返回给调用者。下面的例子用来操作一个矩阵,找到一个具有某一特征的位置上的元素,并返回这个元素的引用。

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

你可以把返回值声明成ref并修改保存在原矩阵中的值。

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

为了防止误用,C#要求在使用ref局部化和返回值时,需要遵守以下规则:

  • 定义方法时,必须在方法签名和所有的return语句上都要加上ref关键字
  • ref返回值可以赋值给一个值变量,可以赋值给引用变量
  • 不能把一个普通方法的返回值赋值一个ref的局部变量,像 ref int i = sequence.Count() 这样的语句是不允许的。
  • 要返回的ref变量,作用域不能小于方法本身。如果是方法的局部变量,方法执行完毕后,其作用域也消失了,这样的变量是不能被ref返回的
  • 不能在异步(async)方法中使用

几个提升性能的代码改进

当以引用的方式操作一些值类型时,可用如下几种方式,起到减少内存分配,提升性能的目的。

  1. 给参数加上 in 修饰符。in 是对现有的 ref 和 out的补充。它指明该参数以引用方式传递,但在方法内它的值不会被修改。在给方法传递值类型参数量,如果没有指定out, ref和in中的任意一种修饰符,那该值在内存中会被复制一份。这三种修饰符指明参数值以引用方式传递,从而避免被复制。当传递的参数类型是比较大的结构(通过批大于IntPtr.Size)时,对性能的提升比较明显;对于一些小的值类型,其作用并不明显,甚至会降低性能,比如sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, enum等。这些修饰行有各自的作用,分别如下:

    • out: 在方法内必须修改参数的值
    • ref: 在方法内可能会修改参数的值
    • in: 在方法内不能修改参数的值
  2. 对ref返回值(参见特性ref局部化和返回值),如果你不想调用方修改返回的值,可以在返回时加上ref readonly,同时调用者也要用ref readonly变量来接收返回值,所以之前的代码可以修改如下:
    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. 声明结构体时加上readonly修饰符,用来指明该struct是不可修改的,并且应当以in参数的形式传给方法

非显式命名参数

命名参数是指给方法传参时可以以“参数名:参数值”的形式传参而不用管该参数在方法签名中的位置。

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);

如上面的调用,是对同一方法的调用,而非重载方法,可见参数位置可以不按方法签名中的位置。如果某一参数出现的位置同它在方法签名中的位置相同,则可以省略参数名,只传参数值。如下所示:

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

上面的例子中orderNum在正确的位置上,只传参数值就可以了,不用指定参数名;但如果参数没有出现丰正确的位置上,就必须指定参数名,下面的语句编译器会抛出异常

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

表达式体成员

有些函数或者属性只有一条语句,它可能只是一个表达式,这时可以用表达式体成员来代替

// 在构造器中使用
public ExpressionMembersExample(string label) => this.Label = label;

// 在终结器中使用
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// 在get, set存取器中使用
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

//在方法中使用
public override string ToString() => $"{LastName}, {FirstName}";

//在只读属性中使用
public string FullName => $"{FirstName} {LastName}";

throw表达式

在7.0之前,throw只能作为语句使用。这使得在一些场景下不支持抛出异常,这些场景包括:

  • 条件操作符。如下面的例子,如果传入的参数是一个空的string数组,则会抛出异常,如果在7.0之前,你需要用到 if / else语句,现在不需要了
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.");
}
  • 在空接合操作符中。在下面的例子中,throw表达式跟空接合操作符一起使用。在给Name属性赋值时,如果传入的value是null, 则抛出异常
public string Name
{
    get => name;
    set => name = value ??
        throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}
  • 在lambda表达式或者具有表达式体的方法中
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");

数值写法的改进

数值常量常常容易写错或者读错。c#引入了更易读的写法

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

开头的 0b 表示这是一个二进制数,_(下划线) 表示数字分隔符。分隔符可以出现在这个常量的任意位置,只要能帮助你阅读就行。比如在写十进制数时,可以写成下面的形式

public const long BillionsAndBillions = 100_000_000_000;

分隔符还可以用于 decimal, float, double类型

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;

从7.2开始,二进制和十六进制的数组还可以 _ 开头

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

private protected访问修饰符

private protected指明一个成员只能被包含类(相对内部类而言)或者在同一程序集下的派生类访问

注:protected internal指明一个成员只能被派生类或者在同一程序集内的其他类访问

条件的ref表达式

现在条件表达式可以返回一个ref的结果了,如下:

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

异步Main方法

async main 方法使你能够在Main方法中使用 await。之前你可能需要这么写:

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

现在你可以这么写:

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

如果你的程序不需要返回任何退出码,你可以让Main方法返回一个Task:

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

default字面的表达式

default字面的表达式是对defalut值表达式的改进,用于给变量赋一个默认值。之前你是这么写的:

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

现在你可以省略右边的类型

Func<string, bool> whereClause = default;

using static

using static语句允许你把一个类中的静态方法导出进来,在当前文件中可以直接使用它的静态方法,而不用带上类名

using static System.Math
//旧写法
System.Math.Abs(1, 2, 3);

//新写法
Abs(1, 2, 3);

空条件操作符(null-conditional operator)

空条件操作符使判空更加容易和流畅。把成员访问操作符 . 换成 ?.

var first = person?.FirstName;

在上述代码中,如果person为null,则把null赋值给first,并不会抛出NullReferenceException;否则,把person.FirstName赋值给first。你还可以把空条件操作符应用于只读的自动属性

通过声明只有get访问器的自动属性,实现该属性只读

public string FirstName { get; }

public string LastName { get; }

自动只读属性在能在构造函数中赋值,任何其他地方的赋值都会报编译错误。

自动属性初始化器

在声明自动属性时,还可以给它指定一个初始值。初始值作为整个声明的一部分。

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

局部函数(Local functions)

有些方法只在一个地方被调用,这想方法通常很小且功能单一,没有很复杂的逻辑。局部函数允许你在一个方法内部声明另一个方法。局部函数使得别人一眼就能看出这个方法只在声明它的方法内使用到。代码如下:

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

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

上面的代码中, AddOne就是一个局部函数,它的作用是给y加1。有时候你可能希望这些局部函数更“独立”一些,不希望它们直接使用上下文中的变量,这时你可以把局部函数声明成静态方法,如果你在静态方法中使用了上下文中的变量,编译器会报错CS8421。如下代码所示:

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

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

这时你的代码要做相应的修改

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

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

序列的下标和范围

在通过下标取序列的元素时,如果在下面前加上 ^ 表示从末尾开始计数。操作符 .. 两边的数表示开始下标和结束下标,假设有如下数组

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

你可以通过 ^1下标来取最后一个元素(注意:^0相当于words.Length,会抛出异常)

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

下面的代码会取出一个包含"quick", "brown"和"fox"的子集,分别对应words[1], words[2], words[3]这3个元素,words[4]不包括

var quickBrownFox = words[1..4];

下面的代码会取出"lazy"和"dog"的子集体,分别对应words[^2]和words[^1]。wrods[^0]不包括。

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

空联合赋值

先回忆一下空联合操作符 ??

它表示如果操作符左边不为null,则返回它。否则,返回操作符右边的计算结果

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

空联合赋值:当??左边为null时,把右边的计算结果赋值给左边

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

内插值替换的string的增强

$@现在等价于@$

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

只读成员(Readonly Members)

可以把 readonly 修饰符应用于struct的成员上,这表明该成员不会修改状态。相对于在 struct 上应用readonly显示更加精细化。

考虑正面这个可变结构体:

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

通常ToString()方法不会也不应该修改状态,所以你可以通过给它加上一个 readonly 修饰符来表明这一点。代码如下:

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

由于ToString()方法中用到了 Distance属性,而Distance并非只读的,所以当编译时会收到如下警告:

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

要想消除这个警告,可以给DIstance添加 readonly 修饰符

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

由于X和Y属性的getter是自动实现的,编译器默认它们是readonly的,所以不会给出警告。

带有 readonly的成员并非一定不能修改状态, 说白了它只起到对程序员的提示作用,没有强制作用,以下代码仍然能编译通过:

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

默认接口方法

你可以在接口定义中给成员添加一个默认实现,如果在实现类中没有重写该成员,则实现类继承了这个默认实现。此时该成员并非公共可见的成员。考虑如下代码:

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

在上面的代码中,SampleClass默认继承了IConrol的Paint()方法,但不会向外显露,即你不能通过SampleClass.Paint()来访问,你需要先把SampleClass转成IControl再访问。代码如下:

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

模式匹配

模式匹配是对现有 is 和 switch 语句的扩展和增强。它包括检验值和提取值两部分功能。

假设我们有如下图形类, 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;
    }
}

对于这些图形,我们写一个方法用来计算它们的面积,传统的写法如下:

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

现在对 is表达式进行一下扩展,使它不仅能用于检查,并且如果检查通过的话,随即赋值给一个变量。这样一来,我们的代码就会变得非常简单,如下:

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

在这个更新后的版本中,is表达式不仅检查变量的类型,还赋值给一个新的拥有合适的类型的变量。另外,这个版本中还包含了 Rectangel 类型,它是一个struct, 也就是说 is表达式不仅能作用于引用类型,还能作用于值类型。上面这种模式匹配称为类型模式

语法如下:

expr is type varname

如果expr是type类型或者其派生类,则把expr转成type类型并赋值给变量varname.

常量模式

string aaa="abc";
if(aaa.Length is 3)
{
   //当长度为3时的处理逻辑
}

if(aaa is null)
{
  //为null时的逻辑
}

从上述代码可以看出is还能判断是否为null。

var模式

语法如下:

expr is var varname

var模式总是成功的,上面的代码主要是为了把expr赋值给变量varname,考虑如下代码

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));

上述代码中的变量s, c, r遵循如下规则:

  • 只有所在的if条件满足时才会被赋值
  • 只有在相应的if分支中可用,在别的地方不可见

上述代码中的if可以用switch语句替换,如下所示

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

这跟传统的switch语句有所不同,传统的swich语句,case后面只能跟常量,所以也限制了swich只能用于检测数值型和string型的变量,而新的语法中switch后面不再限制类型,并且case表达式也不再限制为常量。这意味着之前只有一个case会匹配成功,现在会出现多个case都匹配的情况,这样一来,各case的顺序不同,程序的运行结果也就不同。

接下再说swich表达式,跟switch语句不同,switch语句是一段代码块,而switch表达式是一个表达式,严格来说它表示为一个值。把上面的代码改用swich表达式来写,代码如下

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

这跟swich语句不同的地方有:

  • 变量出现在switch前面,从这个顺序上一眼就能看出这个是switch语句,还是switch表达式
  • case 和 :(冒号) 被 => 代替,更加简洁和直观
  • default 被 _(忽略符) 代替
  • 每个case的body都是一个表达式,而不是语句

接下来的例子中一般会写出两种写法,以做比较。

在case表达式中使用when语句

当正方形边长为0时,其面积为0;当矩形任一边长为0时,其面积为0;当圆形的半径为0时,其面积为0;当三角形的底或者高为0时,其面积为0;为了检测这些情况,我们需要进行额外的条件判断,代码如下:

//switch语句
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表达式
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))
}

在case表达式中使用when语句,可以进行额外的条件判断。

在case表达式中使用var

使用var情况下,编译器会根据switch前的变量推断出类型,该类型是编译时的类型而不是运行时的真正类型。即如果有接口实例或者继承关系,var后面的变量不会是形参的实际类型。

递归模式匹配

所谓递归模式匹配是指一个表达式可以作为另一个表达式的输出,如此反复,可以无限级嵌套。

swich表达式相当于是一个值,它可以作为另一个表达式的输出,当然也可以作为另一个switch表达式的输出,所以可以递归使用

考虑如下场景:中国的地方省、市、县三级,都实现IArea接口,要求给出一个IArea,返回其所在省的名称,其中如果是市的话,要考虑直辖市和地级市。代码如下:

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,//直辖市
        var c                       => c.Parent.Name,
    },
    County ct => ct switch
    {
        var ct when ct.Parent.Parent ==null => ct.Parent.Name,//直辖市下面的县
        var ct                          => ct.Parent.Parent.Name
    }
};

这段代码只是为了演示递归模式,不是解决该问题的最优写法。

属性模式匹配

就是对被检测对象的某些属性做匹配。比如一个电商网站要根据客户所在地区实现不同的税率,直接上代码,很好理解

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)模式

有些算法需要多个输入参数以进行检测,此时可能使用一个tuple作为switch表达的检测对象,如下代码,显示剪刀、石头、布游戏,输入两个的出的什么,根据输入输出结果

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

位置模式

有些类型带有解构(Deconstruct)方法,该方法可以把属性解构到多个变量中。基于这一特性,可以把利用位置模式可以对对象的多个属性应用匹配模式。

比如下面的Point类中含有Deconstruct方法,可以把它的 X 和 Y 属性分解到变量中。

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

下面的枚举表示坐标系统中的不同区域

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

下面这个方法使用位置模式提取x, y 的值,并用when语句确定某个点在坐标系中所处的区域

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声明

using声明是为了声明一个变量,在超出其作用域时,将其销毁(dispose),如下面的代码:

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
}

之前的语法需要用到大括号,当遇到结束大括号时,对象被销毁,代码如下:

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

写法比以前相对简洁了,另外当该方法中有多个需要即时销毁的对象时,你不需要使用using嵌套的写法。考虑如下代码:

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
    //
}

使用新语法代码如下:

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局部化和返回值

(0)

相关推荐

  • 带你了解C#每个版本新特性

    上学时学习C#和.NET,当时网上的资源不像现在这样丰富,所以去电脑城买了张盗版的VS2005的光盘,安装时才发现是VS2003,当时有一种被坑的感觉,但也正是如此,让我有了一个完整的.NET的学习生 ...

  • MySQL8.0新特性

    MySQL从5.7一跃直接到8.0,这其中的缘由,咱就不关心那么多了,有兴趣的朋友自行百度,本次的版本更新,在功能上主要有以下6点: 账户与安全 优化器索引 通用表表达式 窗口函数 InnoDB 增强 ...

  • Vue3.0 新特性以及使用变更总结(实际工作用到的)

    前言 Vue3.0 在去年9月正式发布了,也有许多小伙伴都热情的拥抱Vue3.0.去年年底我们新项目使用Vue3.0来开发,这篇文章就是在使用后的一个总结, 包含Vue3新特性的使用以及一些用法上的变 ...

  • Vue3.0 新特性以及使用经验总结

    vue3.0Vue3.0 在去年9月正式发布了,也有许多小伙伴都热情的拥抱Vue3.0.去年年底我们新项目使用Vue3.0来开发,这篇文章就是在使用后的一个总结, 包含Vue3新特性的使用以及一些用法 ...

  • C#8.0新特性

    只读成员 private struct Point { public Point(double x, double y) { X = x; Y = y; } private double X { ge ...

  • C# 9.0新特性详解系列之一:只初始化设置器(init only setter)

    C# 9.0新特性详解系列之一:只初始化设置器(init only setter)

  • 0基础学Java(三)Java语言的特性

    Java语言的特性 1.简单性 在Java语言当中真正操作内存的是:JVM(Java虚拟机) 所有的java程序都是运行在Java虚拟机当中的. 而Java虚拟机执行过程中再去操作内存. 对于C或者C ...

  • 监控系列讲座(十四)Zabbix5.0与新特性支持ARM

    监控系列讲座(十四)Zabbix5.0与新特性支持ARM

  • 易快讯 | OPPO R15新机驾到,安卓9.0新特性曝光……

    OPPOR15新机驾到 今天,OPPO方面突然宣布了R系列新机R15的消息,而从其发布的海报中显示,这款机型将会采用异型全面屏.根据OPPO官方发布的内容,R15将前置摄像头和传感器集中在异型全面屏的 ...

  • Android 9.0 新特性曝光,切断后台相机访问

    据谷歌的消息称,全新的 Android  9.0 将于 5月8日到5月10日的I/O 2018大会上出现,代号定为Pistachio Ice Cream (中文音译为 开心果冰淇淋) 然而,很多用户都 ...