Sdílet prostřednictvím


Stromy výrazů – data definující kód

Strom výrazu je datová struktura, která definuje kód. Stromy výrazů jsou založeny na stejných strukturách, které kompilátor používá k analýze kódu a generování kompilovaného výstupu. Při čtení tohoto článku si všimnete poměrně trochu podobnosti mezi stromy výrazů a typy používanými v rozhraních Roslyn API k sestavení analyzátorů a oprav kódu. (Analyzátory a opravy kódu jsou balíčky NuGet, které provádějí statickou analýzu kódu a navrhují potenciální opravy pro vývojáře.) Koncepty jsou podobné a konečným výsledkem je datová struktura, která umožňuje zkoumání zdrojového kódu smysluplným způsobem. Stromy výrazů jsou ale založené na jiné sadě tříd a rozhraní API než rozhraní API Roslyn. Tady je řádek kódu:

var sum = 1 + 2;

Pokud analyzujete předchozí kód jako strom výrazu, strom obsahuje několik uzlů. Vnější uzel je příkaz deklarace proměnné s přiřazením (var sum = 1 + 2;) Tento vnější uzel obsahuje několik podřízených uzlů: deklaraci proměnné, operátor přiřazení a výraz představující pravou stranu znaménka rovná se. Tento výraz je dále rozdělen na výrazy, které představují operaci sčítání, a na levý a pravý operand sčítání.

Pojďme se podrobněji ponořit do výrazů, které tvoří pravou stranu znaménka rovná se. Výraz je 1 + 2, binární výraz. Konkrétně jde o výraz pro binární sčítání. Binární výraz sčítání má dva potomky, kteří představují levý a pravý uzel výrazu sčítání. Zde jsou oba uzly konstantními výrazy: Levý operand je hodnota 1, a pravý operand je hodnota 2.

Celý příkaz je strom: Můžete začít na kořenovém uzlu a cestovat do každého uzlu ve stromu, abyste viděli kód, který tento příkaz tvoří:

  • Příkaz deklarace proměnné s přiřazením (var sum = 1 + 2;)
    • Deklarace implicitního typu proměnné (var sum)
      • Implicitní klíčové slovo var (var)
      • Deklarace názvu proměnné (sum)
    • Operátor přiřazení (=)
    • Binární součet výrazu (1 + 2)
      • Levý operand (1)
      • Operátor přičítání (+)
      • Pravý operand (2)

Předchozí strom může vypadat komplikovaně, ale je velmi silný. Po provedení stejného procesu rozložíte mnohem složitější výrazy. Představte si tento výraz:

var finalAnswer = this.SecretSauceFunction(
    currentState.createInterimResult(), currentState.createSecondValue(1, 2),
    decisionServer.considerFinalOptions("hello")) +
    MoreSecretSauce('A', DateTime.Now, true);

Předchozí výraz je také deklarace proměnné s přiřazením. V tomto případě je pravá strana přiřazení mnohem složitější strom. Nechcete rozebírat tento výraz, ale zvažte, jaké by mohly být jednotlivé uzly. Existují volání metod používající aktuální objekt jako příjemce, jedno volání má explicitního this příjemce, a jedno nemá. Existují volání metod pomocí jiných objektů přijímače, existují konstantní argumenty různých typů. A konečně, existuje binární operátor sčítání. V závislosti na typu návratové hodnoty z SecretSauceFunction() nebo MoreSecretSauce(), ten binární operátor sčítání může představovat volání metody přepsaného operátoru sčítání, přičemž se vyřeší jako statické volání metody binárního operátoru sčítání definovaného pro třídu.

Navzdory této vnímané složitosti vytváří předchozí výraz strukturu stromu, která se snadno prochází jako první ukázka. Nadále procházíte podřízené uzly a vyhledáte ve výrazu uzly typu list. Nadřazené uzly mají odkazy na své podřízené uzly a každý uzel má vlastnost, která popisuje, o jaký typ uzlu se jedná.

Struktura stromu výrazů je velmi konzistentní. Jakmile se naučíte základy, rozumíte i nejsložitějšímu kódu, když je reprezentovaný jako strom výrazu. Elegance v datové struktuře vysvětluje, jak kompilátor jazyka C# analyzuje nejsložitější programy jazyka C# a vytvoří správný výstup z takového složitého zdrojového kódu.

Jakmile se seznámíte se strukturou stromů výrazů, zjistíte, že získané znalosti vám umožní pracovat s mnoha pokročilejšími scénáři. Je neuvěřitelná síla ve výrazových stromech.

Kromě překladu algoritmů, které se mají spouštět v jiných prostředích, stromy výrazů usnadňují psaní algoritmů, které před spuštěním kontrolují kód. Napíšete metodu, jejíž argumenty jsou výrazy, a pak tyto výrazy prozkoumáte před spuštěním kódu. Strom výrazu je úplná reprezentace kódu: zobrazí se hodnoty jakéhokoli dílčího výrazu. Zobrazí se názvy metod a vlastností. Zobrazí se hodnota všech konstantních výrazů. Strom výrazů převedete na spustitelný delegát a spustíte kód.

Rozhraní API pro stromy výrazů umožňují vytvářet stromy, které představují téměř jakýkoli platný konstruktor kódu. Pokud ale chcete zachovat co nejjednodušší věci, některé idiomy jazyka C# se ve stromu výrazů nedají vytvořit. Jedním z příkladů jsou asynchronní výrazy (pomocí klíčových slov async a await). Pokud vaše potřeby vyžadují asynchronní algoritmy, budete muset manipulovat s Task objekty přímo, místo abyste se spoléhali na podporu kompilátoru. Dalším je vytváření smyček. Tyto smyčky obvykle vytvoříte pomocí for, foreach, while nebo do smyček. Jak vidíte později v této sérii, rozhraní API pro stromy výrazů podporují jeden výraz smyčky s výrazy break a continue, které řídí opakování smyčky.

Jedinou věcí, kterou nemůžete udělat, je úprava stromu výrazů. Stromy výrazů jsou neměnné datové struktury. Pokud chcete mutovat (změnit) strom výrazu, musíte vytvořit nový strom, který je kopií originálu, ale s požadovanými změnami.