Visual Studio 2008和.NET 3.5是建立在.NET2.0核心的基础之上,C# 3.0新语言特性在.NET2.0基础上进行了改进,这些改进的功能可以大大简化我们编写程序。在此为了自己学习记录下来,并与大家分享
.NET 3.5的新特性包括:
自动属性(Auto-Implemented Properties)
隐含类型局部变量(Local Variable Type Inference)
匿名类型(Anonymous Types)
对象与集合初始化器(Object and Collection Initializers)
扩展方法(Extension Methods)
Lambda表达式和Lambda表达式树 (Lambda Expression and Lambda Expression Trees)
LINQ,语言级集成查询(Language INtegrated Query)
自动属性(Auto-Implemented Properties)
自动属性可以避免原来这样我们手工声明一个私有成员变量以及编写get/set逻辑,在VS2008中可以像下面这样编写一个类,编译器会自动地生成私有变量和默认的get/set 操作。你也可以分别定义get和set的"protected"等访问级别。
在.Net2.0框架下,我们可以这样写一个User类:
public class User
{
private int _id;
private string _name;
private int _age;
public int Id
{
get { return _id; }
set { _id = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Age
{
get { return _age; }
set { _age = value; }
}
}现在,可以这样简化:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
我们可以利用VS.NET提供的ildasm进行反编译,可以发现.NET帮我定义了私有变量,在此不再赘言。
隐含类型局部变量(Local Variable Type Inference)
C#3.0引进了var这个新关键字,在声明局部变量时可用于替代原先的类型名,即当一个变量声明标识为var类型并且该范围域中没有var名称类型存在,那么这个声明就称为隐含类型局部变量。如下(等同于//后面的显式声明):
var i = 5;//int
var j = 23.56;//double
var k = "C Sharp";//string
var x;//错误
var y = null;//错误
var z = { 1, 2, 3 };//错误
注意事项:
必须对var声明的变量赋值,因为C#中的var并不等同于VB于Javascript中的var,后者中 的var为一种变量类型,而在C#中var并不是一种类型,它只用来隐式的声明变量,而你定义的变量类型由编译器判断;
Var声明的变量不能赋null;
Var只能声明局部变量;
不允许,改变var变量的类型
Var x = 100;
X = "Hello world!"
由此可知,var的变量也是强类型的变量,具有安全性。
匿名类型(Anonymous Types)
匿名类型允许定义行内类型,无须显式定义类型。常和var配合使用来声明匿名类型。
var p1 = new { Id = 1, Name = "tony", Age = 21 };//属性也不需要申明
var p2 = new { Id = 2, Name = "dream", Age = 21 };
var p3 = new { Id =3 , age = 21, Name = "tony"}
p1 = p2;//p1,p2结构相同,可以互相赋值 ,p1与p3不是相同的对象,所以使用匿名类型时我们尽量将具有相同属性的变量,属性的顺序保持一致。
在这里编译器会认为p1,p2相当于:
public class SomeType
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}那么数组怎么定义呢?使用"new[]"关键字来声明数组,加上数组的初始值列表。像这样:
var intArray = new[] { 2, 3, 5, 6 };
var strArray = new[] { "Hello", "World" };
var anonymousTypeArray = new[]
{
new { Name = "tony", Age = 21 },
new { Name = "dream", Age = 22 }
};
var a = intArray[0];
var b = strArray[0];
var c = anonymousTypeArray[1].Name;
可以使用new关键字调用匿名初始化器创建一个匿名类型的对象。
匿名类型直接继承自System. Object。
匿名类型的成员是编译器根据初始化器推断而来的一些读写属性。
那么该如何用匿名类型来构造泛型容器呢?像下面这样:
var user = new { name = "Tom", age = 21 };
var userList = Enumerable.Repeat(user, 0).ToList(); // or var userList = (new[] { user }).ToList();
userList.Add(new { name = "jef", age = 12 });
userList.Add(new { name = "adb", age = 13 });
userList.Add(new { name = "dwef", age = 22 });
上面两个创建list的方式有点小区别,Enumerable.Repeat(user, 0).ToList()中没有包含user对象,而var userList = (new[] { user }).ToList()则包含了user对象。
对象初始化器 (Object Initializers) :
.NET2.0框架中的类型非常依赖于属性。当生成对象实例和使用新的类型时,在.Net2.0时候我们像这样写:
User user = new User();
user.Id = 1;
user.Name = "tony";
user.Age = 22;在VS2008中,编译器会自动地生成合适的属性setter代码,使得原来几行的属性赋值操作可以在一行完成。我们可以这样简化:像这样,对象初始化器由一系列成员对象组成,其对象必须初始化,用逗号间隔,使用{}封闭。
User user = new User { Id = 1, Name = "tony", Age = 21 };又例如,我把二个人加到一个基于泛型的类型为User的List集合中:
List<User> user = new List<User>{
new User{Id=1,Name="tony",Age=21},
new User{Id=2,Name="dream",Age=12},
};
如果有相同名字和类型的两个对象初始化器将会产生相同的实例,可以相互赋值。例如:
User user = new User { Id = 1, Name = "tony", Age = 21 };
User user2 = new User { Id = 2, Name = "tony", Age = 12 };
user = user2;
除了在初始化类时设置简单的属性值外,对象初始化器特性也允许我们设置更复杂的嵌套(nested)属性类型。例如我们可以在上面定义的User类型同时拥有一个属于School类型的叫"School"的属性:
User user = new User
{
Id = 1,
Name = "tony",
Age = 21,
School = new School
{
City = "Beijing",
Name = "BTBU"
}
};
集合初始化器(Collection Initializers):
集合初始化器由一系列集合对象组成,用逗号间隔,使用{}封闭。
集合初始化器可以简化把几个对象一起添加到一个集合,编译器会自动为你做集合插入操作。例如我把七个数加到一个基于泛型的类型为int的List集合中
List<int> num = new List<int> { 0, 1, 2, 6, 7, 8, 9 };对象与集合初始化器要点
对象初始化器实际上利用了编译器对对象中对外可见的字段和属性进行按序赋值。
对象初始化器允许只给一部分属性赋值,包括internal访问级别
对象初始化器可以结合构造函数一起使用,并且构造函数初始化先于对象初始化器执行。
集合初始化器会对初始化器中的元素进行按序调用ICollection<T>.Add(T)方法,所以只有具有Add方法的类才可以使用这种方法添加一个元素,例如ArrayList等,例如HashTable,字典等就不支持这种添加方式,因为其存在key,value两个值。
注意对象初始化器和集合初始化器中成员的可见性和调用顺序。
对象与集合初始化器同样是一种编译时技术。
扩展方法(Extension Methods)
往往我们需要对CLR类型进行一些操作,但苦于无法扩展CLR类型的方法,只能创建一些helper方法,或者继承类。我们来修改上面的User类:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Read()
{
return "Id:" + Id + "姓名:" + Name + "年龄:" + Age;
}
}然后调用
var user = new { Id = 1, Name = "tony", Age = 21 };
var str = user.Read();现在有了扩展方法就方便多了。
扩展方法允许开发人员往一个现有的CLR类型的公开契约(contract)中添加新的方法,而不用生成子类或者重新编译原来的类型。扩展方法有助于把今天动态语言中流行的对duck typing的支持之灵活性,与强类型语言之性能和编译时验证融合起来。
扩展方法是可以通过使用实例方法语法调用的静态方法。效果上,使得附加的方法扩展已存在类型和构造类型成为可能。他可以对现有类功能进行扩充,从而使该类型的实例具有更多的方法(功能)。
扩展方法允许我们在不改变源代码的情况下扩展(即添加不能修改)现有类型中的实例方法。
扩展方法给我们一个怎样的思路呢?我们一步一步做一下!
首先声明扩展方法:通过指定关键字this修饰方法的第一个参数。注意扩展方法仅可声明在静态类中。扩展方法具备所有常规静态方法的所有能力,可以使用实例方法语法来调用。接着就可以调用扩展方法了。下面通过一个具体的实例分析一下:
例如我们要检查一个字符串变量是否是合法的电子邮件地址?在.Net2.0框架下像这样:
var email = "[email protected]";
if (EmailValidator.IsValid(email))
{
Response.Write("tony提示:这是一个正确的邮件地址");
}而使用扩展方法的话,我可以添加"IsValidEmailAddress()"方法到string类本身中去,该方法返回当前字符串实例是否是个合法的字符串。
if (email.IsValidEmailAddress())
{
Response.Write("tony提示:这是一个正确的邮件地址");
}我们是怎么把这个IsValidEmailAddress()方法添加到现有的string类里去的呢?先定义一个静态类,再定义"IsValidEmailAddress"这个静态的法来实现的。
public static class Extensions//静态类
{
public static bool IsValidEmailAddress(this string s)
//静态方法和this
{
Regex regex = new Regex(@"^[/w-/.]+@([/w-]+/.)+[/w-]{2,4}$");
return regex.IsMatch(s);
}
}注意,上面的静态方法在第一个类型是string的参数变量前有个"this"关键词,这告诉编译器,这个特定的扩展方法应该添加到类型为"string"的对象中去。然后在IsValidEmailAddress()方法实现里,我可以访问调用该方法的实际string实例的所有公开属性/方法/事件,取决于它是否是合法电子邮件地址来返回true/false。
扩展方法不仅能够应用到个别类型上,也能应用到.NET框架中任何基类或接口上。即可用于整个.NET框架丰富的可组合的框架层扩展。
扩展方法要点
扩展方法的本质为将实例方法调用在编译期改变为静态类中的静态方法调用。事实上,它确实拥有静态方法所具有的所有功能。
扩展方法的作用域是整个namespace可见的,并且可以通过using namespace来导入其它命名空间中的扩展方法。
扩展方法的优先级:现有实例方法优先级最高,其次为最近的namespace下的静态类的静态方法,最后为较远的namespace下的静态类的静态方法。
扩展方法是一种编译时技术,注意与反射等运行时技术进行区别,并慎重使用。
Lambda表达式和Lambda表达式树 (Lambda Expression and Lambda Expression Trees)
Lambda表达式
我们从"所有字符串查找包含tony子字符串"说起。在C# 2.0中,匿名方法允许我们以内联的方式来实现委托实例,它提供强大的函数式编程语言,但是标记显得相当的冗长和带有强制性。我们使用C# 2.0 中的匿名方法查找,代码如下:
var inString = list.FindAll(delegate(string s)
{ return s.Indexof("tony") >= 0; });现在可以使用C# 3.0带来的Lambda表达式允许我们使用一种更接近人的思维、更自然的方式来实现类似于匿名方法同样的效果,看下面的代码多么简洁:
var inString = list.FindAll(s => s.Indexof("tony") >= 0);Lambda表达式格式:(参数列表)=>表达式或语句块
具体意义:定义Lambda接受参数列表,运行表达式或语句块返回表达式或语句块的值传给这个参数列表。
Lambda表达式参数类型可以是隐式类型或显式类型。在显式列表中,每个参数的类型是显式指定的,在隐式列表中,参数的类型由Lambda表达式出现的语境自动推断类型。
Lambda表达式的参数列表可以有一个或多个参数,或者无参数。在有单一的隐型参数的lambda表达式中,圆括号可以从参数列表中省略。
例如:
(x, y) => x * y;//多参数,隐式类型=>表达式
x => x * 10;//单参数,隐式类型=>表达式
x => { return x * 10; }; //单参数,隐式类型=>语句块
(int x) => x * 10;//单参数,显式类型=>表达式
(int x) => { return x * 10; };//单参数,显式类型=>语句块
() => Console.WriteLine(); //无参数下面看这个例子:
在前面的帖子中,我们写了一个User类及增加了2个人,接下来,我们使用由LINQ提供的新的Where和Average方法来返回集合中的人的一个子集,以及计算这个集合中的人的平均年龄:
List<User> user = new List<User>{
new User{Id=1,Name="tony",Age=21},
new User{Id=2,Name="tony",Age=22},
};
//获取特定人时所用的过滤条件,p参数属于User类型
var results = user.Where(p => p.Name == "tony").ToList();
//用User对象的Age值计算平均年龄
var average = user.Average(p => p.Age);
LINQ,语言级集成查询(Language INtegrated Query)
经过了最近 20 年,面向对象编程技术( object-oriented (OO) programming technologies )在工业领域的应用已经进入了一个稳定的发展阶段。程序员现在都已经认同像类(classes)、对象(objects)、方法(methods)这样的语言特性。考察现在和下一代的技术,一个新的编程技术的重大挑战开始呈现出来,即面向对象技术诞生以来并没有解决降低访问和整合信息数据(accessing and integrating information)的复杂度的问题。其中两个最主要访问的数据源与数据库( database )和 XML 相关。
LINQ 提供了一条更常规的途径即给 .Net Framework 添加一些可以应用于所有信息源( all sources of information )的具有多种用途( general-purpose )的语法查询特性( query facilities ),这是比向开发语言和运行时( runtime )添加一些关系数据( relational )特性或者类似 XML 特性( XML-specific )更好的方式。这些语法特性就叫做 .NET Language Integrated Query (LINQ) 。
包含 DLinq 和 XLinq
C#3.0 LINQ 查询语法
首先来看一个很简单的LINQ查询例子,查询一个int 数组中小于5的数字,并按照大小顺序排列:
class Program
{
static void Main(string[] args)
{
int[] arr = new int[] { 8, 5, 89, 3, 56, 4, 1, 58 };
var m = from n in arr where n < 5 orderby n select n;
foreach (var n in m)
{
Console.WriteLine(n);
}
Console.ReadLine();
}
}
上述代码除了LINQ查询语法外,其他都是我们所熟悉的语法,而LINQ查询语法跟SQL查询语法很相似,除了先后顺序。
Q:为何 LINQ 查询语法是以 from 关键字开头的,而不是以 select 关键字开头的?select 开头这种写法跟SQL的写法更接近,更易懂呀?
A:简单来说,为了IDE的智能感知(InteliSence)这个功能,select 关键字放在后面了。
编程语言以 select 开头写LINQ查询语法不是没出现过,你如果使用过2005年的VB9 CTP 版本,那时候VB9的LINQ查询语法就是 select 关键字在前面,但是 select 关键字在前面,在做智能感知(InteliSence)时候就很头大。经过微软IDE组的权衡,确定了把 from 关键字放在最前面。
我们再来看一个稍稍复杂的LINQ查询:
在我们罗列的语言字符串中,我们希望按照字符长短,分类罗列出来,实现代码如下:
static void Main(string[] args)
{
string [] languages = {"Java","C#","C++","Delphi","VB.net","VC.net","C++ Builder","Kylix","Perl","Python"};
var query = from item in languages
orderby item
group item by item.Length into lengthGroups
orderby lengthGroups.Key descending
select lengthGroups;
foreach (var item in query)
{
Console.WriteLine("strings of length ",item.Key);
foreach (var val in item)
{
Console.WriteLine(val);
}
}
Console.ReadLine();
}
其中的 into 关键字表示将前一个查询的结果视为后续查询的生成器,这里是跟 group by一起使用的。
LINQ中的Group by不要跟 SQL 中的Group by 混淆,SQL 由于是二维结构,Group by 的一些逻辑受二维结构的约束,无法象 LINQ 中的Group by 这么灵活。