模式:工程化实现及扩展(设计模式Java 版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.1 说明

本系列《模式——工程化实现及扩展》分为Java和.NET两个版本。尽管多数朋友们认为设计模式无关乎语言,只是思想,但在实际的工程项目中,能够用平台最流行的方式表述设计模式的实现未尝不重要,毕竟设计中PIM(平台无关模型,Platform Independent Model)和PSM(平台相关设计,Platform Specified Model)是项目需要经历的两个阶段。应用的性能、集成性、健壮性等非功能性需求(Non-functional Requirement,NFR)设计必须结合语言平台。

Java和C#是两个伟大开发语言,它们都很有“个性”,作者从不认为“Java和C#差不多”,抛开Java SE、Java EE、.NET平台不谈,仅从语言看如果在项目中深入使用并追随一种语言之后就会发现它们的差别,能写“Hello World”是一回事,能用它写一个项目是一回事,能用它漂亮地完成一个项目则是另一回事。

十多年前作者曾经是Java忠实的用户,但工作后又连续用了十年的.NET,期间对Java SE和Java EE平台有很多误会和误解,可以说总是拿最新的C#与之前认识的Java相比。直到最近两年在一些项目中混合使用Java EE 5/6及.NET 3.5/4.0才发现很多之前所谓的技术信仰是被厂商误导的结果,两个平台都很强大,而且就Java和C#语言而言,也各有千秋。不管选择的是本系列的Java版本还是C#版本,希望本章粗浅的对比能够为读者了解两个语言提供些帮助,毕竟我们是用户而不是软件供应商,技术信仰固然重要,但我们要给自己机会去了解并发现适合自己项目的开发语言、工具平台。

下面我们将对比介绍一下几个语法内容。

3.2 枚举

枚举是一个很有用的语言特性,以往开发人员经常通过定义常量实现类似的功能,但如果采用枚举它会令程序更易写、易读、易于维护。一旦某个枚举项发生变化,我们只需要重新编译就可以保证整个应用范围内的一致性,相对硬编码的常量而言,枚举能帮助我们降低错误风险。此外,枚举作为一个强类型对象,它的有效性可以在编译时检查,并获得开发工具的有效支持,因此如果可枚举项基本固定的时候,尽可能采用枚举是较为明智的选择。

枚举在两个平台的差异很大,但追到中间代码(.NET叫IL,Java 叫Byte Code)时又可以发现很多相同点,表3-1列举了它们的区别。

表3-1 Java和C#中枚举的对比

下面通过一个等价功能的示例展示一下两者的差别。

Java
public enum Season {

SPRING(1,3){ @Override public String getNote(){return "生机盎然";} }, SUMMER(4,6){ @Override public String getNote(){return "烈日炎炎";} }, AUTUMN(7,9){ @Override public String getNote(){return "硕果累累";} }, WINTER(10,12){ @Override public String getNote(){return "冰天雪地";} };
public abstract String getNote(); private static final Map<Integer,Season> monthLookup = new HashMap<>();
static{ for(Season s : EnumSet.allOf(Season.class)) monthLookup.put(s.startMonth - 1,s); } private final int startMonth; private final int endMonth;
private Season(int startMonth,int endMonth){ this.startMonth = startMonth; this.endMonth = endMonth; }
public int getStartMonth(){ return startMonth; }
public int getEndMonth(){ return endMonth; }
public static Season get(int month) { if((month > 12) || (month < 1)) throw new IndexOutOfBoundsException(); return monthLookup.get(((month - 1) /3) * 3); } }
Unit Test
public class SeasonFixture {
    @Test
    public void SwitchSeason(){
        Season winter = Season.WINTER;
        assertNotSame(3,winter);
        assertEquals(3,winter.ordinal());

assertEquals(10,winter.getStartMonth()); assertEquals(12,winter.getEndMonth()); assertNotSame(11,winter.getStartMonth());
assertEquals(Season.WINTER,Season.get(12)); assertEquals(Season.WINTER,Season.get(11)); assertEquals(Season.AUTUMN,Season.get(8));
assertEquals("冰天雪地",Season.WINTER.getNote()); } }
C#
using System;
using System.Collections.Generic;
using System.Linq;
namespace MarvellousWorks.PracticalPattern.JavaAndDotNet{
   public enum Season{Spring,Summer,Autumn,Winter }

public class SeasonCalendar { static IDictionary<Season,SeasonMonthEntry> monthLookup = new Dictionary<Season,SeasonMonthEntry>(); struct SeasonMonthEntry { public Season Season { get; set; } public int StartMonth { get; set; } public int EndMonth { get; set; } public string Note { get; set; } }
static SeasonCalendar() { monthLookup.Add(Season.Spring,new SeasonMonthEntry(){ Season = Season.Spring,StartMonth = 1,EndMonth = 3,Note = "生机盎然"}); monthLookup.Add(Season.Summer,new SeasonMonthEntry(){ Season = Season.Summer,StartMonth = 4,EndMonth = 6,Note = "烈日炎炎"}); monthLookup.Add(Season.Autumn,new SeasonMonthEntry(){ Season = Season.Autumn,StartMonth = 7,EndMonth = 9,Note = "硕果累累"}); monthLookup.Add(Season.Winter,new SeasonMonthEntry(){ Season = Season.Winter,StartMonth = 10,EndMonth = 12,Note= "冰天雪地"}); }
SeasonMonthEntry entry; public SeasonCalendar(Season season) { if (season == null) throw new ArgumentNullException(“season”); entry = monthLookup[season]; }
public Season Season { get { return entry.Season; } } public int StartMonth { get { return entry.StartMonth; } } public int EndMonth { get { return entry.EndMonth; } } public string Note { get { return entry.Note; } }
public static Season GetSeason(int month) { if ((month > 12) || (month < 1)) throw new NotSupportedException(); int startMonth = ((month - 1)/3)*3 + 1; return monthLookup.Values.FirstOrDefault( x => x.StartMonth == startMonth).Season; } } }
Unit Test
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MarvellousWorks.PracticalPattern.JavaAndDotNet.Tests
{
    [TestClass]
public class SeasonFixture
{
       [TestMethod]
       public void SeasonEnum()
       {
          var winter = new SeasonCalendar(Season.Winter);

Assert.AreEqual<int>(10,winter.StartMonth); Assert.AreEqual<int>(12,winter.EndMonth); Assert.AreEqual<string>("冰天雪地",winter.Note); Assert.AreNotEqual<int>(11,new SeasonCalendar(Season.Autumn).StartMonth);
Assert.AreEqual<Season>(Season.Winter, SeasonCalendar.GetSeason(12)); Assert.AreEqual<Season>(Season.Winter, SeasonCalendar.GetSeason(11)); Assert.AreEqual<Season>(Season.Autumn, SeasonCalendar.GetSeason(8)); } } }

上面Java枚举的示例相信会令C#程序员大感意外,因为它包括的内容远远超过常量定义这么简单:

●每个枚举项有构造函数。

●有整体的静态构造函数。

●枚举类型可以有自己的方法和成员变量。

●每个枚举项有自己的序号。

●可以定义抽象方法(模板方法),约束每个枚举项的操作方法。

从根本上讲,上面所有不同是因为Java中枚举类型作为一个引用类型出现,而.NET中枚举作为一个值类型存在,因此出现Java和.NET两个平台上枚举类型的不同。

对于Java程序员而言,C#示例的其他部分倒没有多少不同,毕竟.NET中枚举类型太“瘦”,无法扩充各种操作,但最后的那个LINQ查找也许会激发点兴趣,本章后面将会对此介绍。

3.3 泛型

从使用者的角度看Java和C#的泛型倒是基本旗鼓相当,两者均为语言提供如下功能:

●增强了算法的抽象性,如第2章所介绍,如果说类完成了现实世界到面向对象世界的抽象、接口(狭义的interface和抽象类)完成了对类的抽象,那么泛型就完成了对接口、抽象类、实体类的抽象,而且允许定义更多层次的抽象。

●都提供了编译时的类型检查功能,可以令算法定义更加严谨,也因此获得IDE(集成开发环境)更好的支持,这点在使用Visual Studio或者Eclipse(或其他Java IDE)的时候都会有非常明显的感觉。

●减少了很多类型转换的工作,在.NET平台上性能影响更明显。

●都提供参数类型约束的检查功能(Type Parameter Constraint),确保开发人员可以精确定义类型参数是否继承(或实现了)某些抽象类型,提高泛型算法的严谨性。

不过两者各有些不尽如人意的地方,Java的泛型对于编译器而言是“形而上学”的实现,而C#的泛型在类型参数精细化定义方面存在一些令人遗憾的地方。

1.Java的泛型

●首先它是JVM上的“外挂”。最初它来自一个名为Pizza的项目,而这个项目最后被JSR吸收为Java语言标准,成了我们现在见到的Java Generics。它的设计初衷有一条——“可以运行在没有修改的JVM上”,这个要求虽然为Java提供了一些.NET Generics不具备的特点,但它会令一些熟悉C# Generics和C++ STL的用户在有些地方感觉怪怪的。

●Java的泛型并没有在编译时对类型参数进行强类型替换,而是将这些类型变成Object。也就是说,我们看到的强类型泛型是一个表象,在通过反射获取类型信息时会有更深入的体会。

●基于原因1,Java不能用int、double等做泛型的类型参数,只能使用对应的类如Integer、Double等,这多少有点遗憾。

●同样基于原因1,Java甚至允许构造泛型类型的时候不提供类型参数,这可以理解为一种灵活,但同时或多或少地感觉到有点“不泛型”的味道。

Java
@Test
public void testTypeParameter(){
    List<Integer> list = new ArrayList<>();
    list.add(20);
    list.add(30);
    Assert.assertEquals(2,list.size());

List another = new ArrayList(); another.add(20); another.add(30); Assert.assertEquals(2,another.size());
// List<int> list = new ArrayList<>();错误,Java不允许 }

2.C#的泛型

●C#中泛型的类型参数会在编译时进行类型替换,因此通过反射可以直接获得泛型方式表示的类型,而且因为省去boxing-unboxing确实能够获得性能提升。

●基于原因1,C#可以定义List<int>之类的对象,可以省去int和Integer的转换。

●由于保留了值型的结构体,因此C#泛型在类型参数约束时可以定义哪些类型参数是引用类型,哪些是值类型,是否有无参数的构造函数等。这些细节在编写公共代码库的时候会体现出价值。

C#
class GenericCollection<T1,T2>
   where T1 : struct
   where T2 : class,new(){}

●对于C#而言,泛型类型和非泛型类型是泾渭分明的两个类,设计上它们往往也保存在不同的命名空间(相当于Java的package)下,因此,开发人员如果用泛型集合替换非泛型的集合,必须修改代码。

C#
System.Collections.IList list1;
System.Collections.Generic.IList<int> list2;

3.4 属性和标注

属性和标注在新的C#和Java库中广泛使用,两者通过声明的方式将切入的逻辑与主干逻辑有效分割。在项目实施中,Attribute和Annotation的差异并不明显,而且Attribute和Annotation都可以在运行时通过反射获得。

从外部用户角度看,两者仅存在一些细微的差别:C#要求必须直接或者间接继承自System.Attribute,而Java可以通过@interface标识,因此C#可以在类型中增加一些逻辑处理,这方面C#与Java在“肥瘦”上的取舍正好和枚举(Enum)相反。

Java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface EmployeeCategory{
    String name();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@EmployeeCategory(name="fulltime")
@interface FulltimeEmployee{
    int basicSalary();
}
@FulltimeEmployee(basicSalary=20000)
public class Manager{}

@Test public void loadAnnotationInfo() throws ClassNotFoundException{ EmployeeCategory category = (EmployeeCategory) ( Class.forName("AnnotationFixture$FulltimeEmployee"). getAnnotation(EmployeeCategory.class)); assertEquals("fulltime",category.name()); FulltimeEmployee employee = (FulltimeEmployee) (Class .forName("AnnotationFixture$Manager") .getAnnotation(FulltimeEmployee.class));
assertEquals(20000,employee.basicSalary()); }
C#
[AttributeUsage(AttributeTargets.Class)]
class EmployeeCategoryAttribute : Attribute{
   public String Name { get; private set; }
   public EmployeeCategoryAttribute(string name) { Name = name; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] [EmployeeCategory("fulltime")] class FulltimeEmployeeAttribute : Attribute{ public int BasicSalary { get; private set; } public FulltimeEmployeeAttribute(int basicSalary) { BasicSalary = basicSalary; } }
[FulltimeEmployee(20000)] class Manager{}
[TestMethod] public void LoadAnnotationInfo() {
Assert.AreEqual<string>("fulltime",((EmployeeCategoryAttribute) typeof (FulltimeEmployeeAttribute).GetCustomAttributes( typeof (EmployeeCategoryAttribute),false)[0]).Name); Assert.AreEqual<int>(20000,((FulltimeEmployeeAttribute) typeof (Manager). GetCustomAttributes(typeof (FulltimeEmployeeAttribute), false)[0]).BasicSalary); }

如果读者是一名资深的C#开发人员,可直接跳过本章的后续部分。如果读者是Java开发人员,本章后面的内容也许会令你有些新鲜感。

3.5 操作符重载和类型转换重载

C#操作符重载会令程序代码看起来更直观,尽管类似的功能可以通过一些方法完成,但对于客户程序的开发人员而言,体验有所不同。请看下面示例:

C#
public struct Complex
{
   public int R { get; set; }
   public int I { get; set; }
   public static Complex operator +(Complex c1,Complex c2) {
      return new Complex(){ R = c1.R + c2.R,I = c1.I + c2.I};
}
   …
}

[TestMethod] public void CalculateCompelxNumber() { var c1 = new Complex() {R = 1,I = 2}; // 1+2i var c2 = new Complex() {R = 3,I = -4}; // 3-4i Assert.AreEqual<Complex>(new Complex() {R = 10,I = -2},c1 + c2 + 6); // 10-2i }

这是一个有关复数的示例,C# MSDN教程中用的也是这个案例。示例中最体现价值的就是高亮部分,它用我们熟悉的数学公式方式完成计算过程,如果不重载操作符,我们可以继续用c1.Add(c2).Add(6)达到类似的效果,但总感觉和习惯有所区别。

C#的操作符重载相对C++要弱化很多,能重载的操作符相对有限,但基本可以满足简单计算和日常业务处理的需要,如下面这个关于缴费的公式:

Payment = YF(会员年费) + Q(消费数量) * P(单价)–O(抵扣金额) + D(滞纳金)

如果没有操作符重载,我们可能写成一个一串方法连起来的一段语句,而采用操作符重载后,阅读代码时感觉很业务化。

对于C#开发人员,是否使用这个语法特性可依据个人喜好而定。

3.6 委托、事件、匿名方法

在Java中,事件是通过反射代理(java.lang.reflect.InvocationHandler)、Listener(interface java.util.EventListener)或其他很富有技巧的编码完成的,比较复杂。

笔者是一个从汇编、C/C++过渡到Java和C#的开发人员,委托是我非常喜欢的语言特性,本系列的C#版中也大量运用了委托、事件、匿名方法,因为它相当于一个对象化的函数指针,而且只需一个操作符重载就可以完成函数指针的预定。

下面我们看一个例子,情境很简单——当计数器发生变化时,窗口上控件的指针发生变化、后台日志进行记录、还有一个警报器可以监测计数是否超过了阈值,如果超过就报警。这个例子在系统监控中经常遇到。

我们先看一个不用委托的示例:

(1)抽象一个接口并命名为IInstrumentTarget(可用于管理和监控的对象,如界面上的指针、监控日志、报警逻辑)。

(2)为计数器增加容器对象,保存这些IInstrumentTarget实体类的实例。

(3)在计数器变化的方法中分别通知这些IInstrumentTarget实体类的实例。

C# - 监控对象接口
interface IInstrumentTarget
{
   void Process(int newValue);
}
C# - 监控实体对象
class InstrumentLogger : IInstrumentTarget
{
   public void Process(int newValue){Trace.WriteLine("Log" + newValue);}
}
class RuleEngine : IInstrumentTarget
{
   const int AlertValue = 3;
public void Process(int newValue)
{
       if (newValue >= AlertValue)
          Trace.WriteLine("Alert"+ newValue);
   }
}
class InstrumentWindowUI : IInstrumentTarget
{
public void Process(int newValue)
{
   Trace.WriteLine("display new value"+ newValue);
}
}
C# - 计数器
class Counter
{
   int currentValue;
   List<IInstrumentTarget> instrumentTargets = new
List<IInstrumentTarget>();
public static Counter operator ++(Counter c1)
{
       c1.currentValue++;
       c1.instrumentTargets.ForEach(x => x.Process(c1.currentValue));
       return c1;
   }

public Counter Register(IInstrumentTarget target){…} public Counter Unregister(IInstrumentTarget target){…} }
C# Unit Test
[TestMethod]
public void AddCounter()
{
   var logger = new InstrumentLogger();
var counter = new Counter()
.Register(logger)
.Register(new RuleEngine())
.Register(new InstrumentWindowUI());

Trace.WriteLine("----------- 1 -----------"); counter++; counter++; counter++; Trace.WriteLine("----------- 2 -----------"); counter.Unregister(logger); counter++; }
Output 窗口
------ Test started: Assembly: JAndN.Tests.dll ------
----------- 1 -----------
Log 1
display new value 1
Log 2
display new value 2
Log 3
Alert 3
display new value 3
----------- 2 -----------
Alert 4
display new value 4

1 passed,0 failed,0 skipped,took 7.18 seconds (MSTest 10.0).

上面的例子除了LINQ(ForEach(x => x.Process(c1.currentValue)))语句不够中规中矩外,其他部分Java和C#实现上并没有区别,但这个代码有些复杂,因为:

●需要自己定义注册、反注册监控对象的方法,即被监控对象需要自己管理待通知的实例,似乎不那么“单一职责”了。

●需要定义一个接口,这样Counter在发布通知的时候才有Process(int newValue)方法可以调用。而委托恰好可以比较圆满地克服上面的问题,我们再看一下这个例子的委托版:

C# - 监控实体对象
class InstrumentLogger
{
   public void Process(int newValue){Trace.WriteLine("Log"+ newValue);}
}
C# - 计数器
class Counter
{
   int currentValue;
   public Action<int> OnChanged;

public static Counter operator ++(Counter c1) { c1.currentValue++; c1.OnChanged(c1.currentValue); return c1; } }
C# Unit Test
[TestMethod]
public void AddCounter()
{
   var counter = new Counter();
   var logger = new InstrumentLogger();
   counter.OnChanged += logger.Process;  // = Register()
   counter.OnChanged += (x) => { Trace.WriteLine("display new value"+ x); };
   counter.OnChanged += (x) => { if (x >= 3) Trace.WriteLine("Alert"+ x); };
   Trace.WriteLine("----------- 1 -----------");
   counter++;
   counter++;
   counter++;
   Trace.WriteLine("----------- 2 -----------");
   counter.OnChanged -= logger.Process;  // = Unregister()
   counter++;
}

作为Java开发人员,相信读者可以直观地看到两个例子的差别:

●首先,这里没有IInstrumentTarget接口,因为没必要。委托本身就是对方法的抽象,C#在实现命令模式、策略模式等各类行为型模式时,通过委托可以大大减少接口的定义。

●省去了计数器自己定义登记、撤销待监控对象的方法,通过重载的操作符“+=”和“-=”就完成了。

●读者可能发现代码中没有明确的委托定义,其实这里采用了一个Action<T>委托类型,它的定义如下,即表示那些有一个输入参数返回结果为空的方法:

C# - System.Action<T>
namespace System
{
   public delegate void Action<in T>(T obj);
}

●我们可以定义监控实体类型(InstrumentLogger),也可以不定义,因为可以通过匿名方法完成。项目中很多接口定义的内容就是一个方法,或一个重载方法,如果有委托的话,接口就成了一个命名标签而已,而本例中的实体类(InstrumentLogger)也可以摆脱接口的束缚,它只要满足Action<T>的方法签名就可以,至于到底是叫Process(int)还是Log(int)方法都没有关系。简言之,因为委托的出现,实体类型解脱了。

3.7 Lamada和LINQ

Lamada是语法,类似SQL语言。C#内置了Lamada语法的支持,Java也可以通过一些开源项目,提供对Lamada语法的支持。

而LINQ就好像当年的ODBC,它定义了一整套可以通过Lamada语法进行数据访问的对象规范体系,它是C#中很好的一个特性,它是简洁有效的而且是统一的数据访问手段。因为通过C#我们能提供对各类数据信息的访问,就好像当年用SQL通过ODBC访问各类数据源一样,如以下数据信息:

●关系数据库——可以。

●层次型数据,如XML信息——可以。

●文件、注册表、目录服务、邮件——可以。

●Wiki、Blog、Markup内容——可以。

●线性、关系型、层次性的内存对象——可以。

还有很多,只要有或者能够开发LINQ to <sth.> 的Provider即可。

对于基于.NET 3.5及以后版本.NET Framework的项目,作者的倾向是:

●公共的数据访问组件同时支持ADO.NET和LINQ两套接口方法。

●内存集合对象访问,统一用LINQ。

●非关键项目的数据库访问,统一用LINQ。

●关键项目的数据库访问,采用SQL。

这么做的主要原因在于平衡开发便利性与性能,尤其考虑到出现性能问题后排查的复杂程度,毕竟LINQ生成的SQL语句是厂商优化的,虽然整体上是优化的,但并不代表每个情景都是优化的,这时直接调整SQL更便捷。

下面看3个C# LINQ例子,一个是通过反射获得Attribute的信息;另一个是访问XML信息,还有一个用XML文件模拟Master-Detail关系,完成对象集合间的连接。

示例1:用LINQ查询、筛选反射信息

我们先把前面Attribute的例子做得复杂点:

C# - LINQ to Object目标对象
const string Mark ="technical";
[AttributeUsage(AttributeTargets.Method,AllowMultiple = true)]
class CategoryAttribute : Attribute
{
   public String Name { get; private set; }
   public CategoryAttribute(string name){Name = name;}
}

[AttributeUsage(AttributeTargets.Method,AllowMultiple = true)] class XAttribute : Attribute{} [AttributeUsage(AttributeTargets.Method,AllowMultiple = true)] class YAttribute : Attribute{}
class A { [Category(Mark)] [Category(“M1”)] [X] public int M1() {return 0; }
[Category("M2")] [Y] [X] public int M2(int a) {return -1; }
[Category(Mark)] public void M3(int a,int b) { }
[Category(Mark)] [Category(Mark)] [Category("M4")] [X] public int M4(int a,int b) {return 0; } }

接下来我们要查询其中返回结果为int,而且带有CategoryAttribute的方法,并打印各方法的名称。这里其实相当于查询一个树形的层次对象体系,既要检查Method层次的信息,也要检查Method Attribute层次的信息,如图3-1所示,类似问题在SQL中我们会通过子查询完成,凭借LINQ也可依例处置。

图3-1 示例中反射信息查询对象间的层次结构

C# - Unit Test
[TestMethod]
public void LinqToQueryReflectionInfo()
{
   // outer query
   (from method in typeof(A).GetMethods()
      where
      // sub query
         (from attribute in
method.GetCustomAttributes(typeof(CategoryAttribute),false)
              .AsEnumerable().Cast<CategoryAttribute>()
         where string.Equals(Mark,attribute.Name)
         select attribute)
         .Count() > 0
      && (method.ReturnType == typeof(int))
      select method.Name)
      .ToList().ForEach(x => Trace.WriteLine(x));
}

执行结果:

M1
M4

通过Lamada语法,我们只要一行类似SQL的语句就可以完成,只不过它查询的不是关系数据库,而是对象。

示例2:用LINQ检索一个XML文件

下面选择一段稍微精简过的RSS 2.0反馈信息,它是一个XML,我们同样用LINQ访问其中的信息:

XML
<?xml version="1.0"encoding="utf-8"standalone="yes"?>
<rss version="2.0">
    <channel>
        <title>RSS Channel</title>
        <description>RSS Channel Description.</description>
        <link>http://someSite.com</link>
        <item>
            <title>笑看甲方乙方</title>
            <description>First</description>
            <pubDate>2010-12-05</pubDate>
            <guid>pq7bbf19-3327-4773-543p-892uyt9023ba</guid>
        </item>
        <item>
            <title>总是没完没了</title>
            <description>Second</description>
            <pubDate>2010-12-02</pubDate>
            <guid>bh34nb92-b92n-b2kd-b923-b95n56bkdald</guid>
        </item>
        <item>
            <title>笑看没完没了</title>
            <description>Third</description>
            <pubDate>2010-12-04</pubDate>
            <guid>2bk3b922-bn2x-6kln-bn56-b95n37378dn1</guid>
        </item>
    </channel>
</rss>

我们的要求很简单,希望按照pubDate逆序打印title中包括“笑看”的项目及对应的guid。与上次隐式的层级结构不同,这次要查询的是层次型的XML文件,类似要求也可以用一句LINQ完成,代码如下:

C# - Unit Test
const string TitleKeyWord ="笑看";
const string ItemItem ="item";
const string TitleItem ="title";
const string DateItem ="pubDate";
const string GuidItem ="guid";
[TestMethod]
public void QueryXmlFile()
{
   (from item in XDocument.Parse(Resource.RssData).Descendants(ItemItem)
      where item.Element(TitleItem).Value.Contains(TitleKeyWord)
      orderby (DateTime)item.Element(DateItem) descending
      select new{
                Name = item.Element(TitleItem).Value,
                Guid = item.Element(GuidItem).Value
             })
      .ToList().ForEach(x => Trace.WriteLine(x.Name +":"+ x.Guid));
}
Output 窗口
------ Test started: Assembly: JAndN.Tests.dll ------
笑看甲方乙方:pq7bbf19-3327-4773-543p-892uyt9023ba
笑看没完没了:2bk3b922-bn2x-6kln-bn56-b95n37378dn1

1 passed,0 failed,0 skipped,took 0.87 seconds (MSTest 10.0).

也很容易,和我们查询一个具有Master-Detail关系的数据表没有多少区别。下面再复杂点,用LINQ查询多个集合对象的情况。

示例3:用LINQ查询多个集合对象

继续上面的例子,我们还有一个XML数据文件,其中保存每个<title>对应作者的信息,我们想在查询结果中增加作者姓名的内容,这时就可以用LINQ的Join 语法。

XML
<?xml version="1.0" encoding="utf-8" ?>
<history>
    <item title="笑看甲方乙方"author="A"/>
    <item title="总是没完没了"author="B"/>
    <item title="笑看没完没了"author="A"/>
    <item title="再次遇见"author="Q"/>
</history>
C# - Unit Test
const string AuthorItem =“author”;
[TestMethod]
public void JoinQuery()
{
   (from item in XDocument.Parse(Resource.RssData).Descendants(ItemItem)
      join pubItem in XDocument.Parse
(Resource.AuthorPubHistory).Descendants(ItemItem)
          on item.Element(TitleItem).Value equals
pubItem.Attribute(TitleItem).Value
       where item.Element(TitleItem).Value.Contains(TitleKeyWord)
       orderby (DateTime)item.Element(DateItem) descending
       select new
              {
                 Author = pubItem.Attribute(AuthorItem).Value,
                 Name = item.Element(TitleItem).Value,
                 Guid = item.Element(GuidItem).Value
              }
       )
       .ToList().ForEach(x => Trace.WriteLine(x.Author +"|"+ x.Name +":"+
x.Guid));
}
Output 窗口
------ Test started: Assembly: JAndN.Tests.dll ------
A|笑看甲方乙方:pq7bbf19-3327-4773-543p-892uyt9023ba
A|笑看没完没了:2bk3b922-bn2x-6kln-bn56-b95n37378dn1

1 passed,0 failed,0 skipped,took 0.79 seconds (MSTest 10.0).

两个数据表示方式完全不同的XML文件(前者是Element方式,后者是Element+ Attribute方式)通过一行LINQ代码就解决了,这就像我们称呼SQL为第4代语言一样,只要把想要做的告诉系统,它就帮你做了,LINQ颇神似。试想如果同样的功能不通过LINQ,那将是一个不小的工作量。

3.8 小结

由于本系列同时包括Java和C#两种语言,所以本章试图对Java和C#中较新的典型语法内容进行简单的对比,其中尽管C#篇幅较多,但这与语言本身“哪个更好”无关,而与语言设计者的理念有关系:

●C#尽管是标准,但更倾向是由微软一家管理的,所以很多新特性、新技术是直接补充到语言和.NET Framework中。

●Java是多家厂商共同管理的,很多特性不是在语言层面而是通过外围的开发框架逐步补充的,其中有些成为JSR并最终过渡到Java规范中,而有些则一直游离于标准外围。

对于C#开发人员而言,Java枚举类型的表达能力相信会令大家印象深刻;对于Java开发人员,相信委托、匿名方法和LINQ也能给我们一些冲击。

与其一味地评价语言的优劣,不如放开眼界取长补短,最起码能对另一阵营有更多了解。