1.4 C#语法新特性
在学习ASP.NETMVC之前,有必要先了解一下C#2.0~C# 4.0版本所带来的新的语法特性。这一点尤为重要,因为在MVC项目中我们利用C#的新特性将会大大提高开发效率;同时,在MVC项目中随处可见C#新特性的身影。其实,其本质都是“语法糖”,由编译器在编译时转成原始语法。
1.4.1 C#2.0 新特性
1.泛型(Generics)
微软官方定义:2.0版C#语言和公共语言运行时(CLR)中增加了泛型。泛型将类型参数的概念引入.NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。
下面给出一个简单的泛型例子:
public class List<T>{ }
其中,T就是System.Collections.Generic.List<T>实例所存储类型的占位符。当定义泛型类的实例时,必须指定这个实例所存储的实际类型:
List<string> lst = new List<string>();
泛型允许程序员将一个实际的数据类型规约延迟至泛型的实例被创建时才确定。
泛型主要有两个优点:
● 编译时可以保证类型安全。
● 不用做类型转换,获得一定的性能提升。
2.泛型方法、泛型委托、泛型接口
除了泛型类之外,还有泛型方法、泛型委托、泛型接口:
//泛型委托 public delegate void Del<T>(T item); public static void Notify(int i) { } Del<int> m1 = new Del<int>(Notify); //泛型接口 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; } public void Interactive () { string str1 = "a"; string str2 = "b"; Swap<string>(ref str1, ref str2); Console.WriteLine(str1 + ", " + str2); }
3.泛型约束(constraints)
可以给泛型的类型参数上加约束,要求这些类型参数满足一定的条件,如表1-1所示。
表1-1 泛型的类型参数
4.部分类(partial)
在申明一个类、结构或者接口的时候用partial关键字可以让源代码分布在不同的文件中。过去部分类是为了在ASPX页面和ASPX.cx页面进行Code-Behind的。在EF中使用T4模板自动生成代码的时候部分类的作用非常重要。
部分类仅是编译器提供的功能,在编译的时候会把partial关键字定义的类合在一起去编译。
5.匿名方法
匿名方法的本质其实就是委托,函数式编程的最大特点之一就是把方法作为参数和返回值。ConsoleWrite→MulticastDelegate(intPtr[])→Delegate(object, intPtr)匿名方法:编译后会生成委托对象,生成方法,然后把方法装入委托对象,最后赋值给声明的委托变量。匿名方法可以省略参数:编译的时候会自动为这个方法按照委托签名的参数添加参数。
public delegate void ConsoleWrite(string strMsg); //匿名方法测试 ConsoleWrite delCW1 = new ConsoleWrite(WriteMsg); delCW1("天下第一"); ConsoleWrite delCW2 = delegate(string strMsg) { Console.WriteLine(strMsg); }; delCW2("天下第二");
1.4.2 C# 3.0/C# 3.5 新特性
1.自动属性
这个概念很简单,简化了我们在做C#开发的时候手写一堆私有成员+属性的编程方式,我们只需要使用如下方式声明一个属性,编译器就会自动生成所需的成员变量。
回顾一下传统属性概念,属性的目的一是封装字段,二是控制读写权限及字段的访问规则(如年龄、生日范围),平时主要是用来封装读写权限。
我们来看一下基本用法:
public class User { public int Id { get; set; } //自动属性 public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } }
在C# 3.0之前,我们是这样来实现属性的:
private int id; public int Id { get { return id; } set { id = value; } }
读者可以思考一下:使用自动属性的话程序员写的代码少了,机器做的事情就多了,那我们到底要不要使用它?
如果是针对读写权限的封装,就推荐使用,因为它是在编译的时候产生了负担,并不是在运行的时候,所以不会影响客户运行程序时的效率!但是编译时生成的代码也有一个显而易见的缺点,语法太完整,编译后的程序集会比较大,不过这对于现今的硬件配置而言也算不上什么了。
2.隐式推断类型Var
你可能对这个名称比较陌生,但是var这个关键字应该很熟悉,在C#中使用var声明一个对象时编译器会自动根据赋值语句推断这个局部变量的类型。赋值以后,这个变量的类型也就已经确定并且不可以进行更改。另外,var关键字也可用于匿名类的声明。
应用场合:var主要用于表示一个LINQ查询的结果。这个结果既可能是ObjectQuery<>或IQueryable<>类型的对象,也可能是一个简单的实体类型的对象或者是一个基本类型对象,这时使用var声明这个对象可以节省很多代码书写上的时间。
var customer = new User(); var i = 1;
var隐式类型的限制:
● 被声明的变量必须是一个局部变量,而不是静态或实例字段。
● 变量必须在声明的同时被初始化,因为编译器要根据初始化值推断类型。
● 初始化的对象不能是一个匿名函数。
● 初始化表达式不能是null。
● 语句中只声明一次变量,声明后不能更改类型。
● 赋值的数据类型必须是可以在编译时确定的类型。
3.对象集合初始化器
在.NET 2.0中构造一个对象的方法一是提供一个重载的构造函数,二是用默认的构造函数生成一个对象,然后对其属性进行赋值。在.NET 3.5/C# 3.0中,我们有一种更好的方式来进行对象的初始化,那就是使用对象初始化器。这个特性也是匿名类的一个基础,所以放在匿名类之前介绍。
对象初始化:
User user = new User { Id = 1, Name = "Zouqj", Age = 27 };
集合初始化:
List<Dog> dogs = new List<Dog>() { new Dog() { Name = "Tom", Age = 1 }, new Dog() { Name = "Lucy", Age = 3 } };
创建并初始化数组:
string[] array = { "西施", "貂蝉" };
4.匿名类
有了前面初始化器的介绍,匿名类就简单了。匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无须首先显式定义一个类型。类型名由编译器生成,并且不能在源代码级使用,每个属性的类型由编译器推断。我们可以使用new{object initializer}或new[]{object,…}来初始化一个匿名类或不确定类型的数组。匿名类的对象需要使用var关键字声明。示例代码:
var p = new { Id = 1, Name = " Zouqj ", Age = 27 }; //属性名字和顺序不同会生成不同类
在编译后会生成一个“泛型类”,我们可以使用反编译工具Reflector来查看这个生成的泛型类中都有些什么,如图1-21所示。
图1-21
可以看到包含了如下信息:
● 获取所有初始值的构造函数,顺序与属性顺序一样。
● 属性的私有只读字段。
● 重写Object类中的Equals、GetHashCode、ToString()方法。
● 包含公共只读属性,属性不能为null、匿名函数、指针类型。
用处:
● 避免过度的数据累积。
● 为一种情况特别进行的数据封装。
● 避免进行单调重复的编码。
应用场合:
直接使用select new { object initializer }这样的语法就是将一个LINQ查询的结果返回到一个匿名类中。
注意:
● 当出现“相同”的匿名类时,编译器只会创建一个匿名类。
● 编译器如何区分匿名类是否相同。
● 属性名、属性值(因为这些属性是根据值来确定类型的)、属性个数、属性的顺序。
● 匿名类的属性是只读的,可放心传递,并且可用在线程间共享数据。
5.扩展方法
扩展方法是一种特殊的静态方法,可以像扩展类型上的实例方法一样进行调用,能向现有类型“添加”方法,而无须创建新的派生类型、重新编译或以其他方式修改原始类型。
例如,在编译时直接将str.WriteSelf(2016)替换成:
StringUtil.WriteSelf(str,2016);
想为一个类型添加一些成员时,可以使用扩展方法:
public static class StringUtil { public static void WriteSelf(this string strSelf, int year) { Console.WriteLine(string.Format("我是{0}人,今年是{1}年。", strSelf, year)); } }
测试:
//扩展方法 string str = "冷水江"; str.WriteSelf(2016);
编译器认为一个表达式要使用一个实例方法,但是没有找到,需要检查导入的命名空间和当前命名空间里所有的扩展方法,并匹配到适合的方法。
注意:
(1)实例方法优先于扩展方法(允许存在同名实例方法和扩展方法)。
(2)可以在空引用上调用扩展方法。
(3)可以被继承。
(4)并不是任何方法都能作为扩展方法使用,必须有以下特征:
● 它必须放在一个非嵌套、非泛型的静态类中。
● 它至少有一个参数。
● 第一个参数必须附加this关键字。
● 第一个参数不能有任何其他修饰符(out/ref)。
● 第一个参数不能是指针类型,其类型决定是在何种类型上进行扩展。
6.系统内置委托
Func / Action委托使用可变性:
Action<object> test=delegate(object o){Console.WriteLine(o); }; Action<string> test2=test; Func<string> fest=delegate(){return Console.ReadLine(); }; fest2=fest; public delegate void Action(); public delegate bool Predicate<in T>(T obj); public delegate int Comparison<in T>(T x, T y);
协变指的是委托方法的返回值类型直接或间接继承自委托签名的返回值类型,逆变则是参数类型继承自委托方法的参数类型System.Func,代表有返回类型的委托。
public delegate TResult Func<out TResult>(); public delegate TResult Func<in T, out TResult>(T arg); .....
注:输入泛型参数-in最多可以有16个,输出泛型参数-out只有一个。System.Action代表无返回类型的委托。
public delegate void Action<in T>(T obj); //list.Foreach public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2); ......
注:最多有16个参数,System.Predicate<T>代表返回bool类型的委托,用作执行表达式。
public delegate bool Predicate<in T>(T obj); //list.Find
System.Comparison<T>代表返回int类型的委托,用于比较两个参数的大小。
public delegate int Comparison<in T>(T x, T y); //list.Sort
为什么要定义这么多简单的委托?方便!
7. Lambda表达式
Lambda表达式的本质就是匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。“Lambda表达式”是一个匿名函数,可以包含表达式和语句,并且可用于创建委托或表达式树类型。
Lambda表达式的运算符为=>,读作“goes to”。=>运算符具有与赋值运算符(=)相同的优先级。
Lambda的基本形式是:
(input parameters) => expression
只有在Lambda有一个输入参数时,括号才是可选的,否则括号是必需的。两个或更多输入参数由括在括号中的逗号分隔:(x, y) => x == y。
有时,编译器难于或无法推断输入类型。如果出现这种情况,您可以按以下示例中所示的方式显式指定类型:
(int x, string s) => s.Length > x
使用空括号指定零个输入参数:
() => SomeMethod()
最常用的场景是IEnumerable和IQueryable接口的Where(c=>c.Id>3)。
下列规则适用于Lambda表达式中的变量范围:
● 捕获的变量将不会被作为垃圾回收,直至引用变量的委托超出范围为止。
● 在外部方法中看不到Lambda表达式内引入的变量。
● Lambda表达式无法从封闭方法中直接捕获ref或out参数。
● Lambda表达式中的返回语句不会导致封闭方法返回。
● Lambda表达式不能包含其目标位于所包含匿名函数主体外部或内部的goto语句、break语句或continue语句。
8. Lambda表达式缩写推演
Lambda表达式缩写推演如图1-22所示。
图1-22
例如:
delegate int AddDel(int a, int b); //定义一个委托 #region lambda AddDel fun = delegate(int a, int b) { return a + b; }; //匿名函数 //Console.WriteLine(fun(1, 3)); //lambda 参数类型可以进行隐式推断,可以省略类型lambda本质就是匿名函数 AddDel funLambda = (a, b) => a + b; List<string> strs = new List<string>() { "1", "2", "3" }; var temp = strs.FindAll(s => int.Parse(s) > 1); foreach (var item in temp) { Console.WriteLine(item); } //Console.WriteLine(funLambda(1,3)); #endregion static void Main(string[] args) { List<int> nums = new List<int>() { 1, 2, 3, 4, 6, 9, 12 }; //使用委托的方式 List<int> evenNums = nums.FindAll(GetEvenNum); foreach (var item in evenNums) { Console.WriteLine(item); } Console.WriteLine("使用lambda的方式"); List<int> evenNumLamdas = nums.FindAll(n => n % 2 == 0); foreach (var item in evenNumLamdas) { Console.WriteLine(item); } Console.ReadKey(); static bool GetEvenNum(int num) { if (num % 2 == 0) { return true; } return false; }
9.标准查询运算符(SQO)
标准查询运算符是定义在System.Linq.Enumerable类中的50多个为IEnumerable<T>准备的扩展方法,换句话说IEnumerable<T>上的每个方法都是一个标准查询运算符,这些方法用来对操作的集合进行查询筛选。
标准查询运算符提供了包括筛选、投影、聚合、排序等功能在内的查询功能。
先准备一下测试数据和测试方法,代码如下。
private List<User> InitLstData() { return new List<User>(){ new User { Id = 1, Name = "Zouqj1", Age = 21 }, new User { Id = 2, Name = "Zouqj2", Age = 22 }, new User { Id = 3, Name = "Zouqj3", Age = 23 }, new User { Id = 4, Name = "Zouqj4", Age = 24 }, new User { Id = 5, Name = "Zouqj5", Age = 25 }, new User { Id = 6, Name = "Zouqj6", Age = 26 }, new User { Id = 7, Name = "Zouqj7", Age = 27 }, new User { Id = 8, Name = "Zouqj8", Age = 28 }, new User { Id = 9, Name = "Zouqj9", Age = 29 }, new User { Id = 10, Name = "Zouqj10", Age = 30 }, new User { Id = 11, Name = "Zouqj11", Age = 31 } }; }
(1)筛选集合Where
Where方法提供了我们对于一个集合的筛选功能,但需要提供一个带bool返回值的“筛选器”(匿名方法、委托、Lambda表达式均可),从而表明集合中某个元素是否应该被返回。
这里筛选出所有年龄大于等于30岁的数据,代码如下:
var lst = InitLstData(); var result = lst.Where(x => x.Age >= 30).ToList(); result.ForEach(r => Console.WriteLine(string.Format("{0}, {1}, {2}", r.Id, r.Name, r. Age))); Console.ReadLine();
运行结果如下:
10, Zouqj10,30 11, Zouqj11,31
(2)查询投射Select
返回新对象集合IEnumerable<TSource>Select()。返回年龄大于等于30岁的人的名字,代码如下:
var result = lst.Where(x => x.Age >= 30).Select(s => s.Name).ToList(); result.ForEach(x => Console.WriteLine(x));
运行结果如下:
Zouqj10 Zouqj11
(3)统计数量int Count()
lst.Where(x => x.Age >= 30).Count();
(4)多条件排序OrderBy().ThenBy().ThenBy()
lst.OrderBy(x=>x.Age).OrderBy(x=>x.Id)
(5)集合连接Join()
新建一个Student类,并初始化数据。
public class Student { public int ID { get; set; } public int UserId { get; set; } public string ClassName { get; set; } } List<Student> lstStu = new List<Student>() { new Student{ID=1, UserId=1, ClassName="本科8班"}, new Student{ID=1, UserId=3, ClassName="本科2班"}, new Student{ID=1, UserId=2, ClassName="电信1班"}}; var result = lst.Join(lstStu, u => u.Id, p => p.UserId, (u, p) => new { UserId = u.Id, Name = u.Name, ClassName =p.ClassName});
(6)延迟加载Where
在标准查询运算符中,Where方法就是一个典型的延迟加载案例。在实际的开发中,我们往往会使用一些ORM框架例如EF去操作数据库,Where方法的使用则是每次调用都只是在后续生成SQL语句时增加一个查询条件,EF无法确定本次查询是否已经添加结束,所以没有办法在每个Where方法执行的时候确定最终的SQL语句,只能返回一个DbQuery对象,当使用到这个DbQuery对象的时候,才会根据所有条件生成最终的SQL语句去查询数据库。
IEnumerable<User> usr = lst.Where(x => x.Age >= 30);
(7)即时加载FindAll
在开发中如果使用FindAll方法,EF会根据方法中的条件自动生成SQL语句,然后立即与数据库进行交互获取查询结果,并加载到内存中去。
List<User> lstUsr = lst.FindAll(x => x.Age >= 30);
SQO缺点:语句太庞大复杂。
10. LINQ
C# 3.0新语法,查询表达式,和SQL风格接近的代码。
IEnumerable<Dog> listDogs = from dog in dogs where dog.Age>5 //let d=new{Name=dog.Name} orderby dog.Age descending select dog; //select new{Name=dog.Name}
以from开始,以select或groupby子句结尾。输出是一个IEnumerable<T>或IQueryable<T>集合。
注:T的类型由select或groupby推断出来。
LINQ分组:
IEnumerable<IGrouping<int, Dog>> listGroup = from dog in listDogs where dog.Age > 5 group dog by dog.Age;
遍历分组:
foreach (IGrouping<int, Dog> group in listGroup) { Console.WriteLine(group.Key+"岁数:"); foreach (Dog d in group) { Console.WriteLine(d.Name + ", age=" + d.Age); } }
注意:LINQ查询语句编译后会转成标准查询运算符。
这里提一下LINQPad工具,LINQPad支持object 、xml、sql、to linq。通过这个工具,我们可以分析LINQ语句最终转换为SQL或者lambda表达式树时会是什么样子的。
1.4.3 C# 4.0新特性
1.可选参数和命名参数
可选参数:可选参数是C# 4.0提出来的,当我们调用方法,不给这个参数赋值时,它会使用我们定义的默认值。
需要注意的是:
(1)可选参数不能为参数列表的第1个参数,必须位于所有的必选参数之后(除非没有必选参数);
(2)可选参数必须指定一个默认值,且默认值必须是一个常量表达式,不能为变量;
(3)所有可选参数以后的参数都必须是可选参数。
命名参数:通过命名参数调用,实参顺序可以和形参不同。
对于简单的重载,可以使用可选参数和命名参数混合的形式来定义方法,提高代码的运行效率。
public class Dog { public string Name { get; set; } public int Age { get; set; } /// <summary> /// 参数默认值 和 命名参数 /// </summary> /// <param name="name"></param> /// <param name="age"></param> public void Say(string name = "汪汪汪", int age = 1) { Console.WriteLine(name + ", " + age); } }
运行结果如图1-23所示。
图1-23
如果要让name使用默认值,age怎么给值?
_dog.Say(age: 3); //输入结果:汪汪汪,3
2. Dynamic特性
C# 4.0的Dynamic特性需引用System.Dynamic命名空间:
using System.Dynamic; //Dynamic dynamic Customer = new ExpandoObject(); Customer.Name = "Zouqj"; Customer.Male = true; Customer.Age = 27; Console.WriteLine(Customer.Name + Customer.Age + Customer.Male); Console.ReadKey();
另外,需要提一下params。params并非新的语法特性,但是这里也要简单介绍一下,因为使用params关键字作为方法参数可以指定采用数目可变的参数,可以发送参数声明中所指定类型用逗号分隔的参数列表或指定类型的参数数组,还可以不发送参数。若未发送任何参数,则params列表的长度为零。
提示 在方法声明中的params关键字之后不允许有任何其他参数,并且在方法声明中只允许一个params关键字。
public void ParamsMethod(params int[] list) { for (int i = 0; i < list.Length; i++) { Console.WriteLine(list[i]); } Console.ReadLine(); } ParamsMethod( 25, 24, 21,15); ParamsMethod(25, 24, 21, 15);
此外,建议大家多使用reflector工具来查看C#源码和IL语言,reflector就像一面照妖镜,任何C#语法糖在它面前都将原形毕露。这一章节的内容非常重要,希望大家熟练掌握。
1.4.4 C#5.0新特性
C#5.0的新特性,最重要的就是异步和等待(async和await),其使用方式特别简单,就是在方法的返回值前面添加关键字async,同时在方法体中需要异步调用的方法前面再添加关键字await。需要注意的是这个异步方法必须以Task或者Task<TResult>作为返回值。
更多内容请参考下面的网页:
http://www.cnblogs.com/zhili/archive/2013/05/15/Csharp5asyncandawait.html