C# 2.0
泛型(Generics)
泛型是CLR 2.0中引入的最重要的新特性,使得可以在类、方法中对使用的类型进行参数化。
例如,这里定义了一个泛型类:
class MyCollection<T> { T variable1; private void Add(T param){ } }
使用的时候:MyCollection<string> list2 = new MyCollection<string>(); MyCollection<Object> list3 = new MyCollection<Object>();
泛型的好处
- 编译时就可以保证类型安全
- 不用做类型装换,获得一定的性能提升
泛型方法、泛型委托、泛型接口
除了泛型类之外,还有泛型方法、泛型委托、泛型接口:
//泛型委托
public static delegate T1 MyDelegate<T1, T2>(T2 item);
new MyDelegate<Int32, String>(SomeMethd);
//泛型接口
public class MyClass<T1, T2, T3> : MyInteface<T1, T2, T3>
{
public T1 Method1(T2 param1, T3 param2)
{
throw new NotImplementedException();
}
}
interface MyInteface<T1, T2, T3> {
T1 Method1(T2 param1, T3 param2);
}
//泛型方法
static void Swap<T>(ref T t1, ref T t2) {
T temp = t1; t1 = t2; t2 = temp;
}
String str1 = "a"; String str2 = "b";
ref str1, ref str2);
约束 | 说明 |
where T: struct | 类型参数需是值类型 |
where T : class | 类型参数需是引用类型 |
where T : new() | 类型参数要有一个public的无参构造函数 |
where T : <base class name> | 类型参数要派生自某个基类 |
where T : <interface name> | 类型参数要实现了某个接口 |
where T : U | 这里T和U都是类型参数,T必须是或者派生自U |
这些约束,可以同时一起使用:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new() { // ... }
default 关键字
这个关键可以使用在类型参数上:
default(T);
对于值类型,返回0,引用类型,返回null,对于结构类型,会返回一个成员值全部为0的结构实例。
迭代器(iterator)
可以在不实现IEnumerable就能使用foreach语句,在编译器碰到yield return时,它会自动生成IEnumerable 接口的方法。在实现迭代器的方法或属性中,返回类型必须是IEnumerable, IEnumerator, IEnumerable<T>,或 IEnumerator<T>。迭代器使得遍历一些零碎数据的时候很方便,不用去实现Current, MoveNext 这些方法。
public System.Collections.IEnumerator GetEnumerator() {
yield return -1; for (int i = 1; i < max; i++) { yield return i;
}
}
可空类型(Nullable Type)
可空类型System.Nullable<T>,可空类型仅针对于值类型,不能针对引用类型去创建。System.Nullable<T>简写为T ?。
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的警告信息就都不会显示了。
C# 3.0
类型推断
申明变量的时候,可以不用直指定类型:
var i = 5;
var s = "Hello";
//两种写法是一样的
int i = 5;
string s = "Hello";
类型推断也支持数组:
-
var b = new[] { 1, 1.5, 2, 2.5 }; // double[] -
var c = new[] { "hello", null, "world” }; // string[]
扩展方法
扩展方法必须被定义在静态类中,并且必须是非泛型、非嵌套的静态类。例如:
-
public static class JeffClass -
{ -
publicstaticintStrToInt32(thisstring s) -
{ -
return Int32.Parse(s); -
} -
public static T[] SomeMethd<T>(this T[] source, int pram1, int pram2) -
{ -
/**/ -
} -
}
上面一个是给string类型的对象添加了一个方法,另一个是给所有类型的数组添加了一个方法,方法有两个整型参数。
扩展方法只在当前的命名空间类有效,如果所在命名空间被其它命名空间import引用了,那么在其它命名空间中也有效。扩展方法的优先级低于其它的常规方法,也就是说如果扩展方法与其它的方法相同,那么扩展方法不会被调用。
Lamda表达式
可以看成是对匿名方法的一个语法上的简化,但是λ表达式同时可以装换为表达式树类型。
对象和集合的初始化
-
var contacts = new List<Contact> { -
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;
自动属性
会自动生成一个后台的私有变量
-
public 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() })
表达式树
-
Func<int,int> f = x => x + 1; -
Expression<Func<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;
协变和逆变仅针对引用类型。
动态绑定
看例子:
-
class BaseClass -
{ -
publicvoidprint() -
{ -
Console.WriteLine(); -
} -
} -
Object o = new BaseClass(); -
dynamic a = o; -
//这里可以调用print方法,在运行时a会知道自己是个什么类型。 这里的缺点在于编译的时候无法检查方法的合法性,写错的话就会出运行时错误。 -
a.print();
可选参数,命名参数
这样,最后一个参数不给的话默认值就是1,提供这个特性可以免去写一些重载方法的麻烦。
调用方法的时候,可以指定参数的名字来给值,不用按照方法参数的顺序来制定参数值:
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 接口的实现。
-
C# 6.0
1、自动属性的增强
1.1、自动属性初始化 (Initializers for auto-properties)
C#4.0下的果断实现不了的。
C#6.0中自动属性的初始化方式
只要接触过C#的肯定都会喜欢这种方式。真是简洁方便呀。
1.2、只读属性初始化Getter-only auto-properties
先来看一下我们之前使用的方式吧
再来看一下C#6.0中
和第一条自动属性初始化使用方式一致。
2、Expression bodied function members
2.1 用Lambda作为函数体Expression bodies on method-like members
再来举一个简单的例子:一个没有返回值的函数
2.2、Lambda表达式用作属性Expression bodies on property-like function members
现在C#6中
3、引用静态类Using Static
在Using中可以指定一个静态类,然后可以在随后的代码中直接使用静态的成员
4、空值判断Null-conditional operators
直接来看代码和运行结果
通过结果可以发现返回的都为null,再也不像以前那样繁琐的判断null勒。
5、字符串嵌入值
在字符串中嵌入值
之前一直使用的方式是
现在我们可以简单的通过如下的方式进行拼接
6、nameof表达式nameof expressions
在方法参数检查时,你可能经常看到这样的代码(之前用的少,这次也算学到了)
里面有那个customer是我们手写的字符串,在给customer改名时,很容易把下面的那个字符串忘掉,C#6.0 nameof帮我们解决了这个问题,看看新写法
7、带索引的对象初始化器Index initializers
直接通过索引进行对象的初始化,原来真的可以实现
通过这种方式可以发现字典中只有三个元素,所以也就只有这三个索引可以访问额,其他类型的对象和集合也是可以通过这种方式进行初始化的,在此就不进行一一列举了。
8、异常过滤器 (Exception filters)
先来看一个移植过来的方法
在微软的文档中还给出了另一种用法,这个异常会在日志记录失败时抛给上一层调用者
9、catch和finally 中的 await —— Await in catch and finally blocks
在C#5.0中,await关键字是不能出现在catch和finnaly块中的。而在6.0中
10、无参数的结构体构造函数—— Parameterless constructors in structs
总的来说,这些新特性使 C# 7.0 更容易以函数式编程的思想来写代码,C# 6.0 在这条路上已经做了不少工作, C# 7.0 更近一步!
表达式 everywhere
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; } -
publicKey(string) -
{ -
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
。
当然,元组常用于返回多个值的方法。也有些人喜欢用 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
这个中间变量,这就用到了解构
这里创建元组是以返回值来举例的,其实它就是一个表达式,可以在任何地方创建元组。上面的例子逻辑很简单,可以用表达式解决。下面的示例顺便演示了非语义化的返回类型申明。
-
// 原来的 (DateTime Begin, DateTime End) 申明也是没问题的 -
(DateTime, DateTime) GetDefaultDateRange() -
=> (DateTime.Today.AddDays(1).AddDays(-7), DateTime.Today.AddDays(1));
解构方法 Deconstrct
解构方法可以让任何类(而不仅仅是元组)按定义的参数进行解构。而且神奇的是解构方法可以是成员方法,也可以定义成扩展方法。
-
public class Size -
{ -
public int Width { get; } -
public int Height { get; } -
public int Tall { get; } -
publicSize(intintint) -
{ -
this.Width = width; -
this.Height = height; -
this.Tall = tall; -
} -
// 定义成成员方法的解构 -
publicvoidDeconstruct(outintoutint) -
{ -
width = Width; -
height = Height; -
} -
} -
public static class SizeExt -
{ -
// 定义成扩展方法的解构 -
publicstaticvoidDeconstruct(thisoutintoutintoutint) -
{ -
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 构造方法的改造——我已经醉了!
-
publicSize(intintint) -
=> (Width, Height, Tall) = (width, height, tall);
模式匹配
模式匹配目前支持 is
和 switch
。说起来挺高大上的一个名字,换个接地气一点的说法就是判断类型顺便定义个具体类型的引用,有兴趣还可以加再点额外的判断。
对于 is
来说,就是判断的时候顺便定义个变量再初始化一下,所以像原来这样写的代码
-
// 假设逻辑能保证这里的 v 可能是 string 也 可能是 int -
stringToString(object) { -
if (v is int) { -
int n = (int) v; -
return n.ToString("X4"); -
} else { -
return (string) n; -
} -
}
可以简化成——好吧,直接一步到位写成表达式好了
-
stringToString(object) -
=> (v is int n) ? n.ToString("X4") : (string) v;
当然你可能说之前的那个也可以简化成一个表达式——好吧,不深究这个问题好吗?我只是演示
is
的模式匹配而已。
而 switch
中的模式匹配似乎要有用得多,还是以 ToString 为例吧
-
staticstringToString(object) -
{ -
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
的用法就好了。
ref 局部变量和 ref 返回值
这已经是很接近 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
组织代码。
支持更多 async 返回类型
如果和 JavaScript 中 ES2017 的 async 相比,C# 中的 Task/Task<T>
就比较像 Promise
的角色。不用羡慕 JavaScript 的 async 支持 Promise like,现在 C# 的 async 也支持 Task like 了,只要实现了 GetAwaiter
方法就行。
官方提供了一个 ValueTask
作为示例,可以通过 NuGet 引入:
这个 ValueTask
比较有用的一点就是兼容了数据类型和 Task:
-
string cache; -
ValueTask<string> GetData() -
{ -
return cache == null ? new ValueTask<string>(cache) : new ValueTask<string>(GetRemoteData()); -
// 局部函数 -
asyncstring> GetRemoteData() -
{ -
await Task.Delay(100); -
return "hello async"; -
} -
}
C#7.0
1.out-variables(Out变量)
2.Tuples(元组)
3.Pattern Matching(匹配模式)
4.ref
5.Local Functions (局部函数)
6.More expression-bodied members(更多的函数成员的表达式体)
7.throw
Expressions (异常表达式)
8.Generalized async return types (通用异步返回类型)
9.Numeric literal syntax improvements(数值文字语法改进)
1. out-variables(Out变量)
以前,我们使用out变量的时候,需要在外部先申明,然后才能传入方法,类似如下:
在C#7.0中我们可以不必申明,直接在参数传递的同时申明它,如下:
2.Tuples(元组)
曾今在.NET4.0中,微软对多个返回值给了我们一个解决方案叫元组,类似代码如下:
上面代码展示了一个方法,返回含有3个字符串的元组,然而当我们获取到值,使用的时候 心已经炸了,Item1,Item2,Item3是什么鬼,虽然达到了我们的要求,但是实在不优雅
那么,在C#7.0中,微软提供了更优雅的方案:(注意:需要通过nuget引用System.ValueTuple)如下:
解构元组,有的时候我们不想用var匿名来获取,那么如何获取abc呢?我们可以如下:
3. Pattern Matching(匹配模式)
在C#7.0中,引入了匹配模式的玩法,先举个老栗子.一个object类型,我们想判断他是否为int如果是int我们就加10,然后输出,需要如下:
那么在C#7.0中,首先就是对is的一个小扩展,我们只需要这样写就行了,如下:
这样是不是很方便?特别是经常用反射的同志们..
那么问题来了,挖掘机技术哪家强?!(咳咳,呸 开玩笑)
其实是,如果有多种类型需要匹配,那怎么办?多个if else?当然没问题,不过,微软爸爸也提供了switch的新玩法,我们来看看,如下:
我们定义一个Add的方法,以Object作为参数,返回动态类型
下面运行,传入int类型:
输出如图:
我们传入String类型的参数,代码和输出如下:
通过如上代码,我们就可以体会到switch的新玩法是多么的顺畅和强大了.
匹配模式的Case When筛选
有的基友就要问了.既然我们可以在Switch里面匹配类型了,那我们能不能顺便筛选一下值?答案当然是肯定的.
我们把上面的Switch代码改一下,如下:
在传入-1试试,看结果如下:
4.ref locals and returns(局部变量和引用返回)
已经补上,请移步:javascript:void(0)
5.Local Functions (局部函数)
嗯,这个就有点颠覆..大家都知道,局部变量是指:只在特定过程或函数中可以访问的变量。
那这个局部函数,顾名思义:只在特定的函数中可以访问的函数(妈蛋 好绕口)
使用方法如下:
呃,解释下来 大概就是在DoSomeing中定义了一个DoSomeing2的方法,..在前面调用了一下.(注:值得一提的是局部函数定义在方法的任何位置,都可以在方法内被调用,不用遵循逐行解析的方式)
6.More expression-bodied members(更多的函数成员的表达式体)
C#6.0中,提供了对于只有一条语句的方法体可以简写成表达式。
如下:
但是,并不支持用于构造函数,析构函数,和属性访问器,那么C#7.0就支持了..代码如下:
7.throw Expressions (异常表达式)
在C#7.0以前,我们想判断一个字符串是否为null,如果为null则抛除异常,我们需要这么写:
这样,我们就很不方便,特别是在三元表达式 或者非空表达式中,都无法抛除这个异常,需要写if语句.
那么我们在C#7.0中,可以这样:
8.Generalized async return types (通用异步返回类型)
嗯,这个,怎么说呢,其实我异步用的较少,所以对这个感觉理解不深刻,还是觉得然并卵,在某些特定的情况下应该是有用的.
我就直接翻译官方的原文了,实例代码也是官方的原文.
异步方法必须返回 void,Task 或 Task<T>,这次加入了新的ValueTask<T>,来防止异步运行的结果在等待时已可用的情境下,对 Task<T> 进行分配。对于许多示例中设计缓冲的异步场景,这可以大大减少分配的数量并显著地提升性能。
官方的实例展示的主要是意思是:一个数据,在已经缓存的情况下,可以使用ValueTask来返回异步或者同步2种方案
调用的代码和结果如下:
上面的代码,我们连续调用了2次,第一次,等待了5秒出现结果.第二次则没有等待直接出现结果和预期的效果一致.
9.Numeric literal syntax improvements(数值文字语法改进)
这个就纯粹的是..为了好看了.
在C#7.0中,允许数字中出现"_"这个分割符号.来提高可读性,举例如下:
当然,既然是数字类型的分隔符,那么 decimal
, float
和 double 都是可以这样被分割的..
C#7.1
异步Main函数
最让测试异步代码的开发人员沮丧的,无疑是控制台应用当前不支持异步入口点(EntryPoint)。虽然变通方法是编写多行样板代码,
但是这样的模式依赖于对方法的非正常使用,难于理解。例如:
为解决这个问题,在“异步Main函数建议”中,添加了如下四个新的函数签名,罗列了可能的入口点。
如果代码中不存在另一个非异步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#建议中的一个倒退,可能因为人们通常会对两个非常类似的方法完成同一件事大皱眉头。在设计会议纪要中,就有人提出疑问:
我们是否正在挑起类型之争?
一个使用默认常值的例子如下:
下面例子给出的是对默认常值的非法使用:
后者无疑是一个C#设计上的奇特构件。在设计会议纪要中,给出了如下说法:
在C#中,允许开发人员抛出null。这会引发一个运行时错误,进而导致抛出一个NullReferenceException异常。因此,抛出NullReferenceException并非正大光明的,而是一种丑陋的模式。
完全没有理由允许抛出默认值。我们并不认为用户会感觉这是可行的,或是了解它的工作机制。
Microsoft并未引入默认常值,而是考虑通过扩展“null”实现同一效果。因为在VB中“nothing”和“null”是两个不同的关键词,所以在VB中可以这样做。即使不使用关键字,VB中也具有null的概念。因此,开发人员可以看到“NothingReferenceException”这样的异常。
在C#中,开发人员可能常会有这样的一个疑问:“null是否表示的是实际的空值,或是表示了可能为空值也可能不为空值的默认值?”我们认为,这是一个令人非常困惑的问题。
推导元组名(Infer Tuple Names)
虽然开发人员不常考虑到,但是C#中的匿名类型包括了命名推导。例如,编写如下代码时,对象y将具有名为A和B的属性:
根据“推导元组名建议”,值元组基本具有同样的功能。
但是匿名类型和值元组间存在着一些显著的差异:
- 匿名类型需要属性名,属性明可以是显示指定的,也可以是推导得到的。
- 值元组会将未命名属性标为Item1、Item2等。
- 如果匿名类型具有重复的名字,那么会产生编译错误。
- 如果值元组具有重复的显式名字,那么会产生编译错误。
- 如果值元组具有重复的推导名字,那么推导名会被跳过。例如:(x.A, x.B, y.A)将转化成(Item1, B, Item3)。
- 值元组不能使用如下保留名字:ToString、Rest、ItemN(N是大于0的数字)。
C#和VB间有hen一个有意思的差别,VB可以通过函数去推导匿名属性名。例如:
该功能特性将扩展适用于VB元组。
但如果恰巧有一个扩展方法使用了与推导属性一样的名字,这一特性就会引发破坏性更改。在建议中进一步提出:
考虑到这一更改的破坏性有限,并且在C# 7.0中,交付元组的时间窗很短,兼容性委员会认为这种破坏性更改是可以接受的。
考虑泛型约束的元组名
如果存在元组名不匹配的问题,那么编译器会尽量警告编程人员。例如:
如果开始采用泛型约束,代码就不工作了:
当给出前的解释是,在泛型约束的条件下,编译器是不会去检查元组名的。理论上讲,编译器是可以捕获这类问题的,但是所付出的性能上的代价要远高于所得到的收益。
使用泛型的模式匹配
模式匹配是C# 7.0中新提供的特性。但是使用该特性时,存在设计上的缺陷。让我们看一下Alex Wiese给出的如下代码:
代码会报如下错误:“An expression of type T cannot be handled by a pattern of type KeepalivePacket.”。但如果我们将参数改为System.Object类型,而不是T类型,代码就工作正常了。
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
字面量旨在减少一些样板代码。下面是一个常见的例子:
这多少有点啰嗦,因此,模仿Visual Basic的Nothing
关键字,上述代码可以写成下面这样:
这行代码可以按照预期方式运行。但是,当使用一个可空的值类型时,问题就来了。
这行代码应该把limit
参数置为空,但在C# 7.1中,它实际返回0。
这个问题的修复计划在C# 7.2中进行,该版本会随Visual Studio 15.5一起发布。
C# 7.1:元组名称推断
自从引入了匿名类型,C#就可以隐式命名属性。例如,在下面这行代码中,对象y
会拥有名为A
和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
关键字外,在包含整个表达式的括号外也需要使用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
对应什么。过去,开发人员可以写成下面这样:
但是,如果对maxRecords
参数没有疑问却还需要指定似乎就有点奇怪。在非尾部命名参数提案中,开发人员可以根据需要指定参数。
编者注:当清晰度成为问题时,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的一部分。至于后者的例子,可以看下Roslyn CSharpCompilationOptions
中以Common开头的成员。
C# 7.2:只读引用
我们之前报道过只读引用,所以这里没什么新东西要介绍。本质上讲,只读引用只是为了说明开发人员希望通过引用传递结构从而获得性能收益,而不是真正改变值的能力。
目前,只读引用提案尚处于原型阶段,还没有实现。
ref-like类型编译时安全强化[7.2提案]
该C#特性又称为“内部指针”或“ref-like
类型”。该提案旨在让编译器可以要求特定的类型(Span<T>
)仅出现在栈上。该特性仅对高性能场景而言比较重要。从我们上次报道以来,ref-like类型提案没有任何变化。
放弃的特性
以下特性没有被标记为7.2提案的一部分。虽然这不是说一定不会标记,但可能不会很快发生。
龙腾一族至尊龙骑