[BlueLeaf1336]> PROGRAM>

TStringListのソート小論-001

historyTOP

2004/01/03:作成

overviewTOP

タイトルのとおりですが、TStringListのカスタムソートについての使い方のメモを残します。

TStringList.SortTOP

TStringListにはsortメソッドがあります。VCLリファレンスのTStringList.Sortの説明によると次のように動作します。

■ VCLリファレンス
TStringList.Sort
Sortメソッドは、リスト内の文字列を昇順で並べ替えます。

procedure Sort; virtual;

説明
Sortメソッドを呼び出すと、SortedプロパティをFalseに設定したリストで文字列を並べ替えることができます。SortedプロパティをTrueに設定した文字列リストでは、自動的に並べ替えが実行されます。

注意
Sortメソッドは、CaseSensitiveプロパティがTrueの場合にはAnsiCompareStrを使って、CaseSensitiveプロパティがFalseの場合にはAnsiCompareTextを使って文字列を並べ替えます。カスタマイズした比較演算子を使用する場合には、かわりにCustomSortメソッドを使用します。

最初は、う?となったのですが、つまるところ"普通の"文字列の比較(AnsiCompare***による)でソートするヨ。自動でもよいし、メソッド呼び出し時点でもよいヨ。ということです(もっと分かりにくいか?)

で、どんなソートかというと、クイックソートです。これはTListのソート方法がクイックソートとVCLリファレンスに書いてあって、TStringListがTListから派生していることからの類推ではなく、実際にソースを見たところそう書いてあります。
確認したのは、VCLのソースが付属していないDelphi6 Personalですが、きちんと最新のパッチまで適用していれば、TList/TStringList...が実装されているClasses.pasについてはソースをゲットできているはずです。Delphi 6 RTL アップデート #3に含まれています。

で、Classes.pasをエディタで開いて、"TStringList.Sort"で検索をかけるとすぐに、クイックソートを行っていることが分かります。さらに、このSortメソッド自体もCustomSortメソッドの一例として実装されていることが分かります。分かりにくいですが、「カスタマイズした比較演算子を使用する場合には、かわりにCustomSortメソッドを使用します」という説明がVCLリファレンスにありますが、Sortメソッド自体も内部ではCustomSortメソッドを使用しているということです。

TStringList.CustomSortTOP

で、CustomSortはどう説明されているかというと、

■ VCLリファレンス
TStringList.CustomSort
CustomSortメソッドは、リストの文字列をカスタマイズした順にソートします。

type TStringListSortCompare = function(List: TStringList; Index1, Index2: Integer): Integer;

procedure CustomSort(Compare: TStringListSortCompare); virtual;

説明
CustomSortメソッドを使うと、Compareパラメータでそのソート順が定義されている場合に、リストの文字列をソートできます。
文字列リストの2つの文字列を比較するCompare関数に値を指定してください。Index1パラメータとIndex2パラメータは比較する文字列を表します。Listパラメータは文字列リストへのアクセスを提供します。Stringsプロパティ配列への添字としてIndex1パラメータとIndex2パラメータを使います。Compare関数が返す値は以下のとおりです。

nilをパラメータの値として渡さないでください。

注意
CustomSortメソッドを明示的に呼び出さなくてはなりません。Sortedプロパティを設定しても、Sortメソッドで実装したANSI順を使用して文字列をソートするだけです。

ということで、何がなんだか分かりません(でした)。

TStringList.Sort in Classes.pasTOP

あまりに分かりにくいので、前述のBorlandが配布しているアップデータ内のClasses.pasからTStringList.Sortの実装部分を違法的に抜粋してみます。ただし、コメントをいくつか追加して、順序も並び替えています。

// CustomSortの呼び出し方
procedure TStringList.Sort;
begin
  CustomSort(StringListCompareStrings);
end;

// 使用者側で準備する比較関数
function StringListCompareStrings(List: TStringList;
                                  Index1, Index2: Integer): Integer;
begin
  Result := List.CompareStrings(List.FList^[Index1].FString,
                                List.FList^[Index2].FString);
end;

// 使用者側で準備する比較関数の下請けだけども本体
function TStringList.CompareStrings(const S1, S2: string): Integer;
begin
  if CaseSensitive then
    Result := AnsiCompareStr(S1, S2)
  else
    Result := AnsiCompareText(S1, S2);
end;

で、これだと下請け関数が挟まっているのでちょっと統合して、比較関数と呼び出し側の2本にまとめたのが次です。

(* CustomSortの呼び出し方 *)
procedure TStringList.Sort;
begin
  CustomSort(StringListCompareStrings);
end;

(* 使用者側で準備する比較関数 *)
function StringListCompareStrings(List: TStringList;
                                  Index1, Index2: Integer): Integer;
var
  S1, S2: string;
begin
  // 比較対象を取り出す - 1つ目
  S1 := List.FList^[Index1].FString;
  // 比較対象を取り出す - 2つ目
  S2 := List.FList^[Index2].FString;
  // 比較する
  if CaseSensitive then
    Result := AnsiCompareStr(S1, S2)
  else
    Result := AnsiCompareText(S1, S2);
end;

ここまで来てなんですが、以上がTStringList内部から呼び出した場合の使用例になります。たとえば、おそらくprivateメンバと思われるFListとかFStringとかがバンバンと出てきてます。ですので、当然動作は確認していません。

TStringList.CustomSort usageTOP

というわけで、通常の使用方法は次のようになります(と思います)。

(* 通常のTStringList.Sortと同じ結果 *)
function StringListCompareStrings(List: TStringList; Index1, Index2: Integer): Integer;
var
    S1, S2: string;
begin
    //  比較対象取り出し - 1つ目
    S1 := List[Index1];
    //  比較対象取り出し - 2つ目
    S2 := List[Index2];
    //  比較
    if List.CaseSensitive then
        Result := AnsiCompareStr(S1, S2)
    else
        Result := AnsiCompareText(S1, S2);
end;

(* カスタムソート *)
procedure TForm1.Button1Click(Sender: TObject);
begin
    SL.CustomSort(StringListCompareStrings);
end;

ここで「SL」はTForm1のメンバ変数として宣言しFormCreateで確保・FormDestroyで破棄しているTStringListです。また、カスタムソートの「カスタム」に当たる部分は上記コードの赤太で示した部分です。

さて、このコードには前述のTStringList.CustomSortのリファレンスに登場する「0未満の値」「0」「0を超える値」に関する処理がありません。その理由は「AnsiCompareStr / AnsiCompareText がそういう値を返すような関数だから」です。少しだけAnsiCompareStrのヘルプの記述を抜いてみると

■ VCLリファレンス
AnsiCompareStr 関数

function AnsiCompareStr(const S1, S2: string): Integer;

説明
AnsiCompareStr関数は、大文字と小文字を区別してS1とS2を比較します。比較演算は現在のロケールによって制御されます。戻り値は以下のとおりです。

条件
戻り値
S1 > S2> 0
S1 = S20
S1 < S2< 0

(略)

これってモロに、CustomSortのカスタム比較関数が求める戻り値そのものです。結局これさえ守っていれば嫌になるほど自由なソートが可能になるということです。

文字列比較に限っても

等など。さらに、恐るべきはTStringListですが、Objectsプロパティに任意のTObjectを保持することができます。で、文字列で比較する必要すらなくて、Objectsプロパティを比較関数内で覗いて、そこに保持されている任意のクラスの保持する情報で並び替えることすら可能です。

ここまでくると、特に問題がなければTListやTObjectListなどでもかまわないような気がしますが...。というわけで、次回は、いくつかのカスタムソートの実例を作成してみたいと思います。(ホントか?)

EOFTOP