Skip to content

C# 7.0 中的新增功能

🏷️ C# 新增功能

C# 7.0 向 C# 语言添加了许多新功能,官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。

out 变量

支持 out 参数 的现有语法已在此版本中得到改进。现在可以在方法调用的参数列表中声明 out 变量 ,而不是编写单独的声明语句:

csharp
if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

上例中 result 变量的作用域在 if 外部,而不仅仅是 if 分支内。通过查看 IL 代码可以看的很清晰。

csharp
class Program
{
    static void Main(string[] args)
    {
        var input = Console.ReadLine();

        int result;
        if (int.TryParse(input, out result))
            Console.WriteLine(result);
        else
            Console.WriteLine("Could not parse input");
    }
}
点击查看对应的 IL 代码
csharp
.class /*02000002*/ private auto ansi beforefieldinit OutVariables.Program
       extends [System.Runtime/*23000001*/]System.Object/*0100000C*/
{
  .method /*06000001*/ private hidebysig static 
          void  Main(string[] args) cil managed
  // SIG: 00 01 01 1D 0E
  {
    .entrypoint
    // 方法在 RVA 0x2050 处开始
    // 代码大小       40 (0x28)
    .maxstack  2
    .locals /*11000001*/ init (string V_0,
             int32 V_1,
             bool V_2)
    IL_0000:  /* 00   |                  */ nop
    IL_0001:  /* 28   | (0A)00000B       */ call       string [System.Console/*23000002*/]System.Console/*0100000D*/::ReadLine() /* 0A00000B */
    IL_0006:  /* 0A   |                  */ stloc.0
    IL_0007:  /* 06   |                  */ ldloc.0
    IL_0008:  /* 12   | 01               */ ldloca.s   V_1
    IL_000a:  /* 28   | (0A)00000C       */ call       bool [System.Runtime/*23000001*/]System.Int32/*0100000E*/::TryParse(string,
                                                                                                                           int32&) /* 0A00000C */
    IL_000f:  /* 0C   |                  */ stloc.2
    IL_0010:  /* 08   |                  */ ldloc.2
    IL_0011:  /* 2C   | 09               */ brfalse.s  IL_001c

    IL_0013:  /* 07   |                  */ ldloc.1
    IL_0014:  /* 28   | (0A)00000D       */ call       void [System.Console/*23000002*/]System.Console/*0100000D*/::WriteLine(int32) /* 0A00000D */
    IL_0019:  /* 00   |                  */ nop
    IL_001a:  /* 2B   | 0B               */ br.s       IL_0027

    IL_001c:  /* 72   | (70)000001       */ ldstr      "Could not parse input" /* 70000001 */
    IL_0021:  /* 28   | (0A)00000E       */ call       void [System.Console/*23000002*/]System.Console/*0100000D*/::WriteLine(string) /* 0A00000E */
    IL_0026:  /* 00   |                  */ nop
    IL_0027:  /* 2A   |                  */ ret
  } // end of method Program::Main

  .method /*06000002*/ public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
    // 方法在 RVA 0x2084 处开始
    // 代码大小       8 (0x8)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 28   | (0A)00000F       */ call       instance void [System.Runtime/*23000001*/]System.Object/*0100000C*/::.ctor() /* 0A00000F */
    IL_0006:  /* 00   |                  */ nop
    IL_0007:  /* 2A   |                  */ ret
  } // end of method Program::.ctor

} // end of class OutVariables.Program

通过上面的 locals 指令我们看到创建了 3 个局部变量,其中 int32 V_1 就是声明的 out 变量

如果改为使用之前的单独声明变量的写法,其编译后的 IL 代码也是一样的。

csharp
class Program
{
    static void Main(string[] args)
    {
        var input = Console.ReadLine();

        int result;
        if (int.TryParse(input, out result))
            Console.WriteLine(result);
        else
            Console.WriteLine("Could not parse input");
    }
}

为清晰明了,可能需指定 out 变量的类型,如上所示。但是,该语言 支持使用隐式类型的局部变量

csharp
if (int.TryParse(input, out var answer))
    Console.WriteLine(answer);
else
    Console.WriteLine("Could not parse input");
  • 代码更易于阅读。

    • 在使用 out 变量的地方声明 out 变量,而不是在上面的另一行。
  • 无需分配初始值。

    • 通过在方法调用中使用 out 变量的位置声明该变量,使得在分配它之前不可能意外使用它。

元组

元组(Tuple)类型很早就有了,但在低于 7.0 的版本中,只能通过 Item1Item2 这样的属性来访问,没有实际的语义。

从 7.0 开始元组增加了语义上的支持,使其有类似于属性的效果,且仍然支持使用 Item1 这样的方式访问。

元组是包含多个字段以表示数据成员的轻量级数据结构。 这些字段没有经过验证,并且你无法定义自己的方法。

csharp
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

namedLetters 元组包含称为 AlphaBeta 的字段。这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时(也就是说没法通过反射访问这些有语义的字段)。

通过 IL 代码也可以证实这一点。虽然代码中使用 namedLetters.Alpha 的方式访问,但 IL 代码中实际访问的是 Item1Item2* 属性。

点击查看对应的 IL 代码
csharp
.method /*06000001*/ private hidebysig static 
        void  Main(string[] args) cil managed
// SIG: 00 01 01 1D 0E
{
    .entrypoint
    // 方法在 RVA 0x2050 处开始
    // 代码大小       46 (0x2e)
    .maxstack  3
    .locals /*11000001*/ init (valuetype [System.Runtime/*23000001*/]System.ValueTuple`2/*0100000D*/<string,string> V_0)
    IL_0000:  /* 00   |                  */ nop
    IL_0001:  /* 72   | (70)000001       */ ldstr      "a" /* 70000001 */
    IL_0006:  /* 72   | (70)000005       */ ldstr      "b" /* 70000005 */
    IL_000b:  /* 73   | (0A)00000B       */ newobj     instance void valuetype [System.Runtime/*23000001*/]System.ValueTuple`2/*0100000D*/<string,string>/*1B000001*/::.ctor(!0,
                                                                                                                                                                                !1) /* 0A00000B */
    IL_0010:  /* 0A   |                  */ stloc.0
    IL_0011:  /* 06   |                  */ ldloc.0
    IL_0012:  /* 7B   | (0A)00000C       */ ldfld      !0 valuetype [System.Runtime/*23000001*/]System.ValueTuple`2/*0100000D*/<string,string>/*1B000001*/::Item1 /* 0A00000C */
    IL_0017:  /* 72   | (70)000009       */ ldstr      ", " /* 70000009 */
    IL_001c:  /* 06   |                  */ ldloc.0
    IL_001d:  /* 7B   | (0A)00000D       */ ldfld      !1 valuetype [System.Runtime/*23000001*/]System.ValueTuple`2/*0100000D*/<string,string>/*1B000001*/::Item2 /* 0A00000D */
    IL_0022:  /* 28   | (0A)00000E       */ call       string [System.Runtime/*23000001*/]System.String/*0100000E*/::Concat(string,
                                                                                                                            string,
                                                                                                                            string) /* 0A00000E */
    IL_0027:  /* 28   | (0A)00000F       */ call       void [System.Console/*23000002*/]System.Console/*0100000F*/::WriteLine(string) /* 0A00000F */
    IL_002c:  /* 00   |                  */ nop
    IL_002d:  /* 2A   |                  */ ret
} // end of method Program::Main

在进行元组赋值时,还可以指定赋值右侧的字段的名称:

csharp
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

在某些时候,你可能想要解包从方法返回的元组的成员。可通过为元组中的每个值声明单独的变量来实现此目的。这种解包操作称为 解构元组

csharp
(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

第一次接触到 解构 这个概念是在阅读 《深入理解 ES6》 这本书,讲得是 JavaScript 中的解构功能。有兴趣的可以看一下 这里

C# 中的解构功能和 JavaScript 比起来少了很多,默认仅 元组 支持解构,且仅支持 根据位置解构

csharp
var numbers = (MaxValue: 10, MinValue: 1);
(int max, int min) = numbers;
Console.WriteLine(max); // output: 10
Console.WriteLine(min); // output: 1

其它类型若要实现解构需要编写 Deconstruct 方法。 Deconstruct 方法为你要提取的每个属性提供一组 out 参数

考虑提供析构函数方法的此 Point 类,该方法提取 XY 坐标:

csharp
public class Point
{
    public Point(double x, double y) 
        => (X, Y) = (x, y);

    public double X { get; }
    public double Y { get; }

    public void Deconstruct(out double x, out double y) =>
        (x, y) = (X, Y);
}

可以通过向元组分配 Point 来提取各个字段:

csharp
var p = new Point(3.14, 2.71);
(double X, double Y) = p;

另外还支持通过扩展方法添加 Deconstruct 方法。

csharp
public static class Extensions
{
    public static void Deconstruct(this Point p, out double x, out double y)=>
        (x, y) = (p.X, p.Y);
}

弃元

通常,在进行元组解构或使用 out 参数调用方法时,必须定义一个其值无关紧要且你不打算使用的变量。为处理此情况,C# 增添了对弃元的支持。

弃元是一个名为 _(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。

在以下方案中支持弃元:

  • 在对元组或用户定义的类型进行解构时。

  • 在使用 out 参数调用方法时。

  • 在使用 isswitch 语句匹配操作的模式中。

  • 在要将某赋值的值显式标识为弃元时用作独立标识符。

元组和对象解构中使用弃元

以下示例定义了 QueryCityDataForYears 方法,它返回一个包含两个不同年份的城市数据的六元组。本例中,方法调用仅与此方法返回的两个人口值相关,因此在进行元组解构时,将元组中的其余值视为弃元。

csharp
static void Main(string[] args)
{
    var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

    Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    // Output: Population change, 1960 to 2010: 393,149
}

private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
    int population1 = 0, population2 = 0;
    double area = 0;

    if (name == "New York City")
    {
        area = 468.48;
        if (year1 == 1960)
        {
            population1 = 7781984;
        }
        if (year2 == 2010)
        {
            population2 = 8175133;
        }
        return (name, area, year1, population1, year2, population2);
    }

    return ("", 0, 0, 0, 0, 0);
}

switchis 的模式匹配中使用弃元

以下示例使用 is 语句来确定对象是否提供 IFormatProvider 实现并测试对象是否为 null,最后使用 弃元 来处理任何其他类型的非 null 对象。
(关于 模式匹配 后面会讲)

csharp
switch (obj)
{
    case IFormatProvider fmt:
        Console.WriteLine($"{fmt} object");
        break;
    case null:
        Console.Write("A null object reference: ");
        Console.WriteLine("Its use could result in a NullReferenceException");
        break;
    case object _:
        Console.WriteLine("Some object type without format information");
    break;
}

out 参数中使用弃元

csharp
if (DateTime.TryParse(dateString, out _)) 
    Console.WriteLine($"'{dateString}': valid");
else
    Console.WriteLine($"'{dateString}': invalid");

独立弃元

可使用独立弃元来指示要忽略的任何变量。以下示例使用独立占位符来忽略异步操作返回的 Task 对象。这一操作的效果等同于抑制操作即将完成时所引发的异常。

csharp
using System;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        ExecuteAsyncMethods().Wait();
    }

    private static async Task ExecuteAsyncMethods()
    {
        Console.WriteLine("About to launch a task...");
        _ = Task.Run(() => {
            var iterations = 0;
            for (int ctr = 0; ctr < int.MaxValue; ctr++)
                iterations++;
            Console.WriteLine("Completed looping operation...");
            throw new InvalidOperationException();
        });
        await Task.Delay(5000);
        Console.WriteLine("Exiting after 5 second delay");
    }
}
// The example displays output like the following:
//       About to launch a task...
//       Completed looping operation...
//       Exiting after 5 second delay

对于上面 这一操作的效果等同于抑制操作即将完成时所引发的异常。 可能不太理解,我们改为使用 await 的方式调用 Task.Run() 方法。

其输出大概是这样的:

txt
About to launch a task...
Completed looping operation...

Unhandled Exception: System.AggregateException: One or more errors occurred. (Operation is not valid due to the current state of the object.) ---> System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at StandaloneDiscard.Program.<>c.<ExecuteAsyncMethods>b__1_0() in C:\Users\liujiajia\source\repos\CSharp70\StandaloneDiscard\Program.cs:line 35
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
--- End of stack trace from previous location where exception was thrown ---
   at StandaloneDiscard.Program.ExecuteAsyncMethods() in C:\Users\liujiajia\source\repos\CSharp70\StandaloneDiscard\Program.cs:line 30
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.Wait()
   at StandaloneDiscard.Program.Main() in C:\Users\liujiajia\source\repos\CSharp70\StandaloneDiscard\Program.cs:line 10

可以看出最后一条消息(Exiting after 5 second delay)并没有被打印出来。

注意: _ 也是有效标识符

当在支持的上下文之外使用时,_ 不视为占位符,而视为有效变量。如果名为 _ 的标识符已在范围内,则使用 _ 作为独立占位符可能导致:

  • 将预期的占位符的值赋给范围内 _ 变量,会导致该变量的值被意外修改。例如:

    csharp
    private static void ShowValue(int _)
    {
        byte[] arr = { 0, 0, 1, 2 };
        _ = BitConverter.ToInt32(arr, 0);
        Console.WriteLine(_);
    }
    // The example displays the following output:
    //       33619968
  • 因违反类型安全而发生的编译器错误。例如:

    csharp
    private static bool RoundTrips(int _)
    {
        string value = _.ToString();
        int newValue = 0;
        _ = Int32.TryParse(value, out newValue);
        return _ == newValue;
    }
    // The example displays the following compiler error:
    //      error CS0029: Cannot implicitly convert type 'bool' to 'int'
  • 编译器错误 CS0136:“无法在此范围中声明名为“_”的局部变量或参数,因为该名称用于在封闭的局部范围中定义局部变量或参数”。例如:

    csharp
    public void DoSomething(int _)
    {
        var _ = GetValue(); // Error: cannot declare local _ when one is already in scope
    }
    // The example displays the following compiler error:
    // error CS0136:
    //       A local or parameter named '_' cannot be declared in this scope
    //       because that name is used in an enclosing local scope
    //       to define a local or parameter

模式匹配

模式匹配 是一种可让你对除对象类型以外的属性实现方法分派的功能。

模式匹配支持 is 表达式和 switch 表达式。每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。使用 when 关键字来指定模式的其他规则。

is

is 模式表达式扩展了常用 is 运算符以查询关于其类型的对象,并在一条指令分配结果。以下代码检查变量是否为 int,如果是,则将其添加到当前总和:

csharp
if (input is int count)
    sum += count;

switch

switch 匹配表达式具有常见的语法,它基于已包含在 C# 语言中的 switch 语句。更新后的 switch 语句有几个新构造:

  • switch 表达式的控制类型不再局限于整数类型、Enum 类型、string 或与这些类型之一对应的可为 null 的类型。可能会使用任何类型。

  • 可以在每个 case 标签中测试 switch 表达式的类型。与 is 表达式一样,可以为该类型指定一个新变量。

  • 可以添加 when 子句以进一步测试该变量的条件。

  • case 标签的顺序现在很重要。执行匹配的第一个分支;其他将跳过。

以下代码演示了这些新功能:

csharp
public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
    int sum = 0;
    foreach (var i in sequence)
    {
        switch (i)
        {
            case 0:
                break;
            case IEnumerable<int> childSequence:
            {
                foreach(var item in childSequence)
                    sum += (item > 0) ? item : 0;
                break;
            }
            case int n when n > 0:
                sum += n;
                break;
            case null:
                throw new NullReferenceException("Null found in sequence");
            default:
                throw new InvalidOperationException("Unrecognized type");
        }
    }
    return sum;
}
  • case 0 : 是常见的 常量模式

  • case IEnumerable<int> childSequence : 是一种 类型模式

  • case int n when n > 0: 是 具有附加 when 条件的类型模式

  • case null: 是 null 模式

  • default: 是常见的 默认事例

case 表达式中的 var 声明

引入 var 作为一种匹配表达式也为模式匹配引入了新规则。

  1. 第一条规则是 var 声明遵循正常的类型推理规则:推理出类型是 switch 表达式的静态类型。根据此规则,类型始终匹配

  2. 第二个规则是, var 声明没有其他类型模式表达式中包含的 null 检查。也就是说,变量可为 NULL ,只有在这种情况下,才必须执行 NULL 检查。

    这两个规则表示,在许多情况下, case 表达式中的 var 声明匹配与 default 表达式相同的条件。因为任何非默认事例都优先于 default 事例,所以永远不会执行 default 事例。

    备注

    如果 default 事例已编写但永远不会执行,编译器也不会发出警告。这与在已列出所有可能事例的情况下的当前 switch 语句行为一致。

  3. 第三个规则引入了 var 事例可能适用的用途。假设要进行模式匹配,其中输入是字符串,且要搜索已知命令值。

可以编写如下代码:

csharp
static object CreateShape(string shapeDescription)
{
    switch (shapeDescription)
    {
        case "circle":
            return new Circle(2);

        case "square":
            return new Square(4);

        case "large-circle":
            return new Circle(12);

        case var o when (o?.Trim().Length ?? 0) == 0:
            // white space
            return null;
        default:
            return "invalid shape description";
    }
}

case var o when (o?.Trim().Length ?? 0) == 0: 匹配 null空字符串任何仅包含空白符的字符串

请注意,为了确保不会意外抛出 NullReferenceException ,上面的代码使用 ?. 运算符。 default case 处理此命令分析程序不理解的其他任何字符串值。

Ref 局部变量和返回结果

此功能允许使用并返回对变量的引用的算法,这些变量在其他位置定义。

一个示例是使用大型矩阵并查找具有某些特征的单个位置。下面的方法在矩阵中向该存储返回“引用” :

csharp
public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

可以将返回值声明为 ref 并在矩阵中修改该值,如以下代码所示:

csharp
ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

C# 语言还有多个规则,可保护你免于误用 ref 局部变量和返回结果:

  • 必须将 ref 关键字添加到方法签名和方法中的所有 return 语句中。

    • 这清楚地表明,整个方法返回的是引用。
  • 可以将 ref return 分配给 值变量 或 ref 变量。

    • 调用方控制是否复制返回值。 在分配返回值时省略 ref 修饰符表示调用方需要该值的副本,而不是对存储的引用。

    关于这个规则,引用类型的表现需要额外注意一下。简单来说其机制类似于按引用传递参数。使用 ref 修饰符时,相当于传递 ref 参数时的效果;省略 ref 修饰符时,相当于传递普通参数时的效果。

    分配返回值时若省略 ref 修饰符则表示复制返回值的引用类型的地址并将其赋值给接收的变量(相当于创建了一个新的指针,指向的是同一个地址)。此时将变量赋值为 null,只是将删除了新指针,原本的指针仍然指向原来的位置。

    分配返回值时若使用了 ref 修饰符则相当于返回的就是同一个指针,删除了该指针也等同于删除了原本的指针。

    csharp
    using System;
    
    namespace RefLocalsAndReturns
    {
        class Program
        {
            public static Staff[] Staffs = new Staff[] {
                new Staff()
                {
                    FirstName = "佳佳",
                    LastName = "刘",
                    Blog = "liujiajia.me",
                },
        };
    
            static void Main(string[] args)
            {
                var notRefStaff = FindByIndex(0);
                Console.WriteLine($"notRefStaff = {notRefStaff}"); // Print: notRefStaff = 刘佳佳的博客 | liujiajia.me
                notRefStaff = null;
                Console.WriteLine($"set notRefStaff to null.");
                Console.WriteLine($"notRefStaff = {notRefStaff}"); // Print: notRefStaff =
                Console.WriteLine($"Staffs[0] = {Staffs[0]}"); // Print: Staffs[0] = 刘佳佳的博客 | liujiajia.me
    
                Console.WriteLine();
    
                ref var refStaff = ref FindByIndex(0);
                Console.WriteLine($"refStaff = {refStaff}"); // Print: refStaff = 刘佳佳的博客 | liujiajia.me
                refStaff = null;
                Console.WriteLine($"set refStaff to null.");
                Console.WriteLine($"refStaff = {refStaff}"); // Print: refStaff =
                Console.WriteLine($"Staffs[0] = {Staffs[0]}"); // Print: Staffs[0] =
            }
    
            public static ref Staff FindByIndex(int index)
            {
                return ref Staffs[index];
            }
        }
    
        class Staff
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public string Blog { get; set; }
            public override string ToString() => $"{LastName}{FirstName}的博客 | {Blog}";
        }
    }
  • 不可向 ref 本地变量赋予标准方法返回值。

    • 这将禁止类似 ref int i = sequence.Count(); 这样的语句。

      LINQ 的查询结果(First() 等)都是不允许的。

  • 不能将 ref 返回给其生存期不超出方法执行的变量。

    • 这意味着不可返回对本地变量或对类似作用域变量的引用。
  • ref 局部变量和返回结果不可用于异步方法。

    • 编译器无法知道异步方法返回时,引用的变量是否已设置为其最终值。

添加 ref 局部变量和 ref 返回结果可通过避免复制值或多次执行取消引用操作,使算法更为高效。

本地函数(嵌套函数)

许多类的设计都包括仅从一个位置调用的方法。这些额外的私有方法使每个方法保持小且集中。 本地函数使你能够在另一个方法的上下文内声明方法。本地函数使得类的阅读者更容易看到本地方法仅从声明它的上下文中调用。

对于本地函数有两个常见的用例: 公共迭代器方法公共异步方法 。这两种类型的方法都生成报告错误的时间晚于程序员期望时间的代码。在迭代器方法中,只有在调用枚举返回的序列的代码时才会观察到任何异常。在异步方法中,只有当返回的 Task 处于等待状态时才会观察到任何异常。以下示例演示如何使用本地函数将参数验证与迭代器实现分离:

csharp
public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");

    return alphabetSubsetImplementation();

    IEnumerable<char> alphabetSubsetImplementation()
    {
        for (var c = start; c <= end; c++)
            yield return c;
    }
}
  • 本地函数 仅在定义它的方法体中可见。上例中的 alphabetSubsetImplementation 方法只能在 AlphabetSubset3 内调用,对外是不可见的。

  • 本地函数 中可以访问外部方法上下文中的变量(如上例 alphabetSubsetImplementation 方法中直接使用 startend 变量),但 本地函数 中定义的变量只能在 本地函数 中访问。

  • 本地函数 可以定义在方法内的任何位置。一般放在方法的最后 或 return 的后面。

本地函数 的好处是显而易见的,保持每个方法小的同时,又可以不必向类中添加过多方法。之前方法中处理较多时一般采用两种方法:1. 一个功能创建一个 private 方法然后调用;2. 使用 #region 将处理按功能包起来。第一种方法会导致父类中创建了大量的私有方法,在类视图中看起来会比较混乱,第二种方法只是查看起来方便些。本地函数 就像是上述两种方法的结合,但避免了各自的缺点。

可以对 async 方法采用相同的技术,以确保在异步工作开始之前引发由参数验证引起的异常:

csharp
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

备注

本地函数 支持的某些设计也可以使用 lambda 表达式 来完成。感兴趣的人可以 阅读有关差异的详细信息

虽然本地函数对 lambda 表达式 可能有点冗余,但实际上它们的目的和用法都不一样。 如果想要编写仅从上下文或其他方法中调用的函数,则使用 本地函数 更高效。

更多的 expression-bodied 成员

C# 6 为 成员函数只读属性 引入了 expression-bodied 成员。C# 7.0 扩展了可作为表达式实现的允许的成员。在 C# 7.0 中,你可以在 属性 和 索引器 上实现 构造函数(constructor)终结器(finalizer) 以及 get 和 set 访问器。以下代码演示了每种情况的示例:

csharp
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

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

备注

本示例不需要终结器,但显示它是为了演示语法。不应在类中实现终结器,除非有必要发布非托管资源。还应考虑使用 SafeHandle 类,而不是直接管理非托管资源。

这些 expression-bodied 成员的新位置代表了 C# 语言的一个重要里程碑:这些功能由致力于开发开放源代码 Roslyn 项目的社区成员实现。

引发表达式

在 C# 中,throw 始终是一个语句。因为 throw 是一个语句而非表达式,所以在某些 C# 构造中无法使用它。它们包括 条件表达式null 合并表达式一些 lambda 表达式。添加 expression-bodied 成员将添加更多位置,在这些位置中,throw 表达式会很有用。为了可以编写这些构造,C# 7.0 引入了 throw 表达式

这使得编写更多基于表达式的代码变得更容易。不需要其他语句来进行错误检查。

  • 条件运算符

    下例使用 throw 表达式在向方法传递空字符串数组时引发 ArgumentException。在 C# 7.0 之前,此逻辑将需要显示在 if/else 语句中。

    csharp
    private static void DisplayFirstNumber(string[] args)
    {
        string arg = args.Length >= 1 ? args[0] : 
                                    throw new ArgumentException("You must supply an argument");
        if (Int64.TryParse(arg, out var number))
            Console.WriteLine($"You entered {number:F0}");
        else
            Console.WriteLine($"{arg} is not a number.");                            
    }
  • null 合并运算符

    在以下示例中,如果分配给 Name 属性的字符串为 null,则将 throw 表达式与 null 合并运算符结合使用以引发异常。

    csharp
    public string Name
    {
        get => name;
        set => name = value ?? 
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
  • expression-bodied lambda 或方法

    下例说明了 expression-bodied 方法,由于不支持对 DateTime 值的转换,该方法引发 InvalidCastException

    csharp
    DateTime ToDateTime(IFormatProvider provider) => 
        throw new InvalidCastException("Conversion to a DateTime is not supported.");

通用的异步返回类型

从异步方法返回 Task 对象可能在某些路径中导致性能瓶颈。 Task 是引用类型,因此使用它意味着分配对象。如果使用 async 修饰符声明的方法返回缓存结果或以同步方式完成,那么额外的分配在代码的性能关键部分可能要耗费相当长的时间。如果这些分配发生在紧凑循环中,则成本会变高。

新语言功能意味着异步方法返回类型不限于 TaskTask<T>void。返回类型必须仍满足异步模式,这意味着 GetAwaiter 方法必须是可访问的。作为一个具体示例,已将 ValueTask 类型添加到 .NET framework 中,以使用这一新语言功能:

csharp
public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

备注

需要添加 NuGetSystem.Threading.Tasks.Extensions 才能使用 ValueTask<TResult> 类型。

此增强功能对于库作者最有用,可避免在性能关键型代码中分配 Task

关于 ValueTask<TResult> 的详细说明参阅 这篇官方的博客 (cnblogs 上有篇 中文翻译版,机翻的感觉有点重,再加上有些虽然字面意思知道,但是不太理解,导致看的迷迷糊糊)。

总的来说:需要使用异步方法时,默认的选择仍然还是 Task / Task<TResult>

数字文本语法改进

误读的数值常量可能使第一次阅读代码时更难理解。 位掩码 或 其他符号值 容易产生误解。C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本数字分隔符

在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:

csharp
public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

常量开头的 0b 表示该数字以二进制数形式写入。 二进制数可能会很长,因此通过引入 _ 作为 数字分隔符 通常更易于查看位模式,如上面二进制常量所示。 数字分隔符可以出现在常量的任何位置。

对于十进制数字,通常将其用作千位分隔符:

csharp
public const long BillionsAndBillions = 100_000_000_000;

数字分隔符也可以与 decimalfloatdouble 类型一起使用:

csharp
public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

综观来说,你可以声明可读性更强的数值常量。