一、老项目和新版本的那些事

很多公司手里都有一些“上了岁数”的Delphi项目,当年用旧版本写的,跑得好好的,但维护起来越来越吃力。不是这里不兼容新系统,就是那个库报个错。这时候升级到Embarcadero维护版Delphi,很多人第一反应是“怕折腾”。其实新版带了不少“甜点”特性,专门用来让老代码重获新生。今天咱们就聊一聊,怎么把新特性一点点揉进旧项目里,既不用推倒重来,又能让代码看着顺眼、跑得舒坦。

二、新版Delphi的“甜点”特性实操

2.1 用泛型让集合操作告别手工

老项目里处理一组数据,最常见的就是TList,里面存的是指针,取出来还要强行转类型,一不小心就崩。新版Delphi的泛型集合TList、TDictionary<TKey,TValue>这些,用起来就像“装上了自动挡”,类型安全不说,代码也干净许多。

假设老代码里有个订单列表,原来是这样写的:

// 技术栈:Object Pascal(Delphi)

type
  POrder = ^TOrder;
  TOrder = record
    OrderID: Integer;
    Amount: Double;
  end;

// 老式做法:手动管理指针和内存
procedure OldStyle;
var
  OrderList: TList;
  OrderPtr: POrder;
  i: Integer;
begin
  OrderList := TList.Create;
  try
    // 手动创建对象放入列表
    New(OrderPtr);
    OrderPtr.OrderID := 1001;
    OrderPtr.Amount := 99.5;
    OrderList.Add(OrderPtr);

    // 取出时要强转指针类型
    OrderPtr := POrder(OrderList[0]);
    // 用完之后还要手动释放内存
    Dispose(OrderPtr);
  finally
    OrderList.Free;
  end;
end;

用新版泛型,直接存对象或记录,不用指针,也不用担心泄漏:

// 技术栈:Object Pascal(Delphi)

type
  TOrder = record
    OrderID: Integer;
    Amount: Double;
  end;

// 新式玩法:泛型列表自动管理元素类型
procedure NewStyle;
var
  OrderList: TList<TOrder>;
  Order: TOrder;
begin
  // 创建泛型列表,存的是“值”而不是指针
  OrderList := TList<TOrder>.Create;
  try
    Order.OrderID := 1001;
    Order.Amount := 99.5;
    OrderList.Add(Order);

    // 取出来就是直接可用的TOrder,不用强转
    for Order in OrderList do
      WriteLn(Order.OrderID, ' ', Order.Amount);
  finally
    OrderList.Free;
  end;
end;

就这么一改,代码少了很多“手工活”,也不怕有人忘了释放指针。泛型在老项目里特别好用,尤其是那些列表、映射、栈、队列的场景,替换成本低,收益立竿见影。

2.2 字符串处理从“手动挡”到“自动挡”

老Delphi里拆字符串,常用的就是Pos、Copy、Delete这几个函数,又啰嗦又容易下标越界。新版提供了TStringHelper系列辅助方法,像Split、StartsWith、Contains、Replace这些,让字符串操作变成“家常便饭”。

拿解析CSV来说,以前要写一大段循环,现在一行搞定:

// 技术栈:Object Pascal(Delphi)

procedure ParseCSV;
var
  Line: string;
  Fields: TArray<string>;
  Field: string;
begin
  Line := '张三,25,北京';

  // 新版字符串辅助方法:直接按逗号拆分成数组
  Fields := Line.Split([',']);

  // 遍历数组,不需要自己管理下标
  for Field in Fields do
    WriteLn(Field);
end;

再看一个前后判断的场景。旧代码里想判断一个文件名是不是以.log结尾,还得取长度再Copy比较:

// 技术栈:Object Pascal(Delphi)

// 老式判断字符串结尾
function IsLogFileOld(const FileName: string): Boolean;
var
  Ext: string;
begin
  if Length(FileName) >= 4 then
    Ext := Copy(FileName, Length(FileName) - 3, 4)
  else
    Ext := '';
  Result := SameText(Ext, '.log');
end;

新版直接Use EndsText或者EndsWith,代码是给人看的:

// 技术栈:Object Pascal(Delphi)

function IsLogFileNew(const FileName: string): Boolean;
begin
  // 一行搞定,忽略大小写
  Result := FileName.ToLower.EndsWith('.log');
end;

这种小优化,在旧项目里随处可见。不需要大改架构,只需要把那些绕来绕去的“手工弯”捋直了,代码可读性立刻上升一个档次。而且新版的字符串处理性能并不差,该快的场合一点不虚。

2.3 并行编程:让旧代码跑得更快

老机器都是单核时代,写代码不用考虑多线程。现在电脑随便都是四核八核,可你的旧项目还是“单线程一条道走到黑”,CPU资源白白浪费。新版Delphi自带并行编程库(PPL,Parallel Programming Library),它不像手动创建TThread那么费劲,几行代码就能让循环“飞”起来。

比如处理一个大数组里的数据,原来用for循环一个个算,现在用TParallel.For自动分配到多个线程:

// 技术栈:Object Pascal(Delphi)

uses
  System.Threading;

procedure ProcessData;
var
  Data: TArray<Double>;
  i: Integer;
begin
  SetLength(Data, 10000);

  // 初始化数据(模拟)
  for i := 0 to High(Data) do
    Data[i] := i;

  // 老式串行循环:逐个处理
  for i := 0 to High(Data) do
    Data[i] := Sqrt(Data[i]);

  // 新版并行循环:让多个核心一起干活
  TParallel.For(0, High(Data), procedure(i: Integer)
  begin
    Data[i] := Sqrt(Data[i]);
  end);
end;

不过并行循环不是银弹。如果你的循环体里访问了共享变量,还需要加锁,否则会出现数据竞争。比如多个线程同时往同一个TList里Add,就可能出错。这时候可以用TParallel.For的局部状态,或者用TMonitor、TCriticalSection做同步。但不管怎么说,对于CPU密集型的独立计算,并行库给旧项目提速的效果非常明显,而且改动量极小。

2.4 弱引用解决“循环引用”的烦恼

做GUI开发,最怕的是一个对象持有另一个对象,两个对象互相引用,结果谁都不能释放,内存悄悄上涨。新版Delphi提供了弱引用(Weak Reference)机制,专门解决这种“你中有我,我中有你”的问题。

咱们先看一个典型的循环引用例子。假设有个窗口类,里面持有一个人物对象;人物对象又回指窗口用于刷新UI:

// 技术栈:Object Pascal(Delphi)

type
  TMyForm = class;
  
  TPerson = class
  private
    FForm: TMyForm;  // 强引用,导致循环
  public
    constructor Create(AForm: TMyForm);
  end;

  TMyForm = class(TForm)
  private
    FPerson: TPerson;
  public
    constructor Create;
    destructor Destroy; override;
  end;

implementation

constructor TPerson.Create(AForm: TMyForm);
begin
  FForm := AForm;
end;

constructor TMyForm.Create;
begin
  // 自己持有Person,Person又持有自己
  FPerson := TPerson.Create(Self);
end;

destructor TMyForm.Destroy;
begin
  FPerson.Free;  // 因为循环引用,实际上这里可能无法正确释放
  inherited;
end;

如果不小心处理,释放TPerson时会发现FForm还是有效对象,导致引用计数永远不清零(如果是接口引用)。新版提供了weak关键字,可以把其中一个引用标记为弱引用:

// 技术栈:Object Pascal(Delphi)

type
  TMyForm = class;
  
  TPerson = class
  private
    [Weak] FForm: TMyForm;  // 弱引用:不增加引用计数
  public
    constructor Create(AForm: TMyForm);
  end;

  TMyForm = class(TForm)
  private
    FPerson: TPerson;
  public
    constructor Create;
    destructor Destroy; override;
  end;

implementation

constructor TPerson.Create(AForm: TMyForm);
begin
  FForm := AForm;
end;

constructor TMyForm.Create;
begin
  // 此时不再造成循环引用
  FPerson := TPerson.Create(Self);
end;

destructor TMyForm.Destroy;
begin
  FPerson.Free;
  inherited;
end;

注意,弱引用的意思是“我不拥有你,如果没人强引用你,你就被释放”。所以使用弱引用时一定要小心,对方可能已经不存在了。访问前最好做一次有效性检查。在旧项目里,如果存在界面对象互相持有、回调代理等场景,弱引用能大大减少内存泄漏的可能性。

三、适配老项目时的应用场景和注意事项

3.1 典型应用场景

老项目里最值得优先用新特性的地方,通常是这三类:

第一类,数据列表和字典。原来用TList、TStringList记录业务数据,换成泛型后,类型安全,运行效率也高。比如“用户列表”、“订单映射”等,改动范围小,验证容易。

第二类,字符串解析和格式化。比如协议解析、日志处理、配置文件读取,用上Split、IndexOfAny、Replace等新方法,代码能缩短一半。这类改动不会动摇核心逻辑,风险低。

第三类,耗时计算。比如报表生成、大数据量排序、图像处理,这些地方用TParallel.For去并行化,效果明显。但要注意计算之间不能有强依赖。如果是一个循环前后的结果依赖,就不适合并行,硬上反而更慢。

3.2 需要注意的坑

升级旧项目不是“沉浸式换皮”,有一些细节要盯紧。

第一个坑是编译器版本差异。新版Delphi虽然能编译老代码,但某些旧语法或者第三方组件可能不兼容。比如老代码用了已废弃的运行时方法,或者某个控件库没有升级,编译时会直接报错。这时候不能硬来,选择性地使用新特性,优先保证项目能正常跑起来。

第二个坑是泛型带来的“隐性长度”。TList在存储大对象时是值复制,如果T是一个很大的记录类型,频繁Add和访问会有性能损耗。这时可以改成TList或者用指针,但也要注意释放。

第三个坑是字符串辅助方法在处理null字符串时的表现。老代码里可能把空字符串和null混着用,新方法的有些行为(比如Split遇到空字符串会返回一个空数组)可能会引起边界问题。在改代码前,先跑一遍单元测试,或者写个小测试用例验证一下。

第四个坑是并行循环里的变量捕获。在匿名方法里使用循环变量时,新版编译器对匿名方法的捕获规则和老版不太一样,如果不小心,可能捕获到同一个引用,导致结果不对。建议循环变量在局部变量中复制一份。

第五个坑是弱引用只对接口和托管类型有效。如果TPerson是普通类对象,弱引用起作用需要编译器支持ARC(自动引用计数)或启用相关选项。在Windows原生代码上,弱引用并不能帮助管理非托管对象的内存,别指望用它彻底替代Free调用。

四、技术优缺点总结

4.1 优点

新特性的优点非常实在。泛型让代码更安全、更好读,不用再手动到处“平移指针”。字符串辅助方法把一堆复杂操作简化成一行,出错的概率大大降低。并行库能充分挖掘多核CPU的潜力,老程序也能“跑出新感觉”。弱引用则帮我们理清了对象之间的“债务关系”,内存这块舒服多了。

另外,新版Delphi对高DPI、新版Windows的适配也做了很多优化。把旧项目升级上来,本身就能解决很多“系统一升级就出问题”的毛病。这是最大的隐形收益。

4.2 缺点

升级和适配不是没有代价。一是学习成本,团队里如果都是老Delphi选手,看到匿名方法、泛型这些新东西,一开始会有些懵。二是兼容性问题,老第三方控件可能是最大障碍,如果它停更了,你只能自己改造,工作量不小。三是并行编程带来的不确定性,线程安全、死锁、调试困难,这些都让“提速”变成“头痛”。弱引用虽然好用,但理解不深的话,反而会引发莫名其妙的空引用异常。

所以,旧项目适配新特性,一定要有选择、有步骤,别想一口吃成胖子。先把收益最大、风险最低的改了,跑稳了再下一步。

五、总结

维护版Delphi的新特性,并不是逼着我们把所有旧代码重写一遍,而是给了我们一堆趁手的工具,让我们在修修补补的时候,顺便把老毛病治一治。从泛型集合到字符串辅助,从并行循环到弱引用,每一处改动都可以很小,但带来的体验却是实打实的。

给旧项目做适配,最忌讳的是“偷懒不动”和“热血大改”。正确姿势是:挑几个高频使用、风险可控的地方先试试,比如把一个用TList的地方换成TList,把一段字符串解析改成分离,跑一遍测试,确认没问题再继续推进。这样慢慢渗透,旧项目也能焕发出新活力。路要一步一步走,代码要一行一行改,但方向对了,就不怕远。