C# 语言增强特性

C# 语言增强特性

第一章中,我们回顾了LINQ背后的动机并介绍了一些代码使你对LINQ有一个整体的印象。本章中,我们将使用LINQ查询直接在编程语言中编写成为可能的语言扩展。

LINQ使用新的构造扩展了C#,在我们学习LINQ之前,知道这些语言特性是很重要的。本章是彻底了解LINQ的基石。但是请注意,这些功能强大的新特性不只是可以运用在LINQ当中,也可以应用在其他场景当中。

我们不会对每个特性进行详细的讨论,因为我不想让你离开LINQ太久。这些新特性遍布在本书当中,当你阅读的时候,你会慢慢的了解它。

 

2.1 语言新特性

.NET2.0LINQ工作奠定了基础 。它增强了语言和框架的能力。例如:.NET支持泛型,为了达到深度集成数据查询的目标,我们需要参数化的类型。

C# 2.0同样添加了匿名方法和迭代器。这些特性是集成编程语言和数据的核心。

但是,为了实现LINQ的查询语法,需要编程语言提供更多的特性。这些特性包括:

n  隐式本地变量类型,它允许本地变量的类型可以从初始化该变量的表达式推断得知。

n  对象初始化器,使得构造和初始化对象更加方便

n  Lambda(读作兰穆达)表达式,一个匿名方法的进化体。它为代理类型和表达式树提供了改进的类型推断和转换

n  扩展方法,它可以为现有类型添加新的方法。使用扩展方法,类型没有被扩展,但是看上去好像是被扩展了。

n  匿名类型,它可以从对象初始化器自动创建。

 

相对于把这些新特性一一列举,在示例中讲解会更好一些。这能帮助我们看到这些新特性是如何帮助我们工作的。

我们将使用一个简单的示例,这个示例基于.NET2.0,之后我们会渐渐引入语言新特性。每步都处理一个特定的问题。首先,让我们熟悉一下我们的示例,一个输出正在运行的进程列表的程序。

 

2.1.1   产生一个正在运行的进程的列表

我们要获取一个正在运行的进程的列表,使用System.Diagnostics.Process.GetProcesses API可以很轻松的做到这一点。

列表2.1的显示实现此功能的代码

列表 2.1.列出进程列表的NET 2.0代码

 

using System;

using System.Collections.Generic;

using System.Diagnostics;

 

static class LanguageFeatures

{

static void DisplayProcesses()

{

List<String> processes = new List<String>();

foreach (Process process in Process.GetProcesses())

processes.Add(process.ProcessName);

ObjectDumper.Write(processes);

}

static void Main()

{

DisplayProcesses();

}

}

 

变量processes表示一个字符串列表,它是一个泛型列表。泛型是.NET2.0中的新特性。这使得我们可以重用代码,类型安全和提高性能。泛型的最通用的用法是创建强类型集合。LINQ大量使用了泛型。

列表中,我们使用了一个名为ObjectDumper的类来显示结果。ObjectDumper类是一个微软在LINQ示例代码中提供的工具。我们将在我们的代码示例中使用此类。代码示例可从此处下载:http://LinqInAction.net. ObjectDumper可以将内存中对象的内容输出到控制台上。在调试时,这非常有用,在这里,我们显示了处理的结果。

这是我们代码的第一个版本,示例很简单。接下来,我们将显示更复杂的结果。ObjectDumper将会在显示上为我们节约很多代码。

这是列表2.1所示代码的输出

 

firefox

Skype

WINWORD

devenv

winamp

Reflector

这是示例非常简单,很快,我们将过滤列表,排序或者执行很多其他操作,如分组和映射。

作为开始,如果我们要获取更多的信息而不只是进程名称,我们将会怎么做呢?

2.1.2   分组结果到一个类中

假如我们要获取一个列表,该列表中包含每个进程的ID ,名称和耗费的内存大小。如下所示:

Id=2300

Name=firefox

Memory=78512128

Id=2636

Name=Skype

Memory=23478272

Id=2884

Name=WINWORD

Memory=78442496

Id=2616

Name=devenv

Memory=54296576

Id=1824

Name=winamp

Memory=29188096

Id=2940

Name=Reflector

Memory=83857408

 

这需要创建一个类或者结构来保存我们需要获取的信息,列表2.2显示我们定义的新类ProcessData

 

列表2.2 改进的进程列表的.NET 2.0代码

 

using System;

using System.Collections.Generic;

using System.Diagnostics;

 

static class LanguageFeatures

{

class ProcessData

{

public Int32 Id;

public Int64 Memory;

public String Name;

}

static void DisplayProcesses()

{

List<ProcessData> processes = new List<ProcessData>();

foreach (Process process in Process.GetProcesses())

{

ProcessData data = new ProcessData();

data.Id = process.Id;

data.Name = process.ProcessName;

data.Memory = process.WorkingSet64; processes.Add(data);

}

 

ObjectDumper.Write(processes);

}

 

static void Main()

{

DisplayProcesses();

}

}

虽然我们的代码完成了任务,但是却有一些重复信息。对象的类型指定了两次:一次是声明变量时,一次是调用构造函数时。

List<ProcessData> processes = new List<ProcessData>();

...

ProcessData data = new ProcessData();

New关键字可以让我们的代码更短且避免重复。如下所述。

2.2 隐式类型的本地变量

var i = 5;

C#3.0提供的new关键字允许我们不需要显式指定类型的情况下声明本地变量。编译器会从赋值给该变量的表达式推断该变量的类型。

让我们复习一下new关键字的新用法,然后修改我们的示例代码。

2.2.1    语法

var关键字很容易使用。它必须与局部变量一起使用并且被赋予一个初始化表达式。例如,下面的两段代码是等价的,他们被编译为相同的中间语言。

下面是用隐式变量声明的代码

 

var i = 12;

var s = "Hello";

var d = 1.0;

var numbers = new[] {1, 2, 3}; var process = new ProcessData(); var processes =

new Dictionary<int, ProcessData>();

下面是声明变量的传统用法:

int i = 12;

string s = "Hello";

double d = 1.0;

int[] numbers = new int[] {1, 2, 3};

ProcessData process = new ProcessData();

Dictionary<int, ProcessData> processes = new Dictionary<int, ProcessData>();

隐式类型的局部变量是强类型的。不能把不同类型的值赋予隐式类型变量。

2.2.2   使用隐式类型局部变量改进我们的示例

列表2.3显示var关键字的用法,新代码用粗体表示。

列表2.3 使用var关键字的DisplayProcesses 方法

 

using System;

using System.Collections.Generic;

using System.Diagnostics;

 

static class LanguageFeatures

{

class ProcessData

{

public Int32  Id { get; set; }

public Int64  Memory { get; set; }

public String Name { get; set; }

}

 

static void DisplayProcesses()

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

var data = new ProcessData();

data.Id = process.Id;

data.Name = process.ProcessName;

data.Memory = process.WorkingSet64;

processes.Add(data);

}

 

ObjectDumper.Write(processes);

}

 

static void Main()

{

DisplayProcesses();

}

}

注意: 这次,我们使用了自动完成属性来定义ProcessData类。这是C#3.0编译器的新特性。它创建了匿名私有变量来保持每个属性的值。使用这种新语法,我们可以避免显式声明属性值的持有者。

 

列表2.32.3很相似。看上去有些不同, 但是processes, process等变量仍然是强类型的。

使用隐式类型的局部变量。我们不用两次键入局部变量的类型。编译器自动推断它的类型。这表示我们使用了简单的语法,仍然获得了使用强类型所获得的好处。

现在我们知道,var关键字会使我们的代码更加简短。这种用法一般和LINQ的特性一起使用。然而,如果你想在方法体的上方一次声明所有的变量,那么就应该慎用var关键字。

让我们继续完善我们的示例,初始化ProcessData对象需要很长的代码。下面就将介绍解决这个问题的方法。

2.3 对象和集合的初始化器

new Point {X = 1, Y = 2}

我们以介绍对象和集合初始化器来开始本节。下面我们将使用对象初始化器来改进我们的示例。

2.3.1   我们需要对象初始化器

对象初始化器允许我们在一行代码中指定一个对象的多个属性或者字段的值。它允许在声明初始化的任何对象上使用。

注意:这种行为只能在可访问的字段或者属性上进行。它们的使用方法与对这些字段和属性直接赋值相同。

 

到现在为止,我们已经可以使用这种方法对数组类型初始化,如下所示:

int i = 12;

string s = "abc"

string[] names = new string[] {"LINQ", "In", "Action"}

使用同样的方法对其他对象初始化是不可行的。我们需要使用下面的代码初始化一个对象。

ProcessData data = new ProcessData();

data.Id = 123;

data.Name = "MyProcess";

data.Memory = 123456;

使用 C# 3.0 VB.NET 9.0, 我们可以使用初始化器的方法初始化对象

var data = new ProcessData {Id = 123, Name = "MyProcess", Memory = 123456};

这段代码和没有使用对象初始化器的代码产生相同的IL代码。对象初始化器只是一个简单的快捷方式。

有些情况下,构造函数仍然是有用的。我们仍然可以同时使用构造函数和对象初始化器,如下代码所示:

throw new Exception("message") { Source = "LINQ in Action" };

我们用一行代码初始化了两个属性:Message(通过构造函数初始化)和Source(通过对象初始化器)。如果没有新语法,我们就需要按照如下方式写这段代码了。

var exception = new Exception("message");

exception.Source = "LINQ in Action";

throw exception;

2.3.2   集合初始化器

.NET引入了另一种初始化器:集合初始化器。这种新语法允许我们初始化不同类型的集合,只要集合执行了System.Collections.IEnumerable接口比提供了合适的Add方法。

如下示例:

var digits = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

这行代码和以下代码作用相同:

List<int> digits = new List<int>();

digits.Add(0);

digits.Add(1);

digits.Add(2);

...

digits.Add(9);

当选和集合初始化器在一起使用的时候,这种用法就会变得很有用。下面的两个等价的代码块显示了初始化器是如何为我们节约代码的。下面是使用初始化器的代码:

 

var processes = new List<ProcessData> {

new ProcessData {Id=123, Name="devenv"},

new ProcessData {Id=456, Name="firefox"}

}

下面是传统的初始化代码:

ProcessData tmp;

var processes = new List<ProcessData>();

tmp = new ProcessData();

tmp.Id = 123;

tmp.Name = "devenv";

 

processes.Add(tmp);

tmp = new ProcessData();

tmp.Id = 456;

tmp.Name = "firefox";

processes.Add(tmp);

我们可以初始化执行IEnumberable接口并提供Add方法的集合。可以使用匹配Add方法参数签名形如{x, y, z}的方法来初始化对象。

如此,我们可以使用如下语法初始化一个dictionary:

new Dictionary<int, string> {{1, "one"}, {2, "two"}, {3, "three"}}

2.3.3   使用对象初始化器改进示例

如下代码所示,为了创建ProcessData对象,我们需要写很多代码:

ProcessData data = new ProcessData();

data.Id = process.Id;

data.Name = process.ProcessName;

data.Memory = process.WorkingSet64;

processes.Add(data);

我们可以为ProcessData类提供一个构造函数来在一行代码中初始化这个类型的对象,如列表2.4所示:

 

列表 2.4    DisplayProcesses method using a constructor for ProcessData

 

static void DisplayProcesses()

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

processes.Add( new ProcessData(process.Id, process.ProcessName, process.WorkingSet64) );

}

 

ObjectDumper.Write(processes);

}

添加构造函数需要为ProcessData类添加代码。此外,构造函数可能不能满足我们初始化对象的用法。替代的方法就是使用对象初始化器的语法。如列表2.5所示:

 

列表 2.5    DisplayProcesses method using an object initializer

 

static void DisplayProcesses()

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

processes.Add( new ProcessData { Id=process.Id, Name=process.ProcessName, Memory=process.WorkingSet64 } );

}

 

ObjectDumper.Write(processes);

}

 

虽然这两种语法是相似的,但是后者不需要我们添加构造函数。

由此可见对象初始化器方法的优点如下:

n  我们可以用一条指定初始化对象

n  初始化简单对象时,我们不需要提供一个构造函数。

n  我们不需要不同的构造函数初始化不同的属性

 

这并不表示,对象初始化器可以替代构造函数。对象初始化器和构造函数是互补的语言特性。你仍然需要定义合适的构造函数来初始化你的对象。构造函数能够帮助你阻止不合理的成员初始化顺序。

在使用这些新语法改进我们的示例以后,让我们为示例添加一些新功能。我将使用lambda表达式来做到这些。

 

2.4 Lambda 表达式

 

address => address.City == "Paris"

 

lambda表达式作为语言新特性的一部分,来自lambda运算中。如Lisp这样的功能性语言使用lambda符号定义方法。

Lambda 运算

在数学逻辑和计算机科学中,lambda运算是一种正式的系统设计,用来研究功能定义,功能运行和递归。它由Alonzo Church1930年引入,lambda运算极大的影响了功能编程语言,如LispMLHaskell

让我们回到示例中。假设我们要添加过滤功能到示例中,为了做到这一点。我们可以使用代理,这允许我们传递一个方法作为参数给另一个方法。例如:

在使用lambda表达式之前,让我们回顾一下代理和匿名方法的用法。

2.4.1    代理回顾

现在,我们添加了硬编码的过滤条件,如列表2.6所示:

 

列表 2.6    DisplayProcesses method with a hard-coded filtering condition

 

static void DisplayProcesses()

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

if (process.WorkingSet64 >= 20*1024*1024)

{

processes.Add(new ProcessData { Id=process.Id, Name=process.ProcessName, Memory=process.WorkingSet64 });

}

}

 

ObjectDumper.Write(processes);

}

 

WorkingSet64表示当前进程分配的物理内存大小。现在我们要查找分配大于20M的进程。

为了规范我们的代码,我们将过滤代码作为参数而不是硬编码。在C#2.0及之前,这是可行的,使用代理可以做到这一点,代理是存储一个方法指针的类型。

我们的过滤方法需要将一个进程对象过为参数,并且返回一个bool值表示我们的进程是不是符合指定的标准。下面是这个代理的声明:

delegate Boolean FilterDelegate(Process process);

除了可以使用自己定义的代理,我们还可以使用.NET2.0提供的Predicate<T>类型,下面是该类型的定义:

delegate Boolean Predicate<T>(T obj);

Predicate<T>代理类型标识一个方法基于它的输入返回一个真或者假。这个类型是泛型的,所以我们需要指定它要工作Process对象之上。我们使用的真正的代理类型是Predicate<Process>类型。

列表2.7显示了DisplayProcesses方法使用了谓词代理类型的一个参数。

 

列表 2.7    DisplayProcesses method that uses a delegate for filtering

 

static void DisplayProcesses(Predicate<Process> match)

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

if (match(process))

{

processes.Add(new ProcessData { Id=process.Id, Name=process.ProcessName, Memory=process.WorkingSet64 });

}

}

 

ObjectDumper.Write(processes);

}

 

 

使用DisplayProcesses方法,我们现在可以进行过滤了。在这种情况下,过滤方法包含了我们的过滤条件,当进程匹配条件的时候,它返回true

static Boolean Filter(Process process)

{

return process.WorkingSet64 >= 20*1024*1024;

}

 

为了使用这个方法,我们把它作为参数提供给DisplayProcesses方法。如列表2.8所示。

 

列表 2.8    Calling the DisplayProcesses method using a standard delegate

 

DisplayProcesses(Filter);

2.4.2    匿名方法

代理在C#1.0中就已经存在,但是C#2.0中使代理可以匿名方法一起工作。匿名方法允许你写简短的代码时避免显式命名一个方法。

由于匿名方法的存在,我们不需要声明一个像Filter一样的方法。我们可以直接把代码传送到DisplayProcesses方法中,如列表2.9所示:

列表 2.9    Calling the DisplayProcesses method using an anonymous method

 

DisplayProcesses( delegate (Process process)

{ return process.WorkingSet64 >= 20*1024*1024; } );

注意      VB.NET 不提供匿名方法的支持

.NET2.0System.Collections.Generic. List<T>中引入很多方法,可以将匿名方法作为参数传入这些方法。这些方法包括:ForEach, Find, and FindAll等,它们可以使用很少的代码操作一个列表和数组。

例如:下面是使用接受一个匿名方法的Find方法查找一个指定的进程。

var visualStudio = processes.Find(delegate (Process process)

{ return process.ProcessName == "devenv"; } );

 

2.4.3    lambda 表达式介绍

除了使用匿名方法,使用C#3.0我们可以使用lambda表达式。

列表2.10与前一段代码完成相同的功能

列表 2.10    Calling the DisplayProcesses method using a lambda expression

 

DisplayProcesses(process => process.WorkingSet64 >= 20*1024*1024);

 

注意到,当我们使用lambda表达式的时候,我们的代码是多么的简单。Lambda表达式可以这样理解:“给定一个进程,如果这个进程消耗了大于20M的内存时返回真”。

如你所料,在我们使用lambda表达式的时候,我们不需要提供参数的类型。新的C#编译器会自动从方法签名推断参数类型。

 

lambda 表达式和匿名方法的比较

C#2.0提供了匿名方法,这允许我们在需要代理的时候内联代码块。可是匿名方法的语法并不简洁。而lambda表达式提供了一种更简洁的语法。

Lambda表达式可以看作匿名方法的功能超集,提供了如下额外的功能:

n  Lambda表达式可以自动推断参数类型,允许你省略他们

n  Lambda表达式允许使用语句块或者表达式作为语句体。这比匿名方法的语法更简洁,匿名方法只能使用语句块。

n  Lambda表达式参与参数类型推断,而且还能对方法重载进行分析。

n  带有表达式体的Lambda表达式可以被转换为表达式树。

下面我们将学习一下lambda表达式的结构,不要害怕,你会慢慢的习惯它的。

 

如何表达lambda 表达式

C#中,lambda表示是一个参数列表,后边跟随着=>符号,然后是一个表达式或者是语句块。如图2.1所示:

2.1lambda表达式结构

 

 

 

 

 

 

注意         不必要把符号 =>与比较运算符 <= >=弄混了。

这个lambda运算符可以读作“流入(goes to)”。运算符的左边指定输入的参数列表(如果有参数),右边包含等待运算的表达式和语句块。

这里有两种lambda表达式。带有表达式的lambda表达式被称作表达式lambda。第二种是语句lambda。两者有些类似,除了语句lambda允许在关闭的花括号中有任意数量的语句。

为了让你更好的了解lambda表达式,请看列表2.11的示例。

列表 2.11    Sample lambda expressions in C#

 

x => x + 1                                                B

x => { return x + 1; }                             C

(int x) => x + 1                                       D

(int x) => { return x + 1; }                     E

(x, y) => x * y                                         F

() => 1                                                      G

() => Console.WriteLine()                   H

customer => customer.Name

person => person.City == "Paris"

(person, minAge) => person.Age >= minAge

 

B  隐式类型,表达式体

C  隐式类型,语句体

D  显式类型,表达式体

E  显式类型,语句体

F  多参数

G  无参数,表达式体

H  无参数,语句体

注意 Lambda表达式的参数类型可以显式或隐式定义。

在示例中我们看到,lambda表达式与代理是兼容的。为了让你看到如何把lambda表达式作为代理。我们将使用一些代理类型。.NET2.0引入了System.Action<T>, System.Converter<TInput,  TOutput>,System.Predicate<T>的泛型代理类型。

delegate void Action<T>(T obj);

delegate TOutput Converter<TInput, TOutput>(TInput input);

delegate Boolean Predicate<T>(T obj);

 

另一个有用的代理是前一个.NET版本的代理MethodInvoker。这个类型表示一个没有参数和没有返回值的方法。

 

delegate void MethodInvoker();

 

可惜的是MethodInvoker被声明到System.Windows.Forms空间中。因为在此空间之外,这个代理也很有用。不过.NET3.5已经解决了这个问题。一个新的Action代理类型被引入到System.Core.dll中。

delegate void Action();

System命名空间增加了很多代理类型,这些在System.Core.dll中定义:

delegate void Action<T1, T2>(T1 arg1, T2 arg2);

delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2);

delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

delegate TResult Func<TResult>();

delegate TResult Func<T, TResult>(T arg);

delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2);

delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

遵从以下规则,那么lambda表达式与代理就是兼容的:

n  Lambda必须包含与指定代理相同的参数个数。

n  每个输入参数都可以隐式的转换到对应的代理定义的参数类型

n  Lambda表达式的返回值类型能够隐式转换到指定代理的返回值类型

 

列表2.13 2.14所示代码展现了表达式和代理之间的兼容性。

 

列表 2.13    声明为代理的lambda 表达式

 

Func<DateTime> getDateTime = () => DateTime.Now;                    B

Action<string> printImplicit = s => Console.WriteLine(s);                C

Action<string> printExplicit = (string s) => Console.WriteLine(s);  D

Func<int, int, int> sumInts = (x, y) => x + y;                                          E

Predicate<int> equalsOne1 = x => x == 1;                                             F

Func<int, bool> equalsOne2 = x => x == 1;                                            F

Func<int, int> incInt = x => x + 1;                                                             G

Func<int, double> incIntAsDouble = x => x + 1;                                   G

Func<int, int, int> comparer = (int x, int y) =>                             H

{

if (x > y) return 1; if (x < y) return -1; return 0;

};

 

B  没有参数

C  隐式类型的字符串参数

D  显式类型的字符串参数

E  两个隐式类型参数

F  等价但不兼容

G  相同的lambda 表达式,但是不同的代理类型

H  语句体和显式类型参数

下面是到了改进我们示例的时候了!

2.5      扩展方法

static void Dump(this object o);

 

下一个我们要叙述的主题是扩展方法。你会看到,这种新语言特性允许你在类型已经定义后再为它添加一个新方法。我们还会对扩展方法与静态方法以及实例方法进行比较。

下面我们将创建一个示例扩展方法。

2.5.1   创建一个扩展方法示例

在改进我们示例的过程中,我们会计算列表中所有进程中消耗的内存。我们可以定义一个标准的静态方法,接受一个进程列表参数,该方法会遍历所有进程以获取所有进程使用的内存之和。

如列表2.15所示:

列表 2.15    作为标准静态方法的TotalMemory 方法的代码

 

static Int64 TotalMemory(IEnumerable<ProcessData> processes)

{

Int64 result = 0;

foreach (var process in processes)

result += process.Memory;

return result;

}

 

我们可以如下使用此方法:

Console.WriteLine("Total memory: {0} MB", TotalMemory(processes)/1024/1024);

我们可以做的是将这个静态方法转换为扩展方法,这种新语言特性使得我们可以对现有类型扩展一些其他的方法。

C#中声明扩展方法

为了转换我们的方法为扩展方法。我们要做就是把this关键字加入到第一个参数上,如列表2.16所示:

列表 2.16    作为扩展方法声明的TotalMemory方法

 

static Int64 TotalMemory(this IEnumerable<ProcessData> processes)         B

{

Int64 result = 0;

foreach (var process in processes)

result += process.Memory;

return result;

}

现在我们看到新方法与原来方法的唯一区别就是在在第一个参数前面加了this关键字。

This关键字指示编译器把这个方法当作扩展方法对待。而且这个方法是扩展在IEnumerable<ProcessData>之上的方法。

注意:     C#中,扩展方法必须声明在静态类中,此外扩展方法可以有任意个参数,但是第一个参数的类型必须是要被扩展的类型,以this关键字修饰。

 

现在我们可以像使用实例方法一样使用TotalMemory方法了。下面是使用的语法:

Console.WriteLine("Total memory: {0} MB", processes.TotalMemory()/1024/1024);

 

看看我们是如何为IEnumerable<Pro- cessData>扩展新方法的。类型其实没有变化。编译器将该方法的调用转换为一个静态方法的调用。使用不使用扩展方法看上去区别不大,但是我们得到了智能感知功能提供的好处。如图2.3所示。

2.3智能感知使用特殊的图标显示扩展方法

 

 

 

注意到扩展方法的特定的图标带着一个蓝色的箭头。上图表明ToListToLookup标准查询操作符和我们的TotalMemory方法一样是扩展方法。现在我们可以很容易的使用该方法来获取内存之和。智能感知是扩展方法比原来的静态方法更容易被发现。

扩展方法的另一个优点是它可以将更容易的将操作链在一起。让我们考虑如下所做的:

 

1.         使用一个帮助方法过滤ProcessData的对象集合。

2.         计算所得进程耗费的内存之和

3.         把内存耗费量以M计算

 

使用传统的方法,我们可以写下如下代码:

BytesToMegaBytes(TotalMemory(FilterOutSomeProcesses(processes)));

这种代码的一个问题是操作按照它们指定的相反的方向执行。这使得代码难于编写和阅读。使用扩展方法,我们可以编写如下代码:

processes

.FilterOutSomeProcesses()

.TotalMemory()

.BytesToMegaBytes();

在后者代码中,操作按照与显示的相同的顺序执行,这更容易阅读,你认为呢?

注意 C#3.0不支持其他方式的扩展成员,如属性,事件,操作符。他们可能在将来的版本中支持

为了让你对扩展方法有更好的了解,让我们看看LINQ使用扩展方法的示例。

2.5.2   使用LINQ标准查询运算符的更多示例

LINQ来自一系列扩展方法的集合,你可以与其他扩展方法一样使用这些扩展方法。

OrderByDescending

回到示例,如果我们需要对进程列表按照他们占用的内存大小排序,我们可以使用在System.Linq.Enumerable类中定义的扩展方法OrderByDescending。扩展方法可以使用命名空间导入,如下。

using System.Linq;

注意 你的项目需要添加System.Core.dll的引用,不过这是编译器的默认行为。

现在我们可以调用OrderByDescending方法对进程排序了:

ObjectDumper.Write(

processes.OrderByDescending(process => process.Memory));

你可以看到,我们为扩展方法提供了一个lambda表达式来决定如何排序。在这里,我们指出我们要使用每个进程所占有的内存进行排序。

有一点需要注意,为了简化代码,类型将会被自动推断。虽然OrderByDescending是一个泛型方法,但是,我们不需要显式指定我们要处理的实际类型。C#编译器会从方法调用自动推断OrderByDescending工作在Process对象之上,并且返回Int64对象。

当我们调用泛型方法时没有指定类型,类型推断过程将会从调用进行参数类型推断。这种类型推断允许我们使用更简单的语法来调用泛型方法,而且允许程序员避免指定多余的类型信息。

下面是OrderByDescending的定义:

public static IOrderedSequence<TSource>

OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)

如果没有类型推断,我们将不得不写如下代码来完成相同的工作:

 

processes.OrderByDescending<Process, Int64>(

(Process process) => process.Memory));

如果我们需要在LINQ查询中指定所有类型,那么代码将会很难阅读。

下面让我们看看其他查询运算符。

Take

如果只对占有内存最大的两个进程感兴趣。我们可以使用Take方法。

ObjectDumper.Write(

processes

.OrderByDescending(process => process.Memory)

.Take(2));

Take方法返回列表中前n个元素。这里我们取了两个元素。

Sum

如果我们需要对前两个进程占有的内存大小进行求和。我们可以使用另一个标准的扩展方法SumSum方法可以取代我们TotalMemory方法。用法如下:

ObjectDumper.Write(

processes

.OrderByDescending(process => process.Memory)

.Take(2)

.Sum(process => process.Memory)/1024/1024);

2.5.3   扩展方法在示例中实际应用

列表2.18显示了做了所有我们刚才讲述的操作的DisplayProcess方法

 

列表 2.18    使用扩展方法的DisplayProcesses 方法

 

static void DisplayProcesses(Func<Process, Boolean> match)

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

if (match(process))

{

processes.Add(new ProcessData {

Id=process.Id,

Name=process.ProcessName,

Memory=process.WorkingSet64 });

}

}

 

Console.WriteLine("Total memory: {0} MB", processes.TotalMemory()/1024/1024);

 

var top2Memory =processes

.OrderByDescending(process => process.Memory)

.Take(2)

.Sum(process => process.Memory)/1024/1024;

Console.WriteLine("Memory consumed by the two most hungry processes: {0} MB", top2Memory);

 

ObjectDumper.Write(processes);

}

 

可以看到,扩展方法在一起使用的时候尤其有用,没有扩展方法,我们的代码就很难理解。

这些方法是用了传统的静态方法:

var top2Memory =

Enumerable.Sum( Enumerable.Take(

Enumerable.OrderByDescending(processes,

process => process.Memory),

2),

process => process.Memory)/1024/1024;

 

与使用扩展方法的方法比较:

var top2Memory =

processes

.OrderByDescending(process => process.Memory)

.Take(2)

.Sum(process => process.Memory)/1024/1024;

由此可以看到,扩展方法很容易的使用了链模式,使用.符号可以把这些方法串在一起。这很像一个管道,如Unix管道。

可以看到,后者的代码是多么的简单。很清晰的表达了处理步骤:我们要对进程按内存排序,然后取前两个,然后对他们的内存占有量求和。使用第一段代码,就不是这么明显,因为他们的方法是嵌套调用的。

2.5.4    警告

在允许我们的示例之前,让我们看看扩展方法的缺点。

使用扩展方法会引起非常严重的问题,如果扩展方法与实例方法冲突怎么办呢?知道如何同扩展方法一起工作非常重要。

扩展方法将晚于实例方法被发现式,这表示它们的优先级很低。扩展方法不能隐藏实例方法。请看列表2.19.

列表 2.19    阐述扩展方法可发现性的示例代码

 

using System;

 

class Class1

{

}

 

class Class2

{

public void Method1(string s)

{

Console.WriteLine("Class2.Method1");

}

}

 

class Class3

{

public void Method1(object o)

{

Console.WriteLine("Class3.Method1");

}

}

 

class Class4

{

public void Method1(int i)

{

Console.WriteLine("Class4.Method1");

}

}

 

static class Extensions

{

static public void Method1(this object o, int i)

{

Console.WriteLine("Extensions.Method1");

}

 

static void Main()

{

new Class1().Method1(12);

new Class2().Method1(12);

new Class3().Method1(12);

new Class4().Method1(12);

}

}

 

以上代码将产生如下输出:

 

Extensions.Method1

Extensions.Method1

Class3.Method1

Class4.Method1

可以看到只要存在与调用匹配的实例方法,它将得到执行。扩展方法只有在没有相同的方法签名的情况下才能得到执行。

扩展方法在功能上比实例方法更有限。他们不能访问非公共成员。同时,如果你不清楚扩展方法的使用不是明确,大量使用扩展方法会降低代码的可读性。我们希望能限制扩展方法的使用,只有当实例方法不可用的情况下使用扩展方法。

使用这些新特性,我们已经对我们的代码做了很大的改进。但是我们可以做的更好。如果我们能够摆脱ProcessData类,是不是一个很大的提高呢?因为它是一个临时的类,而且占用很多行代码。没有这些代码,事情将会变得更好。这就是匿名类型可以帮我做的事情。

2.6 匿名类型

 

var contact = new { Name = "Bob", Age = 8 }

这是我们介绍的最后一个新语言特性。

使用类似于对象初始化的语法。我们可以创建匿名类型。这使得我们不用定义新类就可以将数据组合到对象中。

我们将会为你展示,匿名类型是真正的类型,而且有几个缺点。

2.6.1   使用匿名类型组合数据到对象中

现在,我们需要收集进程数据。把信息组合到对象中,为了这个目的声明一个类是件痛苦的事情。

下面是使用匿名类型的C#代码:

var results = new {

TotalMemory = processes.TotalMemory()/1024/1024,

Top2Memory = top2Memory,

Processes = processes };

2.6.2   没有名字的类型,却有类型

匿名类型是没有名字的类型(没有我们可以使用的类型名),但是却仍然有类型。这表示真正的类型是由编译器创建的。我们的results变量表明编译器基于我们代码自动创建匿名类型。属性的类型是从初始化器推断出来的。

2.4显示的是编译器为我们创建的匿名类型

 

 

 

 

2.4    由编译器产生的示例匿名类型,由.NET Reflector工具显式

 

 

上图是.NET Reflector显示的匿名类型的反编译代码的截图。Reflector是一个非常有用的工具,可以从http://aisto.com/ roeder/dotnet下载。

要注意的是,如果在同一个程序中,具有相同属性名称和属性顺序的两个匿名对象,编译器将为他们产生相同的匿名类型。例如,如下代码,编译器将只会产生一个类型。

var v1 = new { Person = "Suzie", Age = 32, CanCode = true }

var v2 = new { Person = "Barney", Age = 29, CanCode = false }

当这段代码被执行的时候,两个边路v1v2是同一个类的两个不同实例。

如果我们使用如下代码,那么编译器将会为v3产生一个不同的类型。

var v3 = new { Age = 17, Person = "Bill", CanCode = false }

2.6.3   使用匿名类型改进我们的示例

现在我们要摆脱ProcessData对象的束缚。列表2.20显示了DisplayProcesses方法使用匿名类型替换了ProcessData类。

列表 2.20    使用匿名类型的DisplayProcesses方法

 

static void DisplayProcesses(Func<Process, Boolean> match)

{

var processes = new List<Object>();

foreach (var process in Process.GetProcesses())

{

if (match(process))

{

processes.Add( new {

process.Id,

Name=process.ProcessName,

Memory=process.WorkingSet64 } );

}

}

 

ObjectDumper.Write(processes);

}

注意 如果没有为属性指定一个名称,而且表达式是简单名车和成员访问,那么结果属性将采用表达式中的成员名。所以第一个成员的名称是Id。为了更清晰的表达意图,显式指定名称是很好的做法。

 

这种代码的最大的好处是,我们不需要声明ProcessData类。这使得处理简单的临时结果变得很容易。由于匿名类型的存在,我们终于不必再声明ProcessData类了。

但是,匿名类型也有很多局限。

2.6.4    局限

一旦你使用了匿名类型,你就不能在使用这个类型方法外部使用这个类型了。这意味着我们只能将该匿名类型对象传递给一个接受Object类型的参数。在方法外部只能使用反射来访问匿名类型。

同样,匿名类型不能作为方法的结果。除非方法的返回值类型是Object。这就是匿名类型应该被用于临时数据的原因。它们不能像普通类型那样使用。

好,这并不完全正确。我们可以使用匿名类型作为泛型方法的返回值。考虑如下方法:

 

public static TResult ReturnAGeneric<TResult>( Func<TResult> creator)

{

return creator();

}

 

ReturnAGeneric方法是泛型的,如果我们隐式指定TResult类型参数,编译器会自动从creator参数推断。现在让我考虑如下调用:

 

var obj = ReturnAGeneric(

() => new {Time = DateTime.Now, AString = "abc"});

因为creator方法返回了一个匿名对象,ReturnAGeneric方法返回了这个对象。然是ReturnAGeneric方法被定义返回一个泛型对象,而不是Object类型。所以obj变量是强类型的。不过这种使用方法没有实际用处。你可以看到LINQ以一种更有效的方式使用了匿名类型。

还有一点需要注意的是,在C#中,匿名类型的实例是不可变得。这意味着一旦你创建了匿名类型的实例,它们的字段和属性值就永远不可变。如图2.4所示,你可以看到属性只有get访问器,却没有set访问器。为该对象属性赋值的唯一方法就是使用构造函数。当使用初始化语法初始化该匿名对象的时候,该匿名对象的构造函数被自动调用,复制操作在此时完成。

因为匿名对象是不可变的,所以匿名类型有固定的hash值。如果一个对象不能修改,那么它的hash值就是不变的(除非它的一个字段不是固定的)。这对于哈希表和数据绑定场景非常有用。

2.7 摘要

本章涵盖了C#3.0提供的很多语言扩展:

n  隐式类型局部变量

n  对象和集合初始化器

n  Lambda 表达式

n  扩展方法

n  匿名类型

所有这些新特性是LINQ的基础,但他们也是C#语言一部分,可以被单独使用。这表示微软将动态和功能性语言的一些好处带给了.NET开发者。

 

 

特性备注

本章中,我们使用自动完成属性,但是这个新特性只存在在C#中,且并不是为支持LINQ而产生的。如果你想学习更多的C#新特性,我们建议你阅读Manning的另一本书:C# in Depth

列表2.22显示了本站介绍的示例的完整代码:

 

列表 2.22    表述新语言特性的完整代码

 

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

 

static class LanguageFeatures

{

class ProcessData

{

public Int32  Id { get; set; }

public Int64  Memory { get; set; }

public String Name { get; set; }

}

 

static void DisplayProcesses(Func<Process, Boolean> match)

{

var processes = new List<ProcessData>();

foreach (var process in Process.GetProcesses())

{

if (match(process))

{

processes.Add(new ProcessData {

Id=process.Id,

Name=process.ProcessName,

Memory=process.WorkingSet64 });

}

}

 

Console.WriteLine("Total memory: {0} MB", processes.TotalMemory()/1024/1024);

var top2Memory =processes

.OrderByDescending(process => process.Memory)

.Take(2)

.Sum(process => process.Memory)/1024/1024;

Console.WriteLine("Memory consumed by the two most hungry processes: {0} MB", top2Memory);

var results = new {

TotalMemory = processes.TotalMemory()/1024/1024,

Top2Memory = top2Memory,

Processes = processes };

ObjectDumper.Write(results, 1);

ObjectDumper.Write(processes);

}

static Int64 TotalMemory(this IEnumerable<ProcessData> processes)

{

Int64 result = 0;

 

foreach (var process in processes)

result += process.Memory;

 

return result;

}

 

static void Main()

{

DisplayProcesses(process => process.WorkingSet64 >= 20*1024*1024);

}

}

下一章我们会看到,本章介绍新语言特性是如何应用到LINQ查询中的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值