学习和使用.NET平台已有六七个年头了,05年开始学习.NET的时候,当时网上的资源不像现在这样丰富,所以去电脑城买了张盗版的VS2005的光盘,安装时才发现是VS2003,当时有一种被坑的感觉,但也正是如此,让我有了一个完整的.NET的学习生涯。 从我从业的经历来看VS2003和VS2008是使用的比较多的,一些历史遗留的老项目由种种原因没有升级,依然在使用VS2003。我现在所在的公司也只使用VS2003和VS2008这两种开发工具,尽管TFS已经是2010了。很多时候公司觉得够用也就不去升级开发工具了。从1.0到4.0的变化主要体现在DotNet平台、语言和开发工具这三个方面。本文只是概括性的讲述在每个版本里都有些什么新的特性。先看下面的一张表格,描述的不同版本的出生年份、对应的VS版本和CLR的版本。
上图中需要注意的一点是:2.0 3.0 3.5的CLR版本都是2.0。
在1.1版本中,从语言的角度就是基本的面向对象的语法,可以说任何一般C#语言的书籍都包含了C#1的所有内容。工具方面VS2003已经很强大了,即便是到了现在的VS2012,我们80%时候用到的也只不过是20%的功能。 在VS2003中创建ASP.NET项目默认是跟IIS相关联的,从VSS中拉VS2003的代码时需要先在IIS中创建好站点,端口要和VSS中代码的相对应,但有时会出现很多问题。我一般的做法是将代码拉到本地后,修改解决方案文件中的路径为本地路径,然后修改项目文件中的ProjectType为Local,默认是Web,就可以很顺利打开项目了。
2.0中对应VS2005我用的也不多,因为很快就被VS2008替代了,不过在语言方面却带来了很多新的东西。
★C#2中最重要的一个特性应该就是泛型。泛型的用处就是在一些场景下可以减少强制转换来提高性能。在C#1中就有很多的强制转换,特别是对一些集合进行遍历时,如ArrayList、HashTable,因为他们是为不同数据类型设计的集合,所以他们中键和值的类型都是object,这就意味着会平凡发生装箱拆箱的才做。C#2中有了泛型,所以我们可以使用List<T>、Dictionary<Tkey,TValue> 。泛型能够带来很好的编译时类型检查,也不会有装箱拆箱的才做,因为类型是在使用泛型的时候就已经指定了。 DotNet已经通过了很多的泛型类型供我们使用,如上面提到的List<T>,Dictionary<Tkey,TValue>,我们也可以自己来创建泛型类型(类、接口、委托、结构)或是方法。在定义泛型类型或时可以通过定义泛型约束来对泛型参数进行限制,更好的使用编译时检查。泛型约束是通过关键字where来实现的,C#2中的泛型约束有4种:
- 引用类型约束:确保类型实参是引用类型,使用where T:class来表示;
- 值类型约束:确保类型实参是值类型,使用where T:truct来表示;
- 构造函数类型约束,使用where T:new()来表示;
- 转换类型约束:约束类型实参是另外的一种类型,例如:where T:IDisposable 。
★C#2提供了分部类(Partil)。分部类可以允许我们在多个文件中为一个类型编写代码,在Asp.Net2.0中用的极为广泛。新建一个Aspx页面,页面的CodeBehind和页面中的控件的定义就是通过分部类来实现的。如下:
public partial class _Default : System.Web.UI.Page public partial class _Default
分布类使用关键字partial来定义,当一个类中的代码非常多时,可以使用分部类来进行拆分,这对代码的阅读很有好处,而且不会影响调用。
★C#2中增加了静态类,静态类中的公用方法必须也是静态的,可以由类名直接调用,不需要实例化,比较适用于编写一些工具类。如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关键字实现,看下面一个例子:
namespace NameSpaceDemo1 { public class ClassDemo{} } namespace NameSpaceDemo2 { public class ClassDemo{} }
using Demo1 = NameSpaceDemo1; using Demo2 = NameSpaceDemo2; namespace WebApplication2 { public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Demo1.ClassDemo c1 = new Demo1.ClassDemo(); Demo2.ClassDemo c2 = new Demo2.ClassDemo(); } } }
如果我们在使用中添加了名为Demo1或是Demo2的类,那么空间别名的使用就会受到影响,在C#1中就得使用完全限定名了。在C#2中可以用“::”符号来解决,如下面代码:
namespace WebApplication2 { class Demo1 { } public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Demo1::ClassDemo c1 = new Demo1::ClassDemo(); Demo2.ClassDemo c2 = new Demo2.ClassDemo(); } } }
如果有两个类,一个在包含在命名空间中,一个没有包含在命名空间中,那么要访问没有包含在命名空间中的类时,在C#2中可以使用“global::”,代码如下:
public class ClassDemo{} namespace NameSpaceDemo1 { public class ClassDemo{} public class Test { static void Main() { Console.WriteLine(typeof(ClassDemo)); //NameSpaceDemo1.ClassDemo Console.WriteLine(typeof(global::ClassDemo)); //ClassDemo Console.ReadLine(); } } }
还有一种命名空间别名叫外部别名,用来处理不同的dll中存在相同的命名空间和类型即完全限定名相同,假设Firs.dll和Second.dll中都有完全限定名为“Demo.Class1"的类型,这时使用完全限定名也无法区分,就必须使用外部别名了,代码如下:
extern alias FirstDemo; extern alias SecondDemo; using System; namespace NameSpaceDemo1 { public class ClassDemo{} public class Test { static void Main() { Console.WriteLine(typeof(FirstDemo::Demo.Class1)); Console.WriteLine(typeof(SecondDemo::Demo.Class1)); Console.ReadLine(); } } }
使用外部别名有两点需要注意:
1 extern alias必须声明在最上方;
2 引用了第三方dll,如First.dll和Second.dll,需要修改引入dll的Aliasces属性值,像上面的例子中分别是FirstDemo和SecondDemo,在引入的dll上点击右键,选择属性,如下:
★C#2中还提供了友元程序集,具体参考我以前博文《C#:友元程序集》
★C#2中提供了可空类型,说直白点就是允许值类型的值为null。正常来说值类型的值是不应该为null的,但我们的很多应用是和数据库打交道的,而数据库中的类型都是可以为null值的,这就造成了我们写程序的时候有时需要将值类型设置为null。在C#1中通常使用”魔值“来处理这种情况,比如DateTiem.MinValue、Int32.MinValue。在ADO.NET中所有类型的空值可以用DBNull.Value来表示。C#2中可空类型主要是使用System.Nullable<T>的泛型类型,类型参数T有值类型约束。可以像下面这样来定义可空类型:
Nullable<int> i = 20; Nullable<bool> b = true;
C#2中也提供了更方便的定义方式,使用操作符?:
int? i = 20; bool? b = true;
★C#2中对迭代器提供了更便捷的实现方式。提到迭代器,有两个概念需要了解,可枚举对象和枚举器,实现了System.Collections.IEnumerable接口的对象是可枚举对象,这些对象可以被C#中的foreach进行迭代;实现了System.Collections.IEnumeror接口的对象被称为枚举器。在C#1中实现迭代器非常繁琐,看下面一个例子:
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#2中提供了匿名方法,匿名方法比较适用于定义必须通过委托调用的方法,用多线程来举个例子,在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<string>(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#2对数据类型的转换提供了TryParse,可以更好的进行容错,看下面代码:
static void Main() { int a = 0; string b = "30", c = "aaa"; int.TryParse(b, out a); Console.WriteLine(a);//30 int.TryParse(c, out a); Console.WriteLine(a);//0 Console.ReadLine(); }
如果说C#2中的核心是泛型的话,那么C#3中的核心就应是Linq了,C#3中的特性几乎都是为Linq服务的,但每一项特性都可以脱离Linq来使用。下面就来看下C#3中有哪些特性。 ★自动实现的属性,这个特性非常简单,就是使定义属性变得更简单了。代码如下:
public string Name { get; set; } public int Age { private set; get; }
在VS中输入prop,并且按tab键两次,就可以快速的添加属性。
★隐式类型的局部变量,该特性是让我们在定义变量时可以比较动态化,使用var关键字作为类型的占位符,然后由编译器来推导变量的类型。具体参见《C#3.0学习(1)---隐含类型局部变量和扩展方法》。隐式类型虽然让编码方便了,但有些不少限制:
- 被声明的变量只能是局部变量,而不能是静态变量和实例字段;
- 变量在声明的同时必须初始化,初始化值不能为null;
- 语句中只能声明一个变量;
★对象集合初始化器,简化了对象和集合的创建,具体参见《C#3.0学习(2)---对象集合初始化器》。
★隐式类型的数组,和隐式类型的局部变量类似,可以不用显示指定类型来进行数组的定义,通常我们定义数组是这样:
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)—匿名类型》。
★扩展方法,可以在现有的类型上添加一些自定义的方法,比如可以在string类型上添加一个扩展方法ToInt32,就可以像“20”.ToInt32()这样调用了。具体参见《C#3.0学习(1)---隐含类型局部变量和扩展方法》。
★Lambda表达式,实际上是一个匿名方法,Lambda表达的表现形式是:(参数列表)=>{语句},看一个例子,创建一个委托实例,获取一个string类型的字符串,并返回字符串的长度。代码如下:
Func<string, int> func = delegate(string s) { return s.Length; }; Console.WriteLine(func("oec2003"));
使用Lambda的写法如下:
Func<string, int> func = (string s)=> { return s.Length; }; Func<string, int> func1 = (s) => { return s.Length; }; Func<string, int> func2 = s => s.Length;
上面三种写法是逐步简化的过程。
★Lambda表达式树,是.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) Func<int> fAdd = Expression.Lambda<Func<int>>(add).Compile(); Console.WriteLine(fAdd()); //3
Lambda和Lambda表达式树为我们使用Linq提供了很多支持,如果我们在做的一个管理系统使用了Linq To Sql,在列表页会有按多个条件来进行数据的筛选的功能,这时就可以使用Lambda表达式树来进行封装查询条件,下面的类封装了And和Or两种条件:
public static class DynamicLinqExpressions { public static Expression<Func<T, bool>> True<T>() { return f => true; } public static Expression<Func<T, bool>> False<T>() { return f => false; } public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>()); return Expression.Lambda<Func<T, bool>> (Expression.Or(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>()); return Expression.Lambda<Func<T, bool>> (Expression.And(expr1.Body, invokedExpr), expr1.Parameters); } }
下面是获取条件的方法:
public Expression<Func<Courses, bool>> GetCondition() { var exp = DynamicLinqExpressions.True<Courses>(); 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 To XML:应用于XML;
- Linq To Sql:应用于SqlServer数据库;
- Linq To DataSet: DataSet;
- Linq To Entities:应用于SqlServer之外的关系数据库,我们还可以通过Linq的扩展框架来实现更多支持Linq的数据源。
下面以Linq To Object为例子来看看Linq是怎么使用的:
public class UserInfo { public string Name { get; set; } public int Age { get; set; } } public class Test { static void Main() { List<UserInfo> users = new List<UserInfo>() { new UserInfo{Name="oec2003",Age=20}, new UserInfo{Name="oec2004",Age=21}, new UserInfo{Name="oec2005",Age=22} }; IEnumerable<UserInfo> selectedUser = 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#3&NET3的一些特性也就说的差不多了,下面就看看C#4&NET4又有哪些变化吧。
★NET4随着VS2010一起发布,VS2010相比较VS2008有了很大的变化,有关VS2010的功能改进方面可以看我以前的博文:
★NET4 FramWork也有一些更新,比如提供了BigInteger、Complex、Tuple等,具体参见《Net4.0---Framwork新增特性》。
★在ASP.NET方面也有不少方便实用的特性,可以参考以下博文:
- Net4.0---AspNet中URL重写的改进
- Net4.0---AspNet中的控件HyperLink的改进(支持路由跳转)
- Net4.0---对HTML净化的处理
- Net4.0---数据绑定控件(GridView ListView…)中的ClientIDRowSuffix属性
在C#方面,比较典型的两个特性就是可选参数和命名实参,还有一个大的特性--动态类型。
★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();
★C#4中还有一些COM互操作性的改进和逆变性和协变性的改进,我几乎没有用到,所以在此就不讲述了。
至此,所以内容已经完成,算是我对C#&NET1.0-4.0知识的一个梳理, 由于本人能力有限,本文只是我所涉及到的一小部分,随着我以后的学习,我会再开篇来记录新的学习体会。希望本文对你有所帮助。