前言

前些日子用 Delphi 写了一个 Windows 外壳扩展程序,大家知道 Windows 外壳扩展实际上就是 COM 的一种应用 -- Shell COM,虽然整个程序写得还算比较顺利,但写完后还是感觉对 Delphi 中 COM 的实现有点雾里看花的感觉,因此我认为有必要花一点时间对 COM 在 Delphi 中的实现做一些研究。另外我也买了李维的新书 --《深入核心 -- VCL架构剖析》,里面有两章涉及了与 COM 相关内容,看完后我知道了COM 在 Delphi 中的实现是基于接口(Interface),而 Delphi 中的接口概念又起源于对 COM 的支持,总之他们之间互相影响,发展成接口在 Delphi 中已经是 First-Class 的地位,并且完全摆脱 COM 而独立存在。
    本系列文章侧重于描述 COM 在 Delphi 中的实现手法,主要配合 VCL 源码片断进行分析,不会涉及过多的基本概念,因此要求读者有一定的 COM 和 接口概念,可以参考我在文章末尾列出的文献。本篇主要讲 COM 对象在 Delphi 中的创建过程。

正文

 

为了让读者能跟着我的分析轻松读完本篇文章,我引用文献[2]中的范例做解释,但为了更清楚地阐述问题,我改写了部分代码。所有分析请在 Delphi 7 上测试。
   在 Delphi 中首先通过选择菜单 File-->New-->Other...新建一个 ActiveX Library 并保存名称为 SimpleComServer,再新建一个 COM Object,在COM Object Wizard 中将对象命名为 SimpleCOMObject,Options 中的两个复选框都可以不必选中其他的保持默认, 现在 COM服务器端的框架已经建立起来了。剩下的就是需要我们把声明的接口 ISimpleCOMObject 的代码实现。

  1. 服务器端代码
  2. library SimpleComServer;
  3. uses
  4. ComServ,
  5. SimpleCOMObject in 'SimpleCOMObject.pas',
  6. SimpleComInterface in 'SimpleComInterface.pas',
  7. exports
  8. DllGetClassObject,
  9. DllCanUnloadNow,
  10. DllRegisterServer,
  11. DllUnregisterServer;
  12. {$R *.RES}
  13. begin
  14. end.
  15. --------------------------------------------------------------------------------
  16. unit SimpleComInterface;
  17. interface
  18. uses Windows;
  19. const
  20. Class_SimpleComObject: TGUID = '{3714CF21-D272-11D3-947F-0050DA73BE5D}';
  21. type
  22. ISimpleComObject = interface
  23. ['{2E2A6DD0-D282-11D3-947F-0050DA73BE5D}']
  24. function Multiply(X, Y: Integer): Integer; stdcall;
  25. function GetClassName: Widestring; stdcall;
  26. end;
  27. implementation
  28. end
  29. --------------------------------------------------------------------------------
  30. unit SimpleCOMObject;
  31. interface
  32. // SimpleCOMObject 的实现部分
  33. uses
  34. Windows, ActiveX, Classes, ComObj, SimpleComInterface;
  35. type
  36. TSimpleComObject = class(TComObject, ISimpleComObject)
  37. protected
  38. function Multiply(X, Y: Integer): Integer; stdcall;
  39. function GetClassName: Widestring; stdcall;
  40. end;
  41. const
  42. Class_SimpleComObject: TGUID = '{3714CF21-D272-11D3-947F-0050DA73BE5D}';
  43. implementation
  44. uses ComServ;
  45. { TSimpleComObject }
  46. function TSimpleComObject.GetClassName: Widestring;
  47. begin
  48. Result := TSimpleComObject.ClassName;
  49. end;
  50. function TSimpleComObject.Multiply(X, Y: Integer): Integer;
  51. begin
  52. Result := X * Y;
  53. end;
  54. initialization
  55. TComObjectFactory.Create(ComServer, TSimpleComObject, Class_SimpleComObject,
  56. 'SimpleComObject', 'A simple implementation of a COM Object',
  57. ciMultiInstance, tmApartment);
  58. end.
  1. //客户端关键代码
  2. procedure TForm1.Button1Click(Sender: TObject);
  3. var
  4. aFactory: IClassFactory;
  5. begin
  6. OleCheck(CoGetClassObject(Class_SimpleComObject, CLSCTX_INPROC_SERVER or
  7. CLSCTX_LOCAL_SERVER, nil, IClassFactory, aFactory));
  8. aFactory.CreateInstance(nil, ISimpleComObject, ComInterface);
  9. ShowMessage('The result is: ' +
  10. IntToStr(ComInterface.Multiply(StrToInt(Edit1.Text), StrToInt(Edit2.Text))));
  11. ComInterface := nil;
  12. end;
  13. procedure TForm1.Button2Click(Sender: TObject);
  14. begin
  15. ComInterface := CreateComObject(Class_SimpleComObject) as ISimpleComObject;
  16. ShowMessage(ComInterface.GetClassName);
  17. ComInterface := nil;
  18. end;

完成服务器端的代码后,我们需要写一个客户端小程序来执行服务器端内的接口代码,我仅列出由我改写的关键代码部分

现在开始进入主题,跟随我一起走进 Delphi 的 COM Framework 世界吧。我主要从客户端程序创建 COM 对象来剖析 VCL 源码。
   客户端代码中我用两种获得创建 SimpleCOMObject 对象并获得 ISimpleCOMObject 接口,一旦获得接口,你就可以自由地使用接口指定的方法了。
    让我们先看看 Button1Click 里如何创建 COM 对象的。代码调用了 CoGetClassObject 获得创建 SimpleCOMObject 对象的类工厂 -- IClassFactory 接口,紧接着又通过调用该接口的 CreateInstance 方法创建了真正的 SimpleCOMObject 对象实例,返回 ISimpleComObject 接口指针。 那么上面整个过程在 VCL 中是如何实现的呢?让我们先从 CoGetClassObject 这个API 说起。
    CoGetClassObject 是 Windows 的一个标准 COM API,该函数存在于 OLE32.DLL中,它是 Windows COM DLL 之一。函数先根据系统注册表中的信息,找到类标识符 CLSID 对应的组件程序(即服务器端程序,我们这里讨论的是一个 DLL 文件)的全路径,然后调用 LoadLibrary(实际上是 CoLoadLibrary)函数初始化服务器(Dll 被加载到客户程序进程中)并调用组件程序的 DllGetClassObject 输出函数。DllGetClassObject 函数负责创建相应的类厂对象,并返回类厂对象的 IClassFactory 接口。至此 CoGetClassObject 函数的任务完成,然后客户程序继续调用类厂对象的 CreateInstance 成员函数,由它负责 COM 对象的创建工作。
    注意:Windows COM 规范中指定你必须在服务器中完成并输出 DllGetClassObject,如果这个没有被发现,Windows 将不能传递对象到客户端,DllGetClassObject 将是进入我们的 dll(COM 服务器)的入口点。
    从上面的一番简要陈述不难看出获得 IClassFactory 接口是通过调用服务器端的 DllGetClassObject 函数获得的,传奇实际也就是从这个输出函数开始的。让我们看看它是如何实现的(如果源码中我附加了注释,请一定仔细看看,下面不再提示):

  1. function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;
  2. var
  3. Factory: TComObjectFactory;
  4. begin
  5. Factory := ComClassManager.GetFactoryFromClassID(CLSID);
  6. if Factory <> nil then
  7. if Factory.GetInterface(IID, Obj) then
  8. Result := S_OK
  9. else
  10. Result := E_NOINTERFACE
  11. else
  12. begin
  13. Pointer(Obj) := nil;
  14. Result := CLASS_E_CLASSNOTAVAILABLE;
  15. end;
  16. end;

ComClassManager 是什么?它是我们需要介绍的 Delphi COM Framework 中的第一个类。

  1. function ComClassManager: TComClassManager;
  2. begin
  3. if ComClassManagerVar = nil then
  4. ComClassManagerVar := TComClassManager.Create;
  5. Result := TComClassManager(ComClassManagerVar);
  6. end;

每个服务器端内存在一个 TComClassManager 实例,即ComClassManagerVar 全局对象变量,它负责管理 COM 服务器中的所有类工厂(class factory)对象(本例中只有一个类工厂)。而类工厂又是什么时候创建的?其实我前面已经列出了,COM Object Wizard 生成的 SimpleCOMObject 的骨架代码的 Initialization 部分已经自动为我们创建一个 TComObjectFactory 对象:

  1. initialization
  2. TComObjectFactory.Create(ComServer, TSimpleComObject,Class_SimpleComObject,'SimpleComObject', 'A simple implementation of a COM Object', ciMultiInstance,
  3. tmApartment);

Delphi关键字Initialization提示我们 dll 在被载入客户端程序进程空间时,负责创建 impleCOMObject 对象的类工厂 TComObjectFactory 就已经被创建了。我们知道,一个服务器端里可以包含多个 COM 对象,并且每一个独立的 COM 对象都必须相应有创建该类的类工厂,假如你设计的服务器端里有十个 COM 对象,那么肯定会有十个负责创建不同类的类工厂,这十个类工厂在程序初始化时都会被一一创建出来。这个概念一定在你的头脑中建立起来,否则后面就不好理解了。再提示一下,VCL 中定义了数种 ClassFactory 类,分别负责某一种类型的 COM 对象创建,TComObjectFactory 是其中最简单的一种[1]。那么 ComClassManager 和 TComObjectFactory 又是如何联系到一起呢?看看 TComObjectFactory 的 Constructor:

  1. constructor TComObjectFactory.Create(ComServer: TComServerObject;
  2. ComClass: TComClass; const ClassID: TGUID; const ClassName,
  3. Description: string; Instancing: TClassInstancing;
  4. ThreadingModel: TThreadingModel);
  5. begin
  6. //.....
  7. //将自己插入到 ComClassManager 的 Factory List 中去
  8. ComClassManager.AddObjectFactory(Self);
  9. FComServer := ComServer;
  10. FComClass := ComClass;
  11. FClassID := ClassID;
  12. FClassName := ClassName;
  13. FDescription := Description;
  14. FInstancing := Instancing;
  15. FErrorIID := IUnknown;
  16. FShowErrors := True;
  17. FThreadingModel := ThreadingModel;
  18. FRegister := -1;
  19. end;

再看看 ComClassManager 相关实现代码:

  1. TComClassManager = class(TObject)
  2. private
  3. FFactoryList: TComObjectFactory;   //维护着一个 TComObjectFactory 链表
  4. //添加Com类工厂
  5. procedure AddObjectFactory(Factory: TComObjectFactory);
  6. procedure RemoveObjectFactory(Factory: TComObjectFactory);
  7. public
  8. //....
  9. function GetFactoryFromClassID(const ClassID: TGUID): TComObjectFactory;
  10. end;
  11. ////
  12. procedure TComClassManager.AddObjectFactory(Factory: TComObjectFactory);
  13. begin
  14. FLock.BeginWrite;
  15. try
  16. Factory.FNext := FFactoryList;
  17. FFactoryList := Factory;
  18. finally
  19. FLock.EndWrite;
  20. end;
  21. end;

ComClassManagerVar 维护着服务器中的所有的类工厂的一个链表,每个单一类工厂的实例都是自动初始化,在我们的服务器 Initialization 节你可以看到,并自动将自己添加到 ComClassManager 的链表(FactoryList)中。现在想想,这样的设计是不是非常棒。
    请跟随我继续往下走。当客户端要求 DllGetClassObject 返回指定创建的类工厂,在函数内部调用了 TComClassManager 的 GetFactoryFromClassID 方法。该方法遍历 FactoryList 链表,根据 ClassID 找到对应的类工厂,并返回类工厂对象实例。

  1. function TComClassManager.GetFactoryFromClassID(const ClassID: TGUID): TComObjectFactory;
  2. begin
  3. FLock.BeginRead;
  4. try
  5. Result := FFactoryList;
  6. while Result <> nil do
  7. begin
  8. if IsEqualGUID(Result.ClassID, ClassID) then Exit;
  9. Result := Result.FNext;
  10. end;
  11. finally
  12. FLock.EndRead;
  13. end;
  14. end;

对上面的代码分析我再多说一下,链表 FFactoryList 变量实际就是 TComObjectFactory 类型,TComObjectFactory 创建时就获得了丰富的关于它要创建的相关 COM 对象信息,例如在我们这个范例里,ClassFactory 知道了它要创建的 COM 对象类型是 TSimpleComObject, ClassID 是 Class_SimpleComObject..等等,这些都为类工厂在创建相关类以及一些辅助方法(函数)都提供了极为重要的信息

DllGetClassObject 获得正确的类工厂对象之后调用它的 GetInterface 方法,这个方法实际上是继承自 TObject.GetInterface,Delphi 为每一个带有 GUID 的接口设计了一个记录结构 -- TInterfaceEntry 记录,实现 IClassFactory 接口的 TComObjectFactory 对象 VMT 中的 vmtIntfTable 指向一个 TInterfaceTable 记录, 该记录包含有它实现的接口数量(IUnknown、IClassFactory)、相应接口的 TInterfaceEntry 记录等信息,通过查询 IClassFactory 接口相应 TInterfaceEntry 记录中的 IOffset 域获得该接口在  TComObjectFactory 对象实例中的正确位置,并返回指向该位置的 IClassFactory 接口指针[1][3]。

  1. function TObject.GetInterface(const IID: TGUID; out Obj): Boolean;
  2. var
  3. InterfaceEntry: PInterfaceEntry;
  4. begin
  5. Pointer(Obj) := nil;
  6. InterfaceEntry := GetInterfaceEntry(IID);
  7. if InterfaceEntry <> nil then
  8. begin
  9. if InterfaceEntry^.IOffset <> 0 then
  10. begin
  11. Pointer(Obj) := Pointer(Integer(Self) + InterfaceEntry^.IOffset);
  12. if Pointer(Obj) <> nil then IInterface(Obj)._AddRef;
  13. end
  14. else
  15. IInterface(Obj) := InvokeImplGetter(Self, InterfaceEntry^.ImplGetter);
  16. end;
  17. Result := Pointer(Obj) <> nil;
  18. end;

至此,CoGetClassObject 内部调用服务器端的 DllGetClassObject 已经正确获得了负责创建 SimpleCOMObject 对象的 IClassFactory 接口。在获得这个接口后,就可以调用它的方法 CreateInstance 创建 SimpleCOMObject 对象并返回 ISimpleCOMObject 接口,现在你可以对 ISimpleCOMObject 接口任意进行操作了

让我们再看看 ButtonClick2 中是如何创建 SimpleCOMObject 对象的。
    ButtonClick2 是调用 CreateComObject 函数创建 SimpleCOMObject 对象的。 CreateComObject 函数只是对 COM API -- CoCreateInstance 的一个简单包装。为什么要包装它,你可以看一下 CoCreateInstance 的参数就知道为什么了,参数多且复杂,这是 Windows API 的通病,而 VCL 实现却很体贴我们,它传递 CLSID 作为唯一的参数,其实平时应用中我们创建的大部分 COM 对象都是 CLSID 已知,并且对象是驻留在本地或进程内服务器的指定对象。

  1. function CreateComObject(const ClassID: TGUID): IUnknown;
  2. begin
  3. try
  4. OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
  5. CLSCTX_LOCAL_SERVER, IUnknown, Result));
  6. except
  7. on E: EOleSysError do
  8. raise EOleSysError.Create(Format('%s, ClassID: %s',[E.Message, GuidToString(ClassID)]),E.ErrorCode,0) { Do not localize }
  9. end;
  10. end;

CoCreateInstance 也存在于 OLE32.DLL中,其内部也是先调用 CoGetClassObject 函数,返回负责创建 SimpleCOMObject 的IClassFactory 接口,然后也还是调用该接口的 CreateInstance 创建 SimpleCOMObject 并返回该对象的 IUnknown 接口,到这一步,与Button1Click 中创建 SimpleCOMObject 的实现方法区别在于 Button1Click 通过 ClassFactory 的 CreateInstance 直接返回 ISimpleCOMObject 接口而不是它的 IUnknown 接口,其他的并没有什么区别,相对 Button1Click 的方法更直观。在获得了 SimpleCOMObject 的 IUnknown 接口之后,我们并不能立即用此接口去调用 ISimpleCOMObject 的方法,为了和对象通信,必须先将它转换成 ISimpleComObject 接口。那么有读者会问为什么 CreateComObject 不设计成能直接返回需要的接口呢,我想还是为了简化这个函数的使用吧。获得 ISimpleComObject 接口可以通过调用 IUnknown 接口的 QueryInterface 方法查询 SimpleCOMObject 对象是否支持该接口, Delphi 为我们提供了更简单的方法 -- “AS”关键字。先让我们看看 As 在幕后到底为我们做了什么(Debug 状态下的反汇编源码):

  1. Unit1.pas.49: ComInterface := CreateComObject(Class_SimpleComObject) as ISimpleComObject;
  2. 0045B2C6 8D55FC        lea edx,[ebp-$04]
  3. 0045B2C9 A16CD24500    mov eax,[$0045d26c]
  4. 0045B2CE E8C9F0FFFF    call CreateComObject
  5. 0045B2D3 8B55FC        mov edx,[ebp-$04]
  6. 0045B2D6 8D8314030000  lea eax,[ebx+$00000314]
  7. 0045B2DC B93CB34500    mov ecx,$0045b33c
  8. 0045B2E1 E87AA9FAFF    call @IntfCast

可以看到, AS 被转换成调用 @IntfCast,即 system 单元的 _IntfCast 函数。呵呵,其实就是调用 IUnknown 接口的 QueryInterface 方法。

  1. procedure _IntfCast(var Dest: IInterface; const Source: IInterface; const IID: TGUID);
  2. var
  3. Temp: IInterface;
  4. begin
  5. if Source = nil then
  6. Dest := nil
  7. else
  8. begin
  9. Temp := nil;
  10. if Source.QueryInterface(IID, Temp) <> 0 then
  11. Error(reIntfCastError)
  12. else
  13. Dest := Temp;
  14. end;
  15. end;

由此可见,第二种方法也可以按照下面的方法调用:

  1. procedure TForm1.Button2Click(Sender: TObject);
  2. const
  3. Class_SimpleComObject: TGUID = '{3714CF21-D272-11D3-947F-0050DA73BE5D}';
  4. var
  5. Unknown: IUnknown;
  6. begin
  7. Unknown := CreateComObject(Class_SimpleComObject) as ISimpleComObject;
  8. ComInterface.QueryInterface(Class_SimpleComObject,ComInterface);
  9. ShowMessage(ComInterface.GetClassName);
  10. ComInterface := nil;
  11. end;

至此两种创建 SimpleCOMObject 对象的方法全部分析完毕。那么在平时的应用中我们到底使用哪种方法创建 COM 对象比较好呢?其实在 Delphi 的官方帮助中已经给了我们答案:当你只创建单一 COM 对象时,你可以调用 CreateComObject;当你需要成批创建同一类 COM 对象时,那么还是直接选择类工厂吧,还是它来得快。
    在我分析后,你是否认为复杂的 COM 结构被 VCL 包装得很完美?至少我认为是这样的,使我不得不佩服 Borland Delphi R&D 小组的高超技术水准。如果你还没尽兴,那么等我的下篇吧...

参考文献

1. 李维.《深入核心 -- VCL架构剖析》第六、七章

2. Fernando Vicaria."Delphi COM In-Process Servers Under the Microscope, Part 1". Hardcore Delphi Magazine, Mar 2000

3. savetime."Delphi 的接口机制浅探", Feb 2004

4. savetime."《COM 原理与应用》学习笔记", Feb 2004

http://blog.csdn.net/procedure1984/article/details/3906945

Delphi 中 COM 实现研究手记(一)的更多相关文章

  1. 如何在 Delphi 中静态链接 SQLite

    搞了我几个小时,终于成功在 Delphi 中静态链接了 SQLite (v3.5.4),下一步就是研究加密了,呵呵中间其实遇到很多问题,今天累了,就不说了,改天补上 下载测试工程 下面说说方法 1.当 ...

  2. Delphi中window消息截获的实现方式(2)

    Delphi是Borland公司提供的一种全新的WINDOWS编程开发工具.由于它采用了具有弹性的和可重用的面向对象Pascal(object-orientedpascal)语言,并有强大的数据库引擎 ...

  3. Delphi中DLL的创建和使用

    参考:http://blog.csdn.net/ninetowns2008/article/details/6311663 结合这篇博客:http://www.cnblogs.com/xumenger ...

  4. Delphi中代替WebBrowser控件的第三方控件

    这几天,接触到在delphi中内嵌网页,用delphi7自带的TWebBrowser控件,显示的内容与本机IE8显示的不一样,但是跟装IE8之前的IE6显示一个效果.现在赶脚是下面两个原因中的一个: ...

  5. Delphi中停靠技术的实现

    随着软件技术的不断进步,软件界面也越来越美观,操作也越来越方便.综观市面上比较专业的各种软件,我们会发现大部分都提供窗体停靠的功能,特别象工具软件,基本上都或多或少有停靠功能.自然,Delphi也支持 ...

  6. DELPHI语法基础学习笔记-Windows 句柄、回调函数、函数重载等(Delphi中很少需要直接使用句柄,因为句柄藏在窗体、 位图及其他Delphi 对象的内部)

    函数重载重载的思想很简单:编译器允许你用同一名字定义多个函数或过程,只要它们所带的参数不同.实际上,编译器是通过检测参数来确定需要调用的例程.下面是从VCL 的数学单元(Math Unit)中摘录的一 ...

  7. 关于Delphi中TRttiContext.FindType失效的问题

    自从Delphi2010后,Delphi中的Rtti功能得到了增强.我们终于可以不用先RegisterClass,再GetClass获取类的信息了.而只是简单的通过TRttiContext.GetTy ...

  8. Delphi中获取Unix时间戳及注意事项(c语言中time()是按格林威治时间计算的,比北京时间多了8小时)

    uses DateUtils;DateTimeToUnix(Now) 可以转换到unix时间,但是注意的是,它得到的时间比c语言中time()得到的时间大了8*60*60这是因为Now是当前时区的时间 ...

  9. delphi中panel控件应用

    delphi中的panel控件是怎么使用的?研究了很久了,还是搞不懂,只知道把它放到form上面,其他操作一律不懂了,有谁可以请教一下,如何把其他控件放到里面去呢?谢谢 提问者采纳   直接把控件放到 ...

随机推荐

  1. iOS开发获取缓存文件的大小并清除缓存

    移动应用在处理网络资源时,一般都会做离线缓存处理,其中以图片缓存最为典型,其中很流行的离线缓存框架为SDWebImage. 但是,离线缓存会占用手机存储空间,所以缓存清理功能基本成为资讯.购物.阅读类 ...

  2. redis 缓存技术与memcache的最大差别

    1 什么是redis  redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对很多其它,包含string(字符串).list(链表).set(集合)和zs ...

  3. c语言中重要函数

    gets函数,从标准输入读取一行文本,一行输入由一串字符组成,以一个换行符结尾: gets函数丢弃换行符,并在该行的末尾存储一个NUL字符(类似‘\0’), 然后返回一个非NULL值. 当gets函数 ...

  4. !!!易控INSPEC组态软件开发小结——-一次工程文件损坏和处理经过

    从加入红橡开始熟悉和使用易控(INSPEC)组态软件,值得赞扬的是INSPEC的开放性和对C#语言的支持,除此之外,便也没有感觉它与其他组态软件有太多优势,有人说INSPEC软件授权比国内其他同类的组 ...

  5. mvc模式jsp+servel+jdbc oracle基本增删改查demo

    mvc模式jsp+servel+jdbc oracle基本增删改查demo 下载地址

  6. 浅谈JSP(一)

    一.JSP引言 JSP全名为Java Server Pages,中文名叫java服务器页面,其根本是一个简化的Servlet设计.它是在传统的网页HTML文件(*.htm,*.html)中插入Java ...

  7. HDU2005-第几天

    描述: 给定一个日期,输出这个日期是该年的第几天. 代码: #include<stdio.h> #include<string.h> #include<iostream& ...

  8. 使用Apache的rewrite技术

    做PHP项目中需要用到URL重定向技术,基本上的需求就是把比如 /user/heiyeluren 重定向到 /user.php?uid=heiyeluren 之类的URL上,当然,你也可以把 /art ...

  9. PhpStorm 10.0.1安装(win7-64位)

    软件下载地址 http://www.xiazaiba.com/html/25261.html#j_down_list 或者:http://pan.baidu.com/s/1brSA9C 密码:tpc7 ...

  10. bzoj 4373: 算术天才⑨与等差数列 hash

    题目链接 题目大意:  给你n个数, 给两种操作, 一种给你l, r, k,问你[l, r]区间里的数排序后能否构成一个公差为k的等差数列. 另一种是将位置x的数变为y. 强制在线. 可以用hash来 ...