다음을 통해 공유


식 트리 - 코드를 정의하는 데이터

표현식 트리는 코드를 정의하는 데이터 구조입니다. 식 트리는 컴파일러가 코드를 분석하고 컴파일된 출력을 생성하는 데 사용하는 것과 동일한 구조를 기반으로 합니다. 이 문서를 읽으면서 식 트리와 Roslyn API에서 분석기 및 CodeFixes를 빌드하는 데 사용되는 형식 간에 상당한 유사성을 알 수 있습니다. (분석기 및 CodeFixes는 코드에 대한 정적 분석을 수행하고 개발자를 위한 잠재적 수정 사항을 제안하는 NuGet 패키지입니다.) 개념은 비슷하며 최종 결과는 의미 있는 방식으로 소스 코드를 검사할 수 있는 데이터 구조입니다. 그러나 식 트리는 Roslyn API와는 다른 클래스 및 API 집합을 기반으로 합니다. 코드 줄은 다음과 같습니다.

var sum = 1 + 2;

위의 코드를 식 트리로 분석하면 트리에 여러 노드가 포함됩니다. 가장 바깥쪽 노드는 대입이 있는 변수 선언 문입니다(var sum = 1 + 2;) 가장 바깥쪽 노드에는 변수 선언, 할당 연산자 및 등호의 오른쪽을 나타내는 식과 같은 여러 자식 노드가 포함됩니다. 해당 식은 더하기 연산을 나타내는 식과 더하기 연산의 왼쪽 및 오른쪽 피연산자를 더 세분화합니다.

등호의 오른쪽을 구성하는 식을 좀 더 자세히 살펴봅시다. 1 + 2는 이진 식입니다. 더 구체적으로는 이진 추가 식입니다. 이진 추가 식에는 더하기 식의 왼쪽 및 오른쪽 노드를 나타내는 두 개의 자식이 있습니다. 여기서 두 노드는 상수 식입니다. 왼쪽 피연산자는 값 1이고 오른쪽 피연산자는 값 2입니다.

시각적으로 전체 문은 트리입니다. 루트 노드에서 시작하고 트리의 각 노드로 이동하여 문을 구성하는 코드를 볼 수 있습니다.

  • 할당이 있는 변수 선언 문(var sum = 1 + 2;)
    • 암시적 변수 형식 선언(var sum)
      • 암묵적 var 키워드(var)
      • 변수 이름 선언(sum)
    • 대입 연산자(=)
    • 이진 추가 식(1 + 2)
      • 왼쪽 피연산자(1)
      • 더하기 연산자(+)
      • 오른쪽 피연산자(2)

위의 트리는 복잡해 보일 수 있지만 매우 강력합니다. 동일한 프로세스에 따라 훨씬 더 복잡한 식을 분해합니다. 다음 식을 고려합니다.

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

앞의 식은 할당이 있는 변수 선언이기도 합니다. 이 경우 할당의 오른쪽은 훨씬 더 복잡한 트리입니다. 이 식을 분해하지 않고 다른 노드가 무엇인지 고려합니다. 현재 개체를 수신기로 사용하는 메서드 호출, 명시적 this 수신기가 있는 메서드 호출, 그렇지 않은 메서드 호출이 있습니다. 다른 수신기 개체를 사용하는 메서드 호출이 있으며 다양한 형식의 상수 인수가 있습니다. 마지막으로 이진 추가 연산자가 있습니다. SecretSauceFunction() 또는 MoreSecretSauce()의 반환 형식에 따라 해당 이진 더하기 연산자는 재정의된 더하기 연산자에 대한 메서드 호출이 될 수 있으며, 클래스에 대해 정의된 이진 더하기 연산자에 대한 정적 메서드 호출로 결정됩니다.

이러한 인식된 복잡성에도 불구하고 앞의 식은 첫 번째 샘플처럼 쉽게 탐색되는 트리 구조를 만듭니다. 자식 노드를 차례로 순회하여 식에서 리프 노드를 찾습니다. 부모 노드에는 자식에 대한 참조가 있으며 각 노드에는 노드의 종류를 설명하는 속성이 있습니다.

식 트리의 구조는 매우 일관적입니다. 기본 사항을 학습한 후에는 식 트리로 표현될 때 가장 복잡한 코드도 이해합니다. 데이터 구조의 우아함은 C# 컴파일러가 가장 복잡한 C# 프로그램을 분석하고 복잡한 소스 코드에서 적절한 출력을 만드는 방법을 설명합니다.

표현식 트리의 구조에 익숙해지면, 얻은 지식이 빠르게 다양한 고급 시나리오에 적용할 수 있도록 도와줍니다. 표현 트리에는 놀라운 강점이 있습니다.

다른 환경에서 실행할 알고리즘을 변환하는 것 외에도 식 트리를 사용하면 코드를 실행하기 전에 코드를 검사하는 알고리즘을 더 쉽게 작성할 수 있습니다. 인수가 식인 메서드를 작성한 다음 코드를 실행하기 전에 해당 식을 검사합니다. 식 트리는 코드의 전체 표현입니다. 모든 하위 식의 값이 표시됩니다. 메서드 및 속성 이름이 표시됩니다. 상수 식의 값이 표시됩니다. 식 트리를 실행 대리자로 변환하고 코드를 실행합니다.

식 트리에 대한 API를 사용하면 거의 모든 유효한 코드 구문을 나타내는 트리를 만들 수 있습니다. 그러나 가능한 한 간단하게 유지하기 위해 일부 C# 관용구를 식 트리에 만들 수 없습니다. 한 가지 예는 asyncawait 키워드를 사용하는 비동기 식입니다. 필요에 따라 비동기 알고리즘이 필요한 경우 컴파일러 지원에 의존하지 않고 개체를 Task 직접 조작해야 합니다. 또 다른 하나는 루프를 만드는 것입니다. 일반적으로 for, foreach, while 또는 do 루프를 사용하여 이러한 루프를 만듭니다. 이 시리즈의 뒷부분에서 볼 수 있듯이, 식 트리에 대한 API는 breakcontinue 표현을 사용하여 반복을 제어하는 단일 루프 식을 지원합니다.

수행할 수 없는 한 가지 작업은 식 트리를 수정하는 것입니다. 식 트리는 변경할 수 없는 데이터 구조입니다. 식 트리를 변경(변경)하려면 원본의 복사본이지만 원하는 변경 내용이 포함된 새 트리를 만들어야 합니다.