代码改变世界

拯救C# 2.0,但是我们真做的到吗?

2009-06-27 18:55 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

似乎还有不少项目在用C# 2.0(本文最后我们来做一个调查),但是C# 2.0的生产力实在不如C# 3.0——如果您不信,那么一会儿就会意识到这一点。有朋友认为语言能力不重要,有了好用的框架/类库也可以有很高的生产力。所以这篇文章,我们就设法使用“类库”来弥补C# 2.0的缺陷。

但是,我们真做的到吗?

C# 2.0之殇

C# 2.0较C# 1.0来说是一个突破,其中引入了泛型,以及匿名方法等新特性。如果前者还可以说是平台的增强,而语言只是个“辅助”的话,而后者则百分之一百是编译器的魔法了。别小看这个特性,它为C# 3.0的高生产力踏出了坚实的一步——不过还是差了很多。例如,我们有一个要求:“把一个字符串数组中的元素转化为整数,再将其中的偶数放入一个List<int>容器中”。如果是C# 3.0,这是再简单不过的功能:

string[] strArray = { "1", "2", "3", "4" };
var even = strArray.Select(s => Int32.Parse(s)).Where(i => i % 2 == 0).ToList();

那么对于C# 2.0(当然对于C# 1.0也一样),代码又该怎么写呢?

List<int> even = new List<int>();
foreach (string s in strArray)
{
    int i = Int32.Parse(s);
    if (i % 2 == 0)
    {
        even.Add(i);
    }
}

有人说函数式编程有什么用,C# 3.0就是个很好的证明。C# 3.0中引入了Lambda表达式,增强了在语言中构造匿名方法的能力——这是一个语言中函数式编程特性的必备条件。C# 3.0的实现与C# 2.0相比,可读性高,可以直接看出转化、过滤,以及构造容器的过程和标准。由于语言能力的增强,程序的表现能力得到了很大的提高,在很多时候,我们可以省去将一些代码提取为独立方法的必要。当然,即使您将其提取为额外的方法,C# 3.0也可以让您写出更少的代码。

如果您觉得以上代码的差距还不是过于明显的话——那么以下功能呢?

int[] intArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 所有偶数的平均数
var evenAverage = intArray.Where(i => i % 2 == 0).Average();

// 都是偶数?
var allEven = intArray.All(i => i % 2 == 0);

// 包含偶数?
var containsEven = intArray.Any(i => i % 2 == 0);

// 第4到第8个数
var fourthToEighth = intArray.Skip(3).Take(5);

如果您使用C# 2.0来写,您会怎么做?

拯救C# 2.0

C# 3.0通过引入了函数式编程特性大幅增强了语言的生产力。如果说C# 2.0和Java还没有太大差距的话,那么C# 3.0已经将Java甩开地太远太远。不过真要说起来,在Java中并非不可以加入函数式编程的理念。只不过,如果没有足够的语言特性进行支持(如快速构造匿名函数、闭包、一定程度的类型推演等等),函数式编程对于某些语言来说几乎只能成为“理念”。不过现在,我们暂且先放下对“函数式编程”相关内容的探索,设法拯救C# 2.0所缺失的生产力吧。

C# 3.0中可以使用Lambda表达式构造一个匿名函数,这个能力其实在C# 2.0中也有。我们姑且认为这点不是造成差距的主要原因,那么有一点是C# 2.0绝对无法实现的,那就是“扩展方法”。C# 3.0中的扩展方法,可以“零耦合”地为一个,甚至一系列类型添加“实例方法”。当然,这也是编译器的功能,实际上我们只是定义了一些静态方法而已。这一点在C# 2.0中还是可以做到的:

public class Enumerable
{
    public static IEnumerable<T> Where<T>(Func<T, bool> predicate, IEnumerable<T> source)
    {
        foreach (T item in source)
        {
            if (predicate(item))
            {
                yield return item;
            }
        }
    }

    public static IEnumerable<TResult> Select<T, TResult>(Func<T, TResult> selector, IEnumerable<T> source)
    {
        foreach (T item in source)
        {
            yield return selector(item);
        }
    }

    public static List<T> ToList<T>(IEnumerable<T> source)
    {
        List<T> list = new List<T>();
        foreach (T item in source)
        {
            list.Add(item);
        }

        return list;
    }
}

于是现在,我们便可以换种写法来实现相同的功能了:

string[] strArray = { "1", "2", "3", "4" };

List<int> even = 
    Enumerable.ToList(
        Enumerable.Where(
            delegate(int i) { return i % 2 == 0; },
            Enumerable.Select(
                delegate(string s) { return Int32.Parse(s); },
                strArray)));

即使您可以接受delegate关键字构造匿名函数的能力,但是上面的做法还是有个天生的缺陷:逻辑与表现的次序想反。我们想表现的逻辑顺序为:转化(Select)、过滤(Where)、及容器构造(ToList),C# 3.0所表现出的顺序和它相同,而C# 2.0的顺序则相反。由于语言能力的缺失,这个差距无法弥补。很多时候,语言的一些“小功能”并不能说是可有可无的特性,它很可能直接决定了是否可以用某种语言来构造Internal DSL或进行BDD。例如,由于F#的灵活语法,FsTest使得开发人员可以写出"foobar" |> should contains "foo"这样的语句来避免机械的Assert语法。同样,老赵也曾经使用actor <= msg这样的逻辑来替代actor.Post(msg)的显式调用方式

封装逻辑

既然没有“扩展方法”,我们要避免静态方法的调用形式,那么就只能在一个类中定义逻辑了。这点并不困难,毕竟在API的设计发展至今,已经进入了关注Fluent Interface的阶段,这方面已经积累了大量的实践。于是我们构造一个Enumerable<T>类,封装IEnumerable<T>对象,以此作为扩展的入口:

public class Enumerable<T>
{
    private IEnumerable<T> m_source;

    public Enumerable(IEnumerable<T> source)
    {
        if (source == null) throw new ArgumentNullException("source");
        this.m_source = source;
    }

    ...
}

并以此定义所需的Select和Where方法:

public Enumerable<T> Where(Func<T, bool> predicate)
{
    if (predicate == null) throw new ArgumentNullException("predicate");
    return new Enumerable<T>(Where(this.m_source, predicate));
}

private static IEnumerable<T> Where(IEnumerable<T> source, Func<T, bool> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

public Enumerable<TResult> Select<TResult>(Func<T, TResult> selector)
{
    if (selector == null) throw new ArgumentNullException("selector");
    return new Enumerable<TResult>(Select(this.m_source, selector));
}

private static IEnumerable<TResult> Select<TResult>(IEnumerable<T> source, Func<T, TResult> selector)
{
    foreach (T item in source)
    {
        yield return selector(item);
    }
}

这些扩展都是些高阶函数,也都有延迟效果,相信很容易理解,在此就不多作解释了。在这里我们直接观察其使用方式:

List<int> even = new Enumerable<string>(strArray)
    .Select(delegate(string s) { return Int32.Parse(s); })
    .Where(delegate(int i) { return i % 2 == 0; })
    .ToList();

不知道您对此有何感觉?

老赵对此并不满意,尤其是和C# 3.0相较之下。我们虽然定义了Enumerable封装类,并提供了Select和Where等逻辑,但是由于匿名函数的构造还是较为丑陋。使用delegate构造匿名函数还是引起了不少噪音

  • 与JavaScript的function关键字,和VB.NET的Function关键字一样,C# 2.0在构造匿名函数时无法省确delegate关键字。
  • 与C# 3.0中的Lambda表达式相比,使用delegate匿名函数缺少了必要的类型推演。
  • 使用delegate构造匿名函数时必须提供完整的方法体,也就是只能提供“语句”,而不能仅为一个“表达式”,因此return和最后的分号无法省确。

我们设法拯救C# 2.0,但是我们真的做到了吗?

框架/类库真能弥补语言的生产力吗?

再谈Java语言

老赵对与Java语言印象非常差,至今没有任何改变——不知这次还会有多少人使用“Java平台”进行反驳。

博客园中的一位高人也曾经在文章末尾提问“您是否特别讨论某种语言,请您仔细思考一下,这个结论站的住脚吗?”——虽然是“疑问句”,但是人人都听的出其中的意思嘛!在国内经常听到一些说法,说比较语言,谈论语言是没有价值的。那么讨论哪些东西有价值呢?编程能力(这个的确很重要),解决方案(例如XX编程三百问)……哦,还有“业务”(技术无用论?)。总之,讨论语言,评价语言在国内社区显得很“浮躁”,没有层次。其实对语言的讨论在行业内是非常重要的一个方面,许多国内“精英”无比崇拜外国技术人员们对这些话题的争论才叫一个热烈。老赵搞不懂的是,为什么这些精英一边看轻国内技术人员,一边崇拜老外,一边却看轻老外也喜欢讨论的内容呢?

“月亮总是外国的圆”,这句话说得真tmd有道理呀。

我们要反对的不应该是“讨论语言”,而是应该反对讨论语言的不良目的。例如有人讨论语言的目的是为了“彰显品质”,是为了嘲笑他人,是为了找出哪种语言“更有前途”,这些才应该是我们需要强烈抵制的。一个类比就是:搓麻将没有错,错的是用麻将来赌博。“做什么”和“怎么做”,两者是有很多区别的。

C# 3.0中对于集合的操作方式的确好用。Java社区也构造一个项目lambdaj,希望可以缓解一下这方面操作的不便。这个项目很有趣,对于编写Fluent Interface是一个不错的参考。

当然,如果真要老赵选择,我会使用Scala,它真比Java好多了。

总结

本文谈论了C# 2.0在集合操作方面与C# 3.0的差距。老赵提出了一种解决方法,但是自认为效果不甚理想。不过聊胜于无,如果可以把C# 3.0这方面的功能移植过来,也算是略有功德的事情——但是最好还是尽快升级到C# 3.0吧。C# 4.0?这的确是个好东西,不过光从语言层面上讲,其中的新特性(如dynamic和协变逆变)对我没有太大的吸引力。不过.NET 4.0对于框架类库的增强的确非常引人注目,如果可以的话,我也希望可以尽快使用.NET 4.0进行开发。

最后我们来做一个简单的调查:http://www.micropoll.com/akira/mpview/617003-179427