Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Ekspresi reguler, atau regex, adalah string yang memungkinkan pengembang untuk mengekspresikan pola yang dicari, menjadikannya cara umum untuk mencari teks dan mengekstrak hasil sebagai subset dari string yang dicari. Di .NET, System.Text.RegularExpressions
namespace digunakan untuk menentukan Regex instans dan metode statis dan cocok dengan pola yang ditentukan pengguna. Dalam artikel ini, Anda akan mempelajari cara menggunakan pembuatan sumber untuk menghasilkan Regex
instans untuk mengoptimalkan performa.
Catatan
Jika memungkinkan, gunakan ekspresi reguler yang dihasilkan sumber alih-alih mengkompilasi ekspresi reguler menggunakan RegexOptions.Compiled opsi . Pembuatan sumber dapat membantu aplikasi Anda mulai lebih cepat, berjalan lebih cepat, dan lebih mudah dipangkas. Untuk mempelajari kapan pembuatan sumber dimungkinkan, lihat Kapan menggunakannya.
Regex yang dikompilasi
Ketika Anda menulis new Regex("somepattern")
, beberapa hal terjadi. Pola yang ditentukan diurai, baik untuk memastikan validitas pola dan mengubahnya menjadi pohon internal yang mewakili regex yang diurai. Pohon ini kemudian dioptimalkan dengan berbagai cara, mengubah pola menjadi variasi yang setara secara fungsional yang dapat dijalankan dengan lebih efisien. Pohon ditulis ke dalam bentuk yang dapat ditafsirkan sebagai serangkaian opcode dan operand yang memberikan instruksi kepada mesin penerjemah regex tentang cara mencocokkan. Ketika kecocokan dilakukan, penerjemah hanya berjalan melalui instruksi tersebut, memprosesnya terhadap teks input. Saat membuat instans baru Regex
atau memanggil salah satu metode statis pada Regex
, interpreter adalah mesin default yang digunakan.
Ketika Anda menentukan RegexOptions.Compiled, semua pekerjaan waktu konstruksi yang sama dilakukan. Instruksi yang dihasilkan diubah lebih lanjut oleh pengkompilasi berbasis pantulan menjadi instruksi IL yang ditulis ke beberapa DynamicMethod objek. Ketika kecocokan dilakukan, metode tersebut DynamicMethod
dipanggil. IL ini pada dasarnya melakukan apa yang akan dilakukan interpreter, kecuali khusus untuk pola yang tepat yang sedang diproses. Misalnya, jika pola berisi [ac]
, interpreter akan melihat opcode yang bertuliskan "cocokkan karakter input pada posisi saat ini terhadap set yang ditentukan dalam deskripsi set ini". Sedangkan IL yang dikompilasi akan berisi kode yang secara efektif mengatakan, "cocokkan karakter input pada posisi saat ini terhadap 'a'
atau 'c'
". Casing khusus ini dan kemampuan untuk melakukan pengoptimalan berdasarkan pengetahuan tentang pola adalah beberapa alasan utama yang menentukan RegexOptions.Compiled
hasil throughput yang jauh lebih cepat cocok daripada penerjemah.
Ada beberapa kelemahan untuk RegexOptions.Compiled
. Yang paling berdampak adalah biaya untuk dibangun. Tidak hanya semua biaya yang sama yang dibayarkan untuk penerjemah, tetapi kemudian perlu mengkompilasi pohon yang dihasilkan RegexNode
dan menghasilkan opcode/operands menjadi IL, yang menambahkan biaya non-sepele. IL yang dihasilkan lebih lanjut perlu dikompilasi JIT pada penggunaan pertama, yang mengarah ke lebih banyak pengeluaran saat startup.
RegexOptions.Compiled
mewakili tradeoff mendasar antara overhead pada penggunaan pertama dan overhead pada setiap penggunaan berikutnya. Penggunaan System.Reflection.Emit juga menghambat penggunaan RegexOptions.Compiled
di lingkungan tertentu; beberapa sistem operasi tidak mengizinkan kode yang dihasilkan secara dinamis untuk dijalankan, dan pada sistem tersebut, Compiled
menjadi no-op.
Pembuatan sumber
.NET 7 memperkenalkan generator sumber baru RegexGenerator
. Generator sumber adalah komponen yang dicolokkan ke pengkompilasi dan menambah unit kompilasi dengan kode sumber tambahan. .NET SDK (versi 7 dan yang lebih baru) menyertakan generator sumber yang mengenali atribut pada metode parsial GeneratedRegexAttributeRegex
yang mengembalikan . Generator sumber menyediakan implementasi metode yang berisi semua logika untuk Regex
. Misalnya, Anda sebelumnya mungkin telah menulis kode seperti ini:
private static readonly Regex s_abcOrDefGeneratedRegex =
new(pattern: "abc|def",
options: RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void EvaluateText(string text)
{
if (s_abcOrDefGeneratedRegex.IsMatch(text))
{
// Take action with matching text
}
}
Untuk menggunakan generator sumber, Anda menulis ulang kode sebelumnya sebagai berikut:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegex().IsMatch(text))
{
// Take action with matching text
}
}
Petunjuk / Saran
Bendera RegexOptions.Compiled
diabaikan oleh generator sumber, sehingga tidak diperlukan dalam versi yang dihasilkan sumber.
Implementasi yang dihasilkan dari AbcOrDefGeneratedRegex()
cache serupa instans singleton Regex
, sehingga tidak ada penembolokan tambahan yang diperlukan untuk menggunakan kode.
Gambar berikut adalah tangkapan layar dari instans cache yang dihasilkan sumber, internal
ke Regex
subkelas yang dipancarkan generator sumber:
Tetapi seperti yang dapat dilihat, itu tidak hanya melakukan new Regex(...)
. Sebaliknya, generator sumber memancarkan sebagai kode C# implementasi kustom Regex
-turunan dengan logika yang mirip dengan apa yang RegexOptions.Compiled
dipancarkan dalam IL. Anda mendapatkan semua manfaat RegexOptions.Compiled
performa throughput (lebih, pada kenyataannya) dan manfaat start-up dari Regex.CompileToAssembly
, tetapi tanpa kompleksitas CompileToAssembly
. Sumber yang dipancarkan adalah bagian dari proyek Anda, yang berarti juga mudah dilihat dan dapat di-debug.
Petunjuk / Saran
Di Visual Studio, klik kanan pada deklarasi metode parsial Anda dan pilih Buka Definisi. Atau, atau, pilih simpul proyek di
Anda dapat mengatur titik henti di dalamnya, Anda dapat menelusurinya, dan Anda dapat menggunakannya sebagai alat pembelajaran untuk memahami dengan tepat bagaimana mesin regex memproses pola Anda dengan input Anda. Generator bahkan menghasilkan komentar tiga garis miring (XML) untuk membantu membuat ekspresi dapat dimengerti sekilas dan di mana ia digunakan.
Di dalam file yang dihasilkan sumber
Dengan .NET 7, generator sumber dan RegexCompiler
hampir sepenuhnya ditulis ulang, pada dasarnya mengubah struktur kode yang dihasilkan. Pendekatan ini telah diperluas untuk menangani semua konstruksi (dengan satu peringatan), dan keduanya RegexCompiler
dan generator sumber masih memetakan sebagian besar 1:1 satu sama lain, mengikuti pendekatan baru. Pertimbangkan output generator sumber untuk salah satu fungsi utama dari abc|def
ekspresi:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 2 alternative expressions, atomically.
{
if (slice.IsEmpty)
{
return false; // The input didn't match.
}
switch (slice[0])
{
case 'A' or 'a':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
case 'D' or 'd':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Tujuan dari kode yang dihasilkan sumber adalah untuk dapat dimengerti, dengan struktur yang mudah diikuti, dengan komentar yang menjelaskan apa yang sedang dilakukan di setiap langkah, dan secara umum dengan kode yang dipancarkan di bawah prinsip panduan bahwa generator harus mengeluarkan kode seolah-olah manusia telah menulisnya. Bahkan ketika backtracking terlibat, struktur backtracking menjadi bagian dari struktur kode, daripada mengandalkan tumpukan untuk menunjukkan di mana harus melompat berikutnya. Misalnya, berikut adalah kode untuk fungsi pencocokan yang dihasilkan yang sama saat ekspresinya adalah [ab]*[bc]
:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
int charloop_starting_pos = 0, charloop_ending_pos = 0;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match a character in the set [ABab] greedily any number of times.
//{
charloop_starting_pos = pos;
int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
if (iteration < 0)
{
iteration = slice.Length;
}
slice = slice.Slice(iteration);
pos += iteration;
charloop_ending_pos = pos;
goto CharLoopEnd;
CharLoopBacktrack:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
if (charloop_starting_pos >= charloop_ending_pos ||
(charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
{
return false; // The input didn't match.
}
charloop_ending_pos += charloop_starting_pos;
pos = charloop_ending_pos;
slice = inputSpan.Slice(pos);
CharLoopEnd:
//}
// Advance the next matching position.
if (base.runtextpos < pos)
{
base.runtextpos = pos;
}
// Match a character in the set [BCbc].
if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
{
goto CharLoopBacktrack;
}
// The input matched.
pos++;
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Anda dapat melihat struktur backtracking dalam kode, dengan label yang CharLoopBacktrack
dipancarkan untuk tempat backtrack ke dan digunakan goto
untuk melompat ke lokasi tersebut ketika bagian regex berikutnya gagal.
Jika Anda melihat penerapan RegexCompiler
kode dan generator sumber, mereka akan terlihat sangat mirip: metode bernama serupa, struktur panggilan serupa, dan bahkan komentar serupa sepanjang implementasi. Untuk sebagian besar, mereka menghasilkan kode yang identik, meskipun satu di IL dan satu di C#. Tentu saja, pengkompilasi C# kemudian bertanggung jawab untuk menerjemahkan C# ke dalam IL, sehingga IL yang dihasilkan dalam kedua kasus kemungkinan tidak akan identik. Generator sumber mengandalkan itu dalam berbagai kasus, memanfaatkan fakta bahwa pengkompilasi C# akan lebih mengoptimalkan berbagai konstruksi C#. Ada beberapa hal spesifik generator sumber dengan demikian akan menghasilkan kode pencocokan yang lebih dioptimalkan daripada .RegexCompiler
Misalnya, dalam salah satu contoh sebelumnya, Anda dapat melihat generator sumber memancarkan pernyataan pengalihan, dengan satu cabang untuk 'a'
dan cabang lain untuk 'b'
. Karena pengkompilasi C# sangat baik dalam mengoptimalkan pernyataan switch, dengan beberapa strategi yang digunakan untuk cara melakukannya secara efisien, generator sumber memiliki pengoptimalan khusus yang RegexCompiler
tidak. Untuk perubahan, generator sumber melihat semua cabang, dan jika dapat membuktikan bahwa setiap cabang dimulai dengan karakter awal yang berbeda, itu akan memancarkan pernyataan pengalihan atas karakter pertama itu dan menghindari output kode backtracking untuk pergantian itu.
Berikut adalah contoh yang sedikit lebih rumit dari itu. Perubahan lebih banyak dianalisis untuk menentukan apakah mungkin untuk merefaktornya dengan cara yang akan membuatnya lebih mudah dioptimalkan oleh mesin backtracking dan itu akan menyebabkan kode yang dihasilkan sumber yang lebih sederhana. Salah satu pengoptimalan tersebut mendukung ekstraksi awalan umum dari cabang, dan jika pergantian adalah atom seperti itu sehingga pengurutan tidak masalah, menyusun ulang cabang untuk memungkinkan lebih banyak ekstraksi tersebut. Anda dapat melihat dampaknya untuk pola Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday
hari kerja berikut , yang menghasilkan fungsi yang cocok seperti ini:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
char ch;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 6 alternative expressions, atomically.
{
int alternation_starting_pos = pos;
// Branch 0
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
{
goto AlternationBranch;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 1
{
if ((uint)slice.Length < 7 ||
!slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
{
goto AlternationBranch1;
}
pos += 7;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch1:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 2
{
if ((uint)slice.Length < 9 ||
!slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
{
goto AlternationBranch2;
}
pos += 9;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch2:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 3
{
if ((uint)slice.Length < 8 ||
!slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
{
goto AlternationBranch3;
}
pos += 8;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch3:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 4
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
!slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
{
goto AlternationBranch4;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch4:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 5
{
// Match a character in the set [Ss].
if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
{
return false; // The input didn't match.
}
// Match with 2 alternative expressions, atomically.
{
if ((uint)slice.Length < 2)
{
return false; // The input didn't match.
}
switch (slice[1])
{
case 'A' or 'a':
if ((uint)slice.Length < 8 ||
!slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 8;
slice = inputSpan.Slice(pos);
break;
case 'U' or 'u':
if ((uint)slice.Length < 6 ||
!slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 6;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
}
AlternationMatch:;
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Pada saat yang sama, generator sumber memiliki masalah lain untuk bersaing dengan itu tidak ada saat menghasilkan IL secara langsung. Jika Anda melihat beberapa contoh kode kembali, Anda dapat melihat beberapa kurung kurawal agak aneh dikomentari. Itu bukan kesalahan. Generator sumber mengenali bahwa, jika kurung kurawal tersebut tidak dikomentari, struktur backtracking mengandalkan lompatan dari luar cakupan ke label yang ditentukan di dalam cakupan itu; label seperti itu tidak akan terlihat seperti goto
itu dan kode akan gagal dikompilasi. Dengan demikian, generator sumber perlu menghindari adanya cakupan di jalan. Dalam beberapa kasus, itu hanya akan mengomentari cakupan seperti yang dilakukan di sini. Dalam kasus lain di mana itu tidak mungkin, terkadang mungkin menghindari konstruksi yang memerlukan cakupan (seperti blok multi-pernyataan if
) jika melakukannya akan bermasalah.
Generator sumber menangani semua RegexCompiler
handel, dengan satu pengecualian. Seperti halnya penanganan RegexOptions.IgnoreCase
, implementasi sekarang menggunakan tabel casing untuk menghasilkan set pada waktu konstruksi, dan bagaimana IgnoreCase
pencocokan backreference perlu berkonsultasi dengan tabel casing tersebut. Tabel tersebut bersifat internal untuk System.Text.RegularExpressions.dll
, dan untuk saat ini, setidaknya, kode eksternal untuk perakitan tersebut (termasuk kode yang dipancarkan oleh generator sumber) tidak memiliki akses ke dalamnya. Itu membuat penanganan IgnoreCase
backreference menjadi tantangan dalam generator sumber dan tidak didukung. Ini adalah salah satu konstruksi yang tidak didukung oleh generator sumber yang didukung oleh RegexCompiler
. Jika Anda mencoba menggunakan pola yang memiliki salah satu dari ini (yang jarang terjadi), generator sumber tidak akan memancarkan implementasi kustom dan sebaliknya akan kembali ke penembolokan instans reguler Regex
:
Selain itu, generator sumber tidak RegexCompiler
mendukung yang baru RegexOptions.NonBacktracking
. Jika Anda menentukan RegexOptions.Compiled | RegexOptions.NonBacktracking
, Compiled
bendera hanya akan diabaikan, dan jika Anda menentukan NonBacktracking
ke generator sumber, bendera tersebut juga akan kembali ke penembolokan instans reguler Regex
.
Kapan harus menggunakannya
Panduan umumnya adalah jika Anda dapat menggunakan generator sumber, gunakan. Jika Anda menggunakan Regex
hari ini di C# dengan argumen yang diketahui pada waktu kompilasi, dan terutama jika Anda sudah menggunakan RegexOptions.Compiled
(karena regex telah diidentifikasi sebagai hot spot yang akan mendapat manfaat dari throughput yang lebih cepat), Anda harus lebih suka menggunakan generator sumber. Generator sumber akan memberi regex Anda manfaat berikut:
- Semua manfaat throughput dari
RegexOptions.Compiled
. - Manfaat startup tidak harus melakukan semua penguraian regex, analisis, dan kompilasi pada waktu proses.
- Opsi untuk menggunakan kompilasi sebelumnya dengan kode yang dihasilkan untuk regex.
- Debuggability dan pemahaman yang lebih baik tentang regex.
- Kemungkinan untuk mengurangi ukuran aplikasi yang dipangkas dengan memangkas swath kode besar yang terkait dengan
RegexCompiler
(dan bahkan berpotensi memancarkan pantulan itu sendiri).
Ketika digunakan dengan opsi seperti RegexOptions.NonBacktracking
generator sumber yang tidak dapat menghasilkan implementasi kustom, itu masih akan memancarkan caching dan komentar XML yang menggambarkan implementasi, membuatnya berharga. Kelemahan utama dari generator sumber adalah bahwa ia memancarkan kode tambahan ke dalam rakitan Anda, sehingga ada potensi peningkatan ukuran. Semakin banyak regex di aplikasi Anda dan semakin besar, semakin banyak kode yang akan dipancarkan untuk mereka. Dalam beberapa situasi, sama seperti RegexOptions.Compiled
mungkin tidak perlu, jadi mungkin juga generator sumber. Misalnya, jika Anda memiliki regex yang hanya jarang diperlukan dan throughput mana yang tidak penting, bisa lebih bermanfaat untuk hanya mengandalkan interpreter untuk penggunaan sporadis tersebut.
Penting
.NET 7 menyertakan penganalisis yang mengidentifikasi penggunaan Regex
yang dapat dikonversi ke generator sumber, dan fixer yang melakukan konversi untuk Anda: