19

我刚刚开始使用泛型,目前在对多个字段进行排序时遇到问题。

案例:
我有一个 PeopleList 作为一个TObjectList<TPerson>,我希望能够通过一次选择一个排序字段来制作类似 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

其中 Smith,26 出现在 Jones,26 之前。所以它似乎没有保留以前的排序。

我知道我只能制作一个比较 LastName 和 Age 的比较器 - 但问题是,我必须为 TPerson 中存在的每个字段组合制作比较器。

是否可以使用多个 TComparers 来做我想做的事情,或者我怎样才能完成我想做的事情?

新年更新

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

首先,我创建了一个基类TSortCriterion<T>和一个TSortCriteriaComparer<T>,以便将来能够在多个类中使用它们。我已将 Criterion 和 list 分别更改为TObjectTObjectList,因为我发现如果 objectlist 自动处理 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;
4

3 回答 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;
于 2011-12-29T21:32:13.653 回答
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;
于 2011-12-29T20:08:07.077 回答
3

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

于 2011-12-29T20:58:14.407 回答