65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 8 的新功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2019 年 11 月 8 日

CPOL

12分钟阅读

viewsIcon

34738

本文将简要介绍 C# 7.1、7.2、7.3 和 8 的新功能。

引言

C# - 编程界一个耳熟能详的名字。作为 .NET 开发的核心,C# 是一种功能强大、类型安全、复杂且面向对象的语言。它使开发人员能够开发 Windows 应用、Web 应用、数据库应用、Web 服务、XML 应用、后端服务以及更多内容。

C# 最初由微软于 1999 年开发,名称为“C-Like Object Oriented Language”,微软称其为 COOL。后来,在 2000 年 7 月,该语言更名为 C#。C# 的第一个公共版本(1.0)于 2002 年 1 月随 Visual Studio 2002 发布。至今,该语言已进行了许多更改。C# 的功能随着每个版本的发布而不断增强。2018 年 5 月,微软发布了 **C# 7.3** 和 Visual Studio 2017。在此之前,C# 7.1 和 7.2 分别于 2017 年 8 月和 2017 年 11 月发布。最新的 C# 版本(8.0)于 2019 年 9 月发布,兼容 .NET Framework 4.8 和 Visual Studio 2019。如果您想了解 C# V7 版本的新功能,可以参考我之前的文章 **这里**。

在本文中,我们将介绍 **C# 7.1、7.2、7.3** 和 **8.0** 中包含的新功能。让我们开始吧。

C# 7.1 的新功能

异步 Main 方法

这是 `main` 方法编译技术的一项重大更改,现在 C# 可以接受 `Async main` 方法,这有助于开发人员在 `main` 方法中使用 `await`。请看下面的示例:

static string Main()
{
    await AsyncMethod1();
}

在此功能之前,如果您想为您的 `async` 方法使用 `await`,则需要使用 `GetAwaiter()` 方法。如果您的 `main` 方法返回 `Task`,则它可以包含 `async` 修饰符。请看下面的示例:

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

默认字面量更改

默认字面量为类型生成默认值,默认字面量的语法有所更改,请看下面的示例:

//previously you wrote
Func<int, string> whereClause = default(Func<int, string>);

现在,右侧部分被省略了,您只需键入 `default` 关键字即可。请看下面的示例:

Func<int, string> whereClause = default;

元组语法更改

元组语法也有所更改。元组在 C# 7 中引入,您需要在其中定义要在程序中使用的元组的名称。请看下面的示例:

int count = 3;
string colors = "colors of the flag";
var tupleCol = (count: count, colors: colors);

在这里,我们需要定义 `count: count` 和 `colors: colors` 名称,但在 C# 7.1 中,它们将语法更改为如下:

int count = 3;
string colors = "colors of the flag";
var tupleCol = (count,colors); // here syntax gets trimmed

C# 7.2 的新功能

在 C# 7.1 版本之后,C# 7.2 发布,带来了一些高级功能,此版本更侧重于效率和性能。让我们来梳理一下 C# 7.2 的一些新功能。

数字字面量中的前导下划线

C# 7.0 具有字面量中的数字分隔符功能,但它不接受 `_`(下划线)作为值的字符。但在 C# 7.2 中,您可以使用二进制和十六进制数字字面量以 `_` 开头。请看下面的示例:

  int val_bi = 0b_0110_01;

私有保护访问修饰符

C# 7.2 中添加了一个新的访问修饰符,名为“`private protected`”。它基本上是 `private` 访问修饰符和 `protected` 访问修饰符的组合,其中 `protected` 访问修饰符将成员访问限制在派生类中,而 `private` 访问修饰符将成员限制在同一类中。因此,最终我们得到一个修饰符,它可以在包含类或派生类中访问,但仅限于同一程序集。

条件 `ref`

顾名思义,我们可以根据条件表达式分配“`ref`”结果。请看下面的示例:

ref var finalVal = ref (value1 != null ? ref val1_arr[0] : ref val2_arr[0]);

在上面的示例中,我们将值赋给 `finalVal` 变量,但此赋值取决于条件运算符 `with`,因此要麽将 `val1_arr` 的值赋给它,要麽将 `var2_arr` 的值赋给它。

命名参数的更改

我们已经了解了函数式命名参数和可选参数。在 C# 7.2 中,如果参数的顺序正确,则无需为参数命名。请看下面的示例:

//suppose I have written a function and used below named arguments
EmpDetails(EmpID: 3, firstName: "Manavya", City: "Daswel");

//here in C#7.2, I can write the above code line as
EmpDetails(EmpID: 3, "Manavya", City: "Daswel");

//if you observed that firstName: "Manavya" is replaced by only "manavya" 
// and it is the beauty of the C#7.2

C# 7.3 的新功能

此版本更侧重于高效的安全编码,以减少内存消耗,避免错误,防止缓冲区溢出,并使现有功能更加强大。让我们逐个分析这些功能。

stackalloc 运算符的改进

我们知道 `stackalloc` 分配一个块。此运算符的好处是,在方法返回并退出控制时,它会释放内存,因此无需进行垃圾回收。

到目前为止,借助 `stackalloc`,您可以按如下方式初始化值:

int* Array1 = stackalloc int[3] {5, 6, 7};

但从现在开始,数组也可以作为 `stackalloc` 的一部分,如下所示:

Span<int> Array = stackalloc [] {10, 20, 30};

Fixed 语句支持更多类型

现在,fixed 语句支持更多类型(fixed 语句是防止垃圾回收器清理可移动变量的语句,您可以使用 fixed 关键字创建这些语句)。因此,从 C# 7.3 开始,fixed 语句支持任何具有 `GetPinnableReference()` 方法的类型,该方法返回一个固定的 `ref T`。

非托管约束

您可以使用非托管约束来指示类型参数必须是非托管类型(没有指针)(如果一个成员是 `byte`、`short`、`sbyte`、`int`、`long`、`char`、`bool` 类型,则该成员是非托管类型)。

unsafe public static byte[] convert_to_byte<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    return result1;
}

上面的示例是无托管且不安全的。

您还可以使用 `System.Enum` 和 `System.Delegate` 作为基类约束。

元组现在支持 != 和 =

从 C# 7.3 版本开始,元组现在支持**等于**和**不等于**运算符。

我们知道,**等于**和**不等于**运算符会比较左右两边的值。请看下面的示例:

var exp1 = (val1: 100, val2: 20);
var exp2 = (val1: 100, val2: 20);
exp1 == exp2; //it will return displays as 'true'

在这里,我们将得到 `true` 的输出,因为两个操作数都为 `true`。

“in”方法在重载中的更改

您知道 `in` 方法是 `out` 或 `ref` 关键字的良好替代品吗?`in` 参数不能被调用函数修改,但 `out` 或 `ref` 参数可以被调用方法修改。现在这里出现了一个问题:当我们为“按引用传递”和“按值传递”的方法赋予相同的名称时,它会抛出歧义异常。为了避免此错误,C# 7.3 使用了“`in`”方法。请看下面的示例:

static void calculateArea(var1 arg);
static void calculateArea(in var1 arg);

扩展 Out 参数边界

C# 7.3 扩展了 `out` 参数的边界,现在您可以在构造函数初始化器、属性初始化器中定义 `out` 参数。请看下面的示例以获取更多详细信息:

//here, we have defined the out parameter in constructor
public class parent
{
   public parent(int Input, out int Output)
   {
      Output = Input;
   }
}

//now let's use the above class in the following class
public class Child : parent
{
   public Child(int i) : base(Input, out var Output)
   {
      //"The value of 'Output' is " + Output
   }
}

多个编译器选项

C# 7.3 提供了多个编译器选项,您可以使用它们来:

  1. 使用公钥签名程序集
  2. 从构建环境中替换路径

使用公钥签名程序集

如果您想用公钥签名任何程序集,则可以使用此选项。此程序集将被标识为已签名,但使用公钥。当您想签名开源代码的程序集时,此选项非常有用。语法非常简单,您只需添加参数 `-public sign`。

从构建环境中替换路径

此选项使您能够用先前映射的源路径替换构建中的源路径。简而言之,它将帮助您将源路径映射到物理路径。语法如下:

-pathmap:path1=sourcePath1,path2=sourcePath2

在这里,`path1` 是源文件的完整路径,`path2` 是需要替换 `path1` 的备用路径。基本上,这些路径可以在 PDB 文件中找到。

C# 8 的新功能

C# 8 带来了非常强大的功能,例如模式匹配的增强、只读成员、`static` 函数(局部)、可空引用类型、嵌套循环中的 `stackalloc`、索引和范围的更改。

让我们逐一分析它们。

using 声明

`using` 声明只不过是一种技术,在这种技术中,变量在 `using` 关键字之前声明。它还告诉编译器该变量应在方法结束之前(在使用该变量的地方)进行释放。请看下面的示例以了解 `using` 声明:

var ReadFile(string szpath)
{
 using StreamReader StrFile = new StreamReader(szpath);
 string iCount;
 string szLine;
 while ((szLine = StrFile.ReadLine()) != null) 
  { 
  Console.WriteLine(szLine); 
  iCount++; 
  } 
 // file is disposed here
}

在上面的示例中,您可以看到 `using` 语句可以用作声明语句。`StrFile` 变量将在方法的大括号结束时立即释放。

您可以使用之前的常规编码步骤编写相同的代码:

var ReadFile(string szpath)
{
using (StreamReader StrFile = new StreamReader(szpath))
 {
  string iCount;
  string szLine;
  while ((szLine = StrFile.ReadLine()) != null) 
  { 
  Console.WriteLine(szLine); 
  iCount++; 
  } 
 
 } //file disposed here
}

在之前的代码中,我们可以看到 `StrFile` 对象在 `using` 作用域结束后被释放。

使用异步流

您想使用异步流吗?那么这个功能对您来说非常有用。这项任务由返回带有 `IAsyncEnumerable` 枚举器的异步流的函数完成。此方法声明为 `async` 修饰符。此函数还包含 `yield return` 语句(用于返回异步流)。要使用 `async` 流,我们需要在实际返回值之前使用 `await` 关键字。请看下面的示例,其中我们返回了一个 `async stream`:

public async System.Collections.Generic.IAsyncEnumerable<int> asyncStream()
{
    for (int iCount = 0; iCount < 10; iCount++)
    {
        await Task.Delay(10);
        yield return iCount;
    }
}
//call above method in mail method
await foreach (var num in asyncStream())
{
    Console.WriteLine(num);
}

在上面的示例中,我们使用 `async stream` 返回了从 1 到 10 的数字。

新的索引和范围类型

C# 8 引入了两种用于从列表/数组中获取项的新类型。`System.Index` 和 `System.Range` 用于获取这些类型。**^ (caret)** 表示索引符号,**..** 表示范围符号。让我们稍微详细地解释一下它们以理解概念。

展开索引(^)

通常,当我们从数组中获取项时,索引从 `0` 开始。但当我们使用这种新类型时,它的索引从末尾开始向上方向计数。

请看下面的示例:

var simArray = new string[]
{                 
    "one",         // 0      ^4
    "two",         // 1      ^3
    "three",       // 2      ^2
    "four",        // 3      ^1
};

在上面的示例中,我们有四个项在数组中。通常,索引从 0 开始,到 3 结束。但在索引的情况下,它从 ^4(末尾)开始到 ^1,所以这是反向索引。

现在,如果我尝试获取任何索引,输出将如下:

Console.WriteLine("fetch using simple array " + simArray[1]);
//above code gives output as "two"
Console.WriteLine("fetch using indices caret operator " + simArray[^1]);
//above code gives output as "four"

展开范围(..)

此运算符获取其操作数(即开始和结束)的范围。

因此,如果我们考虑上面的示例并尝试从“`simArray`”获取值,我们将得到如下输出:

Console.WriteLine("fetch using range operator" + simArray[..]);
//above code gives output as "one" to "four"
Console.WriteLine("fetch using range operator" + simArray[..3]);
//above code gives output as "one" to "three"
Console.WriteLine("fetch using range operator" + simArray[2..]);
//above code gives output as "two" to "four"

在上面的示例中:

  • [..] 表示获取列表的所有项
  • [..3] 表示获取直到索引 3 的所有项
  • [1..] 表示获取从索引 1 开始的所有项

Stackalloc 作为表达式

我们知道 `stackalloc` 运算符在堆栈上分配内存块,并在方法调用退出时释放它,因此它不需要垃圾回收。`stackalloc` 的语法是:

stackalloc T[E]

其中 `T` 是非托管类型,`E` 是类型为 `int` 的表达式。

在 C# 8 中,您可以将 `stackalloc` 用作表达式。如果返回结果是 `System.Span`,则此功能适用。请看下面的示例:

Span<int> num = stackalloc[] { 20, 30, 40 };
var index = num.IndexOfAny(stackalloc[] { 20 });
Console.WriteLine(index);  // output: 0

在上面的示例中,我们使用了 `stackalloc` 作为 `int` 数据的集合,并将 `stackalloc` 作为表达式传递给 `IndexOfAny` 方法,该方法将返回结果 `0`。

插值字符串的增强

C# 8 在插值 `string` 方面进行了一些增强。

你知道什么是插值 `string` 吗?

使用 `$` 来标识 `string` 为插值 `string`。此 `string` 包含插值表达式,然后这些表达式将被实际结果替换。请看下面的示例:

string szName = "ABC";
int iCount = 15;

// String interpolation:
Console.WriteLine($"Hello, {szName}! You have {iCount} apples");

如果我们运行上面的代码,输出将是“`Hello, ABC! You have 15 apples`”。因此,我们可以说花括号中的变量将被实际 `string` 替换。(这里,如果您注意到,您可以看到 `string` 以 `$` 开头,这种 `string` 称为插值 `string`)。

现在在 C# 8 中,`$@""` 或 `@$""` 是有效的,这意味着您可以从 `@` 或 `$` 开始 `string`。在 C# 的早期版本中,`@` 只允许在 `$` 之后。

将结构成员设为只读

如果您想为任何 `struct` 成员提供只读属性,那么在 C# 8 中这是可能的。在这种情况下,您可以将 `struct` 成员设为只读,而不是将整个 `struct` 设为只读。`readonly` 属性表明它不会修改状态。请看下面的代码片段,这是我们通常用于将整个结构设为只读的做法:

public readonly struct getSqure
{
    public int InputA { get; set; }
    public int InputB { get; set; }
    public int output => Math.Pow(A,B);

    public override string ToString() =>
        $"The answer is : {output} ";
}

在上面的示例中,我们使用了 `Math.Pow` 方法来计算输入数字的平方。而在 C# 8 中,我们可以将结构成员设为只读。请看下面的代码片段,其中我们将一些 `struct` 成员设为 `readonly`。

public struct getSqure
{
    public int InputA { get; set; }
    public int InputB { get; set; }
    public readonly int output => Math.Pow(A,B);

    public override string ToString() =>
        $"The answer is : {output} ";
}

 

默认接口方法

这是 C# 8 的一个很好的功能,它允许您在接口中添加成员(如方法),即使在后续版本中,而不会破坏现有实现,因为这里的现有实现继承了默认实现。

局部静态函数

在 C# 8 中,您可以将局部函数设为 `static`,这将有助于确保局部函数不会持有来自内部封闭循环的变量。请看下面的示例,这是一个尝试访问局部变量的局部函数:

int Add()
{
    int A;
    Add();
    return Sum;

    void LocalFunction() => A + 3;
}

现在,为了避免此类问题,我们可以使用局部 `static` 函数,其中我们可以使用方法重写相同的代码:

int Add()
{
    int A = 5;
    return Sum(A);

    static int Sum(int val) => val + 3;
}

null 合并赋值运算符

C# 8 引入了一个名为**null**合并赋值运算符的运算符。此运算符表示为 `??=`。基本上,此运算符用于将右侧表达式的值赋给左侧操作数,但当左侧操作数的值评估为 `null` 时,才可能进行此赋值。简而言之,此运算符在左操作数不为 `null` 时返回其值,否则返回右侧操作数的值。请看下面的代码片段以获取更多详细信息:

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

(lstNum ??= new List<int>()).Add(100);
Console.WriteLine(string.Join(" ", lstNum)); 

// the output would be : 100

lstNum.Add(a ??= 0);
Console.WriteLine(string.Join(" ", numbers));  // output: 100 0
// the output would be : 100 0

Switch 表达式的更改

`switch` 表达式及其模式匹配的语法有所更改。请看下面的示例:

假设我们有一个带有随机数字的 `enum`,如下所示:

public enum RandomNum
{
    One,
    Three,
    Five,
    Seven,
    Six,
}

现在,您可以使用 `switch` 表达式执行以下操作:

public static string getRandomNum(RandomNum iNum) =>
    iNum switch
    {
        RandomNum.One => return "1",
        RandomNum.Three => return "3",
        RandomNum.Five => return "5",
        RandomNum.Seven => return "7",
        RandomNum.Six => return "6",
        _              => throw new Exception("invalid number value"),
    };

在上面的 `switch` 表达式示例中,您会发现语法级别的更改如下:

  • 在原始语法中,`case` 关键字与 `switch` 语句一起使用,但在那里,`case` 关键字被 `=>` 取代,这更具自解释性。
  • 在原始语法中,`default` 关键字用于默认情况,但在那里,default 关键字被 `_`(下划线)符号取代。
  • 在原始语法中,`switch` 关键字出现在变量之前,但在那里,它出现在变量之后。

可 Dispose 的 ref 结构

声明为 `ref` 的 `struct` 没有实现任何接口的权限,因此我们无法 Dispose 它的对象,因为它没有实现 `IDisposable` 接口的权限。所以,我们需要访问 `void Dispose()` 方法才能使可 Dispose 的 `ref` 结构。同样,我们也可以 Dispose `readonly ref` 结构。

总结

在本文中,我们了解了 C# 7.1、7.2、7.3 和 8.0 的各种功能。每个版本都有其独特的能力,使 C# 比以前的版本更强大。在本文的后续版本中,我们将详细介绍每个功能。在此之前,您可以欣赏这篇文章。

欢迎随时提出建议和疑问。

祝您编码愉快!

历史

  • 2019 年 11 月 8 日:初始版本
© . All rights reserved.