.NET


      Zhangdi 同学说,评论我的《Java 和 .Net 在异常处理机制上的区别》帖子说:我并不认为 C#(.NET)这样处理 Exception 是好的。我觉得这与 strong-type 的概念相冲突。几个问题:

  1. 在设计一个接口的时候,是不是需要同时告诉用户,这个接口有可能抛出什么类型的 Exception?

    答:这种说法自然是没错的,异常应该是接口设计的一部分,但是告知用户的方式可以有很多种。Java 的这种方式只是其中较为有效的方式之一,他通过编译器做出严格的限制,这其中兼有优劣。

  2. 如果需要,那么这个 Exception 类型是否应该成为一个接口声明的一部分?

    答:其实这还要从程序整体的设计角度来讨论。在 Java 中非 RuntimeException 其实已经不是一种普通的异常那么简单。因为最终在程序运行的时候,Java 不允许有非 RuntimeException 被抛出到虚拟机,这也就是说这种异常看上去只存在于设计时。他更像是程序运行期间的一种消息传递机制,只是首先这种消息“有别于(Except)”其他的返回类型,另外就是它会沿着调用栈强行向上追溯。显然这种异常更多的是在程序设计阶段起到操控程序非正常运行时行为的作用。因此他更像是一种接口,自然声明这样的接口也是理所应当的。

          但是 Java 还有 RuntimeException 的概念,而 .Net 或许就是没有区分他们而已,因为一旦程序遇到异常,无论是被程序本身恢复,还是被虚拟机捕捉到导致强行非法结束,都是可以接受的,毕竟对用户而言除了造成不方便,并不会有安全上的隐患。

  3. 一个已知的有可能发生的 Exception 被操作系统捕获是否可以说是用户在处理这个接口的声明的时候,出现了疏忽呢?

    答:这个自然,如果不是 Java 概念中的 RuntimeException,那么如果真有这样的异常被虚拟机捕捉到肯定就是 Bug,但至少是一个安全的 Bug。而同时,就像我在帖子里说到的,需要在开发难度和设计完整性上达到一定的平衡。.Net 这样自然忽视了后者,但同时显然使用就方便多了。

  4. strong-type 的目标是在编译器发现尽可能多的关于类型的错误,如果我们把接口的定义看作类型的一部分,那么是否可以说,这个有可能将 checked exception 暴露给操作系统的编译器在 strong-type 上有缺陷呢?

    答:没看懂!:-)

      还是那句话,我觉得我写这篇帖子已经足够公正了。.Net 在异常处理的设计上显然不如 Java 更趋近完美,但毕竟还是可以用的,还是好用的,剩下的瑕疵是可以通过程序员的经验,以及良好的开发习惯和管理来弥补的。

      关于 Java 和 .Net 优劣的争论一直在继续,而在异常处理方面体现得最为激烈,因为他们之间的差异是如此明显。.Net 晚于 Java 出现,那么 Java 对 .Net 就理应起到很重要的借鉴作用,但是伟大的 Anders Hejlsberg 为什么没有继续 Java 的实现方式,而是另辟蹊径,这是一个非常值得研究的问题。因为我们要承认一个真理:正确的东西大家都是一样的正确,错误的却各有个的错误。可以肯定这不可能是 Anders 的疏忽,那么他的道理究竟何在,或者说他们之间究竟有什么区别?

      在你能耐下心来看完这篇帖子之前,我想要明确告诉你一个结论:Java 和 .Net 在异常处理的本质上是没有区别的。

一、Java 是如何处理异常的

      如果一个 Java 方法要抛出异常,那么需要在这个方法后面用 throws 关键字定义可以抛出的异常类型。倘若没有定义,就认为该方法不抛出任何异常。如果从方法的入口和出口的角度去考虑一下这个规范,我们知道参数可以认为是方法的入口(当然某些情况下也可以是出口),而返回值则是方法的出口,这是在程序正常执行的情况下,数据从入口入,出口出。要是程序非正常执行,产生异常又当如何? 被抛出的异常应该如何从方法中释放出来呢? Java 的这种语法规范就如同给异常开了一个后门,让异常可以堂而皇之“正确”地从方法里被抛出。

      这样的规范决定了 Java 语法必须强行对异常进行 try-catch。设想一下,对于以下的方法签名:

public void foo() throws BarException { ... }

暗含了两方面的意思:第一,该方法要抛出 BarException 类型的异常;第二,除了 BarException 外不能抛出其他的异常。而正是这第二点的缘故,我们要如何保证没有除 BarException 之外的任何异常被抛出呢? 很显然,就需要 try-catch 其他的异常。也就是说,一般情况下,方法不抛出哪些异常就要在方法内部 try-catch 这些异常。

      Java 这样的机制既有优点,也有缺点。先来说说优点:

  • 很显然,这种规范是由 Java 编译器决定的。倘若 Java 程序的入口点 main() 方法没有任何异常抛出,就是说要在 main() 方法内部,即整个程序内部捕捉所有的异常,否则将无法通过编译。这样编译器保证了程序对每个异常都有相应的计划和处理,不会有未处理的异常被泄露到虚拟机中,导致程序意外中断或退出,也就是增强了程序的健壮性。当然,Java 有 RuntimeException 的概念,这样的异常仍然可以随时被抛出到虚拟机中。
  • 强行 try-catch 要求把异常作为程序设计的一部分看待。就如同方法的参数和返回值一样,在编写一个方法时,要结合上下文做出通盘的考虑和打算。虽然异常是所谓的“意外情况”,但是这些“例外”理应是被我们全部了解并处理的。
  • 方便调试。异常理应在正确的位置被捕捉。当异常发生时,我们能更清楚的了解到其来源和相应处理程序的位置,而免去了在整个调用栈中摸索的麻烦。
  • 在不借助任何文档的情况下,从方法签名就可以知晓应该对哪些异常进行处理。

      Java 异常处理机制的这些优点也直接导致了他的致命弱点:将程序变得异常繁复。往往一个简单的程序,功能代码寥寥几行,而异常处理部分却占用了程序的绝大部分篇幅;同时导致缩进深度加深,既不利于书写,也不利于阅读。另外他的强行 try-catch 需要程序员有更高深的造诣,能够通盘考虑异常处理设计问题,这个在程序开始之初或者对于初学者是一个不小的门槛。这往往会阻碍其推广与发展,因为低水平初学者的信心往往因此而受到打击。然而对于高手来说,编译器是否能帮助他们找到未被处理的异常只是一个方便与否的问题,只要在编写方法时注意了异常处理,即便没有编译器的支持,情况也不会糟糕太多。反而倒是由于要遵循这样复杂的异常处理规范,以至于大多数人都可能为了图一时方便,对异常的基类型 Exception 或 Throwable 进行笼统地捕捉,这样做的危害就是那些你无法处理的异常被溺死在处理程序中,(按照异常处理原则,我们应该只捕捉那些可以被处理或恢复的异常,而把其他的异常继续抛出。至于这样做的优势,以及不这样做所带来的问题,不是一两句能够说清楚,这里就不展开讨论了。)导致程序的不稳定和不确定。既没有发挥 Java 语法在这方面的优势,反而增加了忧患。

二、.Net 是如何处理异常的

      一句话概括 .Net 的异常处理方式就是随心所欲。没有人要求你一定要抛出异常,也更没有人要求你一定要捕捉异常。未被捕捉的异常会被以 Unhandled Exception 的形式抛出到虚拟机中。在此我就要先解决一下文章开头提到的问题,为什么说 Java 和 .Net 这两种异常处理机制在本质上是相同的。可以从两个方面来考虑:

  1. 默认情况下。Java 在默认情况下 main() 方法是不抛出异常的,正如前面所说的,这要求所有的异常都必须在 main() 方法内部被捕捉;而 .Net 则没有这种约束,他的 Main() 以至于整个应用程序中的任何一个方法对异常都是完全开放的。这样来看,这两者刚好是对立互补的。
  2. 非默认情况下。Java 可以通过在 main() 方法后面加 throws 关键字使得整个应用程序对异常开放;而 .Net 则可以通过给应用程序域(Application Domain)的 UnhandledException 事件添加委托达到捕捉所有异常的目的,很显然这又是对立互补的。

因此,就好像一个是“正反”,一个是“反正”,加在一起“正反反正”都是一样的,对于达到控制异常的目录来说,是没有区别的。

      很多 Java 爱好者都鄙视 .Net 的这种行为,一方面他令程序变得不够健壮,因为默认情况下没有强制的办法要求所有的异常都被处理,或被正确处理;另外,他为调试增加了困难,不借助文档或代码你将无法了解到一个方法可能抛出什么异常,而当一个异常被抛出的时候,同时异常处理代码又写得不够完善,你将不得不仔细查看整个调用栈来确定异常出现的位置,而对于这一点 Java 默认是强制的。

      但是 Anders 的想法总是有道理的。

  1. .Net 代码写起来非常容易。这是对于初学者,或者那些只是想实现一些测试性小功能的人而言,你完全没有必要考虑太多异常处理的细节,你要的就是写代码,然后让他跑起来。这样的简单性无疑是你希望看到的,这样的简单性无疑更有利于 .Net 在市场上的推广。由于他在这方面并没有什么理论上的漏洞,也就仍然适合构建庞大的项目,只是感觉没有那么舒服罢了。
  2. 一定程度上增加了程序的安全性。难道不捕捉异常可以被成为是安全的吗?这个话也许要从另外一方面来想,前面说过,有些 Java 程序员(绝对不占少数)为了图省事,在强行捕捉异常的压迫下,选择捕捉异常的基类型,也就是捕捉所有的异常。这样当有你无法处理的异常出现时,他们就溺死在了你的代码中,而外面的程序全然不知,还在以一种不确定的状态运行着,这就可能是危险的开始。而如果是 .Net,那么 Unhandled Exception 会被虚拟机捕获,导致程序异常退出,虽然这从面子上对于用户不是一个好的交代,但是深层次地他避免了程序在危险的状态下继续运行。

      总之,萝卜白菜各有所爱。我的这篇帖子力求公正地讨论了这个问题,希望能对你有所帮助。

      摘要:本文主要介绍了一种自由曲线 Hermite 曲线的数学原理,并以此为依据构造了 Hermite 曲线的计算机算法,并在微软的 .net 框架下使用 GDI+ 实现。

      关键字:合成曲线,Hermite曲线,C#,GDI+

一、数学原理
      曲线是构建几何模型的最基本元素之一,主要分为解析曲线和合成曲线。解析曲线通常是先有曲线方程,然后才能把曲线画出来,这种方式对于曲线的构造者来说是非常复杂且不直观的,而且改变曲线的一些参数,设计者也无法立刻了解曲线形状会做怎样的变化。在作曲线设计时,设计者通常希望能先将大致形状用很直观的方式描绘出来,并能够很容易的依照所需要的形状作修改,因此合成曲线是比较合适的方式。

      合成曲线通常以参数的形式来表现,是由设计者根据其设计的需求和几何信息,去“合成”出这条曲线。这样由设计者输入的几何数据,就是曲线的控制点。

      一般的合成曲线,至少需要一个三次的参数式:

公式(1)

用向量表示为:

公式(2)

如何确定式中的参数,就形成了不同的合成曲线的构造方法。Hermite 曲线就是通过曲线的起点(P0)、终点(P1)、起点切向量(V0)和终点切向量(V1)来确定曲线的。改变这四个参数,就可以控制 Hermite 曲线的形状。图1就是构建 Hermite 曲线的示意图。

图1

当给定以上四个参数之后,如何来确定这条 Hermite 曲线呢?

首先将(2)式作一次微分得到:

公式(3)

然后将 u=0,u=1 代入(2)式和(3)式中,就可以得到参数式中的系数:

公式(4)

将(4)式作进一步整理,最后可以将 Hermite 曲线方程写成如下形式:

公式(5)

这就是 Hermite 曲线的参数方程。根据此方程,对于 P0=(-2,2),P1=(3,-1),V0=(8,10) 和 V1=(15,10) 这四个参数,可以构造如下的表格:

使用 Matlab 绘制此曲线,得到图2:

图2

      但是在做曲线设计时,Hermite 曲线仍然存在不少问题,例如在建立 Hermite 曲线时设计者必须输入曲线两端切向量的大小和方向,这对设计者来讲仍然是不直观的。另外,Hermite 曲线不具有区域控制的能力,在建立 Hermite 曲线时提供的是个参数,改变任何一项输入,整条曲线的形状都会发生变化,设计者很难对其进行局部的、小范围的修改。

二、算法
      有了以上的数学基础,构造算法就不是件困难的事情。

/// <summary>
/// 绘制Hermite曲线的核心方法
/// </summary>
/// <param name="pen">绘制曲线用的Pen对象</param>
/// <param name="p0">起点坐标</param>
/// <param name="p1">终点坐标</param>
/// <param name="v0">起点切向量</param>
/// <param name="v1">终点切向量</param>
private void DrawHermite(Pen pen,Point p0, Point p1, Point v0, Point v1) {
    // 计算出来的当前坐标
    int x = p0.X;
    int y = p0.Y;
 
    // 计算出来的前一个坐标,使用该两个做标连成一条线,来绘制曲线
    int preX, preY;
 
    // 根据参数计算每个点的坐标,参数的增量为0.01
    for (double i = 0.0; i <= 1.0; i = i + 0.01) {
        preX = x;
        preY = y;
 
        // 保存计算中间结果,避免重复计算,提高算法效率
        double i2 = i * i;
        double i3 = i2 * i;
        double express = 3 * i2 - 2 * i3;
 
        // 计算横坐标和纵坐标
        x = (int)((1 - express) * p0.X + express * p1.X + (i - 2 * i2 + i3) * v0.X + (i3 - i2) * v1.X);
        y = (int)((1 - express) * p0.Y + express * p1.Y + (i - 2 * i2 + i3) * v0.Y + (i3 - i2) * v1.Y);
 
        // 画线
        this.drawingSurface.DrawLine(pen, preX, preY, x, y);
    }
 
    this.drawingSurface.DrawLine(pen, x, y, p1.X, p1.Y);
}

三、使用 GDI+ 实现
      在 .net 框架下,使用 GDI+ 实现这个算法是件轻松的事情。但是在编程过程中仍然出现了几个问题。

      首先,如何确定两个起点和两个切向量。本程序采用了如下的方法:先选定一个点,然后拉出一条直线,以该点为起点(或终点)并以该直线的方向和长度作为起点(或终点)切向量的方向和大小。

      其次,本程序可以实现类似 PhotoShop 中的钢笔功能。所以有一个如何产生拉动的效果的问题。这对这个问题使用了两种不同的解决方法:1、对于画曲线,使用了两个画笔,一个用于绘制,一个用于擦除。当鼠标移动的时候,就会使用绘制的画笔绘制新曲线,并用擦除画笔擦除刚才的曲线。但该方法会导致另一个问题,就是当该曲线覆盖到其他线条上之后,当曲线离开后,该线条就会有部分被擦掉。但是想解决这个问题是很困难的。(不知道 PhotoShop 是如何实现的。)2、画直线的时候,使用了 GDI+ 中自带的一个 ControlPaint.DrawReversibleLine() 方法,该方法可以自己解决以上的问题。

      第三、像这样一遍一遍的重画和擦除,会很占用系统资源,但是没有什么更好的解决方法,从网上找到的文章来看,如果复写(Override)OnMouseDown, OnMouseMove 和 OnMouseUp 事件,会比处理此三个事件的方法要来得效率高一些,因此本程序的所有事件全部采用了这种方法。下面的这段代码,是复写了 OnMouseMove 事件,用于处理当鼠标按下左键移动的时候,产生的拉动效果。

protected override void OnMouseMove(MouseEventArgs e) {
    // 判断当鼠标移动的时候是否有鼠标左键按下
    if (e.Button == MouseButtons.Left) {
        // isContinuedDrawing 是个标志变量,标志所产时的动作是否为了画第二个参量
        if (!isContinedDrawing) {
            endPoint[0].X = e.X;
            endPoint[0].Y = e.Y;
 
            // 使用 ControlPaint 画直线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0])PointToScreen(previousPoint), Color.Black);
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0]), PointToScreen(endPoint[0]), Color.Black);
 
            previousPoint = endPoint[0];
        } else {
            endPoint[1].X = e.X;
            endPoint[1].Y = e.Y; ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(previousPoint), Color.Black);
            
            Point _v0 = new Point();
            _v0.X = endPoint[0].X - startPoint[0].X;
            _v0.Y = endPoint[0].Y - startPoint[0].Y;
 
            Point _v1 = new Point();
            _v1.X = previousPoint.X - startPoint[1].X;
            _v1.Y = previousPoint.Y - startPoint[1].Y;
 
            // 擦除以前的曲线
            this.DrawHermite(erasePen,startPoint[0], startPoint[1], _v0, _v1);
            // 画新的曲线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(endPoint[1]), Color.Black);
            _v1.X = endPoint[1].X - startPoint[1].X;
            _v1.Y = endPoint[1].Y - startPoint[1].Y;
 
            this.DrawHermite(drawPen,startPoint[0], startPoint[1], _v0, _v1);
            previousPoint = endPoint[1];
        }
    }
 
    // 调用父类中的 OnMouseOver 事件三
    base.OnMouseMove(e);
}

四、程序截图

五、参考文献

      今天绝大部分时间用于解决了一个很小的问题,但是从中却了解到 Visual Studio.net 中 Design Mode 的一些实现方式。

      从头开始,先来说说我遇到的这个问题:如何判但一个实例所对应的类是否实现了某一个接口?这其实也算不上什么问题,我起初也是知道的,就是和判断某个对象是否是某个类的实例一样,使用 is 关键字就可以了。但问题偏偏没有那么简单。

      我的目的是想要实现这样一种功能,一个用户控件 (User Control) 被放到一个容器(之所以说是容器,因为它不一定只是 Form, 也有可能是 Tab page, Group box 等等)中,那么我需要这个用户控件去检查这个容器是否实现了我规定的一个接口,如果是才允许在这个容器中创建自己。

      于是,问题接踵而来。首先,如何得到这个用户控件所在的容器,通过搜索 MSDN 得到两个属性,一个是 UserControl.Container, 另一个则是 UserControl.Parent. 前一个似乎很像,不过它返回的是一个 IContainer 接口的实例,无法直接使用;而后者相对较好,返回一个 Control (该类是所有 Windows 控件,包括 Form 的基类)类的实例。很自然,我就开始在这个属性上下文章。

      假如我要指定实现的接口是 IMyInterface. 假如我使用 Form 作为该控件的容器,那么在 IDE 自动给我在默认窗体后面继承一个 System.Windows.Forms.Form 之外,还需要继承这个接口,这部分很简单但是当我使用 is 关键字进行如下判断时就出现了问题 this.Parent is IMyInterface (其中的 this 就是那个用户控件的实例),这个判断永远也通不过。后来我试图将其做一个强制类型转换,却得到了指定转换非法 (Spicified cast is invalid.) 的异常。原因就在于 this.Parent 返回一个 Control 类型,而该类型在 .net 类库中显然没有实现我的那个接口,自然转换非法是正常的。但如何解决呢?

      马上就能想到的自然是自己重写一个 Control 类,让该类实现我的接口。其实完全可以重写一个 Form 类,来实现接口。然后再使得你创建的 Form 从你这个扩展了的 Form 去继承,再作如上的转换就不会有问题了。我也就是这样做的,不过稍微画蛇添足了一点:

public abstract class MyCustomForm : System.Windows.Forms.Form,  IMyInterface {}

就是加了一个 abstract 关键字,以阻止用户从该类生成实例。这是就出现了下一个问题,从这个 MyCustomForm 继承的窗体无法在设计器中展现了!这个问题起初没有弄明白是怎么回事,于是我就先去掉 abstract 关键字,使得一切恢复正常。随后我在这个类中添加了一个返回 bool 类型的虚属性,并默认实现返回 false, 就像这样:

public virtual bool MyProperty {
    get {
        return false;
    }
}

然后我在我真正的 Form 中 (起名为 MainForm)覆盖这个属性,并返回 true. 之后我在用户控件的 Load 事件方法中调用这个属性,然而奇怪的是当向窗体中添加这个用户控件的时候(注意此时仍处于 Design Mode),该属性一直都为 false, 既基类中的实现,也就是说似乎属性没有被覆盖。这时我就想到了一个关于何时使用基类中的方法,何时使用覆盖了的方法的问题。因为可以想得到,当你把 MainForm 转到 Control 之后(通过调用 UserControl.Parent 实现)原有 MainForm 中多余的成员是否会被丢掉,当再转换成 IMyInterface 的实例时是否将无法找到覆盖过的方法,以至于直接去调用积类中的方法。似乎这是理所当然的,但是我随后写了一个类似的小程序试验,结果发现并不是这样,原因也很简单,这些转换只是在引用上发生的,已有的实例中的内容并不会被销毁,那么出现这个问题就太诡异了。

      仔细想呀想呀,联想到上面出现的那个 Design Mode 的问题,一切就都能揭示了。 简单的就一句话:Design Mode 在你操作时需要生成一个实例,然而该实例不是运行时的实例。具体来说,你在 Design Mode 中设计一个窗体,那么你会看见 IDE 自动为你创建了一个名叫 Form1 的类,并且继承自 System.Windows.Forms.Form, 同时生成一个可视化的窗体界面,这个界面是一个窗体的实例。但是这个实例不是 Form1 的实例,而是其基类 Form 的实例,原因就是,IDE 是在帮你设计这个 Form1, 显然此时这个 Form1 还不存在,存在的只是一堆代码而已。这样你就可以解释一切了,当我的基类被 abstract 的时候无法被实例化以后,自然设计器就没有办法为我创建这个类的实例使得我能进行可视化操作,但是你仍然可以将程序无误的跑起来。因为所谓的那个 Form1 并不是 abstract 的。也正因为如此,当我的 UserControl 在 Design Mode 时去调用 Interface 中的方法时,自然得到的就是基类中虚属性的实现,而在运行时,得到的就是覆盖了的属性的返回值,经过试验,答案却是是这样的。

      另外,你可使用 UserControl.DesignMode 来判断其是处于设计时还是运行时。

      我本人对图像处理没什么兴趣,要不是这门课要交作业,我才懒得做这些东西。唉……不过程序写了,自然会有一点想法,发到 Blog 上,以备后用吧。但是,即便是写了程序,也仍然不知道作边缘检测的原理何在,只是模糊的知道大概是对图像灰度求梯度,梯度大的就是边缘了。但毕竟图像是离散化的,可以使用另外的方法求梯度,而不用像高数中那样拼命地算偏导数了。有很多学者提出了很多种不同性能的模板,只要按照模板作简单的四则运算就行了,当然这也是能用程序实现的关键。由于作为教学课程,所有的内容都是以简单的灰度图像来说明举例的,当然边缘检测也不例外,留作业写程序也是一样,所以马上就遇到的一个问题是如何将彩色图像转为灰度图像。在课程中,都是简单的认为灰度图像只有一个亮度,这自然是没错的,但是放到计算机里,灰度也是一种颜色,是颜色就要使用色彩模式(最常用的自然是 RGB 了),那么这种灰度到底应该是怎样的颜色编码呢?索性取向 Photoshop ,看一看各种灰度色调,终于有所发祥。其实也可以这么想,全黑是 #000000 ,全白是 #FFFFFF ,那么是不是只要 RGB 值都相等,这个颜色就是灰度色呢?试了一下,果然如此。这样就好办了,至少第一步知道了转换的目标是什么了。但马上就又有了一个问题,彩色图片的颜色这么多,那么如何知道哪种彩色颜色对应哪种灰度颜色呢?这一点我从 .net Framework 中找到了答案。其实我从一开始就想在 .net Framework 中寻找有没有直接将 RGB 转成灰度或者是 HIS 模式(因为 HIS 模式中的 I 就使亮度,自然就容易转成灰度了)的,是不是太奢望了,所以我也没抱太大的希望,但是在这过程中却发现 Color 中有关一个实例方法 GetBrightness() ,就是用来获得颜色亮度的,真是踏破铁鞋无觅处,得来全不费功夫。该方法返回一个 0~1 之间的浮点数,那么如果 RGB 每个各占一个字节的话,那么刚好可以用这个值去乘以 255 ,然后拼成一个 RGB ,这个颜色就是原始色彩所对应的灰色。程序代码如下:

//定义两个颜色变量,oColor为原始色彩,gColor为对应的灰度色彩
Color oColor,gColor;
//原始色彩的亮度
float brightness;
//灰度色彩用 RGB 来表示,由于 R=G=B 所以只用一个变量就可以了
int gRGB;
//遍历图像中的每个像素
for (int i = 0; i < oBmp.Width; i ++) {
    for (int j = 0; j < oBmp.Height; j ++) {
        //得到像素的原始色彩        
        oColor = oBmp.GetPixel(i,j);
        //得到该色彩的亮度
        brightness = oColor.GetBrightness();
        //用该亮度计算灰度
        gRGB = (int)(brightness * 255);
        //组成灰度色彩
        gColor = Color.FromArgb(gRGB,gRGB,gRGB);
        //最后将该灰度色彩赋予该像素
        gBmp.SetPixel(i,j,gColor); 
    }
}

其实还是很简单的。这之后就可以按照书中所说的模板游历的方法来进行边缘检测了。程序如下:

//template为模板,nThreshold是一个阈值,
//用来将模板游历的结果(也就是梯度)进行划分。
//大于阈值的和小于阈值的分别赋予两种颜色,白或黑来标志边界和背景
private void EdgeDectect(int[,] template,int nThreshold) {
    //取出和模板等大的原图中的区域
    int[,] gRGB = new int[3,3];
    //模板值结果,梯度
    int templateValue = 0;
    //遍历灰度图中每个像素
    for (int i = 1; i < gBmp.Width - 1; i ++) {
        for (int j = 1; j < gBmp.Height - 1; j ++) {
            //取得模板下区域的颜色,即灰度
            gRGB[0,0] = gBmp.GetPixel(i-1,j-1).R;
            gRGB[0,1] = gBmp.GetPixel(i-1,j).R;
            gRGB[0,2] = gBmp.GetPixel(i-1,j+1).R;
            gRGB[1,0] = gBmp.GetPixel(i,j-1).R;
            gRGB[1,1] = gBmp.GetPixel(i,j).R;
            gRGB[1,2] = gBmp.GetPixel(i,j+1).R;
            gRGB[2,0] = gBmp.GetPixel(i+1,j-1).R;
            gRGB[2,1] = gBmp.GetPixel(i+1,j).R;
            gRGB[2,2] = gBmp.GetPixel(i+1,j+1).R;
            //按模板计算
            for (int m = 0; m < 3; m ++) {
                for (int n = 0; n < 3; n ++) {
                    templateValue += template[m,n] * gRGB[m,n];
                }
            }
            //将梯度之按阈值分类,并赋予不同的颜色
            if (templateValue > nThreshold) {
                eBmp.SetPixel(i,j,Color.FromArgb(255,255,255)); //白
            } else {
                eBmp.SetPixel(i,j,Color.FromArgb(0,0,0)); //黑
            }
            templateValue = 0;
        }
    }
}

应用程序域 (Application domain)
      一般的 Windows 应用程序都是以进程的方式在操作系统中运行,操作系统负责分配并管理程序所请求的资源。但是对于使用 .net 编写的托管程序而言,有一个特殊的进程被称为公共语言运行时 (CLR) ,该进程负责加载托管程序并运行。对于在 CLR 中运行的每一个托管程序而言,都有一个被称之为应用程序域的边界,每个托管程序都在自己的应用程序域中安全的运行。不同的应用程序域之间互不干扰。

上下文 (Application context)
      将应用程序域进一步细分,就形成了上下文,上下文确保一套常用约束和使用语法负责管理其中的所有对象访问。每个应用程序域至少包含一个上下文,称为默认上 下文 (Default context) 。除非某个对象明确的要求一个专门的上下文,否则运行时 (Common language runtime) 将在默认上下文中创建那个对象。

.net Remoting 边界
      对于应用程序而言应用程序域的边界是 .net Remoting 的边界。对应用程序域而言上下文的边界是 .net Remoting 边界,一个普通的对象无法穿越 .net Remoting 边界。

不可远程化对象和可远程化对象
一、不可远程化对象 (Non remotable object)
      在默认情况下,没有经过任何特殊处理的对象都是不可远程化对象。不可远程化对象无法以任何方式(拷贝或引用)被跨应用程序域的对象所访问,当企图把对象引用传递到其他的应用程序域中时,会有异常产生。
二、可远程化对象 (emotable object) 如果一个类的实例可以穿越 .net Remoting       边界并可以在边界外被访问,则该对象就是可远程化的。在应用程序域外可远程化对象被访问的方式有两种:可以通过对象的完整副本来访问,还可以通过对象的引用(在 .net Remoting 中是以代理的模式来实现的)来访问。

.net Remoting 的服务器和客户端
      .net Remoting 的服务器和客户端与以往的概念没有什么差别,但是在这里要强调的是 .net Remoting 的服器器和客户端显然是处于不同的应用程序域中,但是并不一定处于不同的计算机上。

按值列集 (Marshal by value) 和按引用列集 (Marshal by reference)
      一、当运行时可以获得一个对象的完整副本的时候,则该对象就可以以所谓按值列集的方式被传递到其他的应用程序域中。实现按值列集的途径就是在声明类的时候添加 Serializable 特性 (Attribute) 或者实现 ISerializable 接口。例如:

[Serializable]
class Foo {
    ...
}

因此能够按值列集的对象也被称为可序列化的对象。客户端将获得该对象的完整副本。

      二、当一个类直接或间接地从 MarshalByRefObject 类继承的时候,运行时就可以在客户端创建一个该对象的代理。例如:

class Foo : MarshalByRefObject {
    ...
}

上下文邦定 (Context bound)
      当一个类型的实例只停留在具体的上下文内的时候,该类型就是上下文邦定的类型,在这个域内的其它上下文中的对象不能直接访问该对象。上下文邦定类型通过继承 System.ContextBoundObject 类来实现。

代理 (Proxy)
      刚才提到,当对象以按引用列集的方式在应用程序域中传递的时候,运行时将在接收方创建一个该对象的代理。该代理可以想象为(通常也是这样实现的)一个封装了该对象所有或部分成员的接口,负责将接收方(客户端)的调用信息发送给发送方(服务器)。

通道 (Channel)
      通道用于在跨应用程序域的远程对象间传递消息 (Message) 。服务器将选择侦听请求的通道,而客户端则选择希望与服务器进行通信的通道。运行时提供了两种内置的通道 Http 通道和 Tcp 通道。

using System.Runtime.Remoting; // .net Remoting 命名空间
using System.Runtime.Remoting.Channels; // .net Remoting 通道命名空间
using System.Runtime.Remoting.Channels.Http; // .net Remoting Http 通道命名空间
using System.Runtime.Remoting.Channels.Tcp; // .net Remoting Tcp 通道命名空间
. . .
// ChannelServices 是一个工具类,里面封装了大量的与通道相关的静态方法
ChannelServices.RegisterChannel ( new HttpChannel() ); // 注册 Http 通道并使用默认端口
ChannelServices.RegisterChannel ( new TcpChannel(4242) ); // 注册 Tcp 通道并使用4242端口

激活 (Activation)
一、服务器端激活 (Server Activation)
      服务器端激活方式是指对象的生存周期(何时生成与何时被垃圾回收)由服务器来决定。服务器端激活有两种方式:单件 (Singleton) 和单调用 (SingleCall) 。
二、客户端激活 (Client Activation)
      客户端激活方式是指对象的生存周期由客户端来决定。客户端激活只有一种方式,称为 CAO (Client Activation Object) 。每一个客户端激活创建一个对象,该对象存在于如下两个事件之一到来之前:客户端失掉对对象的引用,对象租借过期。客户端激活模式可以存储每一个客户端的状态,并接受构造函数参数。可以使用如下的代码,在服务器端设置客户端激活的远程对象:

RemotingConfiguration.RegisterActivatedServiceType( typeof( SomeMBRType ) );

然后在客户端设置如下:

RemotingConfiguration.RegisterActivatedClientType( typeof( SomeMBRType ), "http://SomeURL" );

此处的 SomeURL 是指服务器端的地址或计算机名。

众所周知 (Well-known object) 的对象
      服务器端激活的类型我们就称之为众所周知的。在使用众所周知的对象时,服务器要进行如下的设置:对象的类型,何时以及如何实例化对象,和一个客户端用来与该类型联系的名称(或叫终端 end point )。而同时客户端要设置连接到哪一个服务器,并在终端上获得众所周知的类型。使用 RemotingConfiguration.RegisterWellKnownServiceType 这个方法来注册一个众所周知的类型,该方法需要提供三个参数:待注册的类型,传达给客户端的终端名称和激活模式。例如:

using System.Runtime.Remoting; // .net Remoting 名名空间
. . .
WellKnownServiceTypeEntry WKSTE =  new WellKnownServiceTypeEntry(
                     typeof( MyNameSpace.SomeMBRType ),
                     "SomeURI",
                     WellKnownObjectMode.SingleCall );
RemotingConfiguration.RegisterWellKnownServiceType(WKSTE);

其中先创建一个 WellKnownServiceTypeEntry 类型的对象,用于在 RegisterWellKnownServiceType 方法中传递参数。
typeof( MyNameSpace.SomeMBRType ) 这个参数待注册的远程对象的类型, SomeMBRType 是 MyNameSpace 命名空间下的一个类。
SomeURI 便是传达给客户端的终端名称。(它的用途在后面会介绍。)
WellKnownObjectMode.SingleCall 就是将该类型注册为单调用模式。

单件和单调用
      一、单调用:服务器为每个客户端的方法调用生成一个单调用对象,每个对象服务且仅      服务一个请求。只有当方法调用到达的时候才按需求创建对象,并且对象的生存期直至调用结束。单调用模式适用于无需保存状态的应用程序,是解决负载平衡的最好选择。可以通过如下的方法在服务器端将可远程化类型配置为单调用模式:

// RemotingConfiguration 是一个工具类
RemotingConfiguration.RegisterWellKnownServiceType(  
   typeof( SomeMBRType ),   // 从 MasharlByRef 继承来的数据的类型信息
   "SomeURI",                     // 发布该服务器端激活对象时使用的 URI
   WellKnownObjectMode.SingleCall ); // 设定为单调用模式

然后用如下的方法在客户端再配置一次:

RemotingConfiguration.RegisterWellKnownClientType(
   typeof( SomeMBRType ),               // 要从服务器端得到的远程类型
   "http://SomeWellKnownURL/SomeURI" ); // 众所周知的对象发布的 URL

      二、单件:服务器在任何情况下都只创建该类型的一个实例,客户端的所有请求都由这      一个实例来处理,且该实例的生存期与服务器的生存期相同。单件模式适用于由状态的应用 程序,但是这种模式的对象只能保存非客户端的状态。同上面的代码,只要将 WellKnownObjectMode 设置为 Singleton 就可以了。

众所周知对象的 URL
      服务器端激活的对象是在 URL 上发布的,该 URL 是被客户端众所周知的。众所周知对象的 URL 看上去如下:

ProtocolScheme://ComputerName:Port/ApplicationName/ObjectUri

其中 ProtocolScheme 代表 .net Remoting 通道所使用的协议,例如 Http 或 Tcp 等。
ComputerName 代表 .net Remoting 服务器的名称或地址。
Port 是注册通道时使用的端口。
ApplicationName 就是服务器端应用程序的名称。当使用 IIS 作为服务器端宿主的时候, ApplicationName 就变成了 IIS 的虚拟目录。
ObjectUri 就是在使用 RegisterWellKnownServiceType 时注册的 SomeURI 。 ObjectUri 必须以 .rem 或 .soap 结束,以区别是使用 Tcp 还是 Http 协议。

基于租借的生存期
      客户端激活对象的生存期由与对象相关的租借来控制,租借有一个租借期, .net Remoting 基础设施在租借过期的时候放弃对象的引用,每一次方法调用可以更新租借,客户端可以使用代理更新租借,发起者也可以更新租期。

客户端激活对象的 URL
      客户端激活对象不需要为每一个对象配备一个单独的 URL ,客户端激活对象的 URL 看上去如下:

ProtocolScheme://ComputerName:Port/ApplicationName