目录
C# 2.0
C# 3.0
C# 5.0
C# 6.0
表达式 everywhere
out 变量
元组和解构
解构方法 Deconstrct
改造 Size 的构造方法
模式匹配
ref 局部变量和 ref 返回值
数字字面量语法增强
局部函数
支持更多 async 返回类型
C#7.0
1. out-variables(Out变量)
2.Tuples(元组)
3. Pattern Matching(匹配模式)
4.ref locals and returns(局部变量和引用返回)
5.Local Functions (局部函数)
6.More expression-bodied members(更多的函数成员的表达式体)
7.throw Expressions (异常表达式)
8.Generalized async return types (通用异步返回类型)
9.Numeric literal syntax improvements(数值文字语法改进)
C#7.1/7.2
C# 7.3 中的新增功能
启用更高效的安全代码
索引 fixed 字段不需要进行固定
可能会重新分配 ref 局部变量
stackalloc 数组支持初始值设定项
更多类型支持 fixed 语句
增强的泛型约束
提升了现有功能
元组支持 == 和 !=
将特性添加到自动实现的属性的支持字段
in 方法重载解析决胜属性
扩展初始值设定项中的表达式变量
改进了重载候选项
新的编译器选项
公共或开放源代码签名
pathmap
C# 8.0
Readonly 成员
默认接口成员
在更多位置中使用更多模式
Switch 表达式
属性模式
元组模式
位置模式
using 声明
静态本地函数
可处置的 ref 结构
可为空引用类型
异步流
索引和范围
泛型(Generics)
泛型是CLR 2.0中引入的最重要的新特性,使得可以在类、方法中对使用的类型进行参数化。
例如,这里定义了一个泛型类:
class MyCollection
使用的时候:MyCollection<string> list2 = new MyCollection<string>(); MyCollection<Object> list3 = new MyCollection<Object>();
泛型的好处
泛型方法、泛型委托、泛型接口
除了泛型类之外,还有泛型方法、泛型委托、泛型接口:
//泛型委托 public static delegate T1 MyDelegate
//泛型方法 static void Swap
泛型约束(constraints)
可以给泛型的类型参数上加约束,可以要求这些类型参数满足一定的条件
约束 |
说明 |
where T: struct | 类型参数需是值类型 |
where T : class | 类型参数需是引用类型 |
where T : new() | 类型参数要有一个public的无参构造函数 |
where T : |
类型参数要派生自某个基类 |
where T : |
类型参数要实现了某个接口 |
where T : U | 这里T和U都是类型参数,T必须是或者派生自U |
这些约束,可以同时一起使用:
class EmployeeList
default 关键字
这个关键可以使用在类型参数上:
default(T);
对于值类型,返回0,引用类型,返回null,对于结构类型,会返回一个成员值全部为0的结构实例。
迭代器(iterator)
可以在不实现IEnumerable就能使用foreach语句,在编译器碰到yield return时,它会自动生成IEnumerable 接口的方法。在实现迭代器的方法或属性中,返回类型必须是IEnumerable, IEnumerator, IEnumerable
public System.Collections.IEnumerator GetEnumerator() { yield return -1; for (int i = 1; i < max; i++) { yield return i; } }
可空类型(Nullable Type)
可空类型System.Nullable
int? num = null; if (num.HasValue == true) { System.Console.WriteLine("num = " + num.Value); } else { System.Console.WriteLine("num = Null"); }
如果HasValue为false,那么在使用value值的时候会抛出异常。把一个Nullable的变量x赋值给一个非Nullable的变量y可以这么写:
int y = x ?? -1;
匿名方法(Anonymous Method)
在C#2.0之前,给只能用一个已经申明好的方法去创建一个委托。有了匿名方法后,可以在创建委托的时候直接传一个代码块过去。
delegate void Del(int x); Del d = delegate(int k) { /* ... */ }; System.Threading.Thread t1 = new System.Threading.Thread (delegate() { System.Console.Write("Hello, "); } ); 委托语法的简化// C# 1.0的写法 ThreadStart ts1 = new ThreadStart(Method1); // C# 2.0可以这么写 ThreadStart ts2 = Method1;
委托的协变和逆变(covariance and contravariance)
有下面的两个类:
class Parent { } class Child: Parent { }
然后看下面的两个委托:
public delegate Parent DelgParent();
public delegate Child DelgChild();
public static Parent Method1() { return null; }
public static Child Method2() { return null; }
static void Main() { DelgParent del1= Method1; DelgChild del2= Method2; del1 = del2; }
注意上面的,DelgParent 和DelgChild 是完全不同的类型,他们之间本身没有任何的继承关系,所以理论上来说他们是不能相互赋值的。但是因为协变的关系,使得我们可以把DelgChild类型的委托赋值给DelgParent 类型的委托。协变针对委托的返回值,逆变针对参数,原理是一样的。
部分类(partial)
在申明一个类、结构或者接口的时候,用partial关键字,可以让源代码分布在不同的文件中。我觉得这个东西完全是为了照顾Asp.net代码分离而引入的功能,真没什么太大的实际用处。微软说在一些大工程中可以把类分开在不同的文件中让不同的人去实现,方便团队协作,这个我觉得纯属胡扯。
部分类仅是编译器提供的功能,在编译的时候会把partial关键字定义的类和在一起去编译,和CRL没什么关系。
静态类(static class)
静态类就一个只能有静态成员的类,用static关键字对类进行标示,静态类不能被实例化。静态类理论上相当于一个只有静态成员并且构造函数为私有的普通类,静态类相对来说的好处就是,编译器能够保证静态类不会添加任何非静态成员。
global::
这个代表了全局命名空间(最上层的命名空间),也就是任何一个程序的默认命名空间。
class TestApp { public class System { } const int Console = 7; static void Main() { //用这个访问就会出错,System和Console都被占用了 //Console.WriteLine(number); global::System.Console.WriteLine(number); } }
extern alias
用来消除不同程序集中类名重复的冲突,这样可以引用同一个程序集的不同版本,也就是说在编译的时候,提供了一个将有冲突的程序集进行区分的手段。
在编译的时候,使用命令行参数来指明alias,例如:
/r:aliasName=assembly1.dll
在Visual Studio里面,在被引用的程序集的属性里面可以指定Alias的值,默认是global。
然后在代码里面就可以使用了:
extern alias aliasName; //这行需要在using这些语句的前面 using System; using System.Collections.Generic; using System.Text; using aliasName.XXX;
属性Accessor访问控制
public virtual int TestProperty { protected set { } get { return 0; } }
友元程序集(Friend Assembly)
可以让其它程序集访问自己的internal成员(private的还是不行),使用Attributes来实现,例如:
[assembly:InternalsVisibleTo("cs_friend_assemblies_2")]
注意这个作用范围是整个程序集。
fixed关键字
可以使用fixed关键字来创建固定长度的数组,但是数组只能是bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float, double中的一种。
这主要是为了更好的处理一些非托管的代码。比如下面的这个结构体:
public struct MyArray { public fixed char pathName[128]; }
如果不用fixed的话,无法预先占住128个char的空间,使用fixed后可以很好的和非托管代码进行交互。
volatile关键字
用来表示相关的字可能被多个线程同时访问,编译器不会对相应的值做针对单线程下的优化,保证相关的值在任何时候访问都是最新的。
#pragma warning
用来取消或者添加编译时的警告信息。每个警告信息都会有个编号,如果warning CS01016之类的,使用的时候取CS后面的那个数字,例如:
#pragma warning disable 414, 3021
这样CS414和CS3021的警告信息就都不会显示了。
类型推断
申明变量的时候,可以不用直指定类型:
var i = 5;
var s = "Hello";
//两种写法是一样的
int i = 5;
string s = "Hello";
"Hello";
//两种写法是一样的
int i = 5;
string s = "Hello";
类型推断也支持数组:
var b = new[] { 1, 1.5, 2, 2.5 }; // double[]
var c = new[] { "hello", null, "world” }; // string[]
new[] { 1, 1.5, 2, 2.5 }; // double[]
var c = new[] { "hello", null, "world” }; // string[]
扩展方法
扩展方法必须被定义在静态类中,并且必须是非泛型、非嵌套的静态类。例如:
public static class JeffClass { public static int StrToInt32(this string s) { return Int32.Parse(s); } public static T[] SomeMethd
static class JeffClass { public static int StrToInt32(this string s) { return Int32.Parse(s); } public static T[] SomeMethd(this T[] source, int pram1, int pram2) { /**/ } } (this T[] source, int pram1, int pram2) { /**/ } }
上面一个是给string类型的对象添加了一个方法,另一个是给所有类型的数组添加了一个方法,方法有两个整型参数。
扩展方法只在当前的命名空间类有效,如果所在命名空间被其它命名空间import引用了,那么在其它命名空间中也有效。扩展方法的优先级低于其它的常规方法,也就是说如果扩展方法与其它的方法相同,那么扩展方法不会被调用。
Lamda表达式
可以看成是对匿名方法的一个语法上的简化,但是λ表达式同时可以装换为表达式树类型。
对象和集合的初始化
var contacts = new List
new List{ new Contact { Name = "Chris", PhoneNumbers = { "123455", "6688" } }, new Contact { Name = "Jeffrey", PhoneNumbers = { "112233" } } }; { new Contact { Name = "Chris", PhoneNumbers = { "123455", "6688" } }, new Contact { Name = "Jeffrey", PhoneNumbers = { "112233" } } };
匿名类型
var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;
new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;
自动属性
会自动生成一个后台的私有变量
public Class Point
{
public int X { get; set; }
public int Y { get; set; }
}
Class Point
{
public int X { get; set; }
public int Y { get; set; }
}
查询表达式
这个其实就是扩展方法的运用,编译器提供了相关的语法便利,下面两端代码是等价的:
from g in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }
customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })
in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }
customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })
表达式树
Func<int,int> f = x => x + 1; Expression
int,int> f = x => x + 1; Expressionint,int>> e = x => x + 1; int,int>> e = x => x + 1;
C# 4.0
协变和逆变
这个在C#2.0中就已经支持委托的协变和逆变了,C#4.0开始支持针对泛型接口的协变和逆变:
IList<string> strings = new List<string>();
IList<object> objects = strings;
string> strings = new List<string>();
IList<object> objects = strings;
协变和逆变仅针对引用类型。
动态绑定
看例子:
class BaseClass
{
public void print()
{
Console.WriteLine();
}
}
BaseClass
{
public void print()
{
Console.WriteLine();
}
}
Object o = new BaseClass();
dynamic a = o;
//这里可以调用print方法,在运行时a会知道自己是个什么类型。 这里的缺点在于编译的时候无法检查方法的合法性,写错的话就会出运行时错误。
a.print();
new BaseClass();
dynamic a = o;
//这里可以调用print方法,在运行时a会知道自己是个什么类型。 这里的缺点在于编译的时候无法检查方法的合法性,写错的话就会出运行时错误。
a.print();
可选参数,命名参数
private void CreateNewStudent(string name, int studentid = 0, int year = 1)
void CreateNewStudent(string name, int studentid = 0, int year = 1)
这样,最后一个参数不给的话默认值就是1,提供这个特性可以免去写一些重载方法的麻烦。
调用方法的时候,可以指定参数的名字来给值,不用按照方法参数的顺序来制定参数值:
CreateNewStudent(year:2, name:"Hima", studentid: 4); //没有按照方法定义的参数顺序
"Hima", studentid: 4); //没有按照方法定义的参数顺序
C# 5.0
1. 异步编程
在.Net 4.5中,通过async和await两个关键字,引入了一种新的基于任务的异步编程模型(TAP)。在这种方式下,可以通过类似同步方式编写异步代码,极大简化了异步编程模型。如下式一个简单的实例:
static async void DownloadStringAsync2(Uri uri)
{
var webClient = new WebClient();
var result = await webClient.DownloadStringTaskAsync(uri);
Console.WriteLine(result);
}
而之前的方式是这样的:
static void DownloadStringAsync(Uri uri)
{
var webClient = new WebClient();
webClient.DownloadStringCompleted += (s, e) =>
{
Console.WriteLine(e.Result);
};
webClient.DownloadStringAsync(uri);
}
也许前面这个例子不足以体现async和await带来的优越性,下面这个例子就明显多了:
public void CopyToAsyncTheHardWay(Stream source, Stream destination)
{
byte[] buffer = new byte[0x1000];
Action<IAsyncResult> readWriteLoop = null;
readWriteLoop = iar =>
{
for (bool isRead = (iar == null); ; isRead = !isRead)
{
switch (isRead)
{
case true:
iar = source.BeginRead(buffer, 0, buffer.Length,
readResult =>
{
if (readResult.CompletedSynchronously) return;
readWriteLoop(readResult);
}, null);
if (!iar.CompletedSynchronously) return;
break;
case false:
int numRead = source.EndRead(iar);
if (numRead == 0)
{
return;
}
iar = destination.BeginWrite(buffer, 0, numRead,
writeResult =>
{
if (writeResult.CompletedSynchronously) return;
destination.EndWrite(writeResult);
readWriteLoop(null);
}, null);
if (!iar.CompletedSynchronously) return;
destination.EndWrite(iar);
break;
}
}
};
readWriteLoop(null);
}
public async Task CopyToAsync(Stream source, Stream destination)
{
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
await destination.WriteAsync(buffer, 0, numRead);
}
}
关于基于任务的异步编程模型需要介绍的地方还比较多,不是一两句能说完的,有空的话后面再专门写篇文章来详细介绍下。另外也可参看微软的官方网站:Visual Studio Asynchronous Programming,其官方文档Task-Based Asynchronous Pattern Overview介绍的非常详细, VisualStudio中自带的CSharp Language Specification中也有一些说明。
2. 调用方信息
很多时候,我们需要在运行过程中记录一些调测的日志信息,如下所示:
public void DoProcessing()
{
TraceMessage("Something happened.");
}
为了调测方便,除了事件信息外,我们往往还需要知道发生该事件的代码位置以及调用栈信息。在C++中,我们可以通过定义一个宏,然后再宏中通过__FILE__和__LINE__来获取当前代码的位置,但C#并不支持宏,往往只能通过StackTrace来实现这一功能,但StackTrace却有不是很靠谱,常常获取不了我们所要的结果。
针对这个问题,在.Net 4.5中引入了三个Attribute:CallerMemberName、CallerFilePath和CallerLineNumber。在编译器的配合下,分别可以获取到调用函数(准确讲应该是成员)名称,调用文件及调用行号。上面的TraceMessage函数可以实现如下:
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
另外,在构造函数,析构函数、属性等特殊的地方调用CallerMemberName属性所标记的函数时,获取的值有所不同,其取值如下表所示:
调用的地方 |
CallerMemberName获取的结果 |
方法、属性或事件 |
方法,属性或事件的名称 |
构造函数 |
字符串 ".ctor" |
静态构造函数 |
字符串 ".cctor" |
析构函数 |
该字符串 "Finalize" |
用户定义的运算符或转换 |
生成的名称成员,例如, "op_Addition"。 |
特性构造函数 |
特性所应用的成员的名称 |
例如,对于在属性中调用CallerMemberName所标记的函数即可获取属性名称,通过这种方式可以简化 INotifyPropertyChanged 接口的实现。
1、自动属性的增强
1.1、自动属性初始化 (Initializers for auto-properties)
C#4.0下的果断实现不了的。
C#6.0中自动属性的初始化方式
只要接触过C#的肯定都会喜欢这种方式。真是简洁方便呀。
1.2、只读属性初始化Getter-only auto-properties
先来看一下我们之前使用的方式吧
public class Customer
{
public string Name { get; }
public Customer(string firstName,string lastName)
{
Name = firstName +" "+ lastName;
}
}
再来看一下C#6.0中
public class Customer
{
public string FirstName { get; }="aehyok";
public string LastName { get; }="Kris";
}
和第一条自动属性初始化使用方式一致。
2、Expression bodied function members
2.1 用Lambda作为函数体Expression bodies on method-like members
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
再来举一个简单的例子:一个没有返回值的函数
public void Print() => Console.WriteLine(FirstName + " " + LastName);
2.2、Lambda表达式用作属性Expression bodies on property-like function members
public override string ToString()
{
return FirstName + " " + LastName;
}
现在C#6中
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString() => string.Format("{0}——{1}", FirstName, LastName);
public string FullName => FirstName + " " + LastName;
}
3、引用静态类Using Static
在Using中可以指定一个静态类,然后可以在随后的代码中直接使用静态的成员
4、空值判断Null-conditional operators
直接来看代码和运行结果
通过结果可以发现返回的都为null,再也不像以前那样繁琐的判断null勒。
5、字符串嵌入值
在字符串中嵌入值
之前一直使用的方式是
现在我们可以简单的通过如下的方式进行拼接
6、nameof表达式nameof expressions
在方法参数检查时,你可能经常看到这样的代码(之前用的少,这次也算学到了)
public static void AddCustomer(Customer customer)
{
if (customer == null)
{
throw new ArgumentNullException("customer");
}
}
里面有那个customer是我们手写的字符串,在给customer改名时,很容易把下面的那个字符串忘掉,C#6.0 nameof帮我们解决了这个问题,看看新写法
public static void AddCustomer(Customer customer)
{
if (customer == null)
{
throw new ArgumentNullException(nameof(customer));
}
}
7、带索引的对象初始化器Index initializers
直接通过索引进行对象的初始化,原来真的可以实现
通过这种方式可以发现字典中只有三个元素,所以也就只有这三个索引可以访问额,其他类型的对象和集合也是可以通过这种方式进行初始化的,在此就不进行一一列举了。
8、异常过滤器 (Exception filters)
先来看一个移植过来的方法
try
{
var numbers = new Dictionary {[7] = "seven",[9] = "nine",[13] = "thirteen" };
}
catch (ArgumentNullException e)
{
if (e.ParamName == "customer")
{
Console.WriteLine("customer can not be null");
}
}
在微软的文档中还给出了另一种用法,这个异常会在日志记录失败时抛给上一层调用者
private static bool Log(Exception e)
{
///处理一些日志
return false;
}
static void Main(string[] args)
{
try
{
///
}
catch (Exception e){if (!Log(e))
{
}
}
Console.ReadLine();
}
9、catch和finally 中的 await —— Await in catch and finally blocks
在C#5.0中,await关键字是不能出现在catch和finnaly块中的。而在6.0中
try
{
res = await Resource.OpenAsync(…); // You could do this. …
}
catch (ResourceException e)
{
await Resource.LogAsync(res, e); // Now you can do this …
} finally
{
if (res != null)
await res.CloseAsync(); // … and this.
}
10、无参数的结构体构造函数—— Parameterless constructors in structs
总的来说,这些新特性使 C# 7.0 更容易以函数式编程的思想来写代码,C# 6.0 在这条路上已经做了不少工作, C# 7.0 更近一步!
C# 6.0 中,可以对成员方法和只读属性使用 Lambda 表达式,当时最郁闷的就是为什么不支持属性的 set 访问器。现在好了,不仅 set 方法器支持使用 Lambda 表达式,构造方法、析构方法以及索引都支持以 Lambda 表达式方式定义了。
class SomeModel
{
private string internalValue;
public string Value
{
get => internalValue;
set => internalValue = string.IsNullOrWhiteSpace(value) ? null : value;
}
}
out
变量out
变量是之前就存在的语法,C# 7.0 只是允许它将申明和使用放在一起,避免多一行代码。最直接的效果,就是可以将两个语句用一个表达式完成。这里以一个简化版的 Key
类为例,这个类早期被我们用于处理通过 HTTP Get/Post 传入的 ID 值。
public class Key
{
public string Value { get; }
public Key(string key)
{
Value = key;
}
public int IntValue
{
get
{
// C# 6.0,需要提前定义 intValue,但不需要初始化
// 虽然 C# 6.0 可以为只读属性使用 Lambda 表达式
// 但这里无法用一个表达式表达出来
int intValue;
return int.TryParse(Value, out intValue) ? intValue : 0;
}
}
}
而在 C# 7 中就简单了
// 注意 out var intValue,
// 对于可推导的类型甚至可以用 var 来申明变量
public int IntValue => int.TryParse(Value, out var intValue) ? intValue : 0;
用过 System.Tuple
的朋友一定对其 Item1
、Item2
这样毫无语义的命名深感不爽。不过 C# 7.0 带来了语义化的命名,同时,还减化了元组的创建,不再需要 Tuple.Create(...)
。另外,要使用新的元组特性和解构,需要引入 NuGet 包 System.ValueTuple
。
Install-Package System.ValueTuple
当然,元组常用于返回多个值的方法。也有些人喜欢用 out
参数来返回,但即使现在可以 out
变量,我仍然不赞成广泛使用 out
参数。
下面这个示例方法用于返回一个默认的时间范围(从今天开始算往前一共 7 天),用于数据检索。
// 返回类型是一个包含两个元素的元组
(DateTime Begin, DateTime End) GetDefaultDateRange()
{
var end = DateTime.Today.AddDays(1);
var begin = end.AddDays(-7);
// 这里使用一对圆括号就创建了一个元组
return (begin, end);
}
调用这个方法可以获得元组,因为定义的时候返回值指定了每个数据成员的名称,所以从元组获取数据可以是语义化的,当然仍然可以使用 Item1
和 Item2
。
var range = GetDefaultDateRange();
var begin = range.Begin; // 也可以 begin = range.Item1
var end = range.End; // 也可以 end = range.Item2
上面这个例子还可以简化,不用 range
这个中间变量,这就用到了解构
var (begin, end) = GetDefaultDateRange();
这里创建元组是以返回值来举例的,其实它就是一个表达式,可以在任何地方创建元组。上面的例子逻辑很简单,可以用表达式解决。下面的示例顺便演示了非语义化的返回类型申明。
// 原来的 (DateTime Begin, DateTime End) 申明也是没问题的
(DateTime, DateTime) GetDefaultDateRange()
=> (DateTime.Today.AddDays(1).AddDays(-7), DateTime.Today.AddDays(1));
解构方法可以让任何类(而不仅仅是元组)按定义的参数进行解构。而且神奇的是解构方法可以是成员方法,也可以定义成扩展方法。
public class Size
{
public int Width { get; }
public int Height { get; }
public int Tall { get; }
public Size(int width, int height, int tall)
{
this.Width = width;
this.Height = height;
this.Tall = tall;
}
// 定义成成员方法的解构
public void Deconstruct(out int width, out int height)
{
width = Width;
height = Height;
}
}
public static class SizeExt
{
// 定义成扩展方法的解构
public static void Deconstruct(this Size size, out int width, out int height, out int tall)
{
width = size.Width;
height = size.Height;
tall = size.Tall;
}
}
下面是使用解构的代码
var size = new Size(1920, 1080, 10);
var (w, h) = size;
var (x, y, z) = size;
Size
的构造方法还记得前面提到的构造方法可以定义为 Lambda 表达式吗?下面是使用元组和 Lambda 对 Size 构造方法的改造——我已经醉了!
public Size(int width, int height, int tall)
=> (Width, Height, Tall) = (width, height, tall);
模式匹配目前支持 is
和 switch
。说起来挺高大上的一个名字,换个接地气一点的说法就是判断类型顺便定义个具体类型的引用,有兴趣还可以加再点额外的判断。
对于 is
来说,就是判断的时候顺便定义个变量再初始化一下,所以像原来这样写的代码
// 假设逻辑能保证这里的 v 可能是 string 也 可能是 int
string ToString(object v) {
if (v is int) {
int n = (int) v;
return n.ToString("X4");
} else {
return (string) n;
}
}
可以简化成——好吧,直接一步到位写成表达式好了
string ToString(object v)
=> (v is int n) ? n.ToString("X4") : (string) v;
当然你可能说之前的那个也可以简化成一个表达式——好吧,不深究这个问题好吗?我只是演示
is
的模式匹配而已。
而 switch
中的模式匹配似乎要有用得多,还是以 ToString 为例吧
static string ToString(object v)
{
switch (v)
{
case int n when n > 0xffff:
// 判断类型,匹配的情况下再对值进行一个判断
return n.ToString("X8");
case int n:
// 判断类型,这里 n 肯定 <= 0xffff
return n.ToString("X4");
case bool b:
return b ? "ON" : "OFF";
case null:
return null;
default:
return v.ToString();
}
}
注意一下上面第一个分支中 when
的用法就好了。
这已经是很接近 C/C++ 的一种用法了。虽然官方说法是这样做可以解决一些安全性问题,但我个人目前还是没遇到它的使用场景。如果设计足够好,在目前又加入了元组新特性和解构的情况下,个人认为几乎可以避免使用 out
和 ref
。
既然没用到,我也不多说了,有用到的同学来讨论一下!
这里有两点增强,一点是引入了 0b
前缀的二进制数字面量语法,另一点是可以在数值字面量中任意使用 _
对数字进行分组。这个不用多数,举两个例就明白了
const int MARK_THREE = 0b11; // 0x03
const int LONG_MARK = 0b_1111_1111; // 0xff
const double PI = 3.14_1592_6536
经常写 JavaScript 的同学肯定会深有体会,局部函数是个好东西。当然它在 C# 中带来的最大好处是将某些代码组织在了一起。我之前在项目中大量使用了 Lambda 来代替局部函数,现在可以直接替换成局部函数了。Labmda 和局部函数虽然多数情况下能做同样的事情,但是它们仍然有一些区别
对于 Lambda,编译器要干的事情比较多。总之呢,就是编译效率要低得多
Lambda 通过委托实现,调用过程比较复杂,局部函数可以直接调用。简单地说就是局部函数执行效率更高
Lambda 必须先定义再使用,局部函数可以定义在使用之后。据说这在对递归算法的支持上会有区别
比较常用的地方是 Enumerator 函数和 async 函数中,因为它们实际都不是立即执行的。
我在项目中多是用来组织代码。局部函数代替只被某一个公共 API 调用的私有函数来组织代码虽然不失为一个简化类结构的好方法,但是把公共 API 函数的函数体拉长。所以很多时候我也会使用内部类来代替某些私有函数来组织代码。这里顺便说一句,我不赞成使用 #region
组织代码。
如果和 JavaScript 中 ES2017 的 async 相比,C# 中的 Task/Task
就比较像 Promise
的角色。不用羡慕 JavaScript 的 async 支持 Promise like,现在 C# 的 async 也支持 Task like 了,只要实现了 GetAwaiter
方法就行。
官方提供了一个 ValueTask
作为示例,可以通过 NuGet 引入:
Install-Package System.Threading.Tasks.Extensions
这个 ValueTask
比较有用的一点就是兼容了数据类型和 Task:
string cache;
ValueTask<string> GetData()
{
return cache == null ? new ValueTask<string>(cache) : new ValueTask<string>(GetRemoteData());
// 局部函数
async Task<string> GetRemoteData()
{
await Task.Delay(100);
return "hello async";
}
}
1.out-variables(Out变量)
2.Tuples(元组)
3.Pattern Matching(匹配模式)
4.ref
locals and returns (局部变量和引用返回)
5.Local Functions (局部函数)
6.More expression-bodied members(更多的函数成员的表达式体)
7.throw
Expressions (异常表达式)
8.Generalized async return types (通用异步返回类型)
9.Numeric literal syntax improvements(数值文字语法改进)
以前,我们使用out变量的时候,需要在外部先申明,然后才能传入方法,类似如下:
string ddd = ""; //先申明变量
ccc.StringOut(out ddd);
Console.WriteLine(ddd);
在C#7.0中我们可以不必申明,直接在参数传递的同时申明它,如下:
StringOut(out string ddd); //传递的同时申明
Console.WriteLine(ddd);
Console.ReadLine();
曾今在.NET4.0中,微软对多个返回值给了我们一个解决方案叫元组,类似代码如下:
static void Main(string[] args)
{
var data = GetFullName();
Console.WriteLine(data.Item1);
Console.WriteLine(data.Item2);
Console.WriteLine(data.Item3);
Console.ReadLine();
}
static Tuple GetFullName()
{
return new Tuple("a", "b", "c");
}
上面代码展示了一个方法,返回含有3个字符串的元组,然而当我们获取到值,使用的时候 心已经炸了,Item1,Item2,Item3是什么鬼,虽然达到了我们的要求,但是实在不优雅
那么,在C#7.0中,微软提供了更优雅的方案:(注意:需要通过nuget引用System.ValueTuple)如下:
static void Main(string[] args)
{
var data=GetFullName();
Console.WriteLine(data.a); //可用命名获取到值
Console.WriteLine(data.b);
Console.WriteLine(data.c);
Console.ReadLine();
}
//方法定义为多个返回值,并命名
private static (string a,string b,string c) GetFullName()
{
return ("a","b","c");
}
解构元组,有的时候我们不想用var匿名来获取,那么如何获取abc呢?我们可以如下:
static void Main(string[] args)
{
//定义解构元组
(string a, string b, string c) = GetFullName();
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.ReadLine();
}
private static (string a,string b,string c) GetFullName()
{
return ("a","b","c");
}
在C#7.0中,引入了匹配模式的玩法,先举个老栗子.一个object类型,我们想判断他是否为int如果是int我们就加10,然后输出,需要如下:
object a = 1;
if (a is int) //is判断
{
int b = (int)a; //拆
int d = b+10; //加10
Console.WriteLine(d); //输出
}
那么在C#7.0中,首先就是对is的一个小扩展,我们只需要这样写就行了,如下:
object a = 1;
if (a is int c) //这里,判断为int后就直接赋值给c
{
int d = c + 10;
Console.WriteLine(d);
}
这样是不是很方便?特别是经常用反射的同志们..
那么问题来了,挖掘机技术哪家强?!(咳咳,呸 开玩笑)
其实是,如果有多种类型需要匹配,那怎么办?多个if else?当然没问题,不过,微软爸爸也提供了switch的新玩法,我们来看看,如下:
我们定义一个Add的方法,以Object作为参数,返回动态类型
static dynamic Add(object a)
{
dynamic data;
switch (a)
{
case int b:
data=b++;
break;
case string c:
data= c + "aaa";
break;
default:
data = null;
break;
}
return data;
}
下面运行,传入int类型:
object a = 1;
var data= Add(a);
Console.WriteLine(data.GetType());
Console.WriteLine(data);
我们传入String类型的参数,代码和输出如下:
object a = "bbbb";
var data= Add(a);
Console.WriteLine(data.GetType());
Console.WriteLine(data);
通过如上代码,我们就可以体会到switch的新玩法是多么的顺畅和强大了.
匹配模式的Case When筛选
有的基友就要问了.既然我们可以在Switch里面匹配类型了,那我们能不能顺便筛选一下值?答案当然是肯定的.
我们把上面的Switch代码改一下,如下:
switch (a)
{
case int b when b < 0:
data = b + 100;
break;
case int b:
data=b++;
break;
case string c:
data= c + "aaa";
break;
default:
data = null;
break;
}
在传入-1试试,看结果如下:
已经补上,请移步:http://www.cnblogs.com/GuZhenYin/p/6531814.html
嗯,这个就有点颠覆..大家都知道,局部变量是指:只在特定过程或函数中可以访问的变量。
那这个局部函数,顾名思义:只在特定的函数中可以访问的函数(妈蛋 好绕口)
使用方法如下:
public static void DoSomeing()
{
//调用Dosmeing2
int data = Dosmeing2(100, 200);
Console.WriteLine(data);
//定义局部函数,Dosmeing2.
int Dosmeing2(int a, int b)
{
return a + b;
}
}
呃,解释下来 大概就是在DoSomeing中定义了一个DoSomeing2的方法,..在前面调用了一下.(注:值得一提的是局部函数定义在方法的任何位置,都可以在方法内被调用,不用遵循逐行解析的方式)
C#6.0中,提供了对于只有一条语句的方法体可以简写成表达式。
如下:
public void CreateCaCheContext() => new CaCheContext();
//等价于下面的代码
public void CreateCaCheContext()
{
new CaCheContext();
}
但是,并不支持用于构造函数,析构函数,和属性访问器,那么C#7.0就支持了..代码如下:
// 构造函数的表达式写法
public CaCheContext(string label) => this.Label = label;
// 析构函数的表达式写法
~CaCheContext() => Console.Error.WriteLine("Finalized!");
private string label;
// Get/Set属性访问器的表达式写法
public string Label
{
get => label;
set => this.label = value ?? "Default label";
}
7.throw
Expressions (异常表达式)在C#7.0以前,我们想判断一个字符串是否为null,如果为null则抛除异常,我们需要这么写:
public string IsNull()
{
string a = null;
if (a == null)
{
throw new Exception("异常了!");
}
return a;
}
这样,我们就很不方便,特别是在三元表达式 或者非空表达式中,都无法抛除这个异常,需要写if语句.
那么我们在C#7.0中,可以这样:
public string IsNull()
{
string a = null;
return a ?? throw new Exception("异常了!");
}
嗯,这个,怎么说呢,其实我异步用的较少,所以对这个感觉理解不深刻,还是觉得然并卵,在某些特定的情况下应该是有用的.
我就直接翻译官方的原文了,实例代码也是官方的原文.
异步方法必须返回 void,Task 或 Task
官方的实例展示的主要是意思是:一个数据,在已经缓存的情况下,可以使用ValueTask来返回异步或者同步2种方案
public class CaCheContext
{
public ValueTask CachedFunc()
{
return (cache) ? new ValueTask(cacheResult) : new ValueTask(loadCache());
}
private bool cache = false;
private int cacheResult;
private async Task loadCache()
{
// simulate async work:
await Task.Delay(5000);
cache = true;
cacheResult = 100;
return cacheResult;
}
}
调用的代码和结果如下:
//main方法可不能用async修饰,所以用了委托.
static void Main(string[] args)
{
Action act = async () =>
{
CaCheContext cc = new CaCheContext();
int data = await cc.CachedFunc();
Console.WriteLine(data);
int data2 = await cc.CachedFunc();
Console.WriteLine(data2);
};
// 调用委托
act();
Console.Read();
}
上面的代码,我们连续调用了2次,第一次,等待了5秒出现结果.第二次则没有等待直接出现结果和预期的效果一致.
这个就纯粹的是..为了好看了.
在C#7.0中,允许数字中出现"_"这个分割符号.来提高可读性,举例如下:
int a = 123_456;
int b = 0xAB_CD_EF;
int c = 123456;
int d = 0xABCDEF;
Console.WriteLine(a==c);
Console.WriteLine(b==d);
//如上代码会显示两个true,在数字中用"_"分隔符不会影响结果,只是为了提高可读性
当然,既然是数字类型的分隔符,那么 decimal
, float
和 double 都是可以这样被分割的..
异步Main函数
最让测试异步代码的开发人员沮丧的,无疑是控制台应用当前不支持异步入口点(EntryPoint)。虽然变通方法是编写多行样板代码,
但是这样的模式依赖于对方法的非正常使用,难于理解。例如:
public static void Main()
{
MainAsync().GetAwaiter().GetResult();
}
private static async Task MainAsync()
{
... // 程序主代码。
}
为解决这个问题,在“异步Main函数建议”中,添加了如下四个新的函数签名,罗列了可能的入口点。
static Task Main()
static Task Main()
static Task Main(string[])
static Task Main(string[])
如果代码中不存在另一个非异步Main函数,那么只要给出一个上述的入口点函数,编译器就会生成所需的样板代码。唯一的限制是需要向后兼容。
Microsoft曾考虑允许“async void Main()”,但是这种做法会使编译器更复杂,并且Microsoft总体上并不鼓励在事件处理器之外使用“async void”。
默认值(即Nothing)
VB没有表示“null”的关键字,这是C#和VB间的一个微妙的差别。但是VB有一个关键字“Nothing”。在语言技术规范中,对该关键字给出了如下说明:
Nothing是一个特殊的常值。它没有类型,可转换为类型系统中的任意类型,也包括类型参数。在转换为某个特定类型后,它等价于该类型的默认值。
C#当前使用“default(T)”模式实现同一效果,但略为繁琐,尤其是类的名字很长时。C# 7.1中将提供一个“默认常值”(Default Literal),其描述为:
这一类型的表达式可通过常值转换为默认值或null值,隐式地转换为any类型。
该类型向默认常值的推理与向null常值推理的工作机制一样,除非允许any类型(不只是引用类型)。
在可以使用null的地方,通常也可以使用默认常值。这一做法被看成是C#建议中的一个倒退,可能因为人们通常会对两个非常类似的方法完成同一件事大皱眉头。在设计会议纪要中,就有人提出疑问:
我们是否正在挑起类型之争?
一个使用默认常值的例子如下:
ImmutableArray x = default;
return default;
void Method(ImmutableArray arrayOpt = default)
var x = new[] { default, ImmutableArray.Create(y) };
const int x = default;
if (x == default)
if (x is default)
y = default as RefType //编译器告警:总是null。
int i = default
下面例子给出的是对默认常值的非法使用:
const int? y = default;
if (default == default)
if (default is T)
var i = default
throw default
后者无疑是一个C#设计上的奇特构件。在设计会议纪要中,给出了如下说法:
在C#中,允许开发人员抛出null。这会引发一个运行时错误,进而导致抛出一个NullReferenceException异常。因此,抛出NullReferenceException并非正大光明的,而是一种丑陋的模式。
完全没有理由允许抛出默认值。我们并不认为用户会感觉这是可行的,或是了解它的工作机制。
Microsoft并未引入默认常值,而是考虑通过扩展“null”实现同一效果。因为在VB中“nothing”和“null”是两个不同的关键词,所以在VB中可以这样做。即使不使用关键字,VB中也具有null的概念。因此,开发人员可以看到“NothingReferenceException”这样的异常。
在C#中,开发人员可能常会有这样的一个疑问:“null是否表示的是实际的空值,或是表示了可能为空值也可能不为空值的默认值?”我们认为,这是一个令人非常困惑的问题。
推导元组名(Infer Tuple Names)
虽然开发人员不常考虑到,但是C#中的匿名类型包括了命名推导。例如,编写如下代码时,对象y将具有名为A和B的属性:
var y = new { x.A, x.B };
根据“推导元组名建议”,值元组基本具有同样的功能。
var z1 = (A: x.A, B: x.B); //显式名字。
var z2 = (x.A, x.B); //推导名字。
但是匿名类型和值元组间存在着一些显著的差异:
C#和VB间有hen一个有意思的差别,VB可以通过函数去推导匿名属性名。例如:
var y = new { x.A, x.Bar() }; //编译错误
Dim y = New With {x.A, x.Bar()} //匿名类型{A,Bar}
该功能特性将扩展适用于VB元组。
但如果恰巧有一个扩展方法使用了与推导属性一样的名字,这一特性就会引发破坏性更改。在建议中进一步提出:
考虑到这一更改的破坏性有限,并且在C# 7.0中,交付元组的时间窗很短,兼容性委员会认为这种破坏性更改是可以接受的。
考虑泛型约束的元组名
如果存在元组名不匹配的问题,那么编译器会尽量警告编程人员。例如:
public static (int A, int B) Test1((int A, int B) a)
Test1((A: 1, B: 2));
Test1((X: 1, Y: 2)); //给出警告,元组名不匹配。
如果开始采用泛型约束,代码就不工作了:
public static T Test2(T a) where T : IEnumerable<(int A, int B)>
Test2(new List<(int A, int B)>());
Test2(new List<(int X, int Y)>()); //没有警告。
当给出前的解释是,在泛型约束的条件下,编译器是不会去检查元组名的。理论上讲,编译器是可以捕获这类问题的,但是所付出的性能上的代价要远高于所得到的收益。
使用泛型的模式匹配
模式匹配是C# 7.0中新提供的特性。但是使用该特性时,存在设计上的缺陷。让我们看一下Alex Wiese给出的如下代码:
class Program
{
static void Main(string[] args) {}
public void Send(T packet) where T : Packet
{
if (packet is KeepalivePacket keepalive)
{
// 使用keepalive的功能代码。
}
switch (packet)
{
case KeepalivePacket keepalivePacket:
// 使用keepalivePacket的功能代码。
break;
}
}
}
public class Packet {}
public class KeepalivePacket : Packet {}
代码会报如下错误:“An expression of type T cannot be handled by a pattern of type KeepalivePacket.”。但如果我们将参数改为System.Object类型,而不是T类型,代码就工作正常了。
public void Send(object packet)
C# 7.1,通过对引发模式匹配的规则进行微调,修正了这一问题。
我们改进了“模式匹配技术规范”中的一段内容,下面以粗体标出了我们所建议添加的内容:
我们认为左侧(left-hand-side)静态类型的特定组合与特定类型是不兼容的,这会导致编译时错误。我们称静态类型E的值与类型T是模式兼容的,如果存在标识转换(Identity Conversion)、隐式引用转换(Reference Conversion)、装箱转换(Boxing Conversion)、显式引用转换,或者存在从E到T的拆箱转换(Unboxing Conversion),或者E或T均为开放类型(Open Type)。如果具有类型E的表达式与其所匹配的类型模式中的类型并不模式兼容,就会产生编译时错误。
这被认为是一个软件问题修复问题。由于该更新是“向前不兼容”的,因此只有将编译器设为C# 7.1,才能使用这一更新。
C# 7.1/7.2:default字面量
default
字面量旨在减少一些样板代码。下面是一个常见的例子:
public Task GetOrderAsync(int orderKey, CancellationToken token = default(CancellationToken))
这多少有点啰嗦,因此,模仿Visual Basic的Nothing
关键字,上述代码可以写成下面这样:
public Task GetOrderAsync(int orderKey, CancellationToken token = default)
这行代码可以按照预期方式运行。但是,当使用一个可空的值类型时,问题就来了。
public Task GetOrders(int? limit = default)
这行代码应该把limit
参数置为空,但在C# 7.1中,它实际返回0。
这个问题的修复计划在C# 7.2中进行,该版本会随Visual Studio 15.5一起发布。
C# 7.1:元组名称推断
自从引入了匿名类型,C#就可以隐式命名属性。例如,在下面这行代码中,对象y
会拥有名为A
和B
的属性。
var y = new { x.A, x.B };
在C# 7.1中,值元组也具有这个特性。
var z1 = (A: x.A, B: x.B); //显式名称
var z2 = (x.A, x.B); //推断名称
要了解更多有关元组名称推断的信息,请看下我们之前的报道。
C# 7.1:Async Main
这里没有多少可说的。Main函数现在可以异步执行,这减少了之前需要编写的一些样板代码。
C# 7.2:条件Ref
C#的条件操作符通常被称为“三元运算符”,因为这是这门语言中的唯一一个。C# 7.2将会提供第二个三元操作符,名为条件Ref操作符。
这个小特性让开发人员可以在条件中使用ref
表达式。下面是提案中的一个例子:
ref var r = ref (arr != null ? ref arr[0]: ref otherArr[0]);
注意,除了在靠近两种可能结果的地方需要使用ref
关键字外,在包含整个表达式的括号外也需要使用ref
关键字。
C# 7.2:起始分隔符
该特性扩展了在数值字面量中使用下划线的能力。下面的示例摘自提案:
123 // C# 1.0及更高版本可用
1_2_3 // C# 7.0及更高版本可用
0x1_2_3 // C# 7.0及更高版本可用
0b101 // C# 7.0新增的二进制字面量
0b1_0_1 // C# 7.0及更高版本可用
// 在C# 7.2中,_可以用在`0x`或`0b`之后
0x_1_2 // C# 7.2及更高版本可用
0b_1_0_1 // C# 7.2及更高版本可用
C# 7.2:非尾部命名参数
C#中的命名参数服务于两种目的:
Boolean
参数。该特性处理第二种情况。例如:
void DoSomething(bool delayExecution, bool continueOnError, int maxRecords);
DoSomething(true, false, 100);
除非开发人员记住了函数签名,否则很难一眼就看出了true
和false
对应什么。过去,开发人员可以写成下面这样:
DoSomething(delayExecution: true, continueOnError: false, maxRecords: 100);
但是,如果对maxRecords
参数没有疑问却还需要指定似乎就有点奇怪。在非尾部命名参数提案中,开发人员可以根据需要指定参数。
DoSomething(delayExecution: true, continueOnError: false, 100);
编者注:当清晰度成为问题时,Enum
仍然好于Boolean
。
C# 7.2:Private Protected
C#有5个访问级别:private
、internal
、protected
、protected
或internal
、public
。但是,CLR还有第六个访问级别,名为FamANDAssem
,“允许程序集中的子类型访问”。
冷知识:在CLR中,protected
称为family
,而internal
称为assembly
。
借助新关键字“private protected
”,开发人员可以使用CLR的FamANDAssem
标识了。Private Protected提案说明了这样做的重要性:
在许多情况下,API都会包含一些成员函数,只打算让提供该类型的程序集中的子类实现并使用。CLR提供了用于此目的的访问级别,但C#中没有。因此,别无选择,API所有者要么诉诸于
internal
保护、自律或自定义分析器,要么使用protected
,并提供额外的文档说明,虽然该类型的公开文档中有这个成员函数,但它并不是公有API的一部分。至于后者的例子,可以看下RoslynCSharpCompilationOptions
中以Common开头的成员。
C# 7.2:只读引用
我们之前报道过只读引用,所以这里没什么新东西要介绍。本质上讲,只读引用只是为了说明开发人员希望通过引用传递结构从而获得性能收益,而不是真正改变值的能力。
目前,只读引用提案尚处于原型阶段,还没有实现。
ref-like类型编译时安全强化[7.2提案]
该C#特性又称为“内部指针”或“ref-like
类型”。该提案旨在让编译器可以要求特定的类型(Span
)仅出现在栈上。该特性仅对高性能场景而言比较重要。从我们上次报道以来,ref-like类型提案没有任何变化。
放弃的特性
以下特性没有被标记为7.2提案的一部分。虽然这不是说一定不会标记,但可能不会很快发生。
C# 7.3 版本有两个主要主题。 第一个主题提供使安全代码的性能与不安全代码的性能一样好的功能。 第二个主题提供对现有功能的增量改进。 此外,在此版本中添加了新的编译器选项。
以下新增功能支持使安全代码获得更好的性能的主题:
ref
本地变量。stackalloc
数组上的初始值设定项。fixed
语句。对现有功能进行了以下增强:
==
和 !=
。in
区分的参数的方法解析得到了改进。新的编译器选项为:
-publicsign
,用于启用程序集的开放源代码软件 (OSS) 签名。-pathmap
用于提供源目录的映射。本文的剩余部分提供了详细信息和链接,以便你详细了解每项改进。
你应能够安全地编写性能与不安全代码一样好的 C# 代码。 安全代码可避免错误类,例如缓冲区溢出、杂散指针和其他内存访问错误。 这些新功能扩展了可验证安全代码的功能。 努力使用安全结构编写更多代码。 这些功能使其更容易实现。
fixed
字段不需要进行固定请考虑此结构:
C#复制
unsafe struct S
{
public fixed int myFixedField[10];
}
在早期版本的 C# 中,需要固定变量才能访问属于 myFixedField
的整数之一。 现在,以下代码进行编译,而不将变量 p
固定到单独的 fixed
语句中:
C#复制
class C
{
static S s = new S();
unsafe public void M()
{
int p = s.myFixedField[5];
}
}
变量 p
访问 myFixedField
中的一个元素。 无需声明单独的 int*
变量。 请注意,你仍然需要 unsafe
上下文。 在早期版本的 C# 中,需要声明第二个固定的指针:
C#复制
class C
{
static S s = new S();
unsafe public void M()
{
fixed (int* ptr = s.myFixedField)
{
int p = ptr[5];
}
}
}
有关详细信息,请参阅有关 fixed
语句的文章。
ref
局部变量现在,在对 ref
局部变量进行初始化后,可能会对其重新分配,以引用不同的实例。 以下代码现在编译:
C#复制
ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.
有关详细信息,请参阅有关 ref
返回和 ref
局部变量以及 foreach
的文章。
stackalloc
数组支持初始值设定项当你对数组中的元素的值进行初始值设定时,你已能够指定该值:
C#复制
var arr = new int[3] {1, 2, 3};
var arr2 = new int[] {1, 2, 3};
现在,可向使用 stackalloc
进行声明的数组应用同一语法:
C#复制
int* pArr = stackalloc int[3] {1, 2, 3};
int* pArr2 = stackalloc int[] {1, 2, 3};
Span arr = stackalloc [] {1, 2, 3};
有关详细信息,请参阅语言参考中的 stackalloc
语句一文。
fixed
语句fixed
语句支持有限的一组类型。 从 C# 7.3 开始,任何包含返回 ref T
或 ref readonly T
的 GetPinnableReference()
方法的类型均有可能为 fixed
。 添加此功能意味着 fixed
可与 System.Span
有关详细信息,请参阅语言参考中的 fixed
语句一文。
现在,可以将类型 System.Enum 或 System.Delegate 指定为类型参数的基类约束。
现在也可以使用新的 unmanaged
约束来指定类型参数必须为“非托管类型”。 “非托管类型”不是引用类型,且在任何嵌套级别都不包含任何引用类型。
有关详细信息,请参阅有关 where
泛型约束和类型参数的约束的文章。
将这些约束添加到现有类型是不兼容的更改。 封闭式泛型类型可能不再满足这些新约束的要求。
第二个主题提供了对语言中的功能的改进。 这些功能提升了在编写 C# 时的效率。
==
和 !=
C# 元组类型现在支持 ==
和 !=
。 有关详细信息,请参阅有关元组一文中的转换等式部分。
现在支持此语法:
C#复制
[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }
属性 SomeThingAboutFieldAttribute
应用于编译器生成的 SomeProperty
的支持字段。 有关详细信息,请参阅 C# 编程指南中的属性。
in
方法重载解析决胜属性在添加 in
参数修饰符时,这两个方法将导致多义性:
C#复制
static void M(S arg);
static void M(in S arg);
现在,通过值(前面示例中的第一个)的重载比通过只读引用版本的重载更好。 若要使用只读引用参数调用版本,必须在调用方法前添加 in
修饰符。
备注
这将作为 bug 修复来实现。 即使在设置为“7.2”的语言版本中,这也不再具有多义性。
有关详细信息,请参阅有关 in
参数修饰符的文章。
已对在 C# 7.0 中添加的允许 out
变量声明的语法进行了扩展,以包含字段初始值设定项、属性初始值设定项、构造函数初始值设定项和查询子句。 它允许使用如以下示例中所示的代码:
C#复制
public class B
{
public B(int i, out int j)
{
j = i;
}
}
public class D : B
{
public D(int i) : base(i, out var j)
{
Console.WriteLine($"The value of 'j' is {j}");
}
}
在每个版本中,对重载解析规则进行了更新,以解决多义方法调用具有“明显”选择的情况。 此版本添加了三个新规则,以帮助编译器选取明显的选择:
this
实例接收器无法使用)包含未定义 this
的成员的正文(例如,静态成员),以及不能使用 this
的位置(例如,字段初始值设定项和构造函数初始值设定项)。你将注意到此更改,因为当你确定哪个方法更好时,你将发现多义方法重载具有更少的编译器错误。
新的编译器选项支持 C# 程序的新版本和 DevOps 方案。
-publicsign
编译器选项指示编译器使用公钥对程序集进行签名。 程序集被标记为已签名,但签名取自公钥。 此选项使你能够使用公钥在开放源代码项目中构建签名的程序集。
有关详细信息,请参阅 -publicsign 编译器选项一文。
-pathmap
编译器选项指示编译器将生成环境中的源路径替换为映射的源路径。 -pathmap
选项控制由编译器编写入 PDB 文件或为 CallerFilePathAttribute 编写的源路径。
有关详细信息,请参阅 -pathmap 编译器选项一文。
C# 语言有许多增强功能,可以进行试用。
备注
本文针对 C# 8.0 预览版 5 进行了最后一次更新。
本文的剩余部分将简要介绍这些功能。 如果有详细讲解的文章,则将提供指向这些教程和概述的链接。
可将 readonly
修饰符应用于结构的任何成员。 它指示该成员不会修改状态。 这比将 readonly
修饰符应用于 struct
声明更精细。 请考虑以下可变结构:
C#复制
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Distance => Math.Sqrt(X * X + Y * Y);
public override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
}
像大多数结构一样, ToString()
方法不会修改状态。 可以通过将 readonly
修饰符添加到 ToString()
的声明来对此进行指示:
C#复制
public readonly override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
上述更改会生成编译器警告,因为 ToString
访问 Distance
属性,该属性未标记为 readonly
:
console复制
warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'
需要创建防御性副本时,编译器会发出警告。 Distance
属性不会更改状态,因此可以通过将 readonly
修饰符添加到声明来修复此警告:
C#复制
public readonly double Distance => Math.Sqrt(X * X + Y * Y);
请注意,readonly
修饰符对于只读属性是必需的。 编译器不会假设 get
访问器不修改状态;必须明确声明 readonly
。 编译器会强制实施以下规则:readonly
成员不修改状态。 除非删除 readonly
修饰符,否则不会编译以下方法:
C#复制
public readonly void Translate(int xOffset, int yOffset)
{
X += xOffset;
Y += yOffset;
}
通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。
现在可以将成员添加到接口,并为这些成员提供实现。 借助此语言功能,API 作者可以将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或二进制文件兼容性。 现有的实现继承默认实现。 此功能使 C# 与面向 Android 或 Swift 的 API 进行互操作,此类 API 支持类似功能。 默认接口成员还支持类似于“特征”语言功能的方案。
默认接口成员会影响很多方案和语言元素。 我们的第一个教程介绍如何使用默认实现更新接口。 其他教程和参考更新将适时公开发布。
模式匹配提供了在相关但不同类型的数据中提供形状相关功能的工具。 C# 7.0 通过使用 is
表达式和 switch
语句引入了类型模式和常量模式的语法。 这些功能代表了支持数据和功能分离的编程范例的初步尝试。 随着行业转向更多微服务和其他基于云的体系结构,还需要其他语言工具。
C# 8.0 扩展了此词汇表,这样就可以在代码中的更多位置使用更多模式表达式。 当数据和功能分离时,请考虑使用这些功能。 当算法依赖于对象运行时类型以外的事实时,请考虑使用模式匹配。 这些技术提供了另一种表达设计的方式。
除了可以在新位置使用新模式之外,C# 8.0 还添加了“递归模式”。 任何模式表达式的结果都是一个表达式。 递归模式只是应用于另一个模式表达式输出的模式表达式。
通常情况下,switch
语句在其每个 case
块中生成一个值。 借助 Switch 表达式,可以使用更简洁的表达式语法。 只有些许重复的 case
和 break
关键字和大括号。 以下面列出彩虹颜色的枚举为例:
C#复制
public enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
如果应用定义了通过 R
、G
和 B
组件构造而成的 RGBColor
类型,可使用以下包含 switch 表达式的方法,将 Rainbow
转换为 RGB 值:
C#复制
public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
_ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
};
这里有几个语法改进:
switch
关键字之前。 不同的顺序使得在视觉上可以很轻松地区分 switch 表达式和 switch 语句。case
和 :
元素替换为 =>
。 它更简洁,更直观。default
事例替换为 _
弃元。将其与使用经典 switch
语句的等效代码进行对比:
C#复制
public static RGBColor FromRainbowClassic(Rainbow colorBand)
{
switch (colorBand)
{
case Rainbow.Red:
return new RGBColor(0xFF, 0x00, 0x00);
case Rainbow.Orange:
return new RGBColor(0xFF, 0x7F, 0x00);
case Rainbow.Yellow:
return new RGBColor(0xFF, 0xFF, 0x00);
case Rainbow.Green:
return new RGBColor(0x00, 0xFF, 0x00);
case Rainbow.Blue:
return new RGBColor(0x00, 0x00, 0xFF);
case Rainbow.Indigo:
return new RGBColor(0x4B, 0x00, 0x82);
case Rainbow.Violet:
return new RGBColor(0x94, 0x00, 0xD3);
default:
throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
};
}
借助属性模式,可以匹配所检查的对象的属性。 请看一个电子商务网站的示例,该网站必须根据买家地址计算销售税。 这种计算不是 Address
类的核心职责。 它会随时间变化,可能比地址格式的更改更频繁。 销售税的金额取决于地址的 State
属性。 下面的方法使用属性模式从地址和价格计算销售税:
C#复制
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.75M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};
模式匹配为表达此算法创建了简洁的语法。
一些算法依赖于多个输入。 使用元组模式,可根据表示为元组的多个值进行切换。 以下代码显示了游戏“rock, paper, scissors(石头剪刀布)”的切换表达式::
C#复制
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie"
};
消息指示获胜者。 弃元表示平局(石头剪刀布游戏)的三种组合或其他文本输入。
某些类型包含 Deconstruct
方法,该方法将其属性解构为离散变量。 如果可以访问 Deconstruct
方法,就可以使用位置模式检查对象的属性并将这些属性用于模式。 考虑以下 Point
类,其中包含用于为 X
和 Y
创建离散变量的 Deconstruct
方法:
C#复制
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) =>
(x, y) = (X, Y);
}
此外,请考虑以下表示象限的各种位置的枚举:
C#复制
public enum Quadrant
{
Unknown,
Origin,
One,
Two,
Three,
Four,
OnBorder
}
下面的方法使用位置模式来提取 x
和 y
的值。 然后,它使用 when
子句来确定该点的 Quadrant
:
C#复制
static Quadrant GetQuadrant(Point point) => point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};
当 x
或 y
为 0(但不是两者同时为 0)时,前一个开关中的弃元模式匹配。 Switch 表达式必须要么生成值,要么引发异常。 如果这些情况都不匹配,则 switch 表达式将引发异常。 如果没有在 switch 表达式中涵盖所有可能的情况,编译器将生成一个警告。
可在此模式匹配高级教程中探索模式匹配方法。
using 声明是前面带 using
关键字的变量声明。 它指示编译器声明的变量应在封闭范围的末尾进行处理。 以下面编写文本文件的代码为例:
C#复制
static void WriteLinesToFile(IEnumerable lines)
{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
foreach (string line in lines)
{
// If the line doesn't contain the word 'Second', write the line to the file.
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
}
// file is disposed here
}
在前面的示例中,当到达方法的右括号时,将对该文件进行处理。 这是声明 file
的范围的末尾。 前面的代码相当于下面使用经典 using 语句语句的代码:
C#复制
static void WriteLinesToFile(IEnumerable lines)
{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
foreach (string line in lines)
{
// If the line doesn't contain the word 'Second', write the line to the file.
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
}
} // file is disposed here
}
在前面的示例中,当到达与 using
语句关联的右括号时,将对该文件进行处理。
在这两种情况下,编译器将生成对 Dispose()
的调用。 如果 using 语句中的表达式不可处置,编译器将生成一个错误。
现在可以向本地函数添加 static
修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。 这样做会生成 CS8421
,“静态本地函数不能包含对
考虑下列代码。 本地函数 LocalFunction
访问在封闭范围(方法 M
)中声明的变量 y
。 因此,不能用 static
修饰符来声明 LocalFunction
:
C#复制
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
下面的代码包含一个静态本地函数。 它可以是静态的,因为它不访问封闭范围中的任何变量:
C#复制
int M()
{
int y = 5;
int x = 7;
return Add(x, y);
static int Add(int left, int right) => left + right;
}
用 ref
修饰符声明的 struct
可能无法实现任何接口,因此无法实现 IDisposable。 因此,要能够处理 ref struct
,它必须有一个可访问的 void Dispose()
方法。 这同样适用于 readonly ref struct
声明。
在可为空注释上下文中,引用类型的任何变量都被视为不可为空引用类型。 若要指示一个变量可能为 null,必须在类型名称后面附加 ?
,以将该变量声明为可为空引用类型。
对于不可为空引用类型,编译器使用流分析来确保在声明时将本地变量初始化为非 Null 值。 字段必须在构造过程中初始化。 如果没有通过调用任何可用的构造函数或通过初始化表达式来设置变量,编译器将生成警告。 此外,不能向不可为空引用类型分配一个可以为 Null 的值。
不对可为空引用类型进行检查以确保它们没有被赋予 Null 值或初始化为 Null。 不过,编译器使用流分析来确保可为空引用类型的任何变量在被访问或分配给不可为空引用类型之前,都会对其 Null 性进行检查。
可以在可为空引用类型的概述中了解该功能的更多信息。 可以在此可为空引用类型教程中的新应用程序中自行尝试。 在迁移应用程序以使用可为空引用类型教程中了解迁移现有代码库以使用可为空引用类型的步骤。
从 C# 8.0 开始,可以创建并以异步方式使用流。 返回异步流的方法有三个属性:
async
修饰符声明的。yield return
语句。使用异步流需要在枚举流元素时在 foreach
关键字前面添加 await
关键字。 添加 await
关键字需要枚举异步流的方法,以使用 async
修饰符进行声明并返回 async
方法允许的类型。 通常这意味着返回 Task 或 Task
C#复制
public static async System.Collections.Generic.IAsyncEnumerable GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
可以使用 await foreach
语句来枚举序列:
C#复制
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}
可以在创建和使用异步流的教程中自行尝试异步流。
范围和索引为在数组中指定子范围(Span
此语言支持依赖于两个新类型和两个新运算符。
^
运算符,指定一个索引与序列末尾相关。..
),用于指定范围的开始和末尾,就像操作数一样。让我们从索引规则开始。 请考虑数组 sequence
。 0
索引与 sequence[0]
相同。 ^0
索引与 sequence[sequence.Length]
相同。 请注意,sequence[^0]
不会引发异常,就像 sequence[sequence.Length]
一样。 对于任何数字 n
,索引 ^n
与 sequence.Length - n
相同。
范围指定范围的开始和末尾。 范围是排除的,也就是说“末尾”不包含在范围内。 范围 [0..^0]
表示整个范围,就像 [0..sequence.Length]
表示整个范围。
请看以下几个示例。 请考虑以下数组,用其顺数索引和倒数索引进行注释:
C#复制
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
可以使用 ^1
索引检索最后一个词:
C#复制
Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"
以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。 它包括 words[1]
到 words[3]
。 元素 words[4]
不在此范围内。
C#复制
var quickBrownFox = words[1..4];
以下代码使用“lazy”和“dog”创建一个子范围。 它包括 words[^2]
和 words[^1]
。 不包括结束索引 words[^0]
:
C#复制
var lazyDog = words[^2..^0];
下面的示例为开始和/或结束创建了开放范围:
C#复制
var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the, "lazy" and "dog"
此外可以将范围声明为变量:
C#复制
Range phrase = 1..4;
然后可以在 [
和 ]
字符中使用该范围:
C#复制
var text = words[phrase];
可在有关索引和范围的教程中详细了解索引和范围。