开源软件企业

linq基础之 C#3.0 新特性

2012/8/26 23:29:23

新特性:隐式类型局部变量

C#是强类型语言,因此我们在声明变量时必须指定变量的具体类型,例如:

        static void DeclareExplicitVars()
        { int myInt = 0; bool myBool = true; string myString = "Hello, World!"; 
        }

var关键字就是给懒人用的

C# 3.0为我们提供了一个新的关键字var,你可以使用它代替正式的数据类型名(如int, bool, string)。在使用var关键字时,编译器会根据用于初始化局部变量的初始值推断出变量的数据类型。例如,上面的变量声明可以改为如下代码:

        static void DeclareImplicitVars()
        { // 隐式类型局部变量的声明方式: var varName =   defaultValue;  var myInt = 0; var myBool = true; var myString = "Hello, World!";
        }

上面两种方式是等价的,编译器可以根据初始值推断myInt的类型为System.Int32,myBool的类型为System.Boolean,myString的类型为System.String。

除此之外,我们可以对基类库中的所有类型使用隐式类型,包括数组、泛型、自定义类型。

        public static void DeclareImplicitVars()
        { // declare implicit variables  var numbers = new int[]   { 2, 4, 6, 8 }; var persons = new List<Person>(); var car = new SportsCar(); // verify the data type using reflection  Console.WriteLine("numbers is a: {0}",   numbers.GetType().Name);
            Console.WriteLine("persons is a: {0}",   persons.GetType().Name);
            Console.WriteLine("car is a: {0}", car.GetType().Name);
        }


var在foreach语句中的使用

在foreach循环语句中,我们也可以使用隐式类型。正如你希望的那样,编译器会推断出正确的数据类型:

        static void VarInForeachLoop()
        { var numbers = new int[]   { 2, 4, 6, 8 }; foreach (var item in numbers)
            {
                Console.WriteLine("Item value: {0}", item);
            }
        }

隐式类型变量的限制

需要注意的是,使用var关键字时会存在多种限制。首先,隐式类型只能应用与方法或者属性内局部变量的声明,不能使用var来定义返回值、参数的类型或类型的数据成员。

其次,使用var进行声明的局部变量必须赋初始值,并且不能以null作为初始值。其原因在于编译器必须能够根据初始值推断出该变量的实际类型。

隐式类型数据是强类型数据

隐式类型局部变量最终会产生强类型数据。因此,var关键字与脚本语言(如VBScript或Perl)的Variant数据类型是不一样的,对后者来说,一个变量可以在其生命周期中保存不同类型的值。

其实,类型推断保持了C#语言的强类型特性,并且在编译时只影响变量声明。初始化之后,编译器就已经为隐式类型变量推断出了确切的数据类型。如果把不同类型的值赋给变量会导致编译时错误:

        static void ImplicitTypingStrongTyping()
        { // 编译器知道 s 是System.String类型  var s = "This variable can only hold string   data!";
            s = "It's   OK."; // 可以调用任何基础方法  string upper = s.ToUpper(); // 错误!不能把数值类型数据赋给String类型变量  s = 100;
        }

隐式类型局部变量的作用

看了上面的介绍,你肯定会奇怪这个结构有什么用呢。如果只是为了简单,就不值得了,因为这样做可能会使其他阅读代码的人感到疑惑。但当我们使用LINQ时,var关键字的优势就显现出来了。它可以动态根据查询本身的格式来创建结果集,这样我们就不需要显示定义查询可能返回的类型,而且在很多时候我们并不能一眼就看出LINQ的返回类型。如下例:

        public static void QueryOverInts()
        { int[]   numbers = { 10, 20, 30, 40, 1, 2, 3, 5 }; var subset = from i in numbers where i < 10 select i;
           
            Console.Write("values in subset: "); foreach (var i in subset)
                Console.Write("{0} ",   i);
            Console.WriteLine();
 
            Console.WriteLine("subset is a: {0}",   subset.GetType().Name);
            Console.WriteLine("subset is defined in: {0}",   subset.GetType().Namespace);
        }


 其实,我们可以认为只有在定义从LINQ查询返回的数据时才使用var关键字。

 

自动属性

我们知道.NET语言推荐使用类型属性来封装私有数据字段,而不是 使用GetXXX()和SetXXX()方法。因为.NET基类库总是使用类型属性而不是传统的访问和修改方法,因此使用属性可以获得与.NET平台更好的集成性。需要知道的是,在底层,C#属性会被映射到前缀get_和set_的方法中,即如果定义了Name属性,C#会自动生成get_Name()和set_Name()方法。

考虑如下的C#类型定义:

    class Person
    { private string firstName = string.Empty; public string FirstName
        { get { return firstName; } set { firstName = value; }
        } private string lastName = string.Empty; public string LastName
        { get { return lastName; } set { lastName = value; }
        } private int level = 0; public int Level
        { get { return level; } set { level = value; }
        }
    }

虽然定义属性不难,但如果属性只是赋值和返回值,对次定义字段和属性也很麻烦,特别是在类属性很多的情况下。为了简化这种简单的数据字段封装的过程,C# 3.0提供了自动属性语法。现在,上面的Person可以定义成如下形式:

    class Person
    { public string FirstName { get; set; } public string LastName { get; set; } public int Level { get; set; }
    }

定义自动属性时,我们只需要指定访问修饰符、数据类型、属性名称和空的get/set作用域。在编译时,会使用自动生成的私有支持字段以及get/set逻辑的正确实现。

需要注意的是,定义自动属性时,必须同时提供get和set关键字,因此不能定义只读或者只写的自动属性。

 

匿名类型

作为一个面向对象的程序员,我们知道如何定义类来表达一个给定的编程实体。当我需要一个在项目之间重用的类型时,我们通常创建一个C#类,为该类提供必需的一系列属性、方法和事件等。但有时候,我们可能需要定义类来封装一些相关数据,而不需要任何相关联的方法、事件。并且,给类不需要在项目间重用。尽管如此,我们还是得定义一个“临时”类,虽然工作不是很复杂,但是如果需要定义类来封装很多数据成员的话,那么将消耗你大量的劳动时间。我想,大家都不会希望把编程变成一项机械运动吧。

C# 3.0提供的匿名类型正是为了上述任务而生,匿名类型是匿名方法的自然延伸,可以帮助我们轻松的完成上面的工作。

定义一个匿名类型时,使用新的关键字var和之前介绍的对象初始化语法,如下示例:

        static void TestAnonymousType()
        { // 构造一个匿名对象表示一个雇员  var worker = new { FirstName = "Vincent", LastName = "Ke", Level = 2 }; // 显示并输出  Console.WriteLine("Name: {0}, Level: {1}", worker.FirstName + "" + worker.LastName, worker.Level);
        }

使用上述代码来构建匿名对象时,C#编译器会在编译时自动生成名称唯一的类。因为这个类的名字在C#中是不可见的,所以必需使用var关键字来使用隐式类型化。另外,我们需要通过对象初始化语法来定义一系列属性来封装各个数据。

匿名类型的内部表示

所有的匿名类型都自动继承自System.Object,我们可以在隐式类型话的worker上面调用ToString()、GetHashCode()、Equals()、GetType()等方法。

我们可以定义如下方法来查看匿名类型的信息:

     static void ReflectAnonymousType(object obj)
        {
            Console.WriteLine("Type Name: {0}", obj.GetType().Name);
            Console.WriteLine("Base Class: {0}", obj.GetType().BaseType);
            Console.WriteLine("obj.ToString() = {0}", obj.ToString());
            Console.WriteLine("obj.GetHashCode() = {0}", obj.GetHashCode());
        } static void TestAnonymousType()
        { // 构造一个匿名对象表示一个雇员  var worker = new { FirstName = "Vincent", LastName = "Ke", Level = 2 };
            ReflectAnonymousType(worker);
        }


上例中,worker的类型是<>f__AnonymousType0`3(各版本之中可能会有所不同),匿名类型的类型名完全由编译器决定。更重要的是,使用对象初始化语法定义的每一个名称/值对被映射为同名的只读属性以及被封装的私有数据成员。

方法ToString()和GetHashCode()的实现

从上面可以看到,匿名类型直接了System.Object,并且重写了Equals()、GetHashCode()、ToString()方法。其中ToString()根据每一个名称/值对,生成并返回一个字符串,见上图。

GetHashCode()的实现使用每一个匿名类型的成员变量来计算散列值。当且仅当两个匿名类型有相同的属性别且被赋予相同的值时,就会产生相同的散列值,这样,匿名类型就可以很好的和Hashtable容器一起工作。


扩展方法

下一个与LINQ密切相关的C# 3.0语言功能是扩展方法(Extension method)。在这之前,一旦一个类型被编译进.NET程序集后,我们便不能再修改该类型的定义了。为该类型添加、修改、删除成员的唯一办法就是修改类型的定义代码。

但有时候,当需要为类型添加新功能但并不拥有类型的已有代码时,比如,我们想要为.NET库类型List添加自定义的dump方法时,该怎么做呢,答案是扩展方法。扩展方法允许在不修改类型定义的情况下,让该类型获得功能上的扩展。

定义扩展方法

当定义一个扩展方法时,第一个限制就是必须把方法定义在静态类中,因此每一个扩展方法也必须声明为静态的。第二个限制是扩展方法要用this关键字对第一个参数进行修饰,这个参数也就是我们希望进行扩展的类型。

比如下面的扩展方法允许.NET基类库中的所有对象都拥有全新的方法DisplayDefiningAssembly()。

        static class MyExtensions
        { // 本方法允许任何对象显示它所处的程序集  public static void DisplayDefiningAssemlby(this object obj)
            {
                Console.WriteLine("{0} is defined in: \\n\\t {1}\\n",
                    obj.GetType().Name,
                    System.Reflection.Assembly.GetAssembly(obj.GetType()));
            }
        }

调用扩展方法

我们有两种方式来使用扩展方法,第一种是在实例层次上调用扩展方法,第二种是静态调用扩展方法。

        public void UsingExtensionMethods()
        { int myInt = 12345; // 1. 在实例层次上调用扩展方法  myInt.DisplayDefiningAssemlby(); // 2. 静态调用扩展方法  MyExtensions.DisplayDefiningAssemlby(myInt);
        }

实例上,通过一个对象调用它的扩展方法只是编译器的烟幕弹效果而已,背后编译器会转换成静态方法的调用。

其他注意事项

上面说到,扩展方法本质上是可以从扩展类型的实例上调用的静态方法。所以它和普通的方法是不一样的,扩展方法不能直接访问扩展类型的成员,从另外一个角度讲,扩展方法即不是直接修改,也不是继承。

另外一个要注意的地方是:虽然表面上扩展方法是全局的,但其实他们受制于所处的命名空间,要使用在其他命名空间中定义的扩展方法时,我们首先需要导入该命名空间。

 

Lambda表达式

Lambda表达式的引入是与委托类型的使用密切相关的,本质上,Lambda表达式只是用更简单的方式来书写匿名方法,从而彻底简化.NET委托类型的使用。下面我们一步一步的来看看Lambda表达式的简化之路:

实例找出整数List<T>中的偶数,我们调用了List<T>类型的FindALl()方法,这个方法需要System.Predicate<T>泛型委托,它用于接受类型为T的输入参数并返回一个布尔值。

传统的委托使用方式

传统的委托使用方式会为委托目标定义一个单独的方法,如下:

        public static void TraditionalDelegateSyntax()
        {
            List<int> list = new List<int>();
            list.AddRange(new int[] { 1, 5, 10, 20 ,33 }); //使用传统委托语法调用FindAll  Predicate<int> callback = new Predicate<int>(IsEvenNumber);
            List<int> evenNumbers = list.FindAll(callback); foreach (int num in evenNumbers)
                Console.Write("{0}\\t", num); //Output:   10    20  } // Predicate<>委托的目标  static bool IsEvenNumber(int i)
        { return (i % 2) == 0;
        }

匿名方法取代显示的委托函数

这种方式让我们不再需要完整的方法定义,对于一些专门为了委托而定义的函数而言是一个很大的简化,如下:

        public static void AnonymousMethodSyntax()
        {
            List<int> list = new List<int>();
            list.AddRange(new int[] { 1, 5, 10, 20, 33 }); //使用匿名方法  List<int> evenNumbers = list.FindAll( delegate(int i)
                { return (i % 2) == 0;
                }); foreach (int num in evenNumbers)
                Console.Write("{0}\\t", num); //Output:   10    20  }

Lambda表达式

Lambda表达式让我们进一步简化FindAll()的调用,使用新的语法时,底层的委托语法消失得无影无踪,如下所示:

        public static void LambdaExpressionSyntax()
        {
            List<int> list = new List<int>();
            list.AddRange(new int[] { 1, 5, 10, 20, 33 }); //使用Lambda表达式  List<int> evenNumbers = list.FindAll(i => (i % 2) == 0); foreach (int num in evenNumbers)
                Console.Write("{0}\\t", num); //Output:   10    20  }

Lambda表达式可以应用于任何匿名方法可以应用的场合,而且比匿名方法更加简洁更节省编码时间。其实C#编译器只是把Lambda表达式翻译为相应的普通匿名方法而已。

Lambda表达式的格式:先定义参数列表,”=>”标记(可读为:goes to)紧随其后,然后是表达式。即:ArgumentsToProcess => StatementsToProcessThem

Lambda表达式的参数可以是显示类型化的也可以是隐式类型化的。比如上例中的参数i就是隐式类型化的,我们也可以写为如下:

            // 显示定义参数的类型  List<int> evenNumbers = list.FindAll((int i) => (i % 2) == 0);

Lambda表达式也可以是一个代码块,其中包含多条代码语句,用花括号括起来即可:

            // 使用语句块编写Lambda表达式  List<int> evenNumbers = list.FindAll((int i) =>
                {
                    Console.WriteLine("processing value: {0}", i); bool isEven = (i % 2) == 0; return isEven;
                });

 

对象初始化器

C# 3.0提供的 对象初始化器语法用来初始化新类或新结构变量的状态。使用这种语法,我们可以以一种非常简洁的方式来创建对象和为对象的属性赋值。如下:

    public class Point
    { public Point() { } public Point(int x, int y)
        {
            X = x;
            Y = y;
        } public int X { get; set; } public int Y { get; set; }
    } static void ObjectInitSyntax()
    { // 手动初始化各属性  Point aPoint = new Point();
        aPoint.X = 10;
        aPoint.Y = 20; // 使用新的对象初始化语法进行初始化  Point bPoint = new Point { X = 10, Y = 20 };
    }

使用初始化语法调用构造函数

上面的示例中,对象初始化语法会隐式调用默认的构造函数初始化Point实例,而且我们还可以显示调用定制的构造函数,如下:

        static void ObjectInitSyntax()
        { // 在这里,默认构造函数被隐式调用  Point bPoint = new Point { X = 10, Y = 20 }; // 我们也可以显示调用默认构造函数  Point cPoint = new Point() { X = 10, Y = 20 }; // 我们还可以调用自定义的构造函数,只是这里1, 2会被10, 20覆盖  Point dPoint = new Point(1, 2) { X = 10, Y = 20 };
        }

初始化内部类型

当我们用这种语法来初始化一个“复杂”的对象时,其优点会更具说服力,假如我们有类Rectangle如下,可以明显的看出,对象初始化语法不但大大减少了我们敲打键盘的次数,也更加的简洁明了。

    public class Rectangle
    { public Point TopLeft { get; set; } public Point BottomRight { get; set; }
    } static void CompareObjectInitMethods()
    { // 传统初始化方法  Rectangle r = new Rectangle();
        Point p1 = new Point();
        p1.X = 10;
        p1.Y = 10;
        r.TopLeft = p1;
        Point p2 = new Point();
        p2.X = 20;
        p2.Y = 20;
        r.BottomRight = p2; // 对象初始化语法  Rectangle r2 = new Rectangle
        {
            TopLeft = new Point { X = 10, Y = 10 },
            BottomRight = new Point { X = 20, Y = 20 }
        };
    }

集合的初始化

集合初始化语法非常类似于对象初始化语法,它使得我们可以像初始化普通数组一样初始化容器(如ArrayList或List<T>)。

        static void CollectionInitSyntax()
        { // 初始化标准数组  int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 初始化一个ArrayList  ArrayList list = new ArrayList { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 初始化一个List<T>泛型容器  List<int> list2 = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 如果容器存放的是非简单对象  List<Point> pointList = new List<Point>
            { new Point { X = 2, Y = 2}, new Point { X = 3, Y = 3}
            }; // 使用恰当的缩进和嵌套的大括号会使代码易于阅读,同时节省我们的输入时间 // 想想如果不使用初始化语法构造如下的List,将需要多少行代码  List<Rectangle> rectList = new List<Rectangle>
            { new Rectangle { TopLeft = new Point { X = 1, Y = 1},
                    BottomRight = new Point { X = 2, Y = 2}}, new Rectangle { TopLeft = new Point { X = 3, Y = 3},
                    BottomRight = new Point { X = 4, Y = 4}}, new Rectangle { TopLeft = new Point { X = 5, Y = 5},
                    BottomRight = new Point { X = 6, Y = 6}}
            };
        }