C# 8 的新功能





5.00/5 (15投票s)
本文将简要介绍 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 提供了多个编译器选项,您可以使用它们来:
- 使用公钥签名程序集
- 从构建环境中替换路径
使用公钥签名程序集
如果您想用公钥签名任何程序集,则可以使用此选项。此程序集将被标识为已签名,但使用公钥。当您想签名开源代码的程序集时,此选项非常有用。语法非常简单,您只需添加参数 `-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
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 日:初始版本