上学时学习C#和.NET,当时网上的资源不像如今这样丰富,因此去电脑城买了张盗版的VS2005的光盘,安装时才发现是VS2003,当时有一种被坑的感受,但也正是如此,让我有了一个完整的.NET的学习生涯。前端
一直都认为学习语言应该系统的进行学习,了解每个版本的新增特性,才能在实际应用中作到有的放矢。最近发现团队中有很多人虽然用着最新的技术,但知识储备还停留在一个比较初始的状态,这样在编码过程当中会走很多弯路。数据库
本文梳理下C#从1.0到7.0版本的一些经常使用特性,对于不经常使用的或者我没有用到过的一些特性,会列出来,但不会作详细描述。另外C#8.0如今尚未正式推出,而且目前咱们也只是在使用dotNet Core2.1,因此C#8.0本文也不会涉及。json
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
1.0 | VS2002 | 1.0 | 1.0 |
1.1 | VS2003 | 1.1 | 1.1 |
在C#1.0或1.1版本中,从语言的角度就是基本的面向对象的语法,能够说任何一本C#语言的书籍都包含了C#1.X的全部内容。后端
若是您已经在使用C#语言编写代码,那么C#1.X的相关知识应该已经掌握。基础语法部分这里就再也不赘述了。数组
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
2.0 | VS2005 | 2.0 | 2.0 |
2.0中对应VS2005我用的也很少,由于很快就被VS2008替代了,不过在语言方面却带来了不少新的东西。多线程
C#2中最重要的一个特性应该就是泛型。泛型的用处就是在一些场景下能够减小强制转换来提升性能。在C#1中就有不少的强制转换,特别是对一些集合进行遍历时,如ArrayList、HashTable,由于他们是为不一样数据类型设计的集合,因此他们中键和值的类型都是object,这就意味着会平凡发生装箱拆箱的操做。C#2中有了泛型,因此咱们可使用List框架
.NET已经经过了不少的泛型类型供咱们使用,如上面提到的List前后端分离
分部类能够容许咱们在多个文件中为一个类型(class、struct、interface)编写代码,在Asp.Net2.0中用的极为普遍。新建一个Aspx页面,页面的CodeBehind和页面中的控件的定义就是经过分部类来实现的。以下:异步
public partial class _Default : System.Web.UI.Page public partial class _Default
分部类使用关键字partial来定义,当一个类中的代码很是多时,可使用分部类来进行拆分,这对代码的阅读颇有好处,并且不会影响调用。不过如今咱们先后端分离,后端代码要作到单一职责原则,不会有不少大的类,因此这个特性不多用到。async
静态类中的公用方法必须也是静态的,能够由类名直接调用,不须要实例化,比较适用于编写一些工具类。如System.Math类就是静态类。工具类有一些特色,如:全部成员都是静态的、不须要被继承、不须要进行实例化。在C#1中咱们能够经过以下代码来实现:
//声明为密封类防止被继承 public sealed class StringHelper { //添加私有无参构造函ˉ数防止被实例化,若是不添加私有构造函数 //会自动生成共有无参构造函数 private StringHelper(){}; public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } }
C#2中可使用静态类来实现:
public static class StringHelper { public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } }
在C#1中声明属性,属性中的get和set的访问级别是和属性一致,要么都是public要么都是private,若是要实现get和set有不一样的访问级别,则须要用一种变通的方式,本身写GetXXX和SetXXX方法。在C#2中能够单独设置get和set的访问级别,以下:
private string _name; public string Name { get { return _name; } private set { _name = value; } }
须要注意的是,不能讲属性设置为私有的,而将其中的get或是set设置成公有的,也不能给set和get设置相同的访问级别,当set和get的访问级别相同时,咱们能够直接设置在属性上。
命名空间能够用来组织类,当不一样的命名空间中有相同的类时,可使用彻底限定名来防止类名的冲突,C#1中可使用空间别名来简化书写,空间别名用using关键字实现。但还有一些特殊状况,使用using并不能彻底解决,因此C#2中提供了下面几种特性:
咱们在构建命名空间和类的时候,尽可能避免出现冲突的状况,这个特性也较少用到。
当咱们但愿一个程序集中的类型能够被外部的某些程序集访问,这时若是设置成Public,就能够被全部的外部程序集访问。怎样只让部分程序集访问,就要使用友元程序集了,具体参考以前的博文《C#:友元程序集(http://blog.fwhyy.com/2010/11/csharp-a-friend-assembly/)》
可空类型就是容许值类型的值为null。一般值类型的值是不该该为null的,但咱们不少应用是和数据库打交道的,而数据库中的类型都是能够为null值的,这就形成了咱们写程序的时候有时须要将值类型设置为null。在C#1中一般使用”魔值“来处理这种状况,好比DateTiem.MinValue、Int32.MinValue。在ADO.NET中全部类型的空值能够用DBNull.Value来表示。C#2中可空类型主要是使用System.Nullable
Nullablei = 20; Nullableb = true;
C#2中也提供了更方便的定义方式,使用操做符?:
int? i = 20; bool? b = true;
C#2中对迭代器提供了更便捷的实现方式。提到迭代器,有两个概念须要了解
看下面一个例子:
public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { return new PersonEnumerator(this); } private string this[int index] { get { return _names[index]; } set { _names[index] = value; } } } public class PersonEnumerator : IEnumerator { private int _index = -1; private Person _p; public PersonEnumerator(Person p) { _p = p; } public object Current { get { return _p._names[_index]; } } public bool MoveNext() { _index++; return _index < _p._names.Length; } public void Reset() { _index = -1; } }
C#2中的迭代器变得很是便捷,使用关键字yield return关键字实现,下面是C#2中使用yield return的重写版本:
public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { foreach (string s in _names) { yield return s; } } }
匿名方法比较适用于定义必须经过委托调用的方法,用多线程来举个例子,在C#1中代码以下:
private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(DoWork)); thread.Start(); } private void DoWork() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action(this.ChangeLabel),i.ToString()); } } private void ChangeLabel(string i) { label1.Text = i + "/100"; }
使用C#2中的匿名方法,上面的例子中能够省去DoWork和ChangeLabel两个方法,代码以下:
private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(delegate() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action(delegate() { label1.Text = i + "/100"; })); } })); thread.Start(); }
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
3.0 | VS2008 | 2.0 | 3.0 3.5 |
若是说C#2中的核心是泛型的话,那么C#3中的核心就应是Linq了,C#3中的特性几乎都是为Linq服务的,但每一项特性均可以脱离Linq来使用。下面就来看下C#3中有哪些特性。
这个特性很是简单,就是使定义属性变得更简单了。代码以下:
public string Name { get; set; } public int Age { private set; get; }
隐式类型的局部变量是让咱们在定义变量时能够比较动态化,使用var关键字做为类型的占位符,而后由编译器来推导变量的类型。
扩展方法能够在现有的类型上添加一些自定义的方法,好比能够在string类型上添加一个扩展方法ToInt32,就能够像“20”.ToInt32()这样调用了。
具体参见《C#3.0学习(1)—隐含类型局部变量和扩展方法(http://blog.fwhyy.com/2008/02/learning-csharp-3-0-1-implied-type-of-local-variables-and-extension-methods/)》。
隐式类型虽然让编码方便了,但有些很多限制:
简化了对象和集合的建立,具体参见《C#3.0学习(2)—对象集合初始化器(http://blog.fwhyy.com/2008/02/learning-c-3-0-2-object-collection-initializer/)》。
和隐式类型的局部变量相似,能够不用显示指定类型来进行数组的定义,一般咱们定义数组是这样:
string[] names = { "oec2003", "oec2004", "oec2005" };
使用匿名类型数组能够想下面这样定义:
protected void Page_Load(object sender, EventArgs e) { GetName(new[] { "oec2003", "oec2004", "oec2005" }); } public string GetName(string[] names) { return names[0]; }
匿名类型是在初始化的时候根据初始化列表自动产生类型的一种机制,利用对象初始化器来建立匿名对象的对象,具体参见《C#3.0学习(3)—匿名类型(http://blog.fwhyy.com/2008/03/learning-csharp-3-0-3-anonymous-types/)》。
其实是一个匿名方法,Lambda表达的表现形式是:(参数列表)=>{语句},看一个例子,建立一个委托实例,获取一个string类型的字符串,并返回字符串的长度。代码以下:
Funcfunc = delegate(string s) { return s.Length; }; Console.WriteLine(func("oec2003"));
使用Lambda的写法以下:
Funcfunc = (string s)=> { return s.Length; }; Funcfunc1 = (s) => { return s.Length; }; Funcfunc2 = s => s.Length;
上面三种写法是逐步简化的过程。
是.NET3.5中提出的一种表达方式,提供一种抽象的方式将一些代码表示成一个对象树。要使用Lambda表达式树须要引用命名空间System.Linq.Expressions,下面代码构建一个1+2的表达式树,最终表达式树编译成委托来获得执行结果:
Expression a = Expression.Constant(1); Expression b = Expression.Constant(2); Expression add = Expression.Add(a, b); Console.WriteLine(add); //(1+2) FuncfAdd = Expression.Lambda<Func>(add).Compile(); Console.WriteLine(fAdd()); //3
Lambda和Lambda表达式树为咱们使用Linq提供了不少支持,若是咱们在作的一个管理系统使用了Linq To Sql,在列表页会有按多个条件来进行数据的筛选的功能,这时就可使用Lambda表达式树来进行封装查询条件,下面的类封装了And和Or两种条件:
public static class DynamicLinqExpressions { public static Expression<Func> True() { return f => true; } public static Expression<Func> False() { return f => false; } public static Expression<Func> Or(this Expression<Func> expr1, Expression<Func> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda<Func> (Expression.Or(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func> And(this Expression<Func> expr1, Expression<Func> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda<Func> (Expression.And(expr1.Body, invokedExpr), expr1.Parameters); } }
下面是获取条件的方法:
public Expression<Func> GetCondition() { var exp = DynamicLinqExpressions.True(); if (txtCourseName.Text.Trim().Length > 0) { exp = exp.And(g => g.CourseName.Contains(txtCourseName.Text.Trim())); } if (ddlGrade.SelectedValue != "-1") { exp=exp.And(g => g.GradeID.Equals(ddlGrade.SelectedValue)); } return exp; }
Linq是一个很大的话题,也是NET3.5中比较核心的内容,有不少书籍专门来介绍Linq,下面只是作一些简单的介绍,须要注意的是Linq并不是是Linq To Sql,Linq是一个大的集合,里面包含:
下面以Linq To Object为例子来看看Linq是怎么使用的:
public class UserInfo { public string Name { get; set; } public int Age { get; set; } } public class Test { static void Main() { Listusers = new List() { new UserInfo{Name="oec2003",Age=20}, new UserInfo{Name="oec2004",Age=21}, new UserInfo{Name="oec2005",Age=22} }; IEnumerableselectedUser = from user in users where user.Age > 20 orderby user.Age descending select user; foreach (UserInfo user in selectedUser) { Console.WriteLine("姓名:"+user.Name+",年龄:"+user.Age); } Console.ReadLine(); } }
能够看出,Linq可让咱们使用相似Sql的关键字来对集合、对象、XML等进行查询。
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
4.0 | VS2010 | 4.0 | 4.0 |
VB在很早就已经支持了可选参数,而C#知道4了才支持,顾名思义,可选参数就是一些参数能够是可选的,在方法调用的时候能够不用输入。看下面代码:
public class Test { static void Main() { Console.WriteLine(GetUserInfo()); //姓名:ooec2003,年龄:30 Console.WriteLine(GetUserInfo("oec2004", 20));//姓名:ooec2004,年龄:20 Console.ReadLine(); } public static string GetUserInfo(string name = "oec2003", int age = 30) { return "姓名:" + name + ",年龄:" + age.ToString(); } }
命名实参是在制定实参的值时,能够同时指定相应参数的名称。编译器能够判断参数的名称是否正确,命名实参可让咱们在调用时改变参数的顺序。命名实参也常常和可选参数一块儿使用,看下面的代码:
static void Main() { Console.WriteLine(Cal());//9 Console.WriteLine(Cal(z: 5, y: 4));//25 Console.ReadLine(); } public static int Cal(int x=1, int y=2, int z=3) { return (x + y) * z; }
经过可选参数和命名参数的结合使用,咱们能够减小代码中方法的重载。
C#使用dynamic来实现动态类型,在没用使用dynamic的地方,C#依然是静态的。静态类型中当咱们要使用程序集中的类,要调用类中的方法,编译器必须知道程序集中有这个类,类里有这个方法,若是不能事先知道,编译时会报错,在C#4之前能够经过反射来解决这个问题。看一个使用dynamic的小例子:
dynamic a = "oec2003"; Console.WriteLine(a.Length);//7 Console.WriteLine(a.length);//string 类型不包含length属性,但编译不会报错,运行时会报错 Console.ReadLine();
您可能会发现使用dynamic声明变量和C#3中提供的var有点相似,其余他们是有本质区别的,var声明的变量在编译时会去推断出实际的类型,var只是至关于一个占位符,而dynamic声明的变量在编译时不会进行类型检查。
dynamic用的比较多的应该是替代之前的反射,并且性能有很大提升。假设有一个名为DynamicLib的程序集中有一个DynamicClassDemo类,类中有一个Cal方法,下面看看利用反射怎么访问Cal方法:
namespace DynamicLib { public class DynamicClassDemo { public int Cal(int x = 1, int y = 2, int z = 3) { return (x + y) * z; } } } static void Main() { Assembly assembly = Assembly.Load("DynamicLib"); object obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Type type = obj.GetType(); MethodInfo method = type.GetMethod("Cal"); Console.WriteLine(method.Invoke(obj, new object[] { 1, 2, 3 }));//9 Console.ReadLine(); }
用dynamic的代码以下:
Assembly assembly = Assembly.Load("DynamicLib"); dynamic obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Console.WriteLine(obj.Cal()); Console.ReadLine();
在先后端分离的模式下,WebAPI接口的参数也能够采用dynamic来定义,直接就能够解析前端传入的json参数,不用每个接口方法都定义一个参数类型。很差的地方就是经过Swagger来生产API文档时,不能明确的知道输入参数的每一个属性的含义。
C#4中还有一些COM互操做性的改进和逆变性和协变性的改进,我几乎没有用到,因此在此就不讲述了。
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
5.0 | VS2012\2013 | 4.0 | 4.5 |
异步处理是C#5中很重要的一个特性,会涉及到两个关键字:async和await,要讲明白这个须要单独写一篇来介绍。
能够简单理解为,当Winform窗体程序中有一个耗时操做时,若是是同步操做,窗体在返回结果以前会卡死,固然在C#5以前的版本中有多种方法能够来解决这个问题,但C#5的异步处理解决的更优雅。
与其说是一个特性,不如说是对以前版本问题的修复,看下面的代码:
public static void CapturingVariables() { string[] names = { "oec2003","oec2004","oec2005"}; var actions = new List(); foreach(var name in names) { actions.Add(() => Console.WriteLine(name)); } foreach(Action action in actions) { action(); } }
这段代码在以前的C#版本中,会连续输出三个oec2005,在C#5中会按照咱们的指望依次输出oec200三、oec200四、oec2005。
若是您的代码在以前的版本中有利用到这个错误的结果,那么在升级到C#5或以上版本中就要注意了。
咱们的程序一般是以release形式发布,发布后很难追踪到代码执行的具体信息,在C#5中提供了三种特性(Attribute), 容许获取调用者的当前编译器的执行文件名、所在行数与方法或属性名称。代码以下:
static void Main(string[] args) { ShowInfo(); Console.ReadLine(); } public static void ShowInfo( [CallerFilePath] string file = null, [CallerLineNumber] int number = 0, [CallerMemberName] string name = null) { Console.WriteLine($"filepath:{file}"); Console.WriteLine($"rownumber:{number}"); Console.WriteLine($"methodname:{name}"); }
调用结果以下:
filepath:/Users/ican_macbookpro/Projects/CsharpFeature/CsharpFeature5/Program.cs rownumber:12 methodname:Main
C# | VS版本 | CLR版本 | .NET Framework |
---|---|---|---|
6.0 | VS2015 | 4.0 | 4.6 |
在C#6中提供了很多的新功能,我认为最有用的就是Null条件运算符和字符串嵌入。
在C#中,一个常见的异常就是“未将对象引用到对象的实例”,缘由是对引用对象没有作非空判断致使。在团队中虽然再三强调,但依然会在这个问题上栽跟头。下面的代码就会致使这个错误:
class Program { static void Main(string[] args) { //Null条件运算符 User user = null; Console.WriteLine(user.GetUserName()); Console.ReadLine(); } } class User { public string GetUserName() => "oec2003"; }
要想不出错,就须要对user对象作非空判断
if(user!=null) { Console.WriteLine(user.GetUserName()); }
在C#6中能够用很简单的方式来处理这个问题
//Null条件运算符 User user = null; Console.WriteLine(user?.GetUserName());
注:虽然这个语法糖很是简单,也很好用,但在使用时也须要多想一步,当对象为空时,调用其方法返回的值也是空,这样的值对后续的操做会不会有影响,若是有,仍是须要作判断,并作相关的处理。
字符串嵌入能够简化字符串的拼接,很直观的就能够知道须要表达的意思,在C#6及以上版本中都应该用这种方式来处理字符串拼接,代码以下:
//字符串嵌入 string name = "oec2003"; //以前版本的处理方式1 Console.WriteLine("Hello " + name); //以前版本的处理方式2 Console.WriteLine(string.Format("Hello {0}",name)); //C#6字符串嵌入的处理方式 Console.WriteLine($"Hello {name}");
C# | VS版本 | .NET Framework |
---|---|---|
7.0 | VS2017 15.0 | .NET Core1.0 |
7.1 | VS2017 15.3 | .NET Core2.0 |
7.2 | VS2017 15.5 | .NET Core2.0 |
7.3 | VS2017 15.7 | .NET Core2.1 |
此特性简化了out变量的使用,以前的版本中使用代码以下:
int result = 0; int.TryParse("20", out result); Console.WriteLine(result);
优化后的代码,不须要事先定义一个变量
int.TryParse("20", out var result); Console.WriteLine(result);
这也是一个减小咱们编码的语法糖,直接看代码吧
public class PatternMatching { public void Test() { Listlist = new List(); list.Add(new Man()); list.Add(new Woman()); foreach (var item in list) { //在以前版本中此处须要作类型判断和类型转换 if (item is Man man) Console.WriteLine(man.GetName()); else if (item is Woman woman) Console.WriteLine(woman.GetName()); } } } public abstract class Person { public abstract string GetName(); } public class Man:Person { public override string GetName() => "Man"; } public class Woman : Person { public override string GetName() => "Woman"; }
详细参考官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/pattern-matching
能够在方法中写内部方法,在方法中有时须要在多个代码逻辑执行相同的处理,以前的作法是在类中写私有方法,如今可让这个私有方法写在方法的内部,提升代码可读性。
static void LocalMethod() { string name = "oec2003"; string name1 = "oec2004"; Console.WriteLine(AddPrefix(name)); Console.WriteLine(AddPrefix(name1)); string AddPrefix(string n) { return $"Hello {n}"; } }
这个最大的好处是,在控制台程序中调试异步方法变得很方便。
static async Task Main() { await SomeAsyncMethod(); }
能够限制在同一个程序集中的派生类的访问,是对protected internal的一种补强,protected internal是指同一程序集中的类或派生类进行访问。
每一个特性都须要咱们去编码实现下,了解了真正的含义和用途,咱们才能在工做中灵活的运用。
本文所涉及到的实例代码后面也会上传到Github上。
但愿本文对您有所帮助。