C#7的主要特性

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();
}

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

  1. 首先是显示实现
  2. 其次是变量名(编译器自动推断的,需要C#7.1)
  3. 最后是默认的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中允许后跟表达式(但过去版本不允许)的类型实例成员

你可能感兴趣的:(C#)