식 트리는 일부 코드를 나타내는 데이터 구조입니다. 컴파일 및 실행 코드가 아닙니다. 식 트리가 나타내는 .NET 코드를 실행하려면 이를 실행 가능한 IL 명령으로 변환해야 합니다. 식 트리를 실행하면 값이 반환되거나 메서드 호출과 같은 작업만 수행할 수 있습니다.
람다 식을 나타내는 식 트리만 실행할 수 있습니다. 람다 식을 나타내는 식 트리는 형식 LambdaExpression 이거나 Expression<TDelegate>. 이러한 식 트리를 실행하려면 메서드를 Compile 호출하여 실행 가능한 대리자를 만든 다음 대리자를 호출합니다.
비고
대리자의 형식을 알 수 없는 경우, 즉 람다 식이 LambdaExpression 유형이 아니고 Expression<TDelegate> 유형인 경우, 직접 호출하지 말고 대리자에서 DynamicInvoke 메서드를 호출하십시오.
식 트리가 람다 식을 나타내지 않는 경우 메서드를 호출 Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) 하여 원래 식 트리를 본문으로 포함하는 새 람다 식을 만들 수 있습니다. 그런 다음 이 섹션의 앞부분에서 설명한 대로 람다 식을 실행할 수 있습니다.
함수에 대한 람다 식
모든 LambdaExpression 또는 람다Expression에서 파생된 모든 형식을 실행 파일 IL로 변환할 수 있습니다. 다른 식 형식은 코드로 직접 변환할 수 없습니다. 이 제한은 실제로 거의 영향을 미치지 않습니다. 람다 식은 실행 가능한 IL(중간 언어)로 변환하여 실행할 식의 유일한 형식입니다. (직접 System.Linq.Expressions.ConstantExpression를 실행하는 것이 무엇을 의미하는지 생각해 보십시오. 그것이 유용한 것을 의미할까요?) System.Linq.Expressions.LambdaExpression이거나 LambdaExpression
에서 파생된 형식인 모든 식 트리는 IL로 변환될 수 있습니다. 식 형식 System.Linq.Expressions.Expression<TDelegate> 은 .NET Core 라이브러리의 유일한 구체적인 예입니다. 대리자 유형에 매핑되는 수식을 나타내는 데 사용됩니다. 이 형식은 대리자 형식에 매핑되므로 .NET은 식을 검사하고 람다 식의 서명과 일치하는 적절한 대리자의 IL을 생성할 수 있습니다. 대리자 형식은 식 형식을 기반으로 합니다. 대리자 개체를 강력한 형식으로 사용하려면 반환 형식과 인수 목록을 알고 있어야 합니다. 메서드는 LambdaExpression.Compile()
형식을 반환합니다 Delegate
. 컴파일 시간 도구가 인수 목록 또는 반환 형식을 확인하도록 올바른 대리자 형식으로 캐스팅해야 합니다.
대부분의 경우 식과 해당 대리자 간에 간단한 매핑이 존재합니다. 예를 들어, Expression<Func<int>>
에 의해 표현된 식 트리는 Func<int>
형식의 대리자로 변환됩니다. 반환 형식 및 인수 목록이 있는 람다 식의 경우 해당 람다 식으로 표현되는 실행 코드의 대상 형식인 대리자 형식이 있습니다.
System.Linq.Expressions.LambdaExpression 형식은 식 트리를 실행 가능한 코드로 변환할 때 사용할 수 있는 LambdaExpression.Compile 및 LambdaExpression.CompileToMethod 멤버를 포함합니다. 메서드는 Compile
대리자를 만듭니다.
CompileToMethod
메서드는 식 트리의 컴파일된 출력을 나타내는 IL로 System.Reflection.Emit.MethodBuilder 개체를 업데이트합니다.
중요합니다
CompileToMethod
는 .NET Core 또는 .NET 5 이상이 아닌 .NET Framework에서만 사용할 수 있습니다.
선택적으로, 생성된 대리자 객체에 대한 기호 디버깅 정보를 수신하는 System.Runtime.CompilerServices.DebugInfoGenerator를 제공할 수 있습니다. 생성된 DebugInfoGenerator
대리자에 대한 전체 디버깅 정보를 제공합니다.
다음 코드를 사용하여 식을 대리자로 변환합니다.
Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);
다음 코드 예제에서는 식 트리를 컴파일하고 실행할 때 사용되는 구체적인 형식을 보여 줍니다.
Expression<Func<int, bool>> expr = num => num < 5;
// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();
// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));
// Prints True.
// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));
// Also prints True.
다음 코드 예제에서는 람다 식을 만들고 실행하여 숫자를 전력으로 올리는 것을 나타내는 식 트리를 실행하는 방법을 보여 줍니다. 지수로 제곱된 수를 나타내는 결과가 표시됩니다.
// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));
// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);
// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();
// Execute the lambda expression.
double result = compiledExpression();
// Display the result.
Console.WriteLine(result);
// This code produces the following output:
// 8
실행 및 생명주기
호출 LambdaExpression.Compile()
할 때 만든 대리자를 호출하여 코드를 실행합니다. 앞의 코드는 add.Compile()
대리자를 반환합니다.
func()
를 호출하여 대리자를 활성화하고 코드를 실행합니다.
해당 대리자는 식 트리의 코드를 나타냅니다. 해당 대리자의 핸들을 유지하고 나중에 호출할 수 있습니다. 나타내는 코드를 실행할 때마다 식 트리를 컴파일할 필요가 없습니다. 식 트리는 변경할 수 없으며 나중에 동일한 식 트리를 컴파일하면 동일한 코드를 실행하는 대리자가 만들어집니다.
주의
불필요한 컴파일 호출을 방지하여 성능을 향상시키는 더 정교한 캐싱 메커니즘을 만들지 마세요. 두 개의 임의 식 트리를 비교하여 동일한 알고리즘을 나타내는지 확인하는 것은 시간이 많이 걸리는 작업입니다. 추가 호출 LambdaExpression.Compile()
을 방지하여 저장하는 컴퓨팅 시간은 두 개의 서로 다른 식 트리가 동일한 실행 코드를 생성하는지 여부를 결정하는 코드를 실행하는 데 소요되는 시간보다 더 많을 수 있습니다.
제한 사항
람다 식을 대리자로 컴파일하고 해당 대리자를 호출하는 것은 식 트리를 사용하여 수행할 수 있는 가장 간단한 작업 중 하나입니다. 그러나 이 간단한 작업을 수행하더라도 주의해야 할 사항이 있습니다.
람다 식은 식에서 참조되는 모든 지역 변수에 대해 닫기를 만듭니다. 대리자의 일부가 되어야 하는 변수는 Compile
를 호출하는 위치에서, 그리고 결과 대리자를 실행할 때 모두 사용할 수 있도록 보장해야 합니다. 컴파일러는 변수가 범위에 있는지 확인합니다. 그러나 식이 IDisposable
를 구현하는 변수를 액세스하는 경우, 코드가 식 트리를 여전히 유지하고 있는 동안에도 개체를 삭제할 수 있습니다.
예를 들어, int
이 IDisposable
을 구현하지 않으므로 이 코드는 정상적으로 작동합니다.
private static Func<int, int> CreateBoundFunc()
{
var constant = 5; // constant is captured by the expression tree
Expression<Func<int, int>> expression = (b) => constant + b;
var rVal = expression.Compile();
return rVal;
}
대리자가 지역 변수 constant
에 대한 참조를 캡처했습니다.
CreateBoundFunc
에 의해 반환된 함수가 실행될 때, 이 변수는 언제든지 액세스됩니다.
그러나 다음의 (다소 인위적인) 클래스를 고려해보세요. 이 클래스는 System.IDisposable를 구현합니다.
public class Resource : IDisposable
{
private bool _isDisposed = false;
public int Argument
{
get
{
if (!_isDisposed)
return 5;
else throw new ObjectDisposedException("Resource");
}
}
public void Dispose()
{
_isDisposed = true;
}
}
다음 코드와 같이 식에서 사용하는 경우, System.ObjectDisposedException 속성에 의해 참조되는 코드를 실행할 때 Resource.Argument
를 얻게 됩니다.
private static Func<int, int> CreateBoundResource()
{
using (var constant = new Resource()) // constant is captured by the expression tree
{
Expression<Func<int, int>> expression = (b) => constant.Argument + b;
var rVal = expression.Compile();
return rVal;
}
}
이 메서드에서 반환된 대리자는 삭제된 constant
개체를 참조하고 있습니다. (이는 using
문장으로 선언되었기 때문에 처리되었습니다.)
이제 이 메서드에서 반환된 대리자를 실행하면 ObjectDisposedException
실행 시점에 throw됩니다.
확실히 컴파일 시간 구문을 나타내는 런타임 오류가 있는 것은 이상하게 보일 수 있지만, 그게 바로 식 트리를 다룰 때 들어가는 세계입니다.
이 문제의 순열이 많으므로 이를 피하기 위한 일반적인 지침을 제공하기는 어렵습니다. 식을 정의할 때는 지역 변수에 액세스하는 데 주의해야 하며, 공용 API를 통해 반환되는 this
식 트리를 만들 때 현재 개체(표현됨)의 상태에 액세스하는 데 주의해야 합니다.
식의 코드는 다른 어셈블리의 메서드 또는 속성을 참조할 수 있습니다. 식이 정의될 때, 컴파일될 때, 그리고 결과 대리자가 호출될 때 해당 어셈블리에 액세스할 수 있어야 합니다.
ReferencedAssemblyNotFoundException
는 존재하지 않는 경우에도 당신을 만나게 됩니다.
요약
람다 식을 나타내는 식 트리를 컴파일하여 실행할 수 있는 대리자를 만들 수 있습니다. 식 트리는 식 트리가 나타내는 코드를 실행하는 하나의 메커니즘을 제공합니다.
식 트리는 사용자가 만든 지정된 구문에 대해 실행할 코드를 나타냅니다. 코드를 컴파일하고 실행하는 환경이 식을 만드는 환경과 일치하는 한 모든 것이 예상대로 작동합니다. 그렇지 않은 경우, 오류는 예측 가능하며, 식 트리를 사용하는 코드의 첫 번째 테스트에서 오류가 포착됩니다.
.NET