由扩展方法引申出的编程思维

1. Helper大爆炸

.NET Framework为我们提供了丰富的类库,但是这并不是万能地,在大部分的时间,我们都需要为我们的项目特殊定制我们的通用类库。

常常,我们都可以构造一个类,类里封装一些方法。但是对于很多时候,我们并没有办法提取出这样一个类,举一个小例子,我们在很多时候,需要把url给保存到数据库里,作为一个唯一标识,但是我们知道url所占空间很大,如果用url来建立索引的话是非常耗费空间,而且影响效率的,那么我们最常用的办法就是把url做一个Hash来作为索引的替代品。

这个时候,我们根本就没有办法说我们来怎么样提取一个类,然后在类里写这样一个方法,这个时候,我们通常就只能这样:

public static class HashHelper
{
    public static string GetHashCode(string s)
    {
        //GetHashCode........
        return String.Empty
    }
}

 

然后我们会这样使用:

public static void Main(string[] args)
{
    string url = "www.fandongxi.com"
    string sql = "insert into Test values('"+HashHelper.GetHashCode(url)+"')"
    //执行SQL
}

这里,只是一个例子,并不是说我们要这个样子拼接字符串。

很快,肯定又会出现一个情况,说,我们要保存网页的内容,但是网页的内容直接存储到数据库里太大了,那么我们就需要对网页文本做一个Base64的编码然后压缩。//之前这里些的是错的,被很多人挑了错误.....

那么,我们就又得继续写:

public static class Base64Helper
{
    public static string GetBase64Text(string text)
    {
        //Base64........
        return String.Empty
    }
}

接下来我们在使用的地方就又多出来一个Base64Helper。那么过几天,还会出现SHA1Helper , MD5Helper等等各种各样的Helper。

渐渐地,我们会不会发现,Helper的数量已经让我们难以忍受了呢?

2. 扩展方法的提出

接下来的事情,我们都知道了,在.NET Framework 3.5中,也就是在C#3.0中,引入了扩展方法这个概念。

那就让我们扩展方法来解决上面的难题。

各位现在一定知道,无论是做UrlHashCode,还是Base64压缩,还是SHA1加密,还是MD5加密,这些都是针对字符串,或者说是一段文本的处理,那么很自然地,我们就需要把这些全部写入String类的扩展方法中。

public static class ExtensionClass
{
    public static string GetHashCode(this string s)
    {
        //........
    }
    public static string GetBase64Text(this string text)
    {
        //.......
    }
}

public static void Main(string[] args)
{
    string url = "www.fandongxi.com"
    string sql = "insert into Test values('"+url.GetHashCode()+"')"
        //执行SQL
}

在这里,我不想剖析去读扩展方法的实现本质,这里我们只谈编程思维和扩展方法所带来的意义。

3. 扩展方法让C#更加面向对象

从面向对象的角度来看,世间万物皆为对象,所有属性,所有方法都是属于某一个对象的,那么再从这个角度看开去,本就不应该存在静态类,也不应该存在静态方法,所谓的静态,不过是面向对象语言对并不成熟的语法实现的一种屈从罢了。

我们要求Base64加密后的文本,其实是文本调用自身的一个方法,之所以我们在之前的方法中需要一个Base64Helper,而不能这样子"http://www.fandongxi.com%22.replace(%22com%22,%22cn/")直接调用,只是因为.NET Framework无法预计到我们所有的业务场景,所以把只能把最通用的方法封装到已有的类库中。

4. 从扩展方法向外谈一些

让我们从扩展方法逐渐地向外围来探讨一些关于编码规范,以及一些代码优雅的问题。我们先不妨假设我们并不存在“+”运算符,或者说,我们禁止在程序中使用+运算符,那么也就是说,我们需要对“+”这个操作来做一个简单的封装,那么我们常规意义上会怎么做?

public int Add(int a,int b)
{
    return a+b;
}

public static void Main(string[] args)
{
    int result = Add(3,4)
    Console.WriteLine(result)
}

让我们来看这个函数,我们顺着代码的意思向下读,加,3,4。这明显是不符合我们常规的数学思维的,如果用了扩展方法之后,我们一定是应该这样来写。

public static class Extension
{
    public static int Add(this int a,int b)
    {
        return a+b;
    }
}

public static void Main(string[] args)
{
    int a = 3;
    a.Add(b)
}

可是这个"."运算符看上去还是那么有点别扭…..没办法,至少这样读上去让我们的代码顺畅了很多不是么?像写文章,说话一样写代码一直是我们程序员追求的最高境界,就像这样的代码总是好的。

Good:people.eat(food)

而不是Bad:Eat(people,food)

对把!

5. 前缀,中缀和后缀表达式

说到这,就不得不谈谈前缀,中缀和后缀表达式了。

学过数据结构的朋友们,一定都记得在数据结构中,有一道经典的习题,就是利用“栈”来实现前缀,中缀和后缀表达式的转换。在考试题中也经常会出现这样的习题。那现在让我们来复习一下,什么是前缀,中缀和后缀表达式。

前缀表达式就是不含括号的算术表达式,而且它是将运算符写在前面,操作数写在后面的表达式,也称为“波兰式”。

大名鼎鼎的Lisp就是前缀表达式的典型,让我们看一个最简单的小例子,还是那个经典的斐波那契数列:

(define (fib n)
    (fib-iter 1 0 n))

(defile (fib-iter a b count)
    (if (= count 0)
        b
        (fib-iter (+ a b) a (- count 1))))

每次写Lisp的时候,都会被密密麻麻的括号所吓到,可是真的没什么太好的解决方案呢!

中缀表达式就很简单了,和我们常规所涉及到的代码是一样的,后缀也是一个道理,在此就不再一一赘述。鉴于后缀的应用不是很大,在此我们也只谈谈前缀和中缀的意义。

那么我们想想,为什么Lisp要采用这么蹩脚的前缀表达式语法呢?

记得在大二第一次学习C语言的时候,老师让我们写一个简单的计算器,当时每个同学都写出了+,-,*,/的操作,但是在当时大多数的我们都没有办法写出更为常用的混合运算,以及()的操作,当时只有班上某鹤立鸡群的哥们写出了让我们当时完全无法看懂的代码。再直到大三学习数据结构,再反过来想他当时的代码,才恍然大悟。

废话说了一堆,那么其实前缀表达式最大的意义就是他更贴近计算机的思维,他只需要两种操作就能完成运算,就是入栈和出栈。让我们来看一个简单的小例子

3+(1-4),首先这是一个中缀表达式,把他转换为前缀表达式就是+3 – 1 4,计算机会从右向左来扫描这个表达式,4入栈,1入栈,然后遇到 - ,1和4出栈,并且完成运算,(-3)入栈,3进栈,+入栈,(-3)和3出栈,完成运算。

也就是说,其实在计算机完成我们所编写的数学操作时,其实往往都是把我们的中缀表达式首先转换为前缀表达式,然后完成计算,而Lisp采用前缀表达式,则是省去了这一个步骤,从而提高解释器的效率。

那我们就来总结下前缀和中缀表达式的意义。

前缀表达式更加贴近计算机思维,方便计算。而中缀表达式更加贴近数学思维,容易被我们所理解。

那回顾下,我们之前写Add的代码,如果说我们去掉.运算符,而且方法不加括号,是否采用扩展方法,把C#的语法和Lisp的语法相结合,其实就成了这样的形式。

public int Add(int a,int b)
{
    return a+b;
}

public static void Main(string[] args)
{
    (set! result (Add a b))
}

public static class Extension
{
    public static int Add(this int a,int b)
    {
        return a+b;
    }
}

public static void Main(string[] args)
{
    (set! result (a Add b))
}

还是后者更贴近我们的自然思维一些。

.NET Framework很强大,给我们提供了扩展方法这个概念,那么如果没有了扩展方法,其他语言给出了怎么样的解决方案呢?

那让我们来看看Haskell给出的方案。

5. 看看Haskell的方法

Haskell是一门函数式的语言,在FP大行其道的今天,Haskell这门久居深宫的语言也渐渐地浮出了水面。

废话不多说,我们只来看看Haskell是如何在没有扩展方法的情况下来解决语法和自然思维不相协调的问题的。

让我们先来编写一个简单的Haskell函数。

add x y = x + y

代码很简单,没什么值得多说,让我们来看看Haskell怎么调用。

image

这是我们传统的调用方式,可是Haskell为了更贴近我们的自然思维,为参数个数数量为2的方法提供了这样一个便捷的调用:

image

这就是Haskell为我们提供的“中缀表达式”的解决方案。

扩展方法很好,但是当我们的语言中没有扩展方法的时候,Haskell给我们提供了一个优秀的典范。

6. 语言和类库

说到这,我就想顺便谈谈关于语言扩展和类库扩展的问题。

《Masterminds of Programmming》一书中,Python语言之父Guido在接受采访时,谈到PEP(Python增强处理)时,顺便说到了关于在编写编程语言时,如何来根据用户的意见来处理语言实现的问题。

他谈到:

如果某个用户提出一个新特性,它几乎不会成功。因为用户对实现没有全面的理解,他几乎不可能提出一个合理的新特性。

那么在我看来什么是用户?用户就是使用这门语言来完成工作任务的人,他们往往需要的都是增加一个新功能,换句话说,他们需要的仅仅是一个方法而已。

那么什么是增加语言特性,什么是增加类库方法,Guido也给出了比较合理的解释。

如果某个特性对于Web来说确实很棒,那么,对于加到语言中来说,就未必是优秀的特性了。如果它确实利于编写更短的函数,或者是有利于编写可维护更强的类,把它添加到语言中可能就是一件好事。

其实Guido的意思很简单,是否增加到语言中,关于在于这个特性是否是领域相关的,如果是领域相关的,也许它需要做的仅仅是扩展类库,无论是增加Python的类库,还是用C去扩展Python API,总之无需对语言做出改变。

那么对于C#来说,什么是类库的修改,什么是语言的修改,在我看来,每一个版本的修改都一定有着类库的修改,但是如果说到语言的修改,应该是仅仅当MSIL发生变动的时候,我们才可以说语言发生了修改。//仔细想了一下,这个观点有问题....但是我没找到更合适的语言来做比喻。也许应该说,只有当语法的编译规则发生改变的时候,我们才可以说语言发生了修改。

Python也是一样,增加了方法充其量是类库的修改,而仅仅是语言的解释过程都发生了修改才可以算得上是语言层面的修改,例如从Python 2.x到Python3.x的大版本变动。

7. 总结

在本文中,主要是从扩展方法说起,谈到我们该怎么样更好的编写更贴近自然语言的程序。

然后再到一些没有扩展方法语言给出的折衷实现。而对于Python,C等其他语言,我尚且没有找到合适的方法来解决问题。

如果各位有好的办法,尤其是对于Python,毕竟这是我的工作,希望各位补充给出解决方法。

谢谢。

posted @ 2011-03-17 06:45  飞林沙  阅读(6890)  评论(59编辑  收藏  举报