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