stringのIndexOfは高度な比較処理

stringのIndexOfをいろいろ調べてみました。セキュリティにかかわる場面で仕様を知らずに使うと、セキュリティホールになりかねないもののようです。国際化なんてしないからSystem.Globalizationなんか知らなくていいやっと思ってたけど、知らないと危険かも。

まずはstringのIndexOfをMSDNライブラリで見ると、「このメソッドは、現在のカルチャを使用して、単語 (大文字/小文字を区別し、カルチャに依存した) 検索を実行します。」とありました。単純な文字の比較じゃないようです。
@ITの掲示板でIndexOfのバグ(?)と調査の話題がありました。stringのIndexOfは、CompareInfoのIndexOfを呼んでいるそうです。
CompareInfoクラスのMSDNでの説明を読むと、「セキュリティの決定が文字列の比較や大文字/小文字の変換操作に依存する場合は、システムのカルチャ設定にかかわらず一定の動作を保証するために InvariantCulture を使用してください。」と注意事項があります。なにやら危険な臭い。
CompareInfoのIndexOfにはCompareOptionsを引数に取って、比較方法を指定できるようになっています。
CompareOptionsの詳細はこちら。大文字小文字を区別しない、全角半角を区別しない、空白や句読点などを無視するなど、いくつかのオプションがあり、ずいぶん高機能です。これをUnicodeすべてについてテストしたんだろうか…。@IT掲示板の話題を見る限り、そうでもないんだろうなぁ。誰かあの問題は報告したんでしょうか。
この高度な比較は注意ですね。知らないで使うといろいろと問題を起こしそう。

CompareOptions.Ordinalを使うと単純な比較になるようです。単純な比較で済む場合、これを使うといいかもしれません。検討の価値ありです。

今日も実行速度比較をしてみます。文字列比較の問題は実行速度だけの問題じゃないけど、とりあえず速度は試してみたかったので。コードはかなり手抜きです。「aaa.txt」の名前でUTF-8のファイルを用意してください。このファイル中にある文字「の」を数えます。

using System;
using System.IO;
using System.Text;
using System.Globalization;

namespace Siokoshou
{
  public class GrepCount2
  {
    static string fileName = "aaa.txt";
    static string keyword = "の";
    const int LoopMax = 100;
    const int testNum = 5;

    static int TestDefaultIndexOf( string buff )
    {
      int found = 0;

      for ( int i = 0; i < LoopMax; i++ )
      {
        found = 0;
        int pos = 0;
        while ( 0 <= ( pos = buff.IndexOf( keyword, pos ) ) )
        {
          found++;
          pos++;
        }
      }
      return found;
    }

    static int TestIndexOfOrdinal( string buff )
    {
      int found = 0;
      CompareInfo ci = CompareInfo.GetCompareInfo( "" );

      for ( int i = 0; i < LoopMax; i++ )
      {
        found = 0;
        int pos = 0;
        while ( 0 <= ( pos = ci.IndexOf( buff, keyword, pos, CompareOptions.Ordinal ) ) )
        {
          found++;
          pos++;
        }
      }
      return found;
    }

    [STAThread]
    static void Main( string[] args )
    {
      int[] count = new int[ testNum ];
      int[] tick  = new int[ testNum + 1 ];
      int testCount = 0;

      string buff;
      using ( StreamReader sr = new StreamReader( fileName ) )
      {
        buff = sr.ReadToEnd();
      }

      tick[ testCount ]  = Environment.TickCount;
      count[ testCount++ ] = TestDefaultIndexOf( buff );

      tick[ testCount ]  = Environment.TickCount;
      count[ testCount++ ] = TestIndexOfOrdinal( buff );

      // もう一度
      tick[ testCount ]  = Environment.TickCount;
      count[ testCount++ ] = TestDefaultIndexOf( buff );

      tick[ testCount ]  = Environment.TickCount;
      count[ testCount++ ] = TestIndexOfOrdinal( buff );

      tick[ testCount ]  = Environment.TickCount;

      Console.WriteLine( "TestDefaultIndexOf: {0}, {1}msec", count[ 0 ], tick[ 1 ] - tick[ 0 ] );
      Console.WriteLine( "TestIndexOfOrdinal: {0}, {1}msec", count[ 1 ], tick[ 2 ] - tick[ 1 ] );
      Console.WriteLine( "TestDefaultIndexOf: {0}, {1}msec", count[ 2 ], tick[ 3 ] - tick[ 2 ] );
      Console.WriteLine( "TestIndexOfOrdinal: {0}, {1}msec", count[ 3 ], tick[ 4 ] - tick[ 3 ] );
      Console.ReadLine();
    }
  }
}

ものすごい速度差です…。