如何在使用多个比较器的TObjectList<>中实现类似Excel的先按A排序,再按B排序的功能

19
我刚开始使用泛型,目前在对多个字段进行排序时遇到问题。
情况如下:
我有一个PeopleList作为TObjectList,我希望能够制作类似Excel的排序功能,每次选择一个排序字段,但尽可能保留以前的排序顺序。
编辑:必须可以在运行时更改字段排序顺序。(即,在一个场景中,用户想要排序顺序A、B、C,在另一个场景中,他想要B、A、C,在另一个场景中是A、C、D)
假设我们有一个未排序的人员列表:
Lastname     Age
---------------------
Smith        26
Jones        26
Jones        24
Lincoln      34

现在,如果我按LastName排序:
Lastname ▲   Age
---------------------
Jones        26
Jones        24
Lincoln      34
Smith        26

如果我按年龄排序,我想要这样:
Lastname ▲   Age ▲
---------------------
Jones        24
Jones        26
Smith        26
Lincoln      34

为了实现这个目的,我创建了两个比较器——一个TLastNameComparer和一个TAgeComparer。
现在我调用:
PeopleList.Sort(LastNameComparer)
PeopleList.Sort(AgeComparer)

现在我的问题是,这并没有产生我想要的输出,但是。
Lastname ?   Age ?
---------------------
Jones        24
Smith        26
Jones        26
Lincoln      34

在排序中,26岁的Smith先于26岁的Jones出现。因此,似乎没有保留之前的排序。

我知道可以只创建一个比较器,来比较LastName和Age两个字段 - 但是问题是,这样我就需要为TPerson中每个字段组合创建不同的比较器。

是否可以使用多个TComparers来实现我的需求?或者我应该如何完成我想要的操作呢?

新年更新

仅供将来的访客参考,这几乎是我现在使用的代码。

首先,我创建了一个基类TSortCriterion<T>TSortCriteriaComparer<T>,以便今后能够在多个类中使用它们。 我已经将Criterion和list更改为TObjectTObjectList,因为我发现如果对象列表自动处理Criterion的销毁,那么更容易管理。

  TSortCriterion<T> = Class(TObject)
    Ascending: Boolean;
    Comparer: IComparer<T>;
  end;

  TSortCriteriaComparer<T> = Class(TComparer<T>)
  Private
    SortCriteria : TObjectList<TSortCriterion<T>>;
  Public
    Constructor Create;
    Destructor Destroy; Override;
    Function Compare(Const Right,Left : T):Integer; Override;
    Procedure ClearCriteria; Virtual;
    Procedure AddCriterion(NewCriterion : TSortCriterion<T>); Virtual;
  End;

implementation

{ TSortCriteriaComparer<T> }

procedure TSortCriteriaComparer<T>.AddCriterion(NewCriterion: TSortCriterion<T>);
begin
  SortCriteria.Add(NewCriterion);
end;

procedure TSortCriteriaComparer<T>.ClearCriteria;
begin
  SortCriteria.Clear;
end;

function TSortCriteriaComparer<T>.Compare(Const Right, Left: T): Integer;
var
  Criterion: TSortCriterion<T>;
begin
  for Criterion in SortCriteria do begin
    Result := Criterion.Comparer.Compare(Right, Left);
    if not Criterion.Ascending then
      Result := -Result;
    if Result <> 0 then
      Exit;
  end;
end;

constructor TSortCriteriaComparer<T>.Create;
begin
  inherited;
  SortCriteria := TObjectList<TSortCriterion<T>>.Create(True);
end;

destructor TSortCriteriaComparer<T>.Destroy;
begin
  SortCriteria.Free;
  inherited;
end;

最后,为了使用排序标准: (这只是一个例子,因为创建排序顺序的逻辑真正取决于应用程序):

Procedure TForm1.SortList;
Var
  PersonComparer : TSortCriteriaComparer<TPerson>; 
  Criterion : TSortCriterion<TPerson>;
Begin
  PersonComparer := TSortCriteriaComparer<TPerson>.Create;
  Try
    Criterion:=TSortCriterion<TPerson>.Create;
    Criterion.Ascending:=True;
    Criterion.Comparer:=TPersonAgeComparer.Create
    PersonComparer.AddCriterion(Criterion);
    Criterion:=TSortCriterion<TPerson>.Create;
    Criterion.Ascending:=True;
    Criterion.Comparer:=TPersonLastNameComparer.Create
    PersonComparer.AddCriterion(Criterion);
    PeopleList.Sort(PersonComparer);
    // Do something with the ordered list of people.
  Finally
    PersonComparer.Free;  
  End;  
End;

参见这个类似的问题,应该会给你一些启示。 - ain
3个回答

17

把排序的条件放在一个列表中,包括排序的方向和用于比较项的函数。像这样的记录可以帮助:

type
  TSortCriterion<T> = record
    Ascending: Boolean;
    Comparer: IComparer<T>;
  end;
当用户配置所需的排序方式时,使用该记录的实例填充列表。
var
  SortCriteria: TList<TSortCriterion>;

Comparer成员将引用你已经编写的基于名称和年龄进行比较的函数。现在编写一个单一的比较函数,引用该列表。类似这样:

function Compare(const A, B: TPerson): Integer;
var
  Criterion: TSortCriterion<TPerson>;
begin
  for Criterion in SortCriteria do begin
    Result := Criterion.Comparer.Compare(A, B);
    if not Criterion.Ascending then
      Result := -Result;
    if Result <> 0 then
      Exit;
  end;
end;

3
这是一个不错的解释,我喜欢包含升序/降序的精美封装记录。 - David Heffernan
@Rob:这正是我所需要的,正如David所提到的,升序/降序功能很不错。感谢您花费时间帮助。 - TechnoCowboy

6

您的问题在于您正在执行两个单独的排序操作。您需要进行一次排序并使用所谓的字典排序。您需要使用一个比较器来比较主字段,如果主键相等,则继续比较次要键。像这样:

Result := CompareStr(Left.Name, Right.Name);
if Result=0 then
  Result := Left.Age-Right.Age;

这种方法可以扩展以适应任意数量的键。


在您更新问题时,添加了一个要求:键优先级将在运行时确定。您可以使用类似于以下比较函数来实现此操作:

function TMyClass.Comparison(const Left, Right: TPerson): Integer;
var
  i: Integer;
begin
  for i := low(FSortField) to high(FSortField) do begin
    Result := CompareField(Left, Right, FSortField[i]);
    if Result<>0 then begin
      exit;
    end;
  end;
end;

这里的FSortField是一个包含字段标识符的数组,按降序排列。因此,FSortField[0]表示主键,FSortField[1]表示次要关键字,依此类推。 CompareField函数比较由第三个参数标识的字段。

因此,CompareField函数可能像这样:

function CompareField(const Left, Right: TPerson; Field: TField): Integer;
begin
  case Field of
  fldName:
    Result := CompareStr(Left.Name, Right.Name);
  fldAge:
    Result := Left.Age-Right.Age;
  //etc.
  end;
end;

感谢您的快速回复,David。问题是我希望用户能够在运行时更改排序顺序,而使用这种方法,我必须为每个字段组合创建一个比较器。例如,如果用户有时想先按年龄排序,然后按姓氏排序,在另一种情况下,他想按姓氏排序,然后按年龄排序-那么我需要两个执行此操作的比较器。想象一下,如果还可以按任何字段排序序列的任意组合进行排序,我需要多少个比较器。这包括按名字、电话号码、鞋码等排序。 - TechnoCowboy
@Techno 你只需要一个比较器和一点间接性。请看我的更新。 - David Heffernan
2
那正是我需要的提示,而Rob精心打磨的版本使它完美无缺。感谢你花时间帮助。 - TechnoCowboy

3

如果您有一个稳定的排序算法,那么可以按照相反的顺序应用每个比较器,结果将是按照您所需的顺序排序的列表。Delphi的列表类使用快速排序,这不是一个稳定的排序。您需要自己编写排序程序,而不是使用内置的排序程序。


Delphi是否带有稳定的排序算法(无论Delphi的列表类是否使用它们)? - Marjan Venema
@marjan 我在任何我遇到的 Delphi 中都没有找到稳定排序。 - David Heffernan
@david 谢谢。遗憾的是,在排序标准的反向顺序中进行排序是一个不错而简单的解决方案... - Marjan Venema
@marjan 稳定的排序并不难实现,但使用单个比较器更有效率。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接