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

C#7.1 & C#7.2 新特性

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.35/5 (14投票s)

2018年2月3日

CPOL

8分钟阅读

viewsIcon

20320

downloadIcon

154

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

  1. 首先,在解决方案资源管理器中右键单击项目节点 => 选择属性。这将打开项目属性。

  2. 选择生成选项卡 => 选择高级按钮。这将打开高级生成设置窗口。

  3. 语言版本下拉列表中,选择 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 之前,元组是可用的,但没有语言支持,因此效率不高。此外,元组元素通过 item1item2 等方式引用,这使得很难理解它们引用的内容。在 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 的一些新特性...

私有受保护访问修饰符

到目前为止,我们有 PrivatePublicProtectedInternalInternal 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 protectedprivate protected 之间的区别感到困惑。这是区别(记住粗体行):

protected internal 修饰符表示 protectedinternal,即 - 类成员可以被派生自某个类的子类(直接子类)访问,也可以被当前程序集中的任何类访问,即使该类不是类 A 的子类(因此“protected”所暗示的限制被放宽了)。

private protected 表示 protectedinternal。即 - 成员只能被同一程序集中的子类访问,但不能被程序集外的子类访问(因此“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 参数是对现有 refout 关键字的补充。例如,看看每个参数的作用。

refref 关键字用于将参数作为引用传递。这意味着当该参数的值在方法中更改时,它会反映在调用方法中。使用 ref 关键字传递的参数必须在调用方法中初始化,然后才能传递给被调用方法。

outout 关键字也用于像 ref 关键字一样传递参数,但参数可以传递而无需为其赋值。使用 out 关键字传递的参数必须在被调用方法中初始化,然后才能返回到调用方法。

现在来看看 In 参数 in: in 关键字用于像 ref 关键字一样传递参数,并且使用 in 关键字传递的参数不能由被调用方法初始化。

所以 outin 是相对的:对于 out 参数,它们是方法必需修改的,而 in 参数,它们不是方法必需修改的

以下代码将帮助您理解 refoutin 参数之间的区别和新的 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}");
        }
    }    

上面的代码将显示如下输出。输出清楚地区分了 refoutin 关键字。

请注意:此部分还有更多有趣的更改,但我留给您进一步探索。

C# 8 预览

以下是一些正在讨论的 C#8.0 即将发布版本的功能,其中一些功能只有原型设计。但我猜我们都很期待查看这些功能。

  • 可空引用类型
  • 异步流
  • 默认接口实现
  • 万物皆可扩展

希望您喜欢这次阅读。

编程愉快!

© . All rights reserved.