Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
A reguláris kifejezés, vagy regex, egy karakterlánc, amely lehetővé teszi a fejlesztő számára, hogy kifejezze a keresett mintát, így gyakran alkalmazzák szövegekben való kereséshez és a találatok részhalmazként való kinyeréséhez a keresett karakterláncból. A .NET-ben a System.Text.RegularExpressions névtér a példányok és a statikus metódusok definiálására Regex , valamint a felhasználó által meghatározott mintákon való egyeztetésre szolgál. Ebben a cikkben megtudhatja, hogyan hozhat létre Regex példányokat a teljesítmény optimalizálása érdekében a forrásgenerálás használatával.
Megjegyzés
Ahol lehetséges, használjon forrás által létrehozott reguláris kifejezéseket ahelyett, hogy reguláris kifejezéseket állít össze a RegexOptions.Compiled beállítással. A forrásgenerálás segíthet az alkalmazás gyorsabb indításában, gyorsabb futtatásában és könnyebben finomhangolhatóvá tételében. A forráslétrehozás lehetőségének megismeréséhez lásd : Mikor érdemes használni.
Kompilált reguláris kifejezések
Amikor Ön ír new Regex("somepattern"), néhány dolog történik. A megadott minta elemzése a minta érvényességének biztosítása és az elemzett regexet jelképező belső fává alakítása érdekében történik. A fa ezután különböző módokon van optimalizálva, és funkcionálisan egyenértékű változattá alakítja a mintát, amely hatékonyabban végrehajtható. A fa olyan formában van megírva, amely opkódok és operandusok sorozataként értelmezhető, amelyek útmutatást nyújtanak a regex értelmező motornak az egyezés módjára vonatkozóan. Amikor egy egyezés történik, az értelmező egyszerűen követi az utasításokat, és feldolgozza őket a bemeneti szöveg alapján. Új Regex példány létrehozásakor vagy az egyik statikus metódus Regex meghívásakor az értelmező az alapértelmezett motor.
Ha megadja RegexOptions.Compiled, elvégezzük az összes azonos munkát az építési folyamat során. Az így kapott utasításokat tovább alakítja a tükröződés-kibocsátó alapú fordító il utasításokká, amelyeket néhány DynamicMethod objektumra írnak. Egyezés végrehajtásakor meghívásra kerülnek ezek a DynamicMethod metódusok. Ez az IL lényegében pontosan azt teszi, amit az értelmező tenne, azzal a különbséggel, hogy a feldolgozandó mintára specializálódva. Ha például a minta tartalmaz [ac], az értelmező egy olyan opcode-t lát, amely azt jelzi, hogy "egyezik az aktuális pozícióban lévő bemeneti karakter az ebben a készlet leírásában megadott készlettel". A lefordított IL olyan kódot tartalmazna, amely lényegében azt jelenti, hogy "egyeztesse az aktuális pozícióban lévő bemeneti karaktert 'a' vagy 'c' ellen". Ez a speciális esetkezelés és a minta ismerete alapján történő optimalizálás képessége az egyik fő oka annak, hogy a RegexOptions.Compiled megadásával az illesztési sebesség sokkal gyorsabb, mint az értelmező esetében.
Ennek több hátránya RegexOptions.Compiledis van. A leghatásosabb az, hogy költséges felépíteni. Nemcsak az értelmezőnél felmerülő azonos költségeket kell megfizetni, hanem az eredményül kapott RegexNode fát és az opcode-ok/operandusokat IL-be kell fordítani, ami nem triviális költségeket jelent. A létrehozott IL-t az első használatkor jiT-fordításra kell fordítani, ami még nagyobb költséggel jár az indításkor.
RegexOptions.Compiled alapvető kompromisszumot jelent az első használat során járó többletterhelések és az azt követő használat többletterhelései között. A System.Reflection.Emit használata bizonyos környezetekben gátolja RegexOptions.Compiled használatát; egyes operációs rendszerek nem teszik lehetővé a dinamikusan generált kód végrehajtását, és az ilyen rendszereken Compiled nem hajt végre semmit.
Forráslétrehozás
A .NET 7 új RegexGenerator forrásgenerátort vezetett be. A forrásgenerátor olyan összetevő, amely csatlakoztatja a fordítót, és további forráskóddal bővíti a fordítási egységet. A .NET SDK-ban van egy forrásgenerátor, amely felismeri az GeneratedRegexAttribute attribútumot egy részleges metóduson, amely visszaad egy Regex. A .NET 9-től kezdve az attribútum részleges tulajdonságokra is alkalmazható. A forrásgenerátor a metódus vagy tulajdonság implementálását biztosítja, amely tartalmazza a metódus összes logikáját Regex. Előfordulhat például, hogy korábban a következőhöz hasonló kódot írt:
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
}
}
A forrásgenerátor használatához írja át az előző kódot az alábbiak szerint:
[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
}
}
A .NET 9-től kezdve a GeneratedRegexAttribute részleges metódus helyett egy részleges tulajdonságra is alkalmazhatja. Ezt a C# 13 részleges tulajdonságok támogatása engedélyezi. Az alábbi példában a tulajdonság megfelelője látható:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegexProperty { get; }
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegexProperty.IsMatch(text))
{
// Take action with matching text
}
}
Tipp.
A RegexOptions.Compiled forrásgenerátor figyelmen kívül hagyja a jelölőt, ezért a forrás által létrehozott verzióban nincs rá szükség.
A létrehozott implementáció AbcOrDefGeneratedRegex() szintén cache-el egy szingelton Regex példányt, így nincs szükség további gyorsítótárazásra a kód fogyasztásához.
Az alábbi kép a forrás által létrehozott gyorsítótárazott példánynak a képernyőfelvétele, a internal forrásgenerátor által kibocsátott Regex alosztályra.
De mint látható, ez nem csak csinál new Regex(...). Ehelyett a forrásgenerátor C#-kódként bocsát ki egy olyan egyéni Regex-származtatott implementációt, amely hasonló logikával rendelkezik ahhoz, amit RegexOptions.Compiled bocsát ki az IL-ben. Az átviteli teljesítmény minden előnyét élvezheti RegexOptions.Compiled-nak (sőt, valójában még többet is), valamint az indítási előnyöket Regex.CompileToAssembly, de CompileToAssembly komplexitása nélkül. A kibocsátott forrás a projekt része, ami azt jelenti, hogy könnyen megtekinthető és hibakeresésre alkalmas.
Tipp.
A Visual Studióban kattintson a jobb gombbal a részleges metódusra vagy tulajdonságdeklarációra, és válassza az Ugrás a definícióra lehetőséget. Másik lehetőségként, válassza ki a projektcsomópontot a Megoldáskezelőben, majd bontsa ki a Dependencies>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs elemet a regex generátorból létrehozott C# kód megtekintéséhez.
Töréspontokat állíthat be az eszközben, lépésről lépésre végigkövetheti, és tanulási eszközként használhatja, hogy pontosan megértse, hogyan dolgozza fel a regex feldolgozó a mintát a bemenetével. A generátor még tripla perjeles (XML) megjegyzéseket is generál, hogy a kifejezés egy pillantással érthető legyen, és hogy hol használják.
A forrás által létrehozott fájlokon belül
A .NET 7-tel a forrásgenerátor és RegexCompiler szinte teljesen át lett írva, alapvetően megváltoztatva a létrehozott kód szerkezetét. Ezt a megközelítést kiterjesztették az összes szerkezet kezelésére (egy kikötéssel), és a RegexCompiler és a forráskód-generátor továbbra is többnyire 1:1 arányban leképzik egymást, az új megközelítést követve. Vegye figyelembe a forrásgenerátor kimenetét a kifejezés egyik elsődleges függvényéhez abc|def :
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;
}
A forráskód generálásának célja, hogy érthető legyen, könnyen követhető szerkezettel és megjegyzésekkel, amelyek elmagyarázzák, hogy mi történik minden lépésben. Általában véve, a generátornak úgy kellene kibocsátania a kódot, mintha azt egy ember írta volna. A visszakövetés struktúrája akkor is a kód szerkezetének részévé válik, ha visszakövetésről van szó, ahelyett, hogy egy veremre támaszkodva jelezné, hogy hová kell ugrania. Például, itt van ugyanazon létrehozott egyező függvény kódja, amikor a kifejezés [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;
}
A kódban látható a visszakövetés struktúrája: egy CharLoopBacktrack címke jelzi, hogy hová kell visszakövetni, és egy goto címke arra szolgál, hogy az adott helyre ugorjon, amikor a regex egy későbbi része meghiúsul.
Ha megtekinti a kód implementálását RegexCompiler és a forrásgenerátort, azok rendkívül hasonlóak lesznek: hasonló nevű metódusok, hasonló hívásstruktúra és még hasonló megjegyzések is a megvalósítás során. A legtöbb esetben azonos kódot eredményeznek, bár egy IL-ben és egy C#-ban. Természetesen a C#-fordító feladata a C# il-re fordítása, így az eredményül kapott IL mindkét esetben valószínűleg nem lesz azonos. A forrásgenerátor erre támaszkodik különböző esetekben, kihasználva azt a tényt, hogy a C# fordító tovább optimalizálja a különböző C# szerkezeteket. A forrásgenerátor bizonyos konkrét dolgokkal inkább optimalizált egyező kódot állít elő, mint amilyet a RegexCompiler tesz. Az egyik korábbi példában például láthatja, hogy a forrásgenerátor egy kapcsoló utasítást hoz létre, amelynek egyik ága 'a', és egy másik ága 'b'. Mivel a C#-fordító nagyon jó a switch utasítások optimalizálásában, és több stratégia áll rendelkezésére a hatékony optimalizáláshoz, a forrásgenerátor olyan speciális optimalizálással rendelkezik, amely RegexCompiler esetén nincs. A váltakozások esetében a forrásgenerátor az összes ágat megvizsgálja, és ha bizonyítani tudja, hogy minden ág egy másik kezdő karakterrel kezdődik, akkor egy kapcsolóutasítást bocsát ki az első karakter felett, és elkerüli az adott alternációhoz tartozó visszakövetési kód kimenetét.
Íme egy kicsit bonyolultabb példa erre. Az alternációkat jobban elemzik annak megállapításához, hogy lehetséges-e újrabontásuk oly módon, hogy könnyebben optimalizálják őket a háttérkövetési motorokkal, és ez egyszerűbb forráskódhoz vezet. Az egyik ilyen optimalizálás támogatja a gyakori előtagok kinyerését az ágakból, és ha a váltakozás atomi, akkor a rendezés nem számít, az ágak átrendezésével több ilyen kinyerést lehet lehetővé tenni. Ez látható a következő hétköznapi mintázaton Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, ami a következőhöz hasonló illeszkedő függvényt hoz létre:
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;
}
Ugyanakkor a forrásgenerátornak más problémákkal is meg kell küzdenie, amelyek egyszerűen nem léteznek, amikor közvetlenül IL-re ad ki kimenetet. Ha visszanéz néhány példakódra, láthat néhány kapcsos zárójelet, amelyeket kissé furcsa módon kommentáltak ki. Ez nem hiba. A forrásgenerátor felismeri, hogy ha ezeket a zárójeleket nem fűzték megjegyzéshez, a visszakövetés struktúrája a hatókörön kívülről a hatókörön belül definiált címkére való ugrásra támaszkodik; egy ilyen címke nem lenne látható egy goto ilyen címkén, és a kód fordítása sikertelen lenne. Így a forrásgenerátornak el kell kerülnie, hogy hatókör legyen az útban. Bizonyos esetekben egyszerűen kikommenteli a hatókört, ahogy itt is történt. Más esetekben, amikor ez nem lehetséges, néha elkerülheti azokat a szerkezeteket, amelyek hatóköröket (például többutas if blokkot) igényelnek, ha ez problémás lenne.
A forrásgenerátor mindent kezel, amit a RegexCompiler kezel, egyetlen kivétellel. A kezeléshez RegexOptions.IgnoreCasehasonlóan az implementációk most is egy casing táblát használnak a készletek létrehozásához az építkezés során, és hogy a háttérrendszerek egyeztetésének hogyan IgnoreCase kell áttekintenie ezt a casing táblát. Ez a tábla a System.Text.RegularExpressions.dll-n belül helyezkedik el, és legalábbis egyelőre a szerelvényen kívüli kódnak (beleértve a forrásgenerátor által kibocsátott kódot is) nincs hozzáférése hozzá. Az előhivatkozások kezelése IgnoreCase kihívást jelent a forrásgenerátorban, és nem támogatott. Ez az egyetlen szerkezet, amelyet a RegexCompiler támogat, de a forrásgenerátor nem. Ha olyan mintát próbál használni, amely ezek egyikével rendelkezik (ami ritka), a forrásgenerátor nem bocsát ki egyéni implementációt, és ehelyett vissza fog esni egy normál Regex példány gyorsítótárazásához:
Továbbá sem RegexCompiler sem a forrásgenerátor nem támogatja az új RegexOptions.NonBacktracking. Ha megadja a RegexOptions.Compiled | RegexOptions.NonBacktracking elemet, a rendszer figyelmen kívül hagyja a Compiled jelölőt, és ha a NonBacktracking elemet adja meg a forrásgenerátornak, ugyancsak visszatér egy normál Regex példány gyorsítótárazásához.
Mikor lehet használni
Az általános útmutató az, ha használhatja a forrásgenerátort, használja azt. Ha ma a C#-t használja fordítási időben ismert argumentumokkal, és különösen akkor, ha már használja a RegexOptions.Compiled-t (mivel a regex kinézetét olyan forró pontként azonosították, amely gyorsabb teljesítményből profitálna), érdemes inkább a forrásgenerátort használnia. A forrásgenerátor a regexnek a következő előnyöket nyújtja:
- Az átviteli sebesség minden előnye
RegexOptions.Compiled. - Az indítási műveletek előnyei közé tartozik, hogy nem kell minden reguláris kifejezés elemzést, értelmezést és fordítást futásidőben elvégezni.
- Lehetőség az idő előtti fordítás használatára a regexhez létrehozott kóddal.
- A regex jobb hibakeresése és megértése.
- Lehetőség a levágott alkalmazás méretének csökkentésére, ha levágja a kódhoz társított
RegexCompilernagy kódot (és akár a tükröződést is kibocsátja).
Ha olyan beállítással használják, mint RegexOptions.NonBacktracking amelynél a forrásgenerátor nem tud egyéni implementációt létrehozni, akkor is gyorsítótárazást és XML-megjegyzéseket bocsát ki, amelyek leírják az implementációt, így értékesek lesznek. A forrásgenerátor fő hátránya, hogy további kódot hoz létre az összeállításban, így fennáll a méretnövekedés lehetősége. Minél több regex van az alkalmazásban, és minél nagyobbak, annál több kód jelenik meg számukra. Bizonyos helyzetekben, ahogy RegexOptions.Compiled is szükségtelen lehet, úgy a forrásgenerátor is az lehet. Ha például olyan regexe van, amelyre csak ritkán van szükség, és amelynek átviteli sebessége nem számít, előnyösebb lehet, ha csak az értelmezőre támaszkodik a szórványos használathoz.
Fontos
A .NET 7 tartalmaz egy elemzőt, amely azonosítja azokat az eseteket, ahol a Regex forrásgenerátorral helyettesíthető, valamint egy javítót is, amely elvégzi az átalakítást.