C#6.0到C#9.0的令人激动的新特性(c# 9.0)

C#6.0到C#9.0的令人激动的新特性(c# 9.0)

编码文章call10242025-02-04 12:23:1012A+A-

官网地址:
https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-version-history

C# 9.0

init属性访问器

对象初始化方式对于创建对象来说是一种非常灵活和可读的格式,特别是对树状嵌入型对象的创建。简单的例如

new Person
{
    FirstName = "Scott",
    LastName = "Hunter"
}

原有的要进行对象初始化,我们必须要做就是写一些属性,并且在构造函数的初次调用中,通过给属性的setter赋值来实现。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

就这样的方式而言,set访问器对于初始化来说是必须的,但是如果你想要的是只读属性,这个set就是不合适的。除过初始化,其他情况不需要,而且很多情况下容易引起属性值改变。为了解决这个矛盾,只用来初始化的init访问器出现了。init访问器是一个只在对象初始化时用来赋值的set访问器的变体,并且后续的赋值操作是不允许的。例如:

public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

init属性访问器和只读字段

因为init访问器只能在初始化时被调用,原来只能在构造函数里进行初始化的只读字段,现在可以在属性中进行初始化,不用再在构造函数进行初始化。省略了构造函数。

public class Person
{
    private readonly string firstName;
    private readonly string lastName;
    
    public string FirstName 
    { 
        get => firstName; 
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
    }
    public string LastName 
    { 
        get => lastName; 
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

Records

可轻松创建不可变的引用类型,这个优点在使用共享数据的并发程序中更为明显

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

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

记录支持 with 表达式。 with 表达式指示编译器创建记录的副本,但修改指定的属性 *

var person = new Person("Bill", "Wagner");
Person brother = person with { FirstName = "Paul" };

创建新的 Person 记录,其中 LastName 属性是 person 的副本,FirstName"Paul"。 可在 with 表达式中设置任意数量的属性。 还可以使用 with 表达式来创建精确的副本。 为要修改的属性指定空集

Person clone = person with { };


对象声明

                  //FooInfo foo = new FooInfo(); 
                    FooInfo fooInfo1 = new();  //语法糖
                    FooInfo fooInfo2 = new() { PropA = "123", PropB = "234" };


顶级程序

通常,我们洗一个简单的C#程序,都要求有大量的样例代码:

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

这个对于初学者是无法抗拒,但是这使得代码凌乱,大量堆积,并且增加了缩进层级。在C#9.0中,你可以选择在顶级用如下代码代替写你的主程序:

using System;

Console.WriteLine("Hello World!");

当然,任何语句都是允许的。但是这个程序代码必须出现在using后,在任何类型或者命名空间声明的前面。并且你只能在一个文件里面这样做,像你如今只写一个main方法一样。

如果你想返回状态,你可以那样做,你想用await,也可以那样做。并且,如果你想访问命令行参数,args也是可用的。

本地方法是语句的另一个形式,也是允许在顶级程序代码用的。在顶级代码段外部的任何地方调用他们都会产生错误。

增强的模式匹配

C# 9 包括新的模式匹配改进:

  • 类型模式要求在变量是一种类型时匹配
  • 带圆括号的模式强制或强调模式组合的优先级
  • 联合 and 模式要求两个模式都匹配
  • 析取 or 模式要求任一模式匹配
  • 求反 not 模式要求模式不匹配
  • 关系模式要求输入小于、大于、小于等于或大于等于给定常数。

这些模式丰富了模式的语法。 请考虑下列示例:

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

还可使用可选的括号来明确 and 的优先级高于 or

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

最常见的用途之一是用于 NULL 检查的新语法:

if (e is not null)
{
    // ...
}

这些模式中的任何一种都可在允许使用模式的任何上下文中使用:is 模式表达式、switch 表达式、嵌套模式以及 switch 语句的 case 标签的模式。


增强的类型推导

类型推导是当一个表达式从它所被使用的地方的上下文中获得它的类型时,我们经常使用的一个专业术语。例如null和lambda表达式总是涉及到类型推导的。

在C#9.0中,先前没有实现类型推导的一些表达式现在也可以用他们的上下文来进行类型推导了。

(1)类型推导的new表达式

在C#中,new表达式总是要求一个具体指定的类型(除了隐式类型数组表达式)。现在,如果表达式被指派给一个明确的类型时,你可以忽略new中类型。

Point p = new (3, 5);

(2)类型推导的??和?:

一些时候,条件表达式??和?:在分支中没有明显的共享类型。现有这种情况会失败,但是在C#9.0中,如果各分支可以转换 为目标类型,这种情况时允许的。

Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type

支持协变的返回值

一些时候,在子类的一个重写方法中返回一个更具体的、且不同于父类方法定义的返回类型更为有用,C# 9.0对这种情况提供了支持。

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}

Lambda 参数弃元

                {
                    // C# 9 之前
                    Func zero = (a, b) => 0;
                    Func func = delegate (int a, int b) { return 0; };
                }

                // C# 9
                {
                    Func zero = (_, _) => 0;
                    Func func = delegate (int _, int _) { return 0; };
                }


C#8.0

Readonly 成员

可将 readonly 修饰符应用于结构的成员。 它指示该成员不会修改状态。 这比将 readonly 修饰符应用于 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";
}

与大多数结构一样,ToString() 方法不会修改状态。 可以通过将 readonly 修饰符添加到 ToString() 的声明来对此进行指示:

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

上述更改会生成编译器警告,因为 ToString 访问未标记为 readonlyDistance 属性:

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

请注意,readonly 修饰符对于只读属性是必需的。 编译器会假设 get 访问器可以修改状态;必须显式声明 readonly。 自动实现的属性是一个例外;编译器会将所有自动实现的 Getter 视为 readonly,因此,此处无需向 XY 属性添加 readonly 修饰符。

编译器确实会强制执行 readonly 成员不修改状态的规则。 除非删除 readonly 修饰符,否则不会编译以下方法:

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

通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。

有关详细信息,请参阅结构类型一文中的 readonly 实例成员部分。

在更多位置中使用更多模式

模式匹配 提供了在相关但不同类型的数据中提供形状相关功能的工具。

switch 表达式

通常情况下,switch 语句在其每个 case 块中生成一个值。 借助 Switch 表达式 ,可以使用更简洁的表达式语法。 只有些许重复的 casebreak 关键字和大括号。 以下面列出彩虹颜色的枚举为例:

public enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}

之前写法

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);
        case Rainbow.Yellow:
            return new RGBColor(0xFF, 0xFF, 0x00);
        case Rainbow.Green:
            return new RGBColor(0x00, 0xFF, 0x00);
        case Rainbow.Blue:
            return new RGBColor(0x00, 0x00, 0xFF);
        case Rainbow.Indigo:
            return new RGBColor(0x4B, 0x00, 0x82);
        case Rainbow.Violet:
            return new RGBColor(0x94, 0x00, 0xD3);
        default:
            throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
    };
}

现在的写法

public static RGBColor FromRainbow(Rainbow colorBand) =>
    colorBand switch
    {
        Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
        _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
    };

这里有几个语法改进:

  • 变量位于 switch 关键字之前。 不同的顺序使得在视觉上可以很轻松地区分 switch 表达式和 switch 语句。
  • case: 元素替换为 =>。 它更简洁,更直观。
  • default 事例替换为 _ 弃元。
  • 正文是表达式,不是语句。

将其与使用经典 switch 语句的等效代码进行对比:

属性模式

借助属性模式 ,可以匹配所检查的对象的属性。 请看一个电子商务网站的示例,该网站必须根据买家地址计算销售税。 这种计算不是 Address 类的核心职责。 它会随时间变化,可能比地址格式的更改更频繁。 销售税的金额取决于地址的 State 属性。 下面的方法使用属性模式从地址和价格计算销售税:

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

模式匹配为表达此算法创建了简洁的语法。

元组模式

一些算法依赖于多个输入。 使用元组模式,可根据表示为元组的多个值进行切换 。 以下代码显示了游戏“rock, paper, scissors(石头剪刀布)”的切换表达式: :

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 方法,该方法将其属性解构为离散变量。 如果可以访问 Deconstruct 方法,就可以使用位置模式 检查对象的属性并将这些属性用于模式。 考虑以下 Point 类,其中包含用于为 XY 创建离散变量的 Deconstruct 方法:

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
}

下面的方法使用位置模式 来提取 xy 的值。 然后,它使用 when 子句来确定该点的 Quadrant

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

xy 为 0(但不是两者同时为 0)时,前一个开关中的弃元模式匹配。 Switch 表达式必须要么生成值,要么引发异常。 如果这些情况都不匹配,则 switch 表达式将引发异常。 如果没有在 switch 表达式中涵盖所有可能的情况,编译器将生成一个警告。

可在此模式匹配高级教程中探索模式匹配方法。

using 声明

using 声明 是前面带 using 关键字的变量声明。 它指示编译器声明的变量应在封闭范围的末尾进行处理。 以下面编写文本文件的代码为例:

之前经典 using 语句的代码写法

static int WriteLinesToFile(IEnumerable 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;
}

现在写法

static int WriteLinesToFile(IEnumerable 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
}

在上面的的示例中,当到达方法的右括号时,将对该文件进行处理。 这是声明 file 的范围的末尾。 在前面的示例中,当到达与 using 语句关联的右括号时,将对该文件进行处理。

在这两种情况下,编译器将生成对 Dispose() 的调用。 如果 using 语句中的表达式不可用,编译器将生成一个错误。

静态本地函数

现在可以向本地函数添加 static 修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。 这样做会生成 CS8421,“静态本地函数不能包含对 的引用”。

考虑下列代码。 本地函数 LocalFunction 访问在封闭范围(方法 M)中声明的变量 y。 因此,不能用 static 修饰符来声明 LocalFunction

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

    void LocalFunction() => y = 0;
}

下面的代码包含一个静态本地函数。 它可以是静态的,因为它不访问封闭范围中的任何变量:

int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);

    static int Add(int left, int right) => left + right;
}

可处置的 ref 结构

ref 修饰符声明的 struct 可能无法实现任何接口,因此无法实现 IDisposable。 因此,要能够处理 ref struct,它必须有一个可访问的 void Dispose() 方法。 此功能同样适用于 readonly ref struct 声明。

异步流

从 C# 8.0 开始,可以创建并以异步方式使用流。 返回异步流的方法有三个属性:

  1. 它是用 async 修饰符声明的。
  2. 它将返回 IAsyncEnumerable。
  3. 该方法包含用于在异步流中返回连续元素的 yield return 语句。

使用异步流需要在枚举流元素时在 foreach 关键字前面添加 await 关键字。 添加 await 关键字需要枚举异步流的方法,以使用 async 修饰符进行声明并返回 async 方法允许的类型。 通常这意味着返回 Task 或 Task。 也可以为 ValueTask 或 ValueTask。 方法既可以使用异步流,也可以生成异步流,这意味着它将返回 IAsyncEnumerable。 下面的代码生成一个从 0 到 19 的序列,在生成每个数字之间等待 100 毫秒:

public static async System.Collections.Generic.IAsyncEnumerable GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

可以使用 await foreach 语句来枚举序列:

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

可以在创建和使用异步流的教程中自行尝试异步流。 默认情况下,在捕获的上下文中处理流元素。 如果要禁用上下文捕获,请使用
TaskAsyncEnumerableExtensions.ConfigureAwait 扩展方法。 有关同步上下文并捕获当前上下文的详细信息,请参阅有关使用基于任务的异步模式的文章。

索引和范围

索引和范围为访问序列中的单个元素或范围提供了简洁的语法。

此语言支持依赖于两个新类型和两个新运算符:

  • System.Index 表示一个序列索引。
  • 来自末尾运算符 ^ 的索引,指定一个索引与序列末尾相关。
  • System.Range 表示序列的子范围。
  • 范围运算符 ..,用于指定范围的开始和末尾,就像操作数一样。

让我们从索引规则开始。 请考虑数组 sequence0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 请注意,sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。 对于任何数字 n,索引 ^nsequence.Length - n 相同。

范围指定范围的开始和末尾 。 包括此范围的开始,但不包括此范围的末尾,这表示此范围包含开始但不包含末尾 。 范围 [0..^0] 表示整个范围,就像 [0..sequence.Length] 表示整个范围。

请看以下几个示例。 请考虑以下数组,用其顺数索引和倒数索引进行注释:

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 索引检索最后一个词:

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

以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。 它包括 words[1]words[3]。 元素 words[4] 不在该范围内。

var quickBrownFox = words[1..4];

以下代码使用“lazy”和“dog”创建一个子范围。 它包括 words[^2]words[^1]。 末尾索引 words[^0] 不包括在内:

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

下面的示例为开始和/或结束创建了开放范围:

var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"

此外可以将范围声明为变量:

Range phrase = 1..4;

然后可以在 [] 字符中使用该范围:

var text = words[phrase];

不仅数组支持索引和范围。 还可以将索引和范围用于 string、Span 或 ReadOnlySpan。 有关详细信息,请参阅索引和范围的类型支持。

可在有关索引和范围的教程中详细了解索引和范围。

Null 合并赋值

C# 8.0 引入了 null 合并赋值运算符 ??=。 仅当左操作数计算为 null 时,才能使用运算符 ??= 将其右操作数的值分配给左操作数。

List numbers = null;
int? i = null;

numbers ??= new List();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers));  // output: 17 17
Console.WriteLine(i);  // output: 17

有关详细信息,请参阅 ?? 和 ??= 运算符一文。

非托管构造类型

在 C# 7.3 及更低版本中,构造类型(包含至少一个类型参数的类型)不能为非托管类型。 从 C# 8.0 开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理。

例如,假设泛型 Coords 类型有以下定义:

public struct Coords
{
    public T X;
    public T Y;
}

Coords 类型为 C# 8.0 及更高版本中的非托管类型。 与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块:

Span> coordinates = stackalloc[]
{
    new Coords { X = 0, Y = 0 },
    new Coords { X = 0, Y = 3 },
    new Coords { X = 4, Y = 0 }
};

有关详细信息,请参阅非托管类型。

内插逐字字符串的增强功能

内插逐字字符串中 $@ 标记的顺序可以任意安排:$@"..."@$"..." 均为有效的内插逐字字符串。 在早期 C# 版本中,$ 标记必须出现在 @ 标记之前。

C# 7.0

一、out输出参数

在以前使用out输出参数的时候,必须先定义变量,然后才能使用,例如:

先定义一个方法,方法参数是out类型的输出参数:

 private void DoNoting(out int x, out int y)
 {
     x = 1;
     y = 2;
 }

以前版本的写法:

// 必须先定义i、j,才能使用out参数
 int i = 0;
 int j = 0;
 this.DoNoting(out i, out j);
 Console.WriteLine($"i+j={i+j}");

在C#7.0中,可以不用先定义,就能够直接使用了:

this.DoNoting(out int x, out int y);
 Console.WriteLine($"x+y={x + y}");
 this.DoNoting(out var l, out var m);

结果:

二、模式

  /// 
 /// 具有模式的 IS 表达式
 /// 
 /// 
 public void PrintStars(object o)
 {
       if (o is null) return;     // 常量模式 "null"
      if (!(o is int i)) return; // 类型模式 定义了一个变量 "int i" i的值就是o的值 相当于is类型判断
       Console.WriteLine($"i={i}");
 }

使用方法:

1 this.PrintStars(null);
2 this.PrintStars(3);

结果:

除了可以像上面那样使用外,还可以使用下面的方式:

 1 private void Switch(string text)
 2 {
 3             int k = 100;
 4             switch (text)
 5             {
 6                 case "Tom" when k > 10:   // text="Tom"且k<10才会输出Tom
 7                     Console.WriteLine("Tom");
 8                     break;
 9                 case "Joe" when text.Length < 10:  //text="Joe"且text的长度<10才会输出Joe
10                     Console.WriteLine("Joe");
11                     break;
12                 case string s when s.Length > 7://模式 定义变量s,s就是text的值
13                     Console.WriteLine(s);
14                     break;
15                 default:
16                     Console.WriteLine("default");
17                     break;
18                 case null:
19                     Console.WriteLine("null");
20                     break;
21             }
22 }

调用:

1 this.Switch(null);
2 this.Switch("TomTomKevin");
3 this.Switch("JoeForest");

三、元组

先来看下面的两个方法:

 1 /// 
 2 /// 使用默认参数名称
 3 /// 
 4 /// 
 5 /// 
 6 private (string, string, string) LookupName(long id) // tuple return type
 7 {
 8       return ("first", "middle", "last");
 9 }
10 
11 /// 
12 /// 不使用默认参数名称
13 /// 
14 /// 
15 /// 
16 private (string first, string middle, string last) LookupNameByName(long id) // tuple return type
17 {
18      return ("first", "middle", "last");
19      //return (first: "first", middle: "middle", last: "last");
20 }

调用:

 1 // 使用默认参数名称:Item1、Item2、Item3
 2 Console.WriteLine($"使用默认参数名称");
 3 var result = this.LookupName(1);
 4 Console.WriteLine(result.Item1);
 5 Console.WriteLine(result.Item2);
 6 Console.WriteLine(result.Item3);
 7 // 不使用默认参数名称
 8 Console.WriteLine($"不使用默认参数名称");
 9 var result2 = this.LookupNameByName(1);
10 Console.WriteLine(result2.first);
11 Console.WriteLine(result2.middle);
12 Console.WriteLine(result2.last);
13 // 也可以使用默认参数名称
14 Console.WriteLine($"也可以使用默认参数名称");
15 Console.WriteLine(result2.Item1);
16 Console.WriteLine(result2.Item2);
17 Console.WriteLine(result2.Item3);

结果:

四、数字分割

如果数字太长,可以按照一定的位数用“_”进行分割,例如:

1 long big = 100_000_0000;
2 Console.WriteLine($"big={big}");

五、本地方法

  public static string LocalFunction(string name)
        {
            return ZhxiToString(name);

            string ZhxiToString(string name)
            {
                return name;
            }
        }

六、已扩展 expression bodied 成员

    public class ExpressionBodied
    {
        public ExpressionBodied(string label) => this.Label = label;

        ~ExpressionBodied() => Console.Error.WriteLine("Finalized!");
        private string label;

        // Expression-bodied get / set accessors.
        public string Label
        {
            get => label;
            set => this.label = value ?? "Default label";
        }


        /// 
        /// C#6
        /// 
        public void Show1() => Console.WriteLine("朝夕教育--C#6的语法");

        /// 
        /// C#6
        /// 
        /// 
        public string Show2() => 123456.ToString();
    }

七、default 默认文本表达式

 Func whereClause = default(Func);
 Func whereClause1 = default;

八、throw 表达式

string[] args1 = new string[] { "zhaoxi01", "zhaoxi02", "zhaoxi03" };
                string arg = args1.Length >= 4 ? args1[0] :
                                 throw new ArgumentException("You must supply an argument");

九、命名实参

   public 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("Gift Shop", 31, "Red Mug");

                // 可以按任何顺序为参数提供命名参数。
                PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
                PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

                // 与位置参数混合的命名参数有效 
                // 只要它们被使用在正确的位置。
                PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");
                PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");    // C# 7.2 onwards
                PrintOrderDetails("Gift Shop", orderNum: 31, "Red Mug");                   // C# 7.2 onwards


C# 6.0

一、自动属性初始化

在以前的C#版本中,属性是这样写的:

1 public int Id { get; set; }
2 public string Name { get; set; }

在C#6.0中,属性可以自动赋初始值,例如:

1 public string Name { get; set; } = "summit";
2 public int Age { get; set; } = 22;
3 public DateTime BirthDay { get; set; } = DateTime.Now.AddYears(-20);
4 public IList AgeList
5 {
6       get;
7       set;
8 } = new List { 10, 20, 30, 40, 50 };

二、导入静态类

我们都知道,使用静态类的方法时是使用类名.方法名的形式,例如:

1 Console.WriteLine($"之前的使用方式: {Math.Pow(4, 2)}");

这里的Math是框架自带的静态类,要使用Pow()方法,必须要向上面的代码一样。在C#6.0中可以用下面的方式使用静态类的方法,例如:

1、使用using static导入静态类

2、导入静态类以后,可以像使用普通方法一样,直接使用,例如:

1 Console.WriteLine($"之前的使用方式: {Math.Pow(4, 2)}");
2 Console.WriteLine($"导入后可直接使用方法: {Pow(4, 2)}");

三、字符串内插

在以前版本中,如果要对字符串就行格式化,要使用string.Format,例如:

1 Console.WriteLine(string.Format("当前时间:{0}",DateTime.Now.ToString()));

在C#6.0中,可以直接使用$进行字符串的嵌入了,例如:

1 Console.WriteLine(string.Format("当前时间:{0}",DateTime.Now.ToString()));
2 Console.WriteLine($"年龄:{this.Age}  生日:{this.BirthDay.ToString("yyyy-MM-dd")}");
3 Console.WriteLine($"年龄:{{{this.Age}}}  生日:{{{this.BirthDay.ToString("yyyy -MM-dd")}}}");
4 Console.WriteLine($"{(this.Age <= 22 ? "小鲜肉" : "老鲜肉")}"); 

结果:

四、空值运算符

我们都知道,null值是不能调用ToString()方法的,会报“未将对象引用设置到对象实例”的错误,例如:

1 string name = null;
2 Console.WriteLine(name.ToString());

结果:

在C#6.0中有了空值运算符,如果是null值也可以调用ToString()方法了,这时程序什么都不会输出,例如:

1 int? iValue = 10;
2 Console.WriteLine(iValue?.ToString());//不需要判断是否为空
3 string name = null;
4 Console.WriteLine(name?.ToString()); // 程序不会报错,也不会输出任何值
5 Console.WriteLine("空值运算符");

结果:

这样就不需要像以前一样先判断一下变量是不是null值了。

五、对象初始化器

在前面一篇文章中讲过一次C#语法糖,在那里讲过对象初始化器和集合初始化器,以前初始化字典集合要像下面的方式:

 1 IDictionary dictOld = new Dictionary()
 2 {
 3         { 1,"first"},
 4         { 2,"second"}
 5 };
 6 // 循环输出
 7 foreach(KeyValuePair keyValue in dictOld)
 8 {
 9         Console.WriteLine($"key:{keyValue.Key},value:{keyValue.Value}");
10 }

结果:

在C#6.0中,可以通过索引的方式给字段进行初始化,例如:

 1 IDictionary dictNew = new Dictionary()
 2 {
 3        [4] = "first",
 4        [5] = "second"
 5 };
 6 // 循环输出
 7 foreach (KeyValuePair keyValue in dictNew)
 8 {
 9         Console.WriteLine($"key:{keyValue.Key},value:{keyValue.Value}");
10 }

结果:

六、异常过滤器

在以前的版本中,捕获异常通常使用try{}catch(),只要是发生了异常,就会进入到catch里面,例如:

1 try
2 {
3         Int32.Parse("we");
4 }  
5 catch(Exception ex)
6 {
7         Console.WriteLine(ex);
8 }

结果:

在C#6.0中,可以设置在满足某种条件的情况下,才能进入异常捕获,例如:

1 int exceptionValue = 10;
2 try
3 {
4        Int32.Parse("s");
5 }
6 catch (Exception e) when (exceptionValue > 1)//满足条件才进入catch
7 {
8        Console.WriteLine("catch");
9 }

结果:

七、nameof表达式

我们以前使用过typeof,typeof()可以获取类型,nameof表达式和typeof一样的,例如:

1 Console.WriteLine(nameof(peopleTest)); //获取peopleTest这个字符串
2 Console.WriteLine(nameof(People));
3 Console.WriteLine(nameof(CSharpSix));

结果:

应用场景:打印日志的时候打印出参数的值,如果方法里面参数的名称改了,而日志里面写的还是原来参数的名称,这样就会导致不一致,使用了nameof可以保证日志里面的参数名称和方法里面的参数名称一致。

八、在属性/方法里面使用Lambda表达式

1 public string NameFormat => string.Format("姓名: {0}", "summit");
2 public void Print() => Console.WriteLine(Name);

在Main()方法里面调用:

1 CSharpSix six = new CSharpSix();
2 Console.WriteLine(six.NameFormat);
3 six.Print();


结果:

九、只读自动属性

    public class Student
    {
        public Student(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }

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

        //对只读属性只能通过构造函数对其初始化值
        //public void SetName(string firstName, string lastName)
        //{
        //    FirstName = firstName;
        //    LastName = lastName;
        //}
    }

在main里初始化

            //1.只读自动属性
                {
                    Student student = new Student("Richard", "Richard01");
                    string fullName = student.ToString();
                    string fullName1 = student.FullName;
                }

C#语法系列源码,请进群下载

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4