共用方式為


在 .NET Framework 中使用規則運算式的最佳作法

更新:2011 年 3 月

.NET Framework 中的規則運算式引擎是一項強大而功能完整的工具,會依據模式比對而非比較與比對常值文字的方式處理文字。 在大部分情況下,它會快速且有效率地執行模式比對。 不過,在某些情況下,規則運算式引擎速度可能變得相當慢。 而只有鮮少情況下,它甚至可能在處理相對小的輸入卻耗費數小時甚至數天時停止回應。

本主題說明一些開發人員可以採用的最佳做法,確保其規則運算式達到最佳效能。 它包含以下各節:

  • 考慮輸入來源

  • 適當處理物件執行個體化

  • 控制回溯

  • 必要時才擷取

  • 相關主題

考慮輸入來源

一般而言,規則運算式可以接受兩種類型的輸入:受限制或未受限制。 受限制的輸入是來自已知或可靠來源,並且遵循預先定義格式的文字。 未受限制的輸入是來自不可靠來源 (例如 Web 使用者) 的文字,且可能未依循預先定義或預期的格式。

通常撰寫規則運算式模式的目的在於比對有效輸入。 也就是說,開發人員會檢查要比對的文字,然後撰寫比對該文字的規則運算式模式。 接著開發人員會利用多個有效的輸入項目進行測試,藉此判斷此模式是否需要更正或進一步詳述。 當模式符合所有假設的有效輸入時,即宣告準備好實際執行,並且可以納入已發行的應用程式中。 這種方式使得規則運算式模式相當適合比對限制的輸入, 不過卻不適合比對未受限制的輸入。

若要比對未受限制的輸入,規則運算式必須能夠有效率地處理三種文字:

  • 符合規則運算式模式的文字。

  • 不符合規則運算式模式的文字。

  • 幾乎符合規則運算式模式的文字。

最後一種文字對於專為處理受限制輸入的規則運算式而言尤其繁瑣。 如果該規則運算式也倚賴大量回溯,則規則運算式引擎可能耗費相當長的時間 (有些情況需要許多小時或許多天) 處理看似無關緊要的文字。

例如,像是驗證電子郵件地址別名的規則運算式,這種規則運算式相當常用卻也極為繁瑣。 規則運算式 ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ 主要用來處理一般視為有效的電子郵件地址,其中包含英數字元或底線,後面接著零個或多個字元,而這些字元可以是英數字、句號、底線或連字號。 規則運算式的結尾必須是英數字元或底線。 不過,如下面的範例所示,雖然這個規則運算式可輕鬆處理有效的輸入,但是當它處理幾乎有效的輸入時就非常沒有效率。

Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim sw As Stopwatch    
      Dim addresses() As String = { "AAAAAAAAAAA@anyco.com", 
                                 "AAAAAAAAAAaaaaaaaaaa!@anyco.com" }
      Dim pattern As String = "^[0-9A-Z]([-.\w]*[0-9A-Z])*$"
      Dim input As String 

      For Each address In addresses
         Dim mailBox As String = address.Substring(0, address.IndexOf("@"))       
         Dim index As Integer = 0
         For ctr As Integer = mailBox.Length - 1 To 0 Step -1
            index += 1
            input = mailBox.Substring(ctr, index) 
            sw = Stopwatch.StartNew()
            Dim m As Match = Regex.Match(input, pattern, RegexOptions.IgnoreCase)
            sw.Stop()
            if m.Success Then
               Console.WriteLine("{0,2}. Matched '{1,25}' in {2}", 
                                 index, m.Value, sw.Elapsed)
            Else                     
               Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}", 
                                 index, input, sw.Elapsed)
            End If                  
         Next
         Console.WriteLine()
      Next
   End Sub
End Module
' The example displays output similar to the following:
'     1. Matched '                        A' in 00:00:00.0007122
'     2. Matched '                       AA' in 00:00:00.0000282
'     3. Matched '                      AAA' in 00:00:00.0000042
'     4. Matched '                     AAAA' in 00:00:00.0000038
'     5. Matched '                    AAAAA' in 00:00:00.0000042
'     6. Matched '                   AAAAAA' in 00:00:00.0000042
'     7. Matched '                  AAAAAAA' in 00:00:00.0000042
'     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
'     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
'    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
'    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
'    
'     1. Failed  '                        !' in 00:00:00.0000447
'     2. Failed  '                       a!' in 00:00:00.0000071
'     3. Failed  '                      aa!' in 00:00:00.0000071
'     4. Failed  '                     aaa!' in 00:00:00.0000061
'     5. Failed  '                    aaaa!' in 00:00:00.0000081
'     6. Failed  '                   aaaaa!' in 00:00:00.0000126
'     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
'     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
'     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
'    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
'    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
'    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
'    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
'    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
'    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
'    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
'    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
'    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
'    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
'    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
'    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      Stopwatch sw;    
      string[] addresses = { "AAAAAAAAAAA@anyco.com", 
                             "AAAAAAAAAAaaaaaaaaaa!@anyco.com" };
      string pattern = @"^[0-9A-Z]([-.\w]*[0-9A-Z])*$";
      string input; 

      foreach (var address in addresses) {
         string mailBox = address.Substring(0, address.IndexOf("@"));       
         int index = 0;
         for (int ctr = mailBox.Length - 1; ctr >= 0; ctr--) {
            index++;

            input = mailBox.Substring(ctr, index); 
            sw = Stopwatch.StartNew();
            Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
            sw.Stop();
            if (m.Success)
               Console.WriteLine("{0,2}. Matched '{1,25}' in {2}", 
                                 index, m.Value, sw.Elapsed);
            else                     
               Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}", 
                                 index, input, sw.Elapsed);
         }
         Console.WriteLine();
      }
   }
}

// The example displays output similar to the following:
//     1. Matched '                        A' in 00:00:00.0007122
//     2. Matched '                       AA' in 00:00:00.0000282
//     3. Matched '                      AAA' in 00:00:00.0000042
//     4. Matched '                     AAAA' in 00:00:00.0000038
//     5. Matched '                    AAAAA' in 00:00:00.0000042
//     6. Matched '                   AAAAAA' in 00:00:00.0000042
//     7. Matched '                  AAAAAAA' in 00:00:00.0000042
//     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
//     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
//    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
//    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
//    
//     1. Failed  '                        !' in 00:00:00.0000447
//     2. Failed  '                       a!' in 00:00:00.0000071
//     3. Failed  '                      aa!' in 00:00:00.0000071
//     4. Failed  '                     aaa!' in 00:00:00.0000061
//     5. Failed  '                    aaaa!' in 00:00:00.0000081
//     6. Failed  '                   aaaaa!' in 00:00:00.0000126
//     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
//     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
//     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
//    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
//    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
//    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
//    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
//    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
//    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
//    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
//    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
//    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
//    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
//    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
//    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372

如範例的輸出所示,規則運算式引擎會以大致相同的時間間隔處理有效的電子郵件別名 (無論其長度為何)。 但另一方面,當幾乎有效的電子郵件地址包含超過五個字元時,字串中超出的每個字元其處理時間約為兩倍。 這表示,幾乎有效的 28 個字元字串需要超過一小時的處理時間,而幾乎有效的 33 個字元字串則需要將近一天來處理。

由於這個規則運算式單純是考量所要比對輸入的格式而開發,因此並未考慮不符合模式的輸入。 而這種情況就會讓幾乎符合規則運算式模式的未受限制輸入大幅降低效能。

若要解決這個問題,您可以執行下列操作:

  • 開發模式時,您應考慮回溯可能對規則運算式引擎的效能造成的影響,尤其是規則運算式的設計為處理未受限制的輸入。 如需詳細資訊,請參閱控制回溯一節。

  • 使用無效或幾乎有效的輸入以及有效輸入徹底測試您的規則運算式。 若要針對特殊規則運算式隨機產生輸入,您可以使用 Rex (英文),這是 Microsoft Research 提供的規則運算式探索工具。

適當處理物件執行個體化

System.Text.RegularExpressions.Regex 類別是 .NET Framework 規則運算式物件模型的核心,它代表規則運算式引擎。 使用 Regex 引擎的方式經常是影響規則運算式效能最重要的一項因素。 定義規則運算式的工作與結合規則運算式引擎和規則運算式模式息息相關。 無論是傳遞規則運算式模式給 Regex 物件的建構函式,藉此將該物件執行個體化,或是將規則運算式模式連同要分析的字串一併傳遞給靜態方法,藉此呼叫該方法,這個結合的過程都必然相當昂貴。

注意事項注意事項

如需使用解譯和編譯的規則運算式所造成不良效能影響的詳細討論,請參閱 BCL Team 部落格中的最佳化規則運算式的效能,第 II 部分:控制回溯 (英文)。

您可以結合規則運算式引擎與特定規則運算式模式,然後使用引擎透過數種方式比對文字:

  • 您可以呼叫靜態模式比對方法,例如 Regex.Match(String, String)。 這樣就不需要執行個體化規則運算式物件。

  • 您可以執行個體化 Regex 物件,並且呼叫解譯之規則運算式的執行個體模式比對方法。 這是將規則運算式引擎繫結至規則運算式模式的預設方法。 這個方法會在 Regex 物件執行個體化,但是沒有包含 Compiled 旗標的 options 引數時得出結果。

  • 您可以執行個體化 Regex 物件,並且呼叫編譯之規則運算式的執行個體模式比對方法。 當 Regex 物件執行個體化且包含 Compiled 旗標的 options 引數時,規則運算式物件就會表示編譯的模式。

  • 您可以建立與特殊規則運算式模式緊密結合的特殊目的 Regex 物件、進行編譯,並且將它儲存到獨立的組件中。 您可以藉由呼叫 Regex.CompileToAssembly 方法執行這項作業。

您呼叫規則運算式比對方法的特殊方式可能對應用程式造成大幅影響。 下列各節將討論何時使用靜態方法呼叫、解譯的規則運算式以及編譯的規則運算式改善應用程式的效能。

重要事項重要事項

如果在方法呼叫中重複使用相同的規則運算式,或是應用程式大量使用規則運算式物件,則方法呼叫的形式 (靜態、解譯、編譯) 就會影響效能。

靜態規則運算式

建議您使用靜態規則運算式方法來替代使用相同的規則運算式重複執行個體化規則運算式物件。 與規則運算式物件所使用的規則運算式模式不同的是,規則運算式引擎會在內部快取作業程式碼或是從執行個體方法呼叫中所使用模式編譯的 Microsoft intermediate language (MSIL)。

例如,事件處理常式經常會呼叫另一個方法來驗證使用者輸入。 下列程式碼中會反映這種情況,其中 Button 控制項的 Click 事件會用來呼叫名為 IsValidCurrency 的方法,該方法會檢查使用者是否已輸入貨幣符號且後面至少有一個十進位數字。

Public Sub OKButton_Click(sender As Object, e As EventArgs) _ 
           Handles OKButton.Click

   If Not String.IsNullOrEmpty(sourceCurrency.Text) Then
      If RegexLib.IsValidCurrency(sourceCurrency.Text) Then
         PerformConversion()
      Else
         status.Text = "The source currency value is invalid."
      End If          
   End If
End Sub
public void OKButton_Click(object sender, EventArgs e) 
{
   if (! String.IsNullOrEmpty(sourceCurrency.Text))
      if (RegexLib.IsValidCurrency(sourceCurrency.Text))
         PerformConversion();
      else
         status.Text = "The source currency value is invalid.";
}

下列範例示範非常沒有效率的 IsValidCurrency 方法實作。 請注意,每一個方法呼叫都會使用相同的模式重複執行個體化 Regex 物件。 而這表示,每次呼叫方法時都必須重新編譯規則運算式。

Imports System.Text.RegularExpressions

Public Module RegexLib
   Public Function IsValidCurrency(currencyValue As String) As Boolean
      Dim pattern As String = "\p{Sc}+\s*\d+"
      Dim currencyRegex As New Regex(pattern)
      Return currencyRegex.IsMatch(currencyValue) 
   End Function
End Module
using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      Regex currencyRegex = new Regex(pattern);
      return currencyRegex.IsMatch(currencyValue);
   }
}

您應該用呼叫靜態 Regex.IsMatch(String, String) 方法取代這個沒有效率的程式碼。 這樣就不必在每次您想要呼叫模式比對方法時執行個體化 Regex 物件,並且可讓規則運算式引擎從其快取擷取編譯版的規則運算式。

Imports System.Text.RegularExpressions

Public Module RegexLib
   Public Function IsValidCurrency(currencyValue As String) As Boolean
      Dim pattern As String = "\p{Sc}+\s*\d+"
      Return Regex.IsMatch(currencyValue, pattern)
   End Function
End Module
using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      return Regex.IsMatch(currencyValue, pattern); 
   }
}

根據預設,會快取 15 個最近使用的靜態規則運算式模式。 針對需要大量快取之靜態規則運算式的應用程式,快取的大小可以透過設定 Regex.CacheSize 屬性加以調整。

這個範例中使用的規則運算式 \p{Sc}+\s*\d+ 會驗證輸入字串是否包含貨幣符號和至少一個十進位數字。 模式的定義方式如下表所示。

模式

描述

\p{Sc}+

比對 [Unicode Symbol, Currency] 分類中的一個或多個字元。

\s*

比對零個以上的空白字元。

\d+

比對一個或多個十進位數字。

解譯與編譯的規則運算式

未透過指定 Compiled 選項繫結至規則運算式引擎的規則運算式模式會加以解譯。 執行個體化規則運算式物件時,規則運算式引擎會將規則運算式轉換成一組作業程式碼。 呼叫執行個體方法時,作業程式碼會轉換成 MSIL 並且由 JIT 編譯器執行。 同樣地,當呼叫靜態規則運算式方法而快取中找不到規則運算式時,規則運算式引擎會將規則運算式轉換成一組作業程式碼,並且將它們儲存到快取中。 然後引擎會將這些作業程式碼轉換成 MSIL,JIT 編譯器就可以執行這些作業程式碼。 解譯的規則運算式會藉由放慢執行時間來縮短啟動時間。 因此,在少數方法呼叫中使用規則運算式時,或是雖然不知道規則運算式方法呼叫的確實數目,但是期望數目很少時,最適合使用解譯的規則運算式。 隨著方法呼叫的數目增加,放慢執行速度就會壓縮掉縮短啟動時間所獲得的效能。

透過指定 Compiled 選項繫結至規則運算式引擎的規則運算式模式會加以編譯。 這表示,當執行個體化規則運算式物件,或是當呼叫靜態規則運算式方法而快取中找不到規則運算式時,規則運算式引擎會將規則運算式轉換成一組中繼的作業程式碼,然後將這些程式碼轉換成 MSIL。 呼叫方法時,JIT 編譯器會執行 MSIL。 與解譯的規則運算式相反的是,編譯的規則運算式會延長啟動時間,但加快執行個別模式比對方法的速度。 因此,編譯規則運算式所獲得的效能優勢會與呼叫的規則運算式方法數目成比例。

簡言之,我們建議您在呼叫含有相對較不常用之特定規則運算式的規則運算式方法時,使用解譯的規則運算式。 而當您呼叫含有相對較常用之特定規則運算式的規則運算式方法時,則應該使用編譯的規則運算式。 無論是放慢解譯的規則運算式執行速度所提升的效能超過縮短的啟動時間,或是放慢編譯的規則運算式啟動時間所提升的效能超過加快執行速度,都不容易判斷出其確實的臨界值。 因為臨界值取決於各種不同的因素,包括規則運算式及其處理之特定資料的複雜度。 若要判斷究竟是解譯或編譯的規則運算式能為您的特殊應用程式案例提供最佳效能,您可以使用 Stopwatch 類別比較兩者的執行時間。

下列範例會比較編譯和解譯的規則運算式讀取 Theodore Dreiser 所著 The Financier 一文的前十個句子及讀取所有句子時的效能。 如範例的輸出所示,僅對規則運算式比對方法進行十次呼叫時,解譯的規則運算式能提供比編譯的規則運算式更佳的效能。 但是進行大量呼叫 (此案例中為超過 13,000 次) 時,編譯的規則運算式會提供較佳的效能。

Imports System.Diagnostics
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim pattern As String = "\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]"
      Dim sw As Stopwatch
      Dim match As Match
      Dim ctr As Integer

      Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
      Dim input As String = inFile.ReadToEnd()
      inFile.Close()

      ' Read first ten sentences with interpreted regex.
      Console.WriteLine("10 Sentences with Interpreted Regex:")
      sw = Stopwatch.StartNew()
      Dim int10 As New Regex(pattern, RegexOptions.SingleLine)
      match = int10.Match(input)
      For ctr = 0 To 9
         If match.Success Then
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
         Else
            Exit For
         End If
      Next
      sw.Stop()
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

      ' Read first ten sentences with compiled regex.
      Console.WriteLine("10 Sentences with Compiled Regex:")
      sw = Stopwatch.StartNew()
      Dim comp10 As New Regex(pattern, 
                   RegexOptions.SingleLine Or RegexOptions.Compiled)
      match = comp10.Match(input)
      For ctr = 0 To 9
         If match.Success Then
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
         Else
            Exit For
         End If
      Next
      sw.Stop()
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

      ' Read all sentences with interpreted regex.
      Console.WriteLine("All Sentences with Interpreted Regex:")
      sw = Stopwatch.StartNew()
      Dim intAll As New Regex(pattern, RegexOptions.SingleLine)
      match = intAll.Match(input)
      Dim matches As Integer = 0
      Do While match.Success
         matches += 1
         ' Do nothing with the match except get the next match.
         match = match.NextMatch()
      Loop
      sw.Stop()
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)

      ' Read all sentnces with compiled regex.
      Console.WriteLine("All Sentences with Compiled Regex:")
      sw = Stopwatch.StartNew()
      Dim compAll As New Regex(pattern, 
                     RegexOptions.SingleLine Or RegexOptions.Compiled)
      match = compAll.Match(input)
      matches = 0
      Do While match.Success
         matches += 1
         ' Do nothing with the match except get the next match.
         match = match.NextMatch()
      Loop
      sw.Stop()
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)      
   End Sub
End Module
' The example displays output like the following:
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0047491
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0141872
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1929928
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7635869
'       
'       >compare1
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0046914
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0143727
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1514100
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7432921
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";
      Stopwatch sw;
      Match match;
      int ctr;

      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      // Read first ten sentences with interpreted regex.
      Console.WriteLine("10 Sentences with Interpreted Regex:");
      sw = Stopwatch.StartNew();
      Regex int10 = new Regex(pattern, RegexOptions.Singleline);
      match = int10.Match(input);
      for (ctr = 0; ctr <= 9; ctr++) {
         if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
         else
            break;
      }
      sw.Stop();
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

      // Read first ten sentences with compiled regex.
      Console.WriteLine("10 Sentences with Compiled Regex:");
      sw = Stopwatch.StartNew();
      Regex comp10 = new Regex(pattern, 
                   RegexOptions.Singleline | RegexOptions.Compiled);
      match = comp10.Match(input);
      for (ctr = 0; ctr <= 9; ctr++) {
         if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
         else
            break;
      }
      sw.Stop();
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

      // Read all sentences with interpreted regex.
      Console.WriteLine("All Sentences with Interpreted Regex:");
      sw = Stopwatch.StartNew();
      Regex intAll = new Regex(pattern, RegexOptions.Singleline);
      match = intAll.Match(input);
      int matches = 0;
      while (match.Success) {
         matches++;
         // Do nothing with the match except get the next match.
         match = match.NextMatch();
      }
      sw.Stop();
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);

      // Read all sentnces with compiled regex.
      Console.WriteLine("All Sentences with Compiled Regex:");
      sw = Stopwatch.StartNew();
      Regex compAll = new Regex(pattern, 
                      RegexOptions.Singleline | RegexOptions.Compiled);
      match = compAll.Match(input);
      matches = 0;
      while (match.Success) {
         matches++;
         // Do nothing with the match except get the next match.
         match = match.NextMatch();
      }
      sw.Stop();
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);      
   }
}
// The example displays the following output:
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0047491
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0141872
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1929928
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7635869
//       
//       >compare1
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0046914
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0143727
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1514100
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7432921

範例中所使用規則運算式模式 \b(\w+((\r?\n)|,?\s))*\w+[.?:;!] 的定義方式如下表所示。

模式

描述

\b

開始字緣比對。

\w+

比對一個或多個文字字元。

(\r? \n)|,? \s)

比對後面接著新行字元的零個或一個歸位字元,或是後面接著空白字元的零個或一個逗號。

(\w+((\r? \n)|,? \s))*

比對出現零次或多次的一個或多個文字字元,其後面會接著零個或一個歸位字元和新行字元,或是後面接著空白字元的零個或一個逗號。

\w+

比對一個或多個文字字元。

[.?:;!]

比對句號、問號、冒號、分號或驚嘆號。

規則運算式:編譯為組件

.NET Framework 也可讓您建立包含編譯規則運算式的組件。 這樣會將規則運算式編譯的效能影響從執行階段移至設計階段。 不過,它還包含了一些額外的工作:您必須事先定義規則運算式,並且將其編譯為組件。 接著編譯器就可在編譯使用組件之規則運算式的原始程式碼時參考這個組件。 組件中的每個編譯的規則運算式都會以衍生自 Regex 的類別表示。

若要將規則運算式編譯為組件,請呼叫 Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) 方法,並且將代表要編譯之規則運算式的 RegexCompilationInfo 物件陣列,以及包含所要建立組件之相關資訊的 AssemblyName 物件傳遞給該方法。

建議您在下列情況下將規則運算式編譯為組件:

  • 如果您是元件開發人員,而且想要建立可重複使用的規則運算式程式庫。

  • 如果您希望不定次數 (從一、兩次到數千、數萬次) 地呼叫規則運算式的模式比對方法。 與編譯或解譯的規則運算式不同的是,無論方法呼叫的次數為何,編譯為個別組件的規則運算式都會提供一致的效能。

如果您要使用編譯的規則運算式來最佳化效能,則不應使用反映來建立組件、載入規則運算式引擎,以及執行其模式比對方法。 因此您就必須避免動態建置規則運算式模式,並且在建立組件時指定任何模式比對選項 (例如不區分大小寫的模式比對)。 另外,您也必須將建立組件的程式碼與使用規則運算式的程式碼分開。

下列範例示範如何建立內含編譯的規則運算式的組件。 範例中會建立名為 RegexLib.dll 且內含單一規則運算式類別 SentencePattern 的組件,其中包含解譯與編譯的規則運算式一節中使用的句子比對規則運算式模式。

Imports System.Reflection
Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim SentencePattern As New RegexCompilationInfo("\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                      RegexOptions.Multiline,
                                                      "SentencePattern",
                                                      "Utilities.RegularExpressions",
                                                      True)
      Dim regexes() As RegexCompilationInfo = {SentencePattern}
      Dim assemName As New AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null")
      Regex.CompileToAssembly(regexes, assemName)
   End Sub
End Module
using System;
using System.Reflection;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      RegexCompilationInfo SentencePattern =
                           new RegexCompilationInfo(@"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                    RegexOptions.Multiline,
                                                    "SentencePattern",
                                                    "Utilities.RegularExpressions",
                                                    true);
      RegexCompilationInfo[] regexes = { SentencePattern };
      AssemblyName assemName = new AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null");
      Regex.CompileToAssembly(regexes, assemName);
   }
}

當範例編譯為可執行檔並且執行時,會建立名為 RegexLib.dll 的組件。 規則運算式會以衍生自 Regex 且名為 Utilities.RegularExpressions.SentencePattern 的類別表示。 接著下列範例會使用編譯的規則運算式擷取 Theodore Dreiser 所著 The Financier 一文中的句子。

Imports System.IO
Imports System.Text.RegularExpressions
Imports Utilities.RegularExpressions

Module Example
   Public Sub Main()
      Dim pattern As New SentencePattern()
      Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
      Dim input As String = inFile.ReadToEnd()
      inFile.Close()

      Dim matches As MatchCollection = pattern.Matches(input)
      Console.WriteLine("Found {0:N0} sentences.", matches.Count)      
   End Sub
End Module
' The example displays the following output:
'      Found 13,443 sentences.
using System;
using System.IO;
using System.Text.RegularExpressions;
using Utilities.RegularExpressions;

public class Example
{
   public static void Main()
   {
      SentencePattern pattern = new SentencePattern();
      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      MatchCollection matches = pattern.Matches(input);
      Console.WriteLine("Found {0:N0} sentences.", matches.Count);      
   }
}
// The example displays the following output:
//      Found 13,443 sentences.

控制回溯

通常規則運算式引擎會使用線性迴歸逐一處理輸入字串,並且與規則運算式模式比較。 不過,當規則運算式模式中使用不定數的數量詞 (例如 *、+ 和 ?) 時,規則運算式引擎可能會放棄一部分成功的部分符合結果,並且返回之前儲存的狀態,以便搜尋與整個模式完全相符的結果。 這個程序稱為「回溯」(Backtracking)。

注意事項注意事項

如需有關回溯的詳細資訊,請參閱規則運算式行為的詳細資訊回溯。如需有關回溯的詳細討論,請參閱 BCL Team 部落格中的最佳化規則運算式的效能,第 II 部分:控制回溯 (英文)。

支援回溯能讓規則運算式更強大且更靈活, 同時還能讓規則運算式開發人員負責掌控規則運算式引擎的作業。 由於開發人員經常忽略這個責任而誤用回溯或大量使用回溯,因而時常是造成規則運算式效能低落的最重要原因。 在最糟的情況下,輸入字串中每個超出字元的執行時間可能會倍增。 事實上,如果輸入幾乎符合規則運算式模式的話,大量使用回溯很容易製造相當於程式設計上的無窮迴圈,而規則運算式引擎可能需要數小時,甚至數天來處理相對來說很短的輸入字串。

儘管回溯並不是比對的要件,應用程式常常會因為使用回溯而影響效能。 例如,規則運算式 \b\p{Lu}\w*\b 會比對所有開頭為大寫字元的文字,如下表所示。

模式

描述

\b

開始字緣比對。

\p{Lu}

比對大寫字元。

\w*

比對零個或多個文字字元。

\b

結束字緣比對。

由於字緣與文字字元不同,也不是文字字元的子集,因此規則運算式引擎不可能在比對文字字元時跨越字緣。 這表示對於這個規則運算式來說,回溯不會使任何比對完全成功,只會造成效能降低,因為規則運算式引擎會被迫儲存每一個成功的初始文字字元比對的狀態。

如果您判定不需要回溯,可以使用 (?>subexpression) 語言項目將它停用。 下列範例會使用兩個規則運算式剖析輸入字串。 第一個 \b\p{Lu}\w*\b 倚賴回溯。 第二個 \b\p{Lu}(?>\w*)\b 則停用回溯。 如範例的輸出所示,兩者會產生相同的結果。

Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim input As String = "This this word Sentence name Capital"
      Dim pattern As String = "\b\p{Lu}\w*\b"
      For Each match As Match In Regex.Matches(input, pattern)
         Console.WriteLine(match.Value)
      Next
      Console.WriteLine()

      pattern = "\b\p{Lu}(?>\w*)\b"   
      For Each match As Match In Regex.Matches(input, pattern)
         Console.WriteLine(match.Value)
      Next
   End Sub
End Module
' The example displays the following output:
'       This
'       Sentence
'       Capital
'       
'       This
'       Sentence
'       Capital
using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string input = "This this word Sentence name Capital";
      string pattern = @"\b\p{Lu}\w*\b";
      foreach (Match match in Regex.Matches(input, pattern))
         Console.WriteLine(match.Value);

      Console.WriteLine();

      pattern = @"\b\p{Lu}(?>\w*)\b";   
      foreach (Match match in Regex.Matches(input, pattern))
         Console.WriteLine(match.Value);
   }
}
// The example displays the following output:
//       This
//       Sentence
//       Capital
//       
//       This
//       Sentence
//       Capital

在許多情況下,回溯是比對規則運算式模式與輸入文字時所必要。 在這類情況下,大量回溯可能嚴重降低效能,並且製造應用程式停止回應的印象。 尤其是當數量詞為巢狀,而且符合外部子運算式的文字是符合內部子運算式之文字的子集時,就會發生這種情況。

例如,規則運算式模式 ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ 的目的在於比對至少包含一個英數字元的零件編號。 任何額外的字元都可能包含英數字元、連字號、底線或句號,不過最後一個字元必須是英數字。 $ 符號則結束零件編號。 在某些情況下,這個規則運算式模式可能顯現出極差的效能,因為數量詞為巢狀,而且子運算式 [0-9A-Z] 是 [-.\w]* 子運算式的子集。

在這類情況下,您可以移除巢狀數量詞,並且將外部子運算式取代為零寬度的右合樣或左合樣判斷提示,藉此最佳化規則運算式的效能。 右合樣和左合樣判斷提示是錨點,它們不會移動輸入字串中的指標,而是向右或向左合樣,以檢查是否符合指定的條件。 例如,零件編號規則運算式可以重寫為 ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$。 這個規則運算式模式的定義方式如下表所示。

模式

描述

^

在輸入字串的開頭開始比對。

[0-9A-Z]

比對英數字元。 零件編號必須至少包含這個字元。

[-. \w]*

比對出現零次或多次的任何文字字元、連字號或句號。

\$

比對 $ 符號。

(?<=[0-9A-Z])

向右合樣結尾的 $ 符號,確定前一個字元是英數字。

$

在輸入字串結尾結束比對。

下列範例說明如何使用這個規則運算式比對包含可能零件編號的陣列。

Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim pattern As String = "^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$"
      Dim partNos() As String = { "A1C$", "A4", "A4$", "A1603D$", 
                                  "A1603D#" }

      For Each input As String In partNos
         Dim match As Match = Regex.Match(input, pattern)
         If match.Success Then
            Console.WriteLine(match.Value)
         Else
            Console.WriteLine("Match not found.")
         End If
      Next      
   End Sub
End Module
' The example displays the following output:
'       A1C$
'       Match not found.
'       A4$
'       A1603D$
'       Match not found.
using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string pattern = @"^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$";
      string[] partNos = { "A1C$", "A4", "A4$", "A1603D$", "A1603D#" };

      foreach (var input in partNos) {
         Match match = Regex.Match(input, pattern);
         if (match.Success)
            Console.WriteLine(match.Value);
         else
            Console.WriteLine("Match not found.");
      }      
   }
}
// The example displays the following output:
//       A1C$
//       Match not found.
//       A4$
//       A1603D$
//       Match not found.

.NET Framework 中的規則運算式語言包括下列語言項目,可讓您用來消除巢狀數量詞。 如需詳細資訊,請參閱群組建構

語言項目

描述

(?=subexpression)

零寬度右合樣。 從目前的位置向右合樣,判斷 subexpression 是否符合輸入字串。

(?!subexpression)

零寬度右不合樣。 從目前的位置向右合樣,判斷 subexpression 是否不符合輸入字串。

(?<=subexpression)

零寬度左合樣。 從目前的位置向左合樣,判斷 subexpression 是否符合輸入字串。

(?<!subexpression)

零寬度左不合樣。 從目前的位置向左合樣,判斷 subexpression 是否不符合輸入字串。

必要時才擷取

.NET Framework 中的規則運算式支援許多群組建構,可讓您將規則運算式模式與一個或多個子運算式設為群組。 .NET Framework 規則運算式語言中最常用的群組建構為 (subexpression) (定義編號擷取群組) 和 (?<name>subexpression) (定義具名擷取群組)。 群組建構是建立反向參考和定義套用數量詞之子運算式的要件。

不過,使用這些語言項目也有其代價。 這些語言項目會造成在 Match.Groups 屬性傳回的 GroupCollection 物件中填入最近使用的未命名或具名擷取,而如果單一群組建構擷取了輸入字串中的多個子字串,則這些語言項目也會在特定擷取群組的 Group.Captures 屬性傳回的 CaptureCollection 物件中填入多個 Capture 物件。

通常在規則運算式中使用群組建構的目的在於能夠套用數量詞,而且後續不會使用這些子運算式擷取的群組。 例如,規則運算式 \b(\w+[;,]?\s?)+[.?!] 是設計用來擷取整個句子。 下表描述這個規則運算式模式中的語言項目,及其對於 Match 物件的 Match.GroupsGroup.Captures 集合造成的影響。

模式

描述

\b

開始字緣比對。

\w+

比對一個或多個文字字元。

[;,]?

比對零個或一個逗號或分號。

\s?

比對零個或一個空白字元。

(\w+[;,]? \s?)+

比對出現一次或多次的一個或多個文字字元,後面接著選擇性的逗號或分號,再後面接著選擇性的空白字元。 這會定義必要的第一個擷取群組,如此多個文字字元 (也就是文字) 後面接著選擇性標點符號的組合才會重複,直到規則運算式引擎到達句尾為止。

[.?!]

比對句號、問號或驚嘆號。

如下列範例所示,找到符合的結果時,GroupCollection 和 CapturesCollection 物件中都會填入比對所擷取的項目。 在此案例中,擷取群組 (\w+[;,]?\s?) 會存在,如此 + 數量詞就能套用至其中,這樣就能讓規則運算式模式比對句子中的每個字, 否則就會比對句子中的最後一個字。

Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim input As String = "This is one sentence. This is another."
      Dim pattern As String = "\b(\w+[;,]?\s?)+[.?!]"

      For Each match As Match In Regex.Matches(input, pattern)
         Console.WriteLine("Match: '{0}' at index {1}.", 
                           match.Value, match.Index)
         Dim grpCtr As Integer = 0
         For Each grp As Group In match.Groups
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index)
            Dim capCtr As Integer = 0
            For Each cap As Capture In grp.Captures
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index)
               capCtr += 1
            Next
            grpCtr += 1
         Next          
         Console.WriteLine()        
      Next    
   End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'          Group 1: 'sentence' at index 12.
'             Capture 0: 'This ' at 0.
'             Capture 1: 'is ' at 5.
'             Capture 2: 'one ' at 8.
'             Capture 3: 'sentence' at 12.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
'          Group 1: 'another' at index 30.
'             Capture 0: 'This ' at 22.
'             Capture 1: 'is ' at 27.
'             Capture 2: 'another' at 30.
using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string input = "This is one sentence. This is another.";
      string pattern = @"\b(\w+[;,]?\s?)+[.?!]";

      foreach (Match match in Regex.Matches(input, pattern)) {
         Console.WriteLine("Match: '{0}' at index {1}.", 
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index);
               capCtr++;
            }
            grpCtr++;
         }          
         Console.WriteLine();        
      }
   }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//       
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.

如果您使用子運算式的目的只是要在其中套用數量詞,對於擷取的文字並不感興趣,則應該停用群組擷取。 例如,(?:subexpression) 語言項目會阻止套用該語言項目的群組擷取相符的子字串。 在下列範例中,前一個範例的規則運算式模式會變成 \b(?:\w+[;,]?\s?)+[.?!]。 如輸出所示,它會阻止規則運算式引擎填入 GroupCollection 和 CapturesCollection 集合。

Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim input As String = "This is one sentence. This is another."
      Dim pattern As String = "\b(?:\w+[;,]?\s?)+[.?!]"

      For Each match As Match In Regex.Matches(input, pattern)
         Console.WriteLine("Match: '{0}' at index {1}.", 
                           match.Value, match.Index)
         Dim grpCtr As Integer = 0
         For Each grp As Group In match.Groups
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index)
            Dim capCtr As Integer = 0
            For Each cap As Capture In grp.Captures
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index)
               capCtr += 1
            Next
            grpCtr += 1
         Next          
         Console.WriteLine()        
      Next    
   End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string input = "This is one sentence. This is another.";
      string pattern = @"\b(?:\w+[;,]?\s?)+[.?!]";

      foreach (Match match in Regex.Matches(input, pattern)) {
         Console.WriteLine("Match: '{0}' at index {1}.", 
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index);
               capCtr++;
            }
            grpCtr++;
         }          
         Console.WriteLine();        
      }
   }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//       
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.

您可以透過下列其中一種方式停用擷取:

  • 使用 (?:subexpression) 語言項目。 這個項目會阻止在套用該項目的群組中擷取相符的子字串。 不過,它不會停用任何巢狀群組中的子字串擷取。

  • 使用 ExplicitCapture 選項。 這個選項會停用規則運算式模式中的所有未命名或隱含擷取。 當您使用這個選項時,只會擷取符合 (?<name>subexpression) 語言項目所定義之具名群組的子字串。 ExplicitCapture 旗標可以傳遞至 Regex 類別建構函式的 options 參數,或是 Regex 靜態比對方法的 options 參數。

  • 在 (?imnsx) 語言項目中使用 n 選項。 這個選項會從規則運算式模式中出現該項目的位置開始,停用所有未命名或隱含擷取。 在到達模式結尾或 (-n) 選項啟用未命名或隱含擷取之前,擷取都會是停用狀態。 如需詳細資訊,請參閱其他建構

  • 在 (?imnsx:subexpression) 語言項目中使用 n 選項。 這個選項會停用 subexpression 中的所有未命名或隱含擷取。 任何未命名或隱含巢狀擷取群組所進行的擷取也都會停用。

相關主題

標題

描述

規則運算式行為的詳細資訊

檢查 .NET Framework 中規則運算式引擎的實作。 本主題將強調規則運算式的靈活度,並且說明開發人員應負責確保規則運算式引擎有效率且穩定地運作。

回溯

說明何謂回溯以及回溯如何影響規則運算式的效能,並且檢查提供回溯之替代方式的語言項目。

規則運算式語言項目

描述 .NET Framework 中規則運算式語言的項目,並且提供每個語言項目之詳細文件的連結。

變更記錄

日期

記錄

原因

2011 年 3 月

加入主題。

資訊加強。