(C#)internal interfaceとinterfaceを組み合わせて、DLL外に公開しないメンバを定義する

C#という言語を使い始めて7年ぐらいは経つわけですが、Azukiの実装中に長いこと不満に思っていたコトが一つ解決しましたので備忘録。 Azukiは一応最初からプラットフォームを抽象化して作ってあります。そのため、現在はW … 続きを読む (C#)internal interfaceとinterfaceを組み合わせて、DLL外に公開しないメンバを定義する

テキストエディタの「選択範囲」という概念について、2点

最近いろいろな意味でヤル気が無くなっていたけれど、久しぶりに更新。一年以上何もか書かなかったか。まあそれもまたよし。今日はテキストエディタの「選択範囲」という概念について、2点書いておこう。さすがにTwitterにはつぶ … 続きを読む テキストエディタの「選択範囲」という概念について、2点

正規表現の「^」記号とマッチング範囲

はじめに

(2011/4/6追記。本件はMicrosoftから将来のリリースで修正するとの回答がありました)
(2011/8/27追記。最新版のVisual Studio 2010+最新の.NET Framework 4で試したところ、まだ修正されていないようです。)

本日、各言語での正規表現エンジンを使って「^」記号(文字列または行の先頭を示す、アンカーあるいはゼロ幅アサーション…と呼ぶらしい)に関する動作を調査しました。背景にあるのはAzukiが内蔵している正規表現検索で行頭マッチングが行われないというユーザ報告の不具合(に近いが対策できず黙認していた動作仕様)です。過去に検索機能を実装した際の調査結果では.NETの正規表現エンジンで「Multilineモード」を有効にしても各行の先頭でない位置でマッチする現象が起こり、使えないと判断しました。今回は、この問題を改めて少し掘り下げて調査した結果報告(?)となります。

調査結果のポイントです。

  1. Microsoftの.NETが提供する正規表現エンジンでは、マッチング範囲の終了位置を指定すると「^」が常に「マッチング範囲の開始点」にマッチしてしまう(終了位置を指定しなければ常にはマッチしない)
  2. MSDNの「^」記号の説明Regex.Matchメソッドの説明からは「^」が文字列の先頭でも行頭でもない箇所にマッチする動作は予想しにくい上に、Regex.Matchのオーバーロード間での動作仕様の統一性が失われている
  3. Java (1.5以降)の正規表現エンジンでは「Anchoring Bounds」という概念で「^」の扱いをカスタマイズできる(→ Matcherクラスのリファレンス)が、.NETでは同等の機構が無い
  4. ユーザ指定の正規表現を使うアプリケーションで、特定のマッチング範囲を絞り、「^」をマッチング範囲の開始点にマッチさせたくない場合、実現できないと思われる

ポイント1および2です。Regex.Match( string, int )のオーバーロードを使ってマッチング開始点だけを指定した場合には、行頭でも文字列先頭でもない位置がマッチング範囲の開始点であっても「^」記号は該当位置にマッチしません。しかしRegex.Match( string, int, int )のオーバーロードを使うと、マッチします。したがってオーバーロード引数を追加するだけで動作仕様が変化してしまうため、不自然な印象を受けます。次に例を記します。

string text = "abc";
new Regex( "^[a-z]" ).Match( text, 1 ); // どこにもマッチしない
new Regex( "^[a-z]" ).Match( text, 1, text.Length ); // 1文字目のbでマッチする

ポイント3です。Javaの正規表現エンジンでは「Anchoring Bounds」という概念があり、「^」記号の扱いをカスタマイズできます。Anchoring Boundsを使うよう設定すると、「^」記号および「$」記号のマッチング時にマッチング範囲の前後が考慮されない — つまり問答無用でマッチング範囲の始点に「^」記号がマッチするようになります。そしてAnchoring Boundsを使わないよう設定すると、マッチング範囲の開始点が文字列先頭あるいは行頭でない限り、「^」記号はマッチング範囲の開始点にマッチしません。このように「^」記号のマッチング動作を明示的に指定できる機構があれば、それを使うことで問題回避できますが、残念ながら.NETにはありません。

ポイント4です。どうやら.NETの正規表現エンジンではJavaでいう「Anchoring Bounds」の扱いがRegex.Matchのオーバーロードごとに異なっており、Regex.Match(string,int)はAnchoring Boundsを使わず、Regex.Match(string,int,int)はAnchoring Boundsを使う動作となっています。ここで、もしAnchoring Boundsを「常に使いたい」場合は外部でマッチング対象の文字列をSubstringすることで代替できます。しかしAnchoring Boundsを「常に使いたくない」場合、Regexクラスの外部でこれを実現する方法は無いように思われます。

この仕様はAPIからもドキュメントの記述内容からも想定できるものでなく、また統一が取れていないという点も考えると、意図的な設計結果とは思えません。本件は、改めてMicrosoft社に報告と確認をしておこうと考えています。

以下、各言語・環境での検証コードおよび検証結果を記します。なお言うべきことはすでに記したので、細かい説明はしません。興味のある方や再現してみたい気分になった方へ向けた情報です。

続きを読む “正規表現の「^」記号とマッチング範囲”