C#7.1 & C#7.2 新特性






3.35/5 (14投票s)
C#7.1 和 C#7.2 的新特性,用简单的方式讲解
引言
本文介绍了 C#7.1、C#7.2 的新特性,并列出了一些 C#8 的计划功能。
背景
自 C# 7.0 起,Microsoft 推出了两个小版本更新:C#7.1 和 C#7.2。
在 C#7.1 和 C#7.2 中,Microsoft 在语言中引入了一些虽小但非常实用的特性。
Using the Code
请在此处查找附件项目,或从我的 github 页面下载并查看项目。它很简单易懂,即使不深入细节也能理解所有特性。
注意:要在项目中查看不同的特性,您可以按照本文的说明更改语言版本。
目录
如何在 Visual Studio 中选择 C# 语言版本(C#7.1/C#7.2)?
C# 编译器从 Visual Studio 2017 版本 15.3 开始支持 C# 7.1。最新的版本是 C# 7.2,它于 2017 年与 Visual Studio 2017 版本 15.5 一起发布。然而,7.1 和 7.2 的特性默认是关闭的。要启用这些特性,我们需要更改项目的语言版本设置。
以下是更改 C# 语言版本的两种方法。
方法 1
- 首先,在解决方案资源管理器中右键单击项目节点 => 选择属性。这将打开项目属性。
- 选择生成选项卡 => 选择高级按钮。这将打开高级生成设置窗口。
- 在语言版本下拉列表中,选择 C# 的最新次要版本(latest),或特定的版本 C# 7.1/C#7.2 等,如下图所示。
latest 值表示您希望使用当前机器上的最新次要版本。C# 7.1 意味着即使在发布了新的次要版本后,您也希望使用 C# 7.1。
方法 2
您可以编辑“csproj”文件并添加或修改以下行。
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>
请注意,如果您使用 Visual Studio IDE 更新 csproj 文件,IDE 会为每个生成配置创建单独的节点。您通常会在所有生成配置中设置相同的值,但需要为每个生成配置显式设置,或者在修改此设置时选择“所有配置”。您将在 csproj 文件中看到以下内容:
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
</PropertyGroup>
LangVersion
元素的可接受值为:
ISO-1
ISO-2
3
4
5
6
7
7.1
默认
latest
特殊的字符串 default 和 latest 分别解析为生成机器上安装的最新主要和次要语言版本。
C# 7.1 的新特性
现在,让我们考虑以下类(以便我们轻松理解这些特性):
public class Fruit
{
public int Id { get; set; }
public string Name { get; set; }
private static List<Fruit> GetFruits()
{
return new List<fruit>
{
new Fruit { Id= 1, Name= "Apples" },
new Fruit { Id= 2, Name= "Apricots" },
new Fruit { Id= 3, Name= "Avocados" },
new Fruit { Id= 4, Name= "Bananas" },
new Fruit { Id= 5, Name= "Boysenberries" },
new Fruit { Id= 6, Name= "Blueberries" },
new Fruit { Id= 7, Name= "Bing Cherry" },
new Fruit { Id= 8, Name= "Cherries" },
new Fruit { Id= 9, Name= "Cantaloupe" },
new Fruit { Id= 10, Name= "Crab apples" },
new Fruit { Id= 11, Name= "Clementine" },
new Fruit { Id= 12, Name= "Cucumbers" }
};
}
public static async Task<List<Fruit>> GetFruitsAsync
(CancellationToken cancellationToken = default(CancellationToken))
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
}
以下是 C# 7.1 中的一些新特性。
推断的元组元素名称
在 C#7 之前,元组是可用的,但没有语言支持,因此效率不高。此外,元组元素通过 item1
、item2
等方式引用,这使得很难理解它们引用的内容。在 C#7 中,Microsoft 为元组添加了语言支持,允许为 Tuple
元素提供正确的名称(仍然是可选的)。以下代码显示了 C#7.0 版本,其中指定了 Tuple
元素的名称:
//Retrieve fruits (from any data source)
var fruits = Fruit.GetFruits);
//Old way - C#7.0 (Specify names to the Tuple elements)
var tupleExampleQuery = from fruit in fruits
select (Id: fruit.Id, Name: fruit.Name);
var firstElementOld = tupleExampleQuery.First(); //(int Id, String Name)
string firstFruitOld = $"First fruit is : Id: {firstElementOld.Id.ToString()},
Name: {firstElementOld.Name}";
然而,在示例中(以及大多数情况下),元组名称与我们引用的源元素的名称匹配。在这种情况下,我们可能希望省略名称。幸运的是,在 C#7.1 中,我们有一个特性可以从引用的元素推断元组元素的名称。以下代码显示了 C#7.1 版本,其中推断了元组元素名称:
//New (Inferred Tuple element names)
var inferredTupleExampleQuery = from fruit in fruits
select (fruit.Id, fruit.Name); // Same as above
// but Id and Name names will be inferred
var firstElementNew = inferredTupleExampleQuery.First(); //(int Id, String Name)
string firstFruitNew = $"First fruit is : Id: {firstElementOld.Id.ToString()},
Name: {firstElementOld.Name}";
注意:要使用此功能,您需要添加一个 NuGet 包 System.ValueTuple
。
异步 Main 方法
在学习 C# 的过程中,我们大多数时候会创建控制台应用程序。在测试异步方法代码的场景中,我们不得不编写一些额外的逻辑。我们通常需要编写如下的样板代码:
public static void Main()
{
MainAsync().GetAwaiter().GetResult();
}
private static async Task MainAsync()
{
... // Main body here
}
在我们的示例中,我们可能需要编写如下代码:
//OLD way (before C#7.1)
static int Main(string[] args)
{
var fruits = Fruit.GetFruitsAsync().GetAwaiter().GetResult();
//Do Some work
//return exit code
return 0;
}
幸运的是,在 C#7.1 中,main
方法可以是 async
的,所以在这里,我们的代码将看起来更像普通的 async
方法,如下所示:
static async Task Main(string[] args)
{
var fruits = await Fruit.GetFruitsAsync();
//Do Some work
}
以下是允许的 main()
版本:
static void Main()
static void Main(string[])
static int Main()
static int Main(string[])
已扩展到以下内容:
static Task Main()
static Task<int>Main()
static Task Main(string[])
static Task<int> Main(string[])
默认字面量表达式
当目标类型可以推断时,我们可以在默认值表达式中使用此功能。为了理解此功能,让我们继续上面的示例。在上面的示例中,fruit
方法可以选择接受 CancellationToken
作为参数,该参数可以被选择性地标记为默认值。所以以前,我们通常这样做:
/*Old way (before C#7.1) - Default literal expressions*/
public static async Task<List<Fruit>> GetFruitsAsync
(CancellationToken cancellationToken = default(CancellationToken))
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
在这里,目标类型可以从 default 推断出来,所以我们可以省略它,使代码更好一点。
/*New way (C#7.1) - Default literal expressions (type in the default is inferred)*/
public static async Task<List<Fruit>> GetFruitsAsync(CancellationToken cancellationToken = default)
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
C# 7.2 的新特性
以下是 C# 7.2 的一些新特性...
私有受保护访问修饰符
到目前为止,我们有 Private
、Public
、Protected
、Internal
和 Internal Protected
访问修饰符。所有访问说明符都已得到充分讨论,我们都知道它们的作用,所以我不会详细介绍。
Private Protected
访问修饰符为成员提供了可见性,以便在同一程序集中的派生类可以访问。
以下是一些代码示例,有助于理解所有访问修饰符(只需阅读代码中的注释来理解此功能)。
让我们考虑一个 private
类,如下所示:
public class Parent
{
private int private_var { get; set; }
protected int protected_var { get; set; }
public int public_var { get; set; }
internal int internal_var { get; set; }
internal protected int internal_protected_var { get; set; }
private protected int private_protected_var { get; set; }
}
当前程序集中的子类/派生类
public class Child_WithinCurrentAssembly : Parent
{
public Child_WithinCurrentAssembly()
{
//this.private_var = 0; // not accessible = because private members
// are not accessible outside of the class
this.protected_var = 0; // accessible - because protected members
// are accessible within derived class
this.public_var = 0; // accessible - because public is accessible anywhere
this.internal_var = 0; // accessible - because internal is accessible
// within current assembly
this.internal_protected_var = 0; // accessible - because internal protected is accessible
// within derived class or within current assembly
this.private_protected_var = 0; // accessible - because private protected is accessible
// within derived class and within current assembly
}
}
当前程序集中的非子类
public class NonChild_WithinCurrentAssembly
{
public NonChild_WithinCurrentAssembly()
{
Parent p = new Parent();
//p.private_var = 0; // not accessible = because private members
// are not accessible outside of the class
//p.protected_var = 0; // not accessible - because protected members
// are accessible only within derived class
p.public_var = 0; // accessible - because public is accessible anywhere
p.internal_var = 0; // accessible - because internal is accessible
// within current assembly
p.internal_protected_var = 0; // accessible - because internal protected is accessible
// within derived class or within current assembly
//p.private_protected_var = 0; // not accessible - because private protected
// is accessible
// only within derived class and within current assembly
}
}
当前程序集外的子类/派生类
public class Child_OutsideCurrentAssembly : Parent
{
public Child_OutsideCurrentAssembly()
{
//this.private_var = 0; // not accessible = because private members
// are not accessible out side of the class
this.protected_var = 0; // accessible - because protected members
// are accessible within derived class
this.public_var = 0; // accessible - because public is
// accessible anywhere
//this.internal_var = 0; // not accessible - because internal is
// accessible only within current assembly
this.internal_protected_var = 0; // accessible - because internal protected is
// accessible within derived class
// or within current assembly
//this.private_protected_var = 0; // not accessible - because private protected is
// accessible only within derived and
// within current assembly
}
}
当前程序集外的非子类
public class NonChild_OutsideCurrentAssembly
{
public NonChild_OutsideCurrentAssembly()
{
Parent p = new Parent();
//base.private_var = 0; // not accessible = because private members
// are not accessible out side of the class
//p.protected_var = 0; // not accessible - because protected members
// are accessible only within derived class
p.public_var = 0; // accessible - because public is
// accessible anywhere
//p.internal_var = 0; // not accessible - because internal is
// accessible only within current assembly
//p.internal_protected_var = 0; // not accessible - because internal protected is
// accessible only within derived class
// or within current assembly
//p.private_protected_var = 0; // not accessible - because private protected
// is accessible
// only within derived class and within
// current assembly
}
}
现在,这可能会让你对 internal protected
和 private protected
之间的区别感到困惑。这是区别(记住粗体行):
protected internal
修饰符表示 protected
或 internal
,即 - 类成员可以被派生自某个类的子类(直接子类)访问,也可以被当前程序集中的任何类访问,即使该类不是类 A
的子类(因此“protected
”所暗示的限制被放宽了)。
private protected
表示 protected
且 internal
。即 - 成员只能被同一程序集中的子类访问,但不能被程序集外的子类访问(因此“protected
”所暗示的限制被缩小了——变得更加严格)。
非尾随命名参数
为了理解这个特性,让我们先尝试理解命名参数及其规则。
通常,在调用函数时,所有参数都必须按照函数中指定的顺序传递。命名参数允许调用方法在任何顺序传递参数,只要在调用时指定的参数名称与参数名称匹配。
例如,考虑以下示例方法:
void MethodA(string arg1, int arg2, string arg3)
当不使用命名参数调用时,顺序必须正确,如下所示:
MethodA("arg1_value", 1, "arg3_value")
现在,如果我们想使用命名参数来调用它,我们可以按任何顺序传递参数。
MethodA(arg2:1, arg3:"arg3_value", arg1:"arg1_value")
现在,如果我们想只传递一些参数作为命名参数,这是可能的,但在 C#7.2 之前,位置参数始终需要放在命名参数之前。所以我们可以调用 MethodA
,如下所示:
MethodA("arg1_value", 1, arg3:"arg3_value") OR
MethodA("arg1_value", arg2:1, arg3:"arg3_value") OR
MethodA("arg1_value", arg3:"arg3_value", arg2:1)
现在,因为规则规定位置参数必须始终放在命名参数之前,所以在 C#7.2 之前,以下方法调用是不可能的,而现在是可能的。
MethodA(arg1:"arg1_value", 1, arg3:"arg3_value")
数字字面量中的前导下划线
这是一个非常小但很有用的特性。在 C#7.0 中,Microsoft 为了提高可读性引入了数字分隔符。所以以下是在 C#7.0 中有效的数字:
//After C#7 (and before C#7.2)
int i = 123;
int j = 1_2_3;
int k = 0x1_2_3;
int l_binary = 0b101;
int m_binary = 0b1_0_1;
在二进制和十六进制字面量中使用时,在 C#7.0 中不允许它们紧跟在 0x 或 0b 之后,现在使用 C#7.2 已经允许了。所以现在,以下在 C#7.2 中也是有效的数字:
//After C#7.2
byte n_hex = 0x_1_2;
byte o_hex = 0b_1_0_1;
值类型的引用语义
在此功能中进行了许多更改,但为了简洁起见,我将尝试解释一个我喜欢的有趣更改,称为 in
参数。
in
参数是对现有 ref
和 out
关键字的补充。例如,看看每个参数的作用。
ref
:ref
关键字用于将参数作为引用传递。这意味着当该参数的值在方法中更改时,它会反映在调用方法中。使用 ref
关键字传递的参数必须在调用方法中初始化,然后才能传递给被调用方法。
out
:out
关键字也用于像 ref
关键字一样传递参数,但参数可以传递而无需为其赋值。使用 out
关键字传递的参数必须在被调用方法中初始化,然后才能返回到调用方法。
现在来看看 In
参数 in:
in
关键字用于像 ref
关键字一样传递参数,并且使用 in
关键字传递的参数不能由被调用方法初始化。
所以 out
和 in
是相对的:对于 out
参数,它们是方法必需修改的,而 in
参数,它们不是方法必需修改的。
以下代码将帮助您理解 ref
、out
和 in
参数之间的区别和新的 in
参数:
class Program
{
static void Main(string[] args)
{
int a;
a = 1;
Console.WriteLine($"Inside Main-Method_Passbyval {a} before");
Method_Passbyval(a);
Console.WriteLine($"Inside Main-Method_Passbyval {a} after");
Console.WriteLine(Environment.NewLine);
a = 10;
Console.WriteLine($"Inside Main-Method_Passbyref {a} before");
Method_Passbyref(ref a);
Console.WriteLine($"Inside Main-Method_Passbyref {a} after");
Console.WriteLine(Environment.NewLine);
a = 20;
Console.WriteLine($"Inside Main-Method_out_parameter {a} before");
Method_out_parameter(out a);
Console.WriteLine($"Inside Main-Method_out_parameter {a} after");
Console.WriteLine(Environment.NewLine);
a = 30;
Console.WriteLine($"Inside Main-Method_in_parameter {a} before");
Method_in_parameter(in a);
Console.WriteLine($"Inside Main-Method_in_parameter {a} after");
Console.WriteLine(Environment.NewLine);
Console.WriteLine($"Press any key to exit.");
Console.ReadKey();
}
/// <summary>
///value will be changed in this method but caller will not get updated value for parameter(a)
/// </summary>
/// <param name="a"></param>
private static void Method_Passbyval(int a)
{
a++;
Console.WriteLine($"Inside Method_Passbyval (a++) {a}");
}
/// <summary>
/// value will be changed in this method but called will get updated value for parameter(a)
/// </summary>
/// <param name="a"></param>
private static void Method_Passbyref(ref int a)
{
a++;
Console.WriteLine($"Inside Method_Passbyref (a++) {a}");
}
/// <summary>
/// pass by ref + method it self need to initialize parameter(a)
/// </summary>
/// <param name="a"></param>
private static void Method_out_parameter(out int a)
{
a = 100; // need to initialize before use
a++;
Console.WriteLine($"Inside Method_out_parameter (a=100 & a++) {a}");
}
/// <summary>
/// pass by ref + method cannot change parameter(a)
/// </summary>
/// <param name="a"></param>
private static void Method_in_parameter(in int a)
{
//a++; // modification not allowed
Console.WriteLine($"Inside Method_in_parameter (no change in a) {a}");
}
}
上面的代码将显示如下输出。输出清楚地区分了 ref
、out
和 in
关键字。
请注意:此部分还有更多有趣的更改,但我留给您进一步探索。
C# 8 预览
以下是一些正在讨论的 C#8.0 即将发布版本的功能,其中一些功能只有原型设计。但我猜我们都很期待查看这些功能。
- 可空引用类型
- 异步流
- 默认接口实现
- 万物皆可扩展
希望您喜欢这次阅读。
编程愉快!