C#语言的未来版本已经在最近的PDC上被揭露, 对象、XML、关系数据将被语言深度集成。这不是一个全新的发展方向,而是沿着C#一贯发展道路迈出的又一步。让我们从一个简单的例子——筛选一组符合条件的对象——中看看C#的发展是如何使我们的编程范式更简单和自然。
C#1.1时代的查询语句
我们有这样一个Employee类:
class Employee {
public string Name;
public int Years;
public string Department;
}
若我们想为工龄超过4年的员工发放一只金表作为奖励,我们可能会这样写:
static Employee[] GoldWatch(Employee[] employees) {
int resultCount = 0;
for (int i = 0; i < employees.Length; ++i) {
if (employees[i].Years > 3) {
++resultCount;
}
}
Employee[] results = new Employee[resultCount];
for (int i = 0, j = 0; i < employees.Length; ++i) {
if (employees[i].Years > 3) {
results[j] = employees[i];
++j;
}
}
return results;
}
这个方法首先计算符合条件的员工总数,据此创建一个合适大小的数组,然后用填充这个数组。这个方式是正确的,但你可能已经发现了一些问题:原数组被遍历了两次,规则(Years>3)被编码了两次,总的来说,对于这么一个简单的问题,这样的解决方法可能比我们想象的复杂了许多。下面的代码进行了一点简化:
static Employee[] GoldWatch(Employee[] employees) {
ArrayList results = new ArrayList();
for (int i = 0; i < employees.Length; ++i) {
if (employees[i].Years > 3) {
results.Add(employees[i]);
}
}
return (Employee[])results.ToArray(typeof(Employee));
}
现在,原数组只被遍历了一次。但却是以类型安全作为单价的,因为ArrayList包含的是object类型。代码末尾的复杂的cast就是这样做的后果之一。
上面的两个方法的伸缩性都存在问题:员工的集合一定要以数组形式给出,当然我们可以很容易写出支持别的集合类型的代码,但是为每一种类型都编写依次代码无疑是很浪费的。
我们再换一个方法:
static Employee[] GoldWatch(IEnumerable employees) {
ArrayList results = new ArrayList();
foreach(Employee employee in employees) {
if (employee.Years > 3) {
results.Add(employee);
}
}
return (Employee[])results.ToArray(typeof(Employee));
}
这段代码可以工作在数组,ArryaList等多种集合类型上,但是因为编译器不能确定employees是不是仅包括Employee对象,这样的后果是我们必须牺牲更多类型安全性。
这就是我们在C#1.1时代所能做到的。
C#2.0时代的查询语句
C#2.0为我们改善查询语句提供了一些新特性,我们首先来解决类型安全的问题:
static Employee[]
GoldWatch(IEnumerable<Employee> employees) {
List<Employee> results = new List<Employee>();
foreach (Employee employee in employees) {
if (employee.Years > 3) {
results.Add(employee);
}
}
return results.ToArray();
}
Generic让我们可以应用这样简单的实现,同时保证完全的类型安全。
但我们还有一个潜在的性能问题,我们在返回前构造了整个满足要求的员工组成的列表。万一这样的员工有很多,万一调用着只需要处理少数几个员工的数据呢?
如果有这样的情况并且我们也能接受返回一个IEnumerable<Employee>,那么这样一个应用了迭代器的方法应该是更好的方法:
static IEnumerable<Employee>
GoldWatch(IEnumerable<Employee> employees) {
foreach (Employee employee in employees) {
if (employee.Years > 3) {
yield return employee;
}
}
}
现在没有了Employee的临时拷贝并且调用者也能决定他需要的返回数量,这段代码决不会干比要求得更多的事。
这是一段简单的可维护的代码。
看来我们已经解决了我们的问题了,太好了!
但是如果我们需要返回别的条件的员工时会怎么样呢?假设我们要返回销售部的所有员工。我们很容易可以写出新的SalesForce()函数,和Goldwatch()只有要满足的条件不同的新函数。这样的情况下,让我们来试试把这段重构得更好。
我们可能会用委托来实现一个通用的Filter方法:
delegate bool Choose(Employee employee);
static IEnumerable<Employee>
Filter(IEnumerable<Employee> employees, Choose choose) {
foreach (Employee employee in employees) {
if (choose(employee)) {
yield return employee;
}
}
}
static bool GoldWatchChoose(Employee employee) {
return employee.Years>3;
}
static bool SalesForceChoose(Employee employee) {
return employee.Department=="Sales";
}
static IEnumerable<Employee>
GoldWatch(IEnumerable<Employee> s) {
return Filter(employees, new Choose(GoldWatchChoose));
}
static IEnumerable<Employee>
SalesForce(IEnumerable<Employee> s) {
return Filter(employees, new Choose(SalesForceChoose));
}
这样的话,每次我们添加新的查询条件我们需要加入两个方法:一个查询代码和一个判断是狗满足条件的委托。方法这样的增长会造成可维护性上的问题。
我们可以应用匿名委托--C#2.0的新特性:
static IEnumerable<Employee>
GoldWatch(IEnumerable<Employee> s) {
return Filter(employees,
delegate(Employee employee) {
return employee.Years>3;
}
);
}
static IEnumerable<Employee>
SalesForce(IEnumerable<Employee> s) {
return Filter(employees,
delegate(Employee employee) {
return employee.Department=="Sales";
}
);
}
现在我们只需要为新的查询添加一个新函数就可以了。而且我们也能直接调用Filter(),这样的话一个新函数都不需要添加了。但是另一方面,看着一个小方法的实现作为参数被传到一个方法让人乍舌,而且也会减弱代码的可读性。
用C#2.0,我们能做的就这么多了。
C#3.0时代的查询语句
在C#3.0中我们又有了改善代码的新工具。
匿名委托很不错,但是我们希望有更简单的,更容易维护的代码。C#3.0提供了lamda表达式的概念(译者注:Hmm....Lisp)。你可以把lamda表达式是我们应用匿名委托的捷径,下面是用Lamda表达式重写的查询:
static IEnumerable<Employee>
GoldWatch(IEnumerable<Employee> employees) {
return Filter(employees,
employee => employee.Years>3
);
}
static IEnumerable<Employee>
SalesForce(IEnumerable<Employee> employees) {
return Filter(employees,
employee => employee.Department=="Sales"
);
}
这段代码相当简单而且也很容易维护,但还存在一些问题。
GoldWatch(employees);
SalesForce(employees);
当你看到这样的调用的时候就会意识到这个问题,从OO的视角来看,我们已经熟悉了noun.verb()这样的调用形式,理想情况下,我们希望用这样的语法能查询一个集合:
employees.GoldWatch();
employees.SalesForce();
有人可能会定义一个新的Employee类,它实现了IEnumerable<Employee>。但是问题是,我们的用户可能会希望是用别的IEnumerable<Employee>实现,比如Employee[]和List<Employee>。
C#3.0用扩展方法(Extension method)解决这个方法:
static IEnumerable<Employee>
Filter(this IEnumerable<Employee> employees, Choose choose) {
foreach (Employee employee in employees) {
if (choose(employee)) {
yield return employee;
}
}
}
static IEnumerable<Employee>
GoldWatch(this IEnumerable<Employee> employees) {
return employees.Filter(employee => employee.Years>3);
}
static IEnumerable<Employee>
SalesForce(this IEnumerable<Employee> employees) {
return employees.Filter(
employee => employee.Department=="Sales");
}
注意this关键字的使用,现在我们可以用成员方法的样式调用这些函数:
employees.GoldWatch();
employees.SalesForce();
employees.Filter(employee => employee.Department=="Sales");
用这样字的方式实现,我们的用户还能应用如下的链式调用来完成复杂的查询:
employees
.GoldWatch()
.SalesForce();
我们就能知道在销售部里,谁应该获得金表了。
这看起来很好了,但如果我们想象Employee一样查询Customer呢?或者说,查询我们的存货呢?
不用为每一个类单独写一个Filter方法,我们可以将Filter写成一个通用函数:
delegate bool Choose<T>(T t);
static IEnumerable<T>
Filter<T>(this IEnumerable<T> items, Choose<T> choose) {
foreach (T item in items) {
if (choose(item)) {
yield return item;
}
}
}
现在我们可以筛选我们希望的任何类型了!
int [] a = new int [] {1,2,3,4,5};
a.Filter(i => i==1 || i==3);
事实上,这个筛选方法是如此有用且通用,C#里已经内置了一个称为Where的实现,下面是在PDC上展示的实际的Where实现:
public delegate T Func<A0, T>(A0 arg0);
public static
IEnumerable<T> Where<T>(this IEnumerable<T> source,
Func<T, bool> predicate) {
foreach (T element in source) {
if (predicate(element)) yield return element;
}
}