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); MyDelegate<Int32, String> MyFunc = 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"; Swap<String>(ref str1, ref str2);
泛型约束(constraints)
可以给泛型的类型参数上加约束,可以要求这些类型参数满足一定的条件
约束 |
说明 |
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 { public static int StrToInt32(this string 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 { public void print() { Console.WriteLine(); } }
Object o = new BaseClass(); dynamic a = o; //这里可以调用print方法,在运行时a会知道自己是个什么类型。 这里的缺点在于编译的时候无法检查方法的合法性,写错的话就会出运行时错误。 a.print();
可选参数,命名参数
private void CreateNewStudent(string name, int studentid = 0, int year = 1)
这样,最后一个参数不给的话默认值就是1,提供这个特性可以免去写一些重载方法的麻烦。
调用方法的时候,可以指定参数的名字来给值,不用按照方法参数的顺序来制定参数值:
CreateNewStudent(year:2, name:"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 接口的实现。
C# 6.0
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<int, string> {[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