C# 7.0 添加和增强的功能【基础篇】

乎语百科 205 0

C# 7.0 版是与 Visual Studio 2017 一起发布。 虽然该版本继承和发展了C# 6.0,但不包含编译器即服务。

一、out 变量

以前我们使用out变量必须在使用前进行声明,C# 7.0给我们提供了一种更简洁的语法 “使用时进行内联声明” 。如下所示:

  var input = ReadLine();
  if (int.TryParse(input, out var num))//可直接写明变量的类型也可以写隐式类型,因为out关键字修饰的一定是局部变量
  {
      WriteLine("您输入的数字是:{0}",num);
  }

所谓的 “内联声明” 编译后就是以前的原始写法,只是现在由编译器来完成。即编译后为:

  int num;//提前声明
  string s = Console.ReadLine();
  if (int.TryParse(s, out num))
  {
      Console.WriteLine("您输入的数字是:{0}", num);
  }

二、值元组(ValueTuple)

使用元组语法,可以在一个语句中分配多个不同类型的变量。以下是几种常用的写法:

//声明命名项元组并为其分配元组值,再按名称访问元组项
(string Name, string Capital, double GdpPerCapita) countryInfo = ("Malawi", "Lilongwe", 226.50);
System.Console.WriteLine($@"The poorest country in the world in 2017 was {countryInfo.Name}, {countryInfo.Capital}: {countryInfo.GdpPerCapita}");
//将元组分配到各个已声明且使用分布式语法进行隐式类型化的变量中
var (country, capital, gdpPerCapita) = ("Malawi", "Lilongwe", 226.50);
System.Console.WriteLine($@"The poorest country in the world in 2017 was {country}, {capital}: {gdpPerCapita}");
//将命名项元组分配到一个隐式类型化变量中,再按名称访问元组项
var countryInfo = (Name:"Malawi", Capital:"Lilongwe", GdpPerCapita:226.50);
System.Console.WriteLine($@"The poorest country in the world in 2017 was {countryInfo.Name}, {countryInfo.Capital}: {countryInfo.GdpPerCapita}");
//将未命名的元组分配到一个隐式类型化变量中,再按 Item-number 属性访问元组元素
var countryInfo = ("Malawi", "Lilongwe", 226.50);
System.Console.WriteLine($@"The poorest country in the world in 2017 was {countryInfo.Item1}, {countryInfo.Item2}: {countryInfo.Item3}");
//使用下划线放弃相应的元组部分
(string name, _, double gdpPerCapita) countryInfo = ("Malawi", "Lilongwe", 226.50);

元组是用于将数据封装到一个对象的轻型解决方案,就像用袋子将从商店购买的杂项物品装在一起一样。

与数组不同,元组包含的项数据类型可以不同,几乎不受任何限制(尽管不允许使用指针),但需要由代码标识,且不能在运行时更改,这两点限制除外。

此外,不同于数组的是,编译时也会对元组内的项数量进行硬编码。

另外,元组(Tuple)在 .Net 4.0 的时候就有了,值元组(ValueTuple)的出现,弥补了老版的两个缺点:

  1)ValueTuple 支持语义上的字段命名。舍弃仅按照顺序取值(Item1,Item2...),增强了可读性。

  2)ValueTuple 是值类型(Struct)。舍弃原来的引用类型,更为轻量化。

三、析构元组和其他类型

C# 7.0 及更高版本中,用户可从元组中检索多个元素,或者在单个析构操作中从对象检索多个字段值、属性值和计算值。 若要析构元组,可将其元素分配给各个变量。 析构对象时,将选定值分配给各个变量。

原来的做法是:一旦检索到元组,就必须处理它的各个元素。 按元素逐个操作比较麻烦,如下例所示。 QueryCityData 方法返回一个三元组,并通过单独的操作将其每个元素分配给一个变量。

public class Example
{
    public static void Main()
    {
        var result = QueryCityData("New York City"); // 返回全部字段
        var city = result.Item1; // 再逐个赋值
        var pop = result.Item2;
        var size = result.Item3;
    }
    private static (string, int, double) QueryCityData(string name)
    {
        if (name == "New York City")
            return (name, 8175133, 468.48);
        return ("", 0, 0);
    }
}

用户作为类、结构或接口的创建者,可通过实现一个或多个 Deconstruct方法来析构该类型的实例。 该方法返回 void,且要析构的每个值由方法签名中的 out 参数指示。

新写法示例:重载 Deconstruct 方法以返回 Person 对象的各种属性组合。

using System;

public class Person
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    //构造函数,初始化赋值
    public Person(string fname, string mname, string lname, string cityName, string stateName)
    {
        FirstName = fname;
        MiddleName = mname;
        LastName = lname;
        City = cityName;
        State = stateName;
    }

    // 返回名字和姓氏
    public void Deconstruct(out string fname, out string lname)
    {
        fname = FirstName;
        lname = LastName;
    }
    // 返回名字、中间名和姓氏
    public void Deconstruct(out string fname, out string mname, out string lname)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
    }
    // 返回名字、姓氏、城市、州
    public void Deconstruct(out string fname, out string lname, out string city, out string state)
    {
        fname = FirstName;
        lname = LastName;
        city = City;
        state = State;
    }
}

public class ExampleClassDeconstruction
{
    public static void Main()
    {
        var p = new Person("John", "Quincy", "Adams", "Boston", "MA");//初始化
        // Deconstruct the person object.
        var (fName, lName, city, state) = p;//自动调用对应的析构方法
        Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
    }
}
// The example displays the following output:
//    Hello John Adams of Boston, MA!

具有相同数量参数的多个 Deconstruct 方法是不明确的。 在定义 Deconstruct 方法时,必须小心使用不同数量的参数。 在重载解析过程中,不能区分具有相同数量参数的 Deconstruct 方法。

四、析构函数

区别于构造函数,构造函数又叫构造方法,它是一种特殊的成员函数,它主要用于为对象分配存储空间,对数据成员进行初始化,也就是就是对类进行初始化。

析构函数是实现销毁一个类的实例的方法成员。析构函数不能有参数,不能任何修饰符而且不能被调用。

由于析构函数的目的与构造函数的相反,就以相同的名字然后加前缀 ~ 以示区别。语法如下:

public class ResourceHolder
{
    ~ResourceHolder()
    {
        // 这里是清理非托管资源的用户代码段
    }
}

虽然 C#(更确切的说是 CLR)提供了一种新的内存管理机制——自动内存管理机制,资源的释放是可以通过“垃圾回收器” 自动完成的,一般不需要用户干预,但在有些特殊情况下还是需要用到析构函数的,如在 C# 中非托管资源的释放。

构造函数与析构函数虽然是一个类中形式上较简单的函数,但它们的使用决非看上去那么简单,因此灵活而正确的使用构造函数与析构函数能够更好的管理系统中的资源。

五、模式匹配

“模式匹配”是一种测试表达式是否具有特定特征的方法。 C# 模式匹配提供更简洁的语法,用于测试表达式并在表达式匹配时采取措施。

is表达式”目前支持通过模式匹配测试表达式并有条件地声明该表达式结果。

switch表达式”允许你根据表达式的首次匹配模式执行操作。 这两个表达式支持丰富的模式词汇。

// is 声明模式,用于 测试变量类型 并将其分配给 新变量
int? maybe = 12;
if (maybe is int number)
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
else
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
// is 常数模式,将变量与 null 进行比较,not 为一种逻辑模式,在否定模式不匹配时与该模式匹配
string? message = "This is not the null string";

if (message is not null)
{
    Console.WriteLine(message);
}
// is 测试变量是否与给定类型匹配
public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
        return list[list.Count / 2];
    else if (sequence is null)
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}
// switch 测试变量(枚举、常量...)找到特定值的匹配项,如下是通过枚举类型
public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),//最终 _ 案例为与所有数值匹配的弃元模式
   };
// switch 使用关系模式测试如何将数值与常量进行比较
string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",//最终 _ 案例为与所有数值匹配的弃元模式
};
// switch 检查一个对象的多个属性的模式
public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        // ( > 10,  > 1000.00m) => 0.10m, // 等效 写法
        // 如果 Order 类型定义了适当的 Deconstruct 方法,则可以省略模式的属性名称,并使用析构检查属性
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };
// is 使用列表模式检查列表或数组中的元素
public void MatchElements(int[] array)
{
    if (array is [0,1]) // 属于二进制数字
        Console.WriteLine("Binary Digits");
    else if (array is [1,1,2,3,5,8, ..])// 属于斐波那契数列
        Console.WriteLine("array looks like a Fibonacci sequence");
    else //数列无法识别
        Console.WriteLine("Array shape not recognized");
}

六、本地函数

本地函数是一种嵌套在另一成员中的类型的私有方法。 仅能从其包含成员中调用它们。

可以声明和调用本地函数的地方:

  方法(尤其是迭代器方法和异步方法)、构造函数、属性访问器、事件访问器、匿名方法、Lambda 表达式、终结器、其他本地函数等。

以下示例定义了一个名为 AppendPathSeparator 的本地函数,该函数对于名为 GetText 的方法是私有的:

private static string GetText(string path, string filename)
{
     var reader = File.OpenText($"{AppendPathSeparator(path)}{filename}");
     var text = reader.ReadToEnd();
     return text;
     string AppendPathSeparator(string filepath)
     {
        return filepath.EndsWith(@"\") ? filepath : filepath + @"\";
     }
}

本地函数的一个实用功能是可以允许立即显示异常。

对于方法迭代器,仅在枚举返回的序列时才显示异常,而非在检索迭代器时。

对于异步方法,在等待返回的任务时,将观察到异步方法中引发的任何异常。

本地函数和lambda表达式是十分相似的,但两者中选用一种的时机和条件其实是存在差别。

// 本地函数
public static int LocalFunctionFactorial(int n)
{
    return nthFactorial(n);
    int nthFactorial(int number) => number < 2
        ? 1
        : number * nthFactorial(number - 1);
}
// Lambda 表达式
public static int LambdaFactorial(int n)
{
    Func<int, int> nthFactorial = default(Func<int, int>);
    nthFactorial = number => number < 2
        ? 1
        : number * nthFactorial(number - 1);
    return nthFactorial(n);
}

本地函数的命名方式与方法相同。 Lambda表达式是一种匿名方法,需要分配给 delegate 类型的变量,通常是 Action 或 Func 类型。 声明本地函数时,此过程类似于编写普通方法;声明一个返回类型和一个函数签名。

Lambda表达式在声明时转换为委托。 本地函数更加灵活,可以像传统方法一样编写,也可以作为委托编写。 只有在用作委托时,本地函数才转换为委托。如果声明了本地函数,但只是通过像调用方法一样调用该函数来引用该函数,它将不会转换成委托。

本地函数可以避免Lambda表达式始终需要的堆分配。 如果本地函数永远不会转换为委托,并且本地函数捕获的变量都不会被其他转换为委托的Lambda或本地函数捕获,则编译器可以避免堆分配。

yield 关键字的用法:

// 可将本地函数作为迭代器实现,使用 yield return 语法生成一系列值
public IEnumerable<string> SequenceToLowercase(IEnumerable<string> input)
{
    if (!input.Any())
        throw new ArgumentException("There are no items to convert to lowercase.");
    return LowercaseIterator();
    IEnumerable<string> LowercaseIterator()
    {
        foreach (var output in input.Select(item => item.ToLower()))
            yield return output;
    }
}

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

七、扩展 Expression bodied 成员

在C# 6.0中已经新增了表达式主体定义,而在当前版本中进行了扩展。扩展的内容包括下面几点。

可以使用表达式主体定义来实现属性 get 和 set 访问器。

public class Location
{
   private string locationName;
   public Location(string name) => Name = name;
   public string Name
   {
      get => locationName;
      set => locationName = value;
   }
}

构造函数的表达式主体定义通常包含单个赋值表达式或一个方法调用,该方法调用可处理构造函数的参数,也可初始化实例状态。

// 其构造函数具有一个名为“name”的字符串参数,表达式主体定义向 Name 属性分配参数
public class Location
{
   private string locationName;
   public Location(string name) => Name = name;
   public string Name
   {
      get => locationName;
      set => locationName = value;
   }
}

终结器的表达式主体定义通常包含清理语句,例如释放非托管资源的语句。

// 定义一个终结器,该终结器使用表达式主体定义来指示已调用该终结器
public class Destroyer
{
   public override string ToString() => GetType().Name;
   ~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");
}

索引器与使用属性一样,如果 get 访问器包含返回值的单个表达式或 set 访问器执行简单的赋值,则索引器 get 和 set 访问器包含表达式主体定义。

using System;
using System.Collections.Generic;
// 其中包含一个内部 String 数组,该数组包含大量体育运动的名称
// 索引器的 get 和 set 访问器都以表达式主体定义的形式实现
public class Sports
{
   private string[] types = { "Baseball", "Basketball", "Football",
                              "Hockey", "Soccer", "Tennis",
                              "Volleyball" };
   public string this[int i]
   {
      get => types[i];
      set => types[i] = value;
   }
}

八、ref 局部变量

在变量类型之前添加 ref 关键字以声明 ref 局部变量。

public ref Person GetContactInformation(string fname, string lname)
// 对 p 进行的操作不会改变原方法的返回值
Person p = contacts.GetContactInformation("Brandie", "Best");

上面的分配将 p 声明为本地变量。 它的初始值是通过读取 GetContactInformation 返回的值进行复制。 之后对 p 的任何分配都不会更改 GetContactInformation 返回的变量值。 变量 p 不再是返回的变量的别名。

声明引用本地变量,复制原始值的别名。 在下面的分配中,p 是从 GetContactInformation 返回的变量的别名。

// 对 p 所做的更改,也会更改从 GetContactInformation 返回的变量
ref Person p = ref contacts.GetContactInformation("Brandie", "Best");

后续使用 p 等同于使用 GetContactInformation 返回的变量,因为 p 是此变量的别名。

可通过相同方式按引用访问值。 在某些情况下,按引用访问值可避免潜在的高开销复制操作,从而提高性能。

编译器对ref变量强制实施范围规则:ref类型中的ref struct局部变量、ref参数和ref字段。 这些规则可确保引用不会与引用的对象不相称。

九、引用返回

返回值可以通过引用返回 (ref 返回) 。 借助引用返回值,方法可以将对变量的引用(而不是值)返回给调用方。 然后,调用方可以选择将返回的变量视为按值返回或按引用返回。

如果声明方法返回引用返回值,表明方法返回变量别名。 这样做通常是为了让调用代码有权通过别名访问此变量(包括修改它)。 因此,方法的引用返回值不得包含返回类型 void

引用返回值是指,方法返回对某变量的引用(或别名)。

返回引用返回值的方法必须满足以下两个条件:

  1)方法签名在返回类型前面有ref关键字;

  2)方法主体中的每个 return 语句都在返回实例的名称前面有 ref 关键字。

public ref Person GetContactInformation(string fname, string lname)
{
    return ref p;
}

十、弃元

弃元是一种在应用程序代码中人为取消使用的临时虚拟变量。弃元相当于未赋值的变量;它们没有值。

弃元将意图传达给编译器和其他读取代码的文件:你打算忽略表达式的结果、元组表达式的一个或多个成员、方法的 out 参数或模式匹配表达式的目标。

弃元使代码意图更加明确。 弃元指示代码永远不会使用变量。 它们可以增强其可读性和可维护性。

通过将下划线 (_) 赋给一个变量作为其变量名,指示该变量为一个占位符变量。

// 以下方法调用返回一个元组,其中第一个值和第二个值为弃元。
// area 是以前声明的变量,设置为由 GetCityInformation 返回的第三个组件
(_, _, area) = city.GetCityInformation(cityName);

当 _ 是有效弃元时,尝试检索其值或在赋值操作中使用它时会生成编译器错误 CS0301:“当前上下文中不存在名称 "_"”。 出现此错误是因为 _ 未赋值,甚至可能未分配存储位置。 如果它是一个实际变量,则不能像之前的示例那样对多个值使用弃元。

独立弃元用来指示要忽略的任何变量。

一种典型的用法是使用赋值来确保一个参数不为 null。 下面的代码使用弃元来强制赋值。 赋值的右侧使用 Null 合并操作符,用于在参数为 null 时引发 System.ArgumentNullException。 此代码不需要赋值结果,因此将对其使用弃元。

// 使用弃元来强制赋值
// 赋值的右侧使用 Null 合并操作符,用于在参数为 null 时引发 System.ArgumentNullException
// 此代码不需要赋值结果,因此将对其使用弃元
public static void Method(string arg)
{
    _ = arg ?? throw new ArgumentNullException(paramName: nameof(arg), message: "arg can't be null");
}

十一、二进制文本

固定值称为文字。文字是变量使用的值。

在 C# 7.0 之前,有六种类型的文字可用,它们是一个整数浮点字符字符串空值布尔文字。在 C# 7.0 中,增加了一个名为进制文字的文字。

二进制文字用于将二进制值存储在变量中。二进制文字由 0b 表示。二进制文字主要用于位掩码。

// 当编译器在变量值中看到 0b 时,它会自动将这个 num 视为二进制文字
var num = 0b10001;// 当输出 num 值时,自动转为十进制:9

十二、数字分隔符

新增了数字分隔符:下划线(_),来分隔长度过长而可读性差的数字。

int bin = 0b1001_1010_0001_0100;// 二进制表示
int hex1 = 0x64_95_ED;          // 十六进制表示
int hex2 = 0x_64_95_ED;         // 十六进制表示
int dec1 = 1_000_000;           // 十进制
int dec2 = 100_0000;            // 十进制
int weird = 1_2__3___4____5_____6______7_______8________9;
double real = 1_000.111_1e-5;
decimal d = 1_222_345;
// 编译后实际运行的结果:(会自动忽略下划线)
int bin = 39444;
int hex1 = 6591981;
int hex2 = 6591981;
int dec1 = 1000000;
int dec2 = 1000000;
int weird = 123456789;
double real = 0.010001111;
decimal d = 1222345m;

下划线目的是使数字可读性增加,所以不能乱用。比如在小数和指数中使用时,不能出现在小数点(10_.0)的前后、指数字符(1.1e_1)的前后和类型说明符(10_f)的前面。

十三、引发(throw)表达式

从 C# 7.0 开始,throw 可以用作表达式或语句。 支持在上下文中引发异常。 相关方法包括:

// 条件运算符
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 合并运算符
public string Name
{
    get => name;
    set => name = value ??
        throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}
// expression-bodied lambda 或方法
// 由于不支持对 DateTime 值的转换,该方法引发 InvalidCastException
DateTime ToDateTime(IFormatProvider provider) =>
    throw new InvalidCastException("Conversion to a DateTime is not supported.");

十四、default 文本表达式

从 C# 7.1 开始,当编译器可以推断表达式类型时,可以使用 default 文本生成类型的默认值。 default 文本表达式生成与 default(T) 表达式(其中,T 是推断的类型)相同的值。

可以在以下任一情况下使用 default 文本:  对变量进行赋值或初始化时。  在声明可选方法参数的默认值时。  在方法调用中提供参数值时。  在 return 语句中或作为表达式主体成员中的表达式时。

T[] InitializeArray<T>(int length, T initialValue = default)
{
    if (length < 0)
        throw new ArgumentOutOfRangeException(nameof(length), "Array length must be nonnegative.");
    var array = new T[length];
    for (var i = 0; i < length; i++)
        array[i] = initialValue;
    return array;
}
void Display<T>(T[] values) => Console.WriteLine($"[ {string.Join(", ", values)} ]");
Display(InitializeArray<int>(3));  // output: [ 0, 0, 0 ]
Display(InitializeArray<bool>(4, default));  // output: [ False, False, False, False ]
System.Numerics.Complex fillValue = default;
Display(InitializeArray(3, fillValue));  // output: [ (0, 0), (0, 0), (0, 0) ]

标签: # c#

留言评论

  • 这篇文章还没有收到评论,赶紧来抢沙发吧~