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新语法
现在可以在数字中加下划线,增加数字的可读性。编译器或忽略所有数字中的下划线
int million = 1_000_000;
虽然编译器允许在数字中任意位置添加任意个数的下划线,但显然,遵循管理,下划线应该每三位使用一次,而且,不可以将下划线放在数字的开头(_1000)或结尾(1000_)
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})");
}
}
模式匹配(Pattern matching)是C#7中引入的重要概念,它是之前is和case关键字的扩展。目前,C#拥有三种模式:
下面的例子简单地演示了这三种模式:
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的范围重叠,编译器只会选择第一个匹配上的分支
元组(Tuple)的概念早在C#4就提出来,它是一个任意类型变量的集合,并最多支持8个变量。在我们不打算手写一个类型或结构体来盛放一个变量集合时(例如,它是临时的且用完即弃),或者打算从一个方法中返回多个值,我们会考虑使用元组。不过相比C#7的元组,C#4的元组更像一个半成品,先看看C#4如何使用元组:
var beforeTuple = new Tuple<int, int>(2, 3);
var a = beforeTuple.Item1;
通过上面的代码发现,C#4中元组最大的两个问题是:
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#的关键字,则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();
}
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;
}
}
如果类型提供了解构方法,你又在扩展方法中定义了与签名相同的解构方法,则编译器会优先选用类型提供的解构方法
局部函数(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方法中可用
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中允许后跟表达式(但过去版本不允许)的类型实例成员