雁过请留痕...
代码改变世界

C# 3.0 新特性概览

2012-07-23 22:33  xiashengwang  阅读(597)  评论(0编辑  收藏  举报

1. 隐式类型局部变量(Implicitly Typed Local Variables)

     隐式类型局部变量的具体类型取决于初始化它的表达式。在一个局部变量声明时,前面加一个var关键字,这个局部变量就被叫做隐式类型局部变量。例如:

              var i = 0;
            var s = "Hello";
            var d = 1.0;
            var numbers = new int[] { 2, 4, 7, 8 };
            var order = new Dictionary<string, string>();

上面的隐式类型局部变量的声明等价于下面的显示类型声明:

              int i = 0;
            string s = "Hello";
            double d = 1.0;
            int[] numbers = new int[] { 2, 4, 7, 8 };
            Dictionary<string,string> order = new Dictionary<string, string>();

用隐式类型局部变量声明的本地变量声明要遵循下面的规则:

●声明的同时必须赋初始值。

●初始值必须是一个表达式。

●初始表达式必须是一个编译时类型,不能是null类型。

●局部变量声明不能包含多个声明。

●初始化不能引用声明的变量本身。

下面都是不正确的隐式类型局部变量声明:

            var x;                 //Error:声明时没有赋值
            var y = { 1, 2, 3 };   //Error:不允许是集合初始化器
            var z = null;          //Error:不能是null类型
            var u = x => x + 1;    //Error:Lamda表达式没有类型
            var d1 = 0.1, d2 = 0.2;//Error:一个var不能包含多个声明
            var v = v++;           //Error:初始表达式不能引用声明的变量本身

注意:除本地变量声明外,for语句,using语句,foreach语句也可以用隐式类型局部变量。例如: 

            using (var fs = new MemoryStream()) { }
            for (var i = 0; i < 100; i++){ }
            foreach (var num in numbers) { }

2. 扩展方法(Extension Methods)

 扩展方法是静态方法,它们只能被定义在非泛型,非嵌套的静态类里。下面是一个例子:

    public static class Extensions
    {
        public static Int32 ToInt32(this string s)
        {
            return Convert.ToInt32(s);
        }
        public static void ForEach<T>(this IEnumerable<T> enumer, Action<T> action)
        {
            foreach (var item in enumer)
            {
                action(item);
            }
        }
    }

扩展方法第一个参数要以this开头,表示要在紧跟this的类型上进行扩展。扩展方法具有静态方法的所有特征,并且,它能像调用实例方法那样进行调用,就像是“类型本身的方法”一样。下面的代码调用了扩展方法:

            var s = "20";
            var num = s.ToInt32();//用实例方法的语法,调用扩展方法
            var num2 = Extensions.ToInt32(s);//也可以像静态方法那样调用扩展方法

 方法调用的优先级:实例方法>内部命名空间的扩展方法>外部命名空间的扩展方法。例子:

public static class E
{
   public static void F(this object obj, int i) { }
   public static void F(this object obj, string s) { }
}
class A { }
class B
{
   public void F(int i) { }
}
class C
{
   public void F(object obj) { }
}
class X
{
   static void Test(A a, B b, C c) {
      a.F(1);            // E.F(object, int)
      a.F("hello");      // E.F(object, string)
      b.F(1);            // B.F(int)
      b.F("hello");      // E.F(object, string)
      c.F(1);            // C.F(object)
      c.F("hello");      // C.F(object)
   }
}

3. Lambda表达式(Lambda Expressions)

    C#2.0提供了匿名方法(anonymous methods)语法,它允许以“内联”的方式书写代码块,大大的简化了代码的编写。Lambda是对匿名方法语法的进一步简化,所有 Lambda 表达式都使用 Lambda 运算符 =>,该运算符读为“goes to”。 Lambda 运算符的左边是输入参数(如果有),右边包含表达式或语句块。Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法(如 Where)的参数。

3.1 Lambda表达式

表达式在右边的 Lambda 表达式称为“Lambda 表达式”。 Lambda 表达式在构造表达式树(C# 和 Visual Basic)时广泛使用。 Lambda 表达式返回表达式的结果,并采用以下基本形式:

(input parameters) => expression

只有在 Lambda 有一个输入参数时,括号才是可选的;否则括号是必需的。 两个或更多输入参数由括在括号中的逗号分隔: 

(x, y) => x == y

 有时,编译器难于或无法推断输入类型。 如果出现这种情况,您可以按以下示例中所示方式显式指定类型: 

(int x, string s) => s.Length > x

 使用空括号指定零个输入参数:

() => SomeMethod()

3.2 Lambda语句

Lambda 语句与 Lambda 表达式类似,只是语句括在大括号中:

(input parameters) => {statement;}

Lambda 语句的主体可以包含任意数量的语句;但是,实际上通常不会多于两个或三个语句。 

delegate void TestDelegate(string s);
…
TestDelegate myDel = n => { string s = n + " " + "World"; Console.WriteLine(s); };
myDel("Hello");

像匿名方法一样,Lambda 语句无法用于创建表达式树。 

3.3 带有标准查询运算符的 Lambda

许多标准查询运算符都具有输入参数,其类型是泛型委托的 Func<T, TResult> 系列的其中之一。它的定义的类型如下: 

public delegate TResult Func<TArg0, TResult>(TArg0 arg0)

 可以将委托实例化为 Func<int,bool> myFunc,其中 int 是输入参数,bool 是返回值。 始终在最后一个类型参数中指定返回值。 Func<int, string, bool> 定义包含两个输入参数(int 和 string)且返回类型为 bool 的委托。 在调用下面的 Func 委托时,该委托将返回 true 或 false 以指示输入参数是否等于 5: 

Func<int, bool> myFunc = x => x == 5;
bool result = myFunc(4); // returns false of course

 当参数类型为 Expression<Func> 时,您也可以提供 Lambda 表达式,例如在 System.Linq.Queryable 内定义的标准查询运算符中。 如果指定Expression<Func> 参数,Lambda 将编译为表达式树。

此处显示了一个标准查询运算符,Count 方法: 

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);

 编译器可以推断输入参数的类型,或者您也可以显式指定该类型。 这个特别的 Lambda 表达式将计算整数 (n) 的数量,这些整数除以 2 时余数为 1。

以下方法将生成一个序列,其中包含 numbers 数组中在 9 左边的所有元素,因为 9 是序列中不满足条件的第一个数字: 

var firstNumbersLessThan6 = numbers.TakeWhile(n => n < 6);

 此示例演示如何通过将输入参数括在括号中来指定多个输入参数。 该方法将返回数字数组中的所有元素,直至遇到一个值小于其位置的数字为止。 不要将 Lambda 运算符 (=>) 与大于等于运算符 (>=) 混淆。 

var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);

 3.4 Lambda的类型推理

在编写 Lambda 时,通常不必为输入参数指定类型,因为编译器可以根据 Lambda 主体、基础委托类型以及 C# 语言规范中描述的其他因素推断类型。 对于大多数标准查询运算符,第一个输入是源序列中的元素的类型。 因此,如果要查询 IEnumerable<Customer>,则输入变量将被推断为 Customer 对象,这意味着您可以访问其方法和属性: 

customers.Where(c => c.City == "London");

 Lambda 的一般规则如下:

  • Lambda 包含的参数数量必须与委托类型包含的参数数量相同。

  • Lambda 中的每个输入参数必须都能够隐式转换为其对应的委托参数。

  • Lambda 的返回值(如果有)必须能够隐式转换为委托的返回类型。

请注意,Lambda 表达式本身没有类型,因为常规类型系统没有“Lambda 表达式”这一内部概念。但是,有时会不正式地论及 Lambda 表达式的“类型”。 在这些情况下,类型是指委托类型或 Lambda 表达式所转换为的 Expression 类型。

3.5 Lambda表达式的类型范围

 Lambda 可以引用“外部变量”,这些变量位于在其中定义 Lambda 的封闭方法或类型的范围内。 将会存储通过这种方式捕获的变量以供在 Lambda 表达式中使用,即使变量将以其他方式超出范围或被作为垃圾回收。 必须明确地声明外部变量,然后才能在 Lambda 表达式中使用该变量。 下面的示例演示这些规则: 

 delegate bool D();
    delegate bool D2(int d);
    class LamdaDemo
    {
        D del;
        D2 del2;
        public void TestMethod(int input)
        {
            int k = 0;
            // Initialize the delegates with lambda expressions.
            // Note access to 2 outer variables.
            // del will be invoked within this method.
            del = () =>{k = 10;return k > input;};

            // del2 will be invoked after TestMethod goes out of scope.
            del2 = (n) => {return n == k;};

            // Output: k = 0 
            Console.WriteLine("k={0}", k);
            bool b = del();

            // Output: k = 10 b = True
            Console.WriteLine("k={0},b={1}", k, b);
        }
        public static void Demo()
        {
            LamdaDemo demo = new LamdaDemo();
            demo.TestMethod(5);

            // Prove that del2 still has a copy of
            // local variable j from TestMethod.
            bool result = demo.del2(10);

            // Output: True
            Console.WriteLine(result);
        }
    }

 下列规则适用于 Lambda 表达式中的变量范围: 

  • 捕获的变量将不会被作为垃圾回收,直至引用变量的委托超出范围为止。

  • 在外部方法中看不到 Lambda 表达式内引入的变量。

  • Lambda 表达式无法从封闭方法中直接捕获 ref 或 out 参数。

  • Lambda 表达式中的返回语句不会导致封闭方法返回。

  • Lambda 表达式不能包含其目标位于所包含匿名函数主体外部或内部的 goto 语句、break 语句或 continue 语句。

3.6 Lambda表达式与匿名方法的异同

  • 匿名方法可以完全省略参数列表,Lambda表达式至少要留一个()括号。
  • Lambda表达式的参数允许省略类型,匿名方法必须显示声明类型。
  • Lambda表达式的类容可以是一个表达式,也可以是一个语句块。匿名方法只能是一个语句块。
  • Lambda表达式的类容如果是一个表达式,可以转换成表达式树。
  • Lambda表达式和匿名方法都是C#语法层面的东西,对于CLR来说,它只认识被C#编译器编译后的delegate。

4. 对象和集合初始化器(Object and Collection Initializers)

对象初始化器允许在声明一个对象,并对对象进行初始化时,不用显示调用类型的构造函数。但要注意,对象初始化器要调用默认的构造函数,如果默认的构造函数被声明为private或protected将会引起编译错误。下面首先定义一个Student类:

    public class Student
    {
        public Student() { }//这是默认构造函数,注意它是public的
        public Student(string firstName, string lastName)
        {
            this.FirstName = firstName;
            this.LastName = lastName;
        }
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}", FirstName, LastName ,ID);
        }
    }

然后可以像下面这样初始化对象,注意它们之间的区别:

        public static void Demo()
        {
            //调用的是有两个参数的构造函数
            Student student1 = new Student("zhang", "qiang");

            //用对象初始化器初始化对象,只指定FirstName和
            //LastName字段,它会调用默认构造函数
            Student student2 = new Student { FirstName = "zhang", LastName = "qiang" };

            //用对象初始化器初始化对象,只指定ID字段,
            //它会调用默认构造函数
            Student student3 = new Student { ID = 100 };

            //用对象初始化器初始化对象,指定全部的字段,
            //它会调用默认构造函数
            Student student4 = new Student { ID =116,FirstName ="zhang",LastName = "qiang"};

            Console.WriteLine(student1);
            Console.WriteLine(student2);
            Console.WriteLine(student3);
            Console.WriteLine(student4);
        }

需要注意的是:对象初始化器并不需要指定对象的全部字段。

下面的示例演示如何使用集合初始器初始化一个 StudentName 类型集合。注意,对象和对象之间用逗号隔开。 

            List<Student> students = new List<Student>()
            {
                new Student{ID =100,FirstName ="zhang",LastName = "qiang"},
                new Student{ID =101,FirstName ="wang",LastName = "li"},
                new Student{ID =102,FirstName ="qiao",LastName = "xin"}
            };

 5.匿名类型(Anonymous Types)

匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。可通过使用 new 运算符和对象初始器创建匿名类型。

            var v = new { Amount = 100, Message = "Hello" };
            // Rest the mouse pointer over v.Amount and v.Message in the following
            // statement to verify that their inferred types are int and string.
            Console.WriteLine(v.Amount + v.Message);

匿名类型通常用在Linq查询表达式的 select 子句中,以便返回数据源中每个对象的属性子集。

匿名类型包含一个或多个公共只读属性。 包含其他种类的类成员(如方法或事件)为无效。 用来初始化属性的表达式不能为 null、匿名函数或指针类型。 

最常见的方案是用其他类型的属性初始化匿名类型。 在下面的示例中,假定名为 Product 的类存在。 类 Product 包括 Color 和 Price 属性,以及您不感兴趣的其他属性。 变量 products 是 Product 对象的集合。 匿名类型声明以 new 关键字开始。 声明初始化了一个只使用 Product 的两个属性的新类型。 这将导致在查询中返回较少数量的数据。 

如果您没有在匿名类型中指定成员名称,编译器会为匿名类型成员指定与用于初始化这些成员的属性相同的名称。 必须为使用表达式初始化的属性提供名称,如下面的示例所示。 在下面示例中,匿名类型的属性名称都为 Color 和 Price 

var productQuery = 
    from prod in products
    select new { prod.Color, prod.Price };

foreach (var v in productQuery)
{
    Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}

 可通过将隐式类型局部变量与隐式类型数组相结合创建匿名的元素的数组,如下面的示例所示。 

            var anonArray = new[] { new { ID = 10, City = "beijing" }, new { ID = 12, City = "Shanghai" } };
            foreach (var e in anonArray)
            {
                Console.WriteLine(e.ID + e.City);
            }

 6.隐式类型数组(Implicitly Typed Arrays)

可以创建隐式类型的数组,在这样的数组中,数组实例的类型是从数组初始值设定项中指定的元素推断而来的。 有关任何隐式类型变量的规则也适用于隐式类型的数组。

它的基本语法格式是: 

new[] {/*数组初始化器 */}

 下面是一些例子:

            var a = new[] { 1, 2, 3,4 };//int[]
            var b = new[] { "hello", null, "world" };//string[]
            //二维的int数组
            var c = new[]{
                new[]{1,2,3,4},
                new[]{5,6,7,8}
            };
            //string类型的锯齿数组
            var d = new[]{
                new[]{"Luca", "Mads", "Luke", "Dinesh"},
                new[]{"Karen", "Suma", "Frances"}
            };

创建包含数组的匿名类型时,必须在该类型的对象初始值设定项中对数组进行隐式类型化。 在下面的示例中,contacts 是一个隐式类型的匿名类型数组,其中每个匿名类型都包含一个名为 PhoneNumbers 的数组。 请注意,对象初始值设定项内部未使用 var 关键字。 

            var contacts = new[]{
                new {Name = "Killy",PhoneNumbers=new []{"001-12345","002-25825"}},
                new {Name = "Jimmy",PhoneNumbers=new []{"123-45678"}}
            };

 7.查询表达式(Query Expressions)

查询表达式提供语言集成语法来支持查询。它类似于拥有关系和层级结构的查询语言,如SQL和XQuery。查询表达式是编程语言级别的语法,C#3.0和VB9.0都支持这样的语法,但CLR却并不知道,它只认识具体的类型和方法。查询表达式被语言编译器编译成CLR可以识别的方法,他们都定义在IEnumerable<T> 或 IQueryable<T>的一个静态类里,并且都是些扩展方法。这些扩展方法中的一部分和查询表达式一一对应,我们完全可以用这种方法调用的方式来书写查询,但很明显,查询表达式提供了清晰且易阅读的语法,简化了这种书写量。

查询表达式必须以 from 子句开头,并且必须以 select 或 group 子句结尾。 在第一个 from 子句和最后一个 select 或 group 子句之间,查询表达式可以包含一个或多个下列可选子句:whereorderbyjoinlet 甚至附加的 from 子句。 还可以使用 into 关键字使 join 或 group 子句的结果能够充当同一查询表达式中附加查询子句的源。它们会被翻译成CLR能识别的WhereSelectSelectManyJoinGroupJoinOrderByOrderByDescendingThenByThenByDescendingGroupBy, and Cast扩展方法。

下面是一个例子: 

        public static void Demo()
        {
            //Data Source
            int[] scores = { 90, 71, 82, 93, 75, 82 };

            //Query Expression
            IEnumerable<int> scoreQuery =//query variable
                from score in scores//required
                where score > 80// optional
                orderby score descending// optional
                select score;//must end with select or group

            // Execute the query to produce the results
            foreach (var score in scoreQuery)
            {
                Console.WriteLine(score);
            }            
        }
        //OutPut: 93 90 82 82

 在上一个示例中,scoreQuery 是一个查询变量,有时简称为“查询”。 查询变量并不存储实际的结果数据(这些数据是在 foreach 循环中产生的)。 另外,当foreach 语句执行时,查询结果并不是通过查询变量 scoreQuery 返回的。 相反,它们是通过迭代变量 testScore 返回的。 可以在另一个 foreach 循环中迭代scoreQuery 变量。 只要该变量和数据源都没有修改,该变量都将产生相同的结果。

查询变量可以存储用查询语法或方法语法(或二者的组合)表示的查询。 在下面的示例中,queryMajorCities 和 queryMajorCities2 都是查询变量: 

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;


// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

 查询表达式是Linq的基础,内容很丰富,这里不详细解释每个关键字的用法,只做简单的展示。想了解更加详细的内容,可以参看MSDN:http://msdn.microsoft.com/zh-cn/library/bb384065.aspx

8.表达式树(Expression Trees)

表达式树允许用Lambda表达式来表示一个数据结构,而不是执行代码。如果一个Lambda表达式可以转化成一个delegate类型D,那么它也可以转换成一个表达式树类型System.Query.Expression<D>。然而,当Lambda表达式转换成一个delegate时会生成一个delegate类型的可执行代码;转换成表达式树的时候,只会生成一颗发散的表达式的实例,它是一种高效透明的数据结构。

下面的代码展示了Lamba表达式如何转换成可执行代码(delegate)和一个数据结构(表达式树)。因为一个转换成了Func<int,int>,一个转换成了Expression<Func<int,int>>。 

Func<int,int> f = x => x + 1;                  // Code
Expression<Func<int,int>> e = x => x + 1;      // Data

 上面的代码中,delegate 参照一个方法,它返回 x + 1; 而表达式树 e 参照一个数据结构,它描述了一个表达式 x + 1.

9.自动实现属性(Automatically Implemented Properties)

在 C# 3.0 和更高版本中,当属性的访问器中不需要其他逻辑时,自动实现的属性可使属性声明更加简洁。 客户端代码还可通过这些属性创建对象。 

    public class ParamInfo
    {
        public string Name { get; set; }//自动属性
        public string Value { get; set; }//自动属性
    }

 使用方式和一般属性相似:

var param = new ParamInfo { Name = "Text", Value = "ID" };

可以对自动属性设置访问级别,如构造一个只读属性的类型,可以将set访问器设为private,对象只能通过构造函数初始化值:

    public class ParamInfo
    {
        public string Name { get; private set; }//自动属性
        public string Value { get;private  set; }//自动属性
        //构造函数
        public ParamInfo(string name, string value)
        {
            this.Name = name;
            this.Value = value;
        }
    }

注意:自动属性可以简化语法,但并不是万能的,使用时需要注意,下面的场合不适合用自动属性。

  • 属性访问器中含有复杂逻辑。
  • 包含属性的类型需要序列化时。

 (全篇完)