学习和使用.NET平台已有六七个年头了,05年开始学习.NET的时候,当时网上的资源不像现在这样丰富,所以去电脑城买了张盗版的VS2005的光盘,安装时才发现是VS2003,当时有一种被坑的感觉,但也正是如此,让我有了一个完整的.NET的学习生涯。

从我从业的经历来看VS2003和VS2008是使用的比较多的,一些历史遗留的老项目由种种原因没有升级,依然在使用VS2003。我现在所在的公司也只使用VS2003和VS2008这两种开发工具,尽管TFS已经是2010了。很多时候公司觉得够用也就不去升级开发工具了。从1.0到4.0的变化主要体现在DotNet平台、语言和开发工具这三个方面。本文只是概括性的讲述在每个版本里都有些什么新的特性。先看下面的一张表格,描述的不同版本的出生年份、对应的VS版本和CLR的版本。

2012-07-04_230408

上图中需要注意的一点是: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、Dictionary 。泛型能够带来很好的编译时类型检查,也不会有装箱拆箱的才做,因为类型是在使用泛型的时候就已经指定了。

DotNet已经通过了很多的泛型类型供我们使用,如上面提到的List,Dictionary,我们也可以自己来创建泛型类型(类、接口、委托、结构)或是方法。在定义泛型类型或时可以通过定义泛型约束来对泛型参数进行限制,更好的使用编译时检查。泛型约束是通过关键字where来实现的,C#2中的泛型约束有4种:

  • 引用类型约束:确保类型实参是引用类型,使用where T:class来表示;
  • 值类型约束:确保类型实参是值类型,使用where T:truct来表示;
  • 构造函数类型约束,使用where T:new()来表示;
  • 转换类型约束:约束类型实参是另外的一种类型,例如:where T:IDisposable 。

★C#2提供了分部类(Partil)。分部类可以允许我们在多个文件中为一个类型编写代码,在Asp.Net2.0中用的极为广泛。新建一个Aspx页面,页面的CodeBehind和页面中的控件的定义就是通过分部类来实现的。如下:

1
2
public partial class _Default : System.Web.UI.Page
public partial class _Default

分布类使用关键字partial来定义,当一个类中的代码非常多时,可以使用分部类来进行拆分,这对代码的阅读很有好处,而且不会影响调用。

★C#2中增加了静态类,静态类中的公用方法必须也是静态的,可以由类名直接调用,不需要实例化,比较适用于编写一些工具类。如System.Math类就是静态类。工具类有一些特点,如:所有成员都是静态的、不需要被继承、不需要进行实例化。在C#1中我们可以通过如下代码来实现:

1
2
3
4
5
6
7
8
9
10
11
12
//声明为密封类防止被继承
public sealed class StringHelper
{
//添加私有无参构造函ˉ数防止被实例化,如果不添加私有构造函数 //会自动生成共有无参构造函数
private StringHelper(){};
public static int StringToInt32(string input)
{
int result=0;
Int32.TryParse(input, out result);
return result;
}
}

C#2中可以使用静态类来实现:

1
2
3
4
5
6
7
8
9
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的访问级别,如下:

1
2
3
4
5
6
private string name;
public string Name
{
get { return name; }
private set { name = value; }
}

需要注意的是,不能讲属性设置为私有的,而将其中的get或是set设置成公有的,也不能给set和get设置相同的访问级别,当set和get的访问级别相同时,我们可以直接设置在属性上。

★命名空间可以用来组织类,当不同的命名空间中有相同的类时,可以使用完全限定名来防止类名的冲突,C#1中可以使用空间别名来简化书写,空间别名用using关键字实现,看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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中可以用“::”符号来解决,如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
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::”,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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″的类型,这时使用完全限定名也无法区分,就必须使用外部别名了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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上点击右键,选择属性,如下:

2012-07-04_2304081

★C#2中还提供了友元程序集,具体参考我以前博文《C#:友元程序集

★C#2中提供了可空类型,说直白点就是允许值类型的值为null。正常来说值类型的值是不应该为null的,但我们的很多应用是和数据库打交道的,而数据库中的类型都是可以为null值的,这就造成了我们写程序的时候有时需要将值类型设置为null。在C#1中通常使用”魔值“来处理这种情况,比如DateTiem.MinValue、Int32.MinValue。在ADO.NET中所有类型的空值可以用DBNull.Value来表示。C#2中可空类型主要是使用System.Nullable的泛型类型,类型参数T有值类型约束。可以像下面这样来定义可空类型:

1
2
Nullable<int> i = 20;
Nullable<bool> b = true;

C#2中也提供了更方便的定义方式,使用操作符?:

1
2
int? i = 20;
bool? b = true;

★C#2中对迭代器提供了更便捷的实现方式。提到迭代器,有两个概念需要了解,可枚举对象和枚举器,实现了System.Collections.IEnumerable接口的对象是可枚举对象,这些对象可以被C#中的foreach进行迭代;实现了System.Collections.IEnumeror接口的对象被称为枚举器。在C#1中实现迭代器非常繁琐,看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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的重写版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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两个方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
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,可以更好的进行容错,看下面代码:

1
2
3
4
5
6
7
8
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中有哪些特性。

★自动实现的属性,这个特性非常简单,就是使定义属性变得更简单了。代码如下:

1
2
public string Name { get; set; }
public int Age { private set; get; }

在VS中输入prop,并且按tab键两次,就可以快速的添加属性。

★隐式类型的局部变量,该特性是让我们在定义变量时可以比较动态化,使用var关键字作为类型的占位符,然后由编译器来推导变量的类型。具体参见《C#3.0学习(1)—隐含类型局部变量和扩展方法》。隐式类型虽然让编码方便了,但有些不少限制:

  • 被声明的变量只能是局部变量,而不能是静态变量和实例字段;
  • 变量在声明的同时必须初始化,初始化值不能为null;
  • 语句中只能声明一个变量;

★对象集合初始化器,简化了对象和集合的创建,具体参见《C#3.0学习(2)—对象集合初始化器》。

★隐式类型的数组,和隐式类型的局部变量类似,可以不用显示指定类型来进行数组的定义,通常我们定义数组是这样:

1
string[] names = { "oec2003", "oec2004", "oec2005" };

使用匿名类型数组可以想下面这样定义:

1
2
3
4
5
6
7
8
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类型的字符串,并返回字符串的长度。代码如下:

1
2
Func<string, int> func = delegate(string s) { return s.Length; };
Console.WriteLine(func("oec2003"));

使用Lambda的写法如下:

1
2
3
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的表达式树,最终表达式树编译成委托来得到执行结果:

1
2
3
4
5
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两种条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
}

下面是获取条件的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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是怎么使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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方面也有不少方便实用的特性,可以参考以下博文:

在C#方面,比较典型的两个特性就是可选参数和命名实参,还有一个大的特性–动态类型。

★VB在很早就已经支持了可选参数,而C#知道4了才支持,顾名思义,可选参数就是一些参数可以是可选的,在方法调用的时候可以不用输入。看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
}

★命名实参是在制定实参的值时,可以同时指定相应参数的名称。编译器可以判断参数的名称是否正确,命名实参可以让我们在调用时改变参数的顺序。命名实参也经常和可选参数一起使用,看下面的代码:

1
2
3
4
5
6
7
8
9
10
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的小例子:

1
2
3
4
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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的代码如下:

1
2
3
4
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知识的一个梳理, 由于本人能力有限,本文只是我所涉及到的一小部分,随着我以后的学习,我会再开篇来记录新的学习体会。希望本文对你有所帮助。