强强的个人技术博客 医技科室软件(PACS, RIS)码农

C# 7.0 的主要特性

C# 7.0 的主要特性


C#7集成到 .NET Framework4.6.2和Visual Studio2017中,增加了元组和模式匹配,使得C#更具函数式语言特点

要使用C#7的语法特性,需要 .NET Framework4.6.2或以上版本。Visual Studio2017的各个不同版本都预装了4.6.2或4.7,不过默认是使用4.6.1建立新工程,需要选择4.6.2或以上版本建立新工程,才能使用C#7新语法

1.数字字面量

现在可以在数字中加下划线,增加数字的可读性。编译器或忽略所有数字中的下划线 

int million = 1_000_000;

虽然编译器允许在数字中任意位置添加任意个数的下划线,但显然,遵循管理,下划线应该每三位使用一次,而且,不可以将下划线放在数字的开头(_1000)或结尾(1000_)

2.改进的out关键字

C#7支持了out关键字的即插即用 

var a = 0;
int.TryParse("345", out a);
 
// 就地使用变量作为返回值
int.TryParse("345", int out b);

允许以_(下划线)形式舍弃某个out参数,方便你忽略不关系的参数。例如下面的例子中,获得一个二维坐标的X可以重用获得二维坐标的X,Y方法,并舍弃掉Y: 

struct Point
{
    public int x;
    public int y;
    private void GetCoordinates(out int x, out int y)
    {
       x = this.x;
       y = this.y;
    }
    public void GetX()
    {
       // y被舍弃了,虽然GetCoordinates方法还是会传入2个变量,且执行y=this.y
       // 但它会在返回之后丢失
       GetCoordinates(out int x, out _);
       WriteLine($"({x})");
    }
}

3. 模式匹配

模式匹配(Pattern matching)是C#7中引入的重要概念,它是之前is和case关键字的扩展。目前,C#拥有三种模式: 

常量模式:简单地判断某个变量是否等于一个常量(包括null)

类型模式:简单地判断某个变量是否为一个类型的实例

变量模式:临时引入一个新的某个类型的变量(C#7新增)

下面的例子简单地演示了这三种模式: 

class People
{
    public int TotalMoney { get; set; }
    public People(int a)
    {
       TotalMoney = a;
    }
}
class Program
{
    static void Main(string[] args)
    {
       var peopleList = new List<People>() {
           new People(1),
           new People(1_000_000)
       };
       foreach (var p in peopleList)
       {
           // 类型模式
           if (p is People) WriteLine("是人");
           // 常量模式
           if (p.TotalMoney > 500_000) WriteLine("有钱");
           // 变量模式
           // 加入你需要先判断一个变量p是否为People,如果是,则再取它的TotalMoney字段
           // 那么在之前的版本中必须要分开写
           if (p is People)
           {
              var temp = (People)p;
              if (temp.TotalMoney > 500_000) WriteLine("有钱");
           }
           // 变量模式允许你引入一个变量并立即使用它
           if (p is People ppl && ppl.TotalMoney > 500_000) WriteLine("有钱");
       }
       ReadKey();
    }
}

可以看出,变量模式引入的临时变量ppl(称为模式变量)的作用域也是整个if语句体,它的类型是People类型

case关键字也得到了改进。现在,case后面也允许模式变量,还允许when子句,代码如下: 

static void Main(string[] args)
{
    var a = 13;
    switch (a)
    {
       // 现在i就是a
       // 由于现在case后面可以跟when子句的表达式,不同的case有机会相交
       case int i when i % 2 == 1:
           WriteLine(i + " 是奇数");
           break;
       // 只会匹配第一个case,所以这个分支无法到达
       case int i when  i > 10:
           WriteLine(i + " 大于10");
           break;
       // 永远在最后被检查,即使它后面还有case子句
       default:
           break;
    }
    ReadKey();
}


上面的代码运行的结果是打印出13是奇数,我们可以看到,现在case功能非常强大,可以匹配更具体、跟他特定的范围。不过,多个case的范围重叠,编译器只会选择第一个匹配上的分支 

4.值类型元组

元组(Tuple)的概念早在C#4就提出来,它是一个任意类型变量的集合,并最多支持8个变量。在我们不打算手写一个类型或结构体来盛放一个变量集合时(例如,它是临时的且用完即弃),或者打算从一个方法中返回多个值,我们会考虑使用元组。不过相比C#7的元组,C#4的元组更像一个半成品,先看看C#4如何使用元组: 

var beforeTuple = new Tuple<int, int>(2, 3);
var a = beforeTuple.Item1;

通过上面的代码发现,C#4中元组最大的两个问题是: 

Tuple类将其属性命名为Item1、Item2等,这些名称是无法改变的,只会让代码可读性变差

Tuple是引用类型,使用任一Tuple类意味着在堆上分配对象,因此,会对性能造成负面影响

C#7引入的新元组(ValueTuple)解决了上面两个问题,它是一个结构体,并且你可以传入描述性名称(TupleElementNames属性)以便更容易地调用他们: 

static void Main(string[] args)
{
    // 未命名的元组,访问方式和之前的元组相同
    var unnamed = ("one", "two");
    var b = unnamed.Item1;
    // 带有命名的元组
    var named = (first : "one", second : "two");
    b = named.first;
    ReadKey();
}

在背后,他们被编译器隐式地转化为: 

ValueTuple<string, string> unnamed = new ValueTuple<string, string>() ("one", "two");
string b = unnamed.Item1;
ValueTuple<string, string> named = new ValueTuple<string, string>() ("one", "two");
b = named.Item1;

我们看到,编译器将带有命名元组的实名访问转换成对应的Item,转换是使用特性实现的 

元组的字段名称

可以在元组定义时传入变量。此时,元组的字段名称为变量名。如果没有指明字段名称,又传入了常量,则只能使用Item1、Item2等访问元组的成员 

static void Main(string[] args)
{
    var localVariableOne = 5;
    var localVariableTwo = "some text";
    // 显示实现的字段名称覆盖变量名
    var tuple = (explicitFieldOne : localVariableOne, explicitFieldTwo : localVariableTwo);
    var a = tuple.explicitFieldOne;
    
    // 没有指定字段名称,又传入了变量名(需要C#7.1版本)
    var tuple2 = (localVariableOne, localVariableTwo);
    var b = tuple.localVariableOne;
    
    // 如果没有指明字段名称,又传入了常量,则只能使用Item1、Item2等访问元组的成员
    var tuple3 = (5, "some text");
    var c = tuple3.Item1;
    ReadKey();
}

上面的代码给出了元组字段名称的优先级: 

首先是显示实现

其次是变量名(编译器自动推断的,需要C#7.1)

最后是默认的Item1、Item2作为保留名称

另外,如果变量名或显示指定的描述名称是C#的关键字,则C#会改用ItemX作为字段名称(否则就会导致语法错误,例如将变量名为ToString的变量传入元组) 

var ToString = "1";
var Item1 = 2;
var tuple4 = (ToString, Item1);
 
// ToString不能用作元组字段名称,强制改为Item1
var d = tuple4.Item1; // "1"
// Item1不能用作元组字段名,强制改为Item2
var e = tuple4.Item2; // 2
ReadKey();


元组作为方法的参数和返回值

因为元组实际上是一个结构体,所以它当然可以作为方法的参数和返回值。因此,我们就有了可以返回多个变量的最简单、最优雅的方法(比使用out的可读性好很多): 

// 使用元组作方法的参数和返回值
(int, int) MultiplyAll(int multiplier, (int a, int b) members)
{
    // 元组没有实现IEnumerator接口,不能foreach
    // foreach(var a in members)
    // 操作元组
    return (members.a * multiplier, members.b * multiplier);
}


上面代码中的方法会将输入中的a和b都乘以multiplier,然后返回结构。由于元组是结构体,所以即使含有引用类型,其值类型的部分也会在栈上进行分配,相比C#4的元组,C#7中的元组有着更好的性能和更友好的访问方式 

相同类型元组的赋值

如果它们的基数(即成员数)相同,且每个元素的类型要么相同,要么可以实现隐式转换,则两个元组被看作相同的类型: 

static void Main(string[] args)
{
    var a = (first : "one", second : 1);
    WriteLine(a.GetType());
    var b = (a : "hello", b : 2);
    WriteLine(b.GetType());
    var c = (a : 3, b : "world");
    WriteLine(c.GetType());
    
    WriteLine(a.GetType() == b.GetType()); // True,两个元组基数和类型相同
    WriteLine(a.GetType() == c.GetType()); // False,两个元组基数相同但类型不同
    
    (string a, int b) d = a;
    // 属性first,second消失了,取而代之的是a和b
    WriteLine(d.a);
    // 定义了一个新的元组,成员为string和object类型
    (string a, object b) e;
    // 由于int可以被隐式转换为object,所以可以这样赋值
    e = a;
    ReadKey();
}



5.解构

C#7允许你定义结构方法(Deconstructor),注意,它和C#诞生即存在的析构函数(Destructor)不同。解构函数和构造函数做的事情某种程度上是相对的——构造函数将若干个类型组合为一个大的类型,而结构方法将大类型拆散为一堆小类型,这些小类型可以是单个字段,也可以是元组。当类型成员很多而需要的部分通常较小时,解构方法会很有用,它可以防止类型传参时复制的高昂代价 

元组的解构

可以在括号内显示地声明每个字段的类型,为元组中的每个元素创建离散变量,也可以用var关键字 

static void Main(string[] args)
{
    // 定义元组
    (int count, double sum, double sumOfSquares) tuple = (1, 2, 3);
    // 使用方差的计算公式得到方差
    var variance = tuple.sumOfSquares - tuple.sum * tuple.sum / tuple.count;
 
    // 将一个元组放在等号右边,将对应的变量值和类型放在等号左边,就会导致解构
    (int count, double sum, double sumOfSquares) = (1, 2, 3);
    // 解构之后的方差计算,代码简洁美观
    variance = sumOfSquares - sum * sum / count;
    // 也可以这样解构,这会导致编译器推断元组的类型为三个int
    var (a, b, c) = (1, 2, 3);
    ReadKey();
}


上面的代码中,出现了两次解构方法的隐式调用:左边是一个没有元组变量名的元组(只有一些成员变量名),右边是元组的实例。解构方法所做的事情,就是将右边元组的实例中每个成员,逐个指派给左边元组的成员变量。例如: 

(int count, double sum, double sumOfSquares) = (1, 2, 3);



就会使得count,sum和sumOfSquares的值分别为1,2,3。如果没有这个功能,就需要定义3个变量,然后赋值3次,最终得到6行代码,大大提高了代码的可读性。

对于元组,C#提供了内置的解构支持,因此不需要手动写解构方法,如果需要对非元组类型进行解构,就需要定义自己的解构方法,显而易见,上面的解构通过如下的签名的函数完成: 

public void Deconstruct(out int count, out double sum, out double sumOfSquares)


解构其他类型

解构函数的名称必须为Deconstruct,下面的例子从一个较大的类型People中解构出我们想要的三项成员: 

// 示例类型
public class People
{
    public int ID;
    public string FirstName;
    public string MiddleName;
    public string LastName;
    public int Age;
    public string CompanyName;
    // 解构全名,包括姓、名字和中间名
    public void Deconstruct(out string f, out string m, out string l)
    {
       f = FirstName;
       m = MiddleName;
       l = LastName;
    }
}
 
static void Main(string[] args)
{
    var  p = People();
    p.FirstName = "Test";
    var (fName, mName, lName) = p;
    WriteLine(fName);
 
    ReadKey();
}


解构方法不能有返回值,且要解构的每个成员必须以out标识出来。如果编译器对一个类型的实例解构,却没发现对应的解构函数,就会发生编译时异常。如果在解构时发生隐式类型转换,则不会发生编译时异常,例如将上述的解构函数的输入参数类型都改为object类型,仍然可以完成解构,可以通过重载解构函数对类型实现不同方式的解构

 

忽略类型成员

为了少写代码,我们可以在解构时忽略类型成员。例如,我们如果只关系People的姓和名字,而不关心中间名,则不需要多写一个解构函数,而是利用现有的:

 
var (fName, _, lName) = p;



通过使用下划线来忽略类型成员,此时仍然会调用带有三个参数的解构函数,但是p将会只有fName和lName两个成员

元组也支持忽略类型成员的解构

 

使用扩展方法进行解构

即使类型并非由自己定义,仍然可以通过解构扩展方法来解构类型,例如解构.NET自带的DateTime类型:

 

class Program
{
    static void Main(string[] args)
    {
       var d = DateTime.Now;
       (string s, DayOfWeek dow) = d;
       WriteLine($"今天是 {s}, 是 {d}");
       ReadKey();
    }
}
public static class ReflectionExtensions
{
    // 解构DateTime并获得想要的值
    public static void Deconstruct(this DateTime dateTime, out string DateString, out DayOfWeek dayOfWeek)
    {
       DateString = dateTime.ToString("yyyy-MM-dd");
       dayOfWeek = dateTime.DayOfWeek;
    }
}


如果类型提供了解构方法,你又在扩展方法中定义了与签名相同的解构方法,则编译器会优先选用类型提供的解构方法

 

6.局部函数

局部函数(local functions)和匿名方法很像,当你有一个只会使用一次的函数(通常作为其他函数的辅助函数)时,可以使用局部函数或匿名方法。如下是一个利用局部函数和元组计算斐波那契数列的例子:

 

static void Main(string[] args)
{
    WriteLine(Fibonacci(10));
    ReadKey();
}
public static int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("输入正整数", nameof(x));
    return Fib(x).current;
 
    // 局部函数定义
    (int current, int previous) Fib(int i)
    {
       if (i == 1) return (1, 0);
       var (p, pp) = Fib(i - 1);
       return (p + pp, p);
    }
}


局部函数是属于定义该函数的方法的,在上面的例子中,Fib函数只在Fibonacci方法中可用 

局部函数只能在方法体中使用

不能在匿名方法中使用

只能用async和unsafe修饰局部函数,不能使用访问修饰符,默认是私有、静态的

局部函数和某普通方法签名相同,局部函数会将普通方法隐藏,局部函数所在的外部方法调用时,只会调用到局部函数


7.更多的表达式体成员

C#6允许类型的定义中,字段后跟表达式作为默认值。C#7进一步允许了构造函数、getter、setter以及析构函数后跟表达式:

 

class CSharpSevenClass
{
    int a;
    // get, set使用表达式
    string b
    {
       get => b;
       set => b = "12345";  
    }
    // 构造函数
    CSharpSevenClass(int x) => a = x;
    // 析构函数
    ~CSharpSevenClass() => a = 0;
}


上面的代码演示了所有C#7中允许后跟表达式(但过去版本不允许)的类型实例成员




————————————————

版权声明:本文为CSDN博主「AaronChenH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/u013457933/article/details/89208065






2022年4月22日 | 发布:强强 | 分类:ASP.NET | 评论:0

发表留言: