
Если поискать по Хабру статьи связанные с Expression, то можно найти несколько десятков страниц статей. Однако, я решил написать еще одну. Цель с которой я решил это сделать- разобрать деревья выражений для разработчиков которые еще не имели с ними дела. Для тех кому это кажется магией. И самое главное, показать для чего они могут пригодиться.
Начнем с делегатов и лямбд
Итак, читатель, я предполагаю, что у тебя есть общее знание что такое делегаты. Если нет, то лучше с ними ознакомиться перед прочтением статьи.
Что же такое Expression и при чем тут делегаты. Небольшое лирическое отступление.
Можно сказать, что делегаты это такие сущности, которые хранят ссылки на методы. Т.е. делегат может "хранить метод".
Например:
//Делегат someDelegate получает ссылку на метод SomeMethod
Func<int, int> someDelegate = SomeMethod;
//Переменная будет содержать 4
var result = someDelegate(3);
//Метод который мы помещаем в делегат
public int SomeMethod(int number)
{
return number+1;
}
Делегаты могут хранить не только ссылки на обычные методы, как показано выше. Но могут хранить анонимные методы определяемые через лямбду. Кстати, лямбда "под капотом" разворачивается в обычный метод.
//Делегат someDelegate получает ссылку на анонимный метод
Func<int, int> someDelegate = (x)=>x+1;
//Переменная будет содержать 4
var result = someDelegate(3);
То есть делегаты хранят метод, который при вызове делегата выполняется. Такие объявленные делегаты сразу компилируются в IL код и выполняются напрямую (без динамического создания кода на этапе выполнения программы в рантайме).
Но что делать, если мы заранее не знаем как должен будет работать метод? Например мы хотим убрать из кода какие-нибудь проверки или действия, при определенных условиях. Т.е. хотим создать сам метод на этапе работы программы (в рантайме). Тогда нам на помощь приходит класс Expression.
Переходим к Expression
Что же такое деревья выражений? Как понятно из слова "дерево" - это структура данных которая хранит данные в виде дерева (разновидности графа). И это дерево хранит исходный код программы.
После того как мы создадим дерево выражений мы можем скомпилировать из него обычный делегат который будет хранить ссылку на созданный метод.
Давайте попробуем сделать дерево выражений из примера выше, сделать из него лямбду, скомпилировать в делегат и выполнить. В этом мало практической пользы, но мы разберем как это работает.
Вот код нашего метода:
using System.Linq.Expressions;
//Вызываем метод который создает лямбду из дерева выражений
Expression<Func<int, int>> someExpression = GenerateExpression();
//Создаем делегат
Func<int, int> someDelegate = someExpression.Compile();
//Переменная будет содержать 4
var result = someDelegate(3);
//Метод который создает дерево выражений и создает из него лямбду
public static Expression<Func<int, int>> GenerateExpression()
{
//Создаем выражение параметр
var parameter = Expression.Parameter(typeof(int), "number");
//Создаем выражение константу
var constantOne = Expression.Constant(1, typeof(int));
//Создаем выражение операцию сложения
var addition = Expression.Add(parameter, constantOne);
//Возвращаем лямбду которая содержит первым параметром корень(верхушку) дерева,
//вторым параметром идут параметры принимаемые лямбдой
return Expression.Lambda<Func<int, int>>(addition, parameter);
}
А вот условная диаграмма нашего дерева и его компиляции. Смотреть нужно снизу вверх:

В пространстве имен System.Linq.Expressions несколько десятков классов, которые активно используются при построении деревьев выражений. Тут можно посмотреть их список и описание. Например, есть аналог оператора while — LoopExpression, оператора if‑else — ConditionalExpression или используемого выше оператора для бинарных операций (сложение, вычитание и т. д.) — BinaryExpression. Если мы хотим вызвать какой‑нибудь метод в дереве выражений, то используем —MethodCallExpression.
Важно добавить, что мы можем построить дерево выражений просто через лямбду (точнее его создаст компилятор).
Expression<Func<int, int>> someExpression = (x)=>x+1;
Но нужно помнить, что при создании дерева выражений через лямбду есть ограничения. Например, нельзя создавать лямбду из нескольких строчек.
Пример практического применения №1. Простой мэппер.
Как-то я попал на собеседование в одну крупную компанию. Интервьюер спросил меня, знаю ли я, почему так быстро работает библиотека Automapper, хотя она использует рефлескию. В то время я был мало знаком с выражениями, но предположил, что это из-за их применения. Как оказалось потом, я частично попал в точку.
Многие известные библиотеки мэпперов используют выражения. Скорость того же Automapper достигается засчет того, что для конвертирования одного типа класса в другой создается выражение, которое кэшируется (сохраняется, и в случае его нового использования переиспользуется). А на финальном этапе через рефлексию создается сам объект нового класса.
Давайте попробуем реализовать простенький мэппер, который будет работать через выражения. Скорость такого мэппера будет в десятки раз быстрее чем мэппинг "вручную", копированием полей одного класса в поля другого . И будет быстрее чем создание объектов через рефлексию.
Вот исходный код:
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
namespace SimpleExpressionMapper.Core;
public interface IMapper
{
//Мэппер простой, один метод для конвертирования и кэширования конвертируемых типов
TDestination Map<TSource, TDestination>(TSource source);
}
public class ExpressionMapper : IMapper
{
//Словарь для хранения делегатов производящих мэппинг
private readonly ConcurrentDictionary<TypesMapStorage, Delegate?> _delegatesStorage = new();
//Основной метод для вызова мэппинга.
//TSource - исходный тип для конвертации
//TDestination - тип в который будем конвертировать
public TDestination Map<TSource, TDestination>(TSource source)
{
var creatorDelegate = GetCreatorDelegate<TSource, TDestination>();
if(creatorDelegate == null)
throw new NullReferenceException("The creator delegate is null.");
return creatorDelegate(source);
}
//Метод, который возвращает делегат для мэппинга.
private Func<TSource, TDestination>? GetCreatorDelegate<TSource, TDestination>()
{
//Получаем ключ для словаря.
var key = new TypesMapStorage(typeof(TSource), typeof(TDestination));
//Если делегата еще нет, то он создается и сохраняется в словарь _delegatesStorage.
//Если есть нужный делегат в словаре, то просто возвращаем его.
if (_delegatesStorage.TryGetValue(key, out var creatorDelegate))
{
return creatorDelegate as Func<TSource, TDestination>;
}
else
{
var newCreatorDelegate = CompileCreatorDelegate<TSource, TDestination>();
_delegatesStorage.GetOrAdd(key, newCreatorDelegate);
return newCreatorDelegate;
}
}
//Метод для компиляции делегата конвертации из дерева выражений
private static Func<TSource, TDestination> CompileCreatorDelegate<TSource, TDestination>()
{
//Извлекаем свойства из исходного типа(класса) и типа в который будем конвертировать.
//Проверяем, что свойство результирующего типа не только для чтения.
var sourceTypeProperties = typeof(TSource).GetProperties(BindingFlags.Instance|BindingFlags.Public);
var destinationTypeProperties = typeof(TDestination).GetProperties(BindingFlags.Instance|BindingFlags.Public)
.Where(p=> p.CanWrite)
.ToList();
//Сопоставляем совпадающие имена свойства исходного и результирующего типа.
//И проверяем что тип свойства в обоих классах одинаковый (например int и int).
var propertiesPair = sourceTypeProperties
.Join(destinationTypeProperties,
s => s.Name,
d => d.Name,
(s, d) => (sourceProperty: s, destinationProperty: d))
.Where(p => p.sourceProperty.PropertyType.IsAssignableTo(p.destinationProperty.PropertyType))
.ToList();
//Выражение для создания параметра лямбды, по сути он будет компилироваться в такое лямбда выражение: (source) => ...
var sourceTypeParameter = Expression.Parameter(typeof(TSource), "source");
//Выражение для присвоения значения свойств исходного типа свойствам результирующего типа.
var propertiesBindings = propertiesPair.Select(p=>
Expression.Bind(p.destinationProperty, Expression.Property(sourceTypeParameter, p.sourceProperty)));
//Выражение для создания объекта результирующего типа, и назначения свойств
var createInstanceExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), propertiesBindings);
//Компиляция делегата для мэппинга
return Expression.Lambda<Func<TSource, TDestination>>(createInstanceExpression, sourceTypeParameter).Compile();
}
}
//А это у нас ключ для кэширования в словарь делегата. Он состоит из исходного и результирующего типа.
//Как вы помните метод GetHashCode(и Equals) в record переопределяется автоматически.
public record TypesMapStorage(Type SourceType, Type DestinationType);
А вот пример использования:
using SimpleExpressionMapper.Core;
//Исходный объект класса
var order = new Order
{
OrderId = 1,
OrderName = "OrderName",
OrderDate = DateTime.Now
};
var mapper = new ExpressionMapper();
var orderDto = mapper.Map<Order, OrderDto>(order);
//Так как делегат уже создан, при втором вызове с одинаковыми типами,
//он достанется из кэша (словаря)
var orderDto1 = mapper.Map<Order, OrderDto>(order);
var orderDto2 = mapper.Map<Order, OrderDto2>(order);
Console.Read();
//Исходный тип
public class Order
{
public int OrderId { get; set; }
public string OrderName { get; set; } = null!;
public DateTime OrderDate { get; set; }
}
//Результирующий тип
public class OrderDto
{
public int OrderId { get; set; }
public string OrderName { get; set; } = null!;
public DateTime OrderDate { get; set; }
}
//Еще один результирующий тип
public class OrderDto2
{
public int OrderId { get; set; }
public string OrderName { get; set; } = null!;
public DateTime OrderDate { get; set; }
}
Так как при втором вызове мэппира с одинаковыми типами
mapper.Map
<Order, OrderDto>(order)
, делегат уже будет в кэше (словаре), и нам не придется его создавать, мэппинг отработает в разы быстрее. В методе создания дерева выражения используется рефлексия только для получения свойств.
Надо сказать, что этот пример просто демонстрирует пример использование Expression. Чтобы довести код до "рабочего" применения, необходимо добавить проверки на null, является ли конструктор типа публичным, сделать рекурсивный мэппинг вложенных типов и т.д. Если вы хотите написать мэппер для себя и знать как работает его код, то оно того стоит.
Пример практического применения №2. Пишем методы расширения для Entity Framework Core
Если вы читаете это статью, и заинтересовались деревьями выражений, то думаю, что вы уже сталкивались с ними в самой популярной для.NET ORM — Entity Framework Core. Хотя, не все кто с ней работают лезут «под капот» и смотрят как это все устроенно изнутри. Обычно для собеседований достаточно знать, что для создания запросов используется интерфейс IQueryable. Его использование позволяет не дергать СУБД(базу данных) каждый раз когда мы что‑то делаем при состовлении запроса (например фильтруем), а сформировать и выполнить запрос только после его составления.
Предлагаю взлянуть немного глубже, для общего понимания как все это устроено изнутри. А затем самим реализовать метод расширение для запроса Entity Framework Core. Вот интерфейс IQueryable:
namespace System.Linq
{
//Предоставляет функциональность для оценки запросов по конкретному источнику данных, в котором тип данных не указан.
public interface IQueryable : IEnumerable
{
//Получает дерево выражения, которое связано с экземпляром IQueryable
Expression Expression { get; }
//Получает тип элемента(ов), которые возвращаются, когда выполняется дерево выражения, связанное с этим экземпляром IQueryable
Type ElementType { get; }
//Получает поставщик запросов(провайдера), который связан с этим источником данных
IQueryProvider Provider { get; }
}
}
Как мы видим интерфейс IQueryable наследуется от IEnumerable. Что это значит? Это значит, что мы по прежнему работаем с IEnumerable (c набором каких-то объектов), только теперь у этого IEnumerable появились дополнительные свойства. Мы видим что появляется дерево выражений и провайдер конкретной СУБД которую мы будем использовать. То есть будет создаваться дерево выражений которое что-то будет делать с набором данных(фильтровать, изменять), а затем этот Expression и результирующий набор данных будет провайдером превращаться в запрос к СУБД.
Давайте еще взглянем на IQueryProvider, который создает конечный код запроса в СУБД
public interface IQueryProvider
{
//Создает новый объект IQueryable который может создать запрос
//по дереву выражений передаваемое в параметрах метода.
IQueryable CreateQuery(Expression expression);
//Создает новый объект IQueryable который может создать запрос
//по дереву выражений передаваемое в параметрах метода.
IQueryable<TElement> CreateQuery<TElement>(Expression expression);
//Выполняет запрос который предствлен деревом выражений к СУБД.
object? Execute(Expression expression);
//Выполняет запрос который предствлен деревом выражений к СУБД.
//Результат запроса будет определенного типа - TResult
TResult Execute<TResult>(Expression expression);
}
И еще для примера взглянем на метод Where который применяется в Linq IQueryable:
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);
return source.Provider.CreateQuery<TSource>(
Expression.Call(
null,
new Func<IQueryable<TSource>, Expression<Func<TSource, bool>>, IQueryable<TSource>>(Where).Method,
source.Expression,
Expression.Quote(predicate)
)
);
}
Теперь попробуем разобраться как это работает на примере метода Where.
Метод получает объект IQueryable - this IQueryable<TSource> source
и выражение Expression<Func<TSource, bool>> predicate
для изменения дерева выражений в исходном IQueryable<TSource> source
.
Дерево выражений в исходном IQueryable<TSource> source
хранится в его свойстве Expression Expression { get; }
Затем в провайдере полученного исходного IQueryable<TSource> source
мы вызываем метод провайдера, который создает новый объект IQueryable<TSource> source
с новым деревом выражений — свойством Expression Expression { get; }
Вызов для создания нового объекта IQueryable<TSource>
с новым деревом выражений происходит в строчке — source.Provider.CreateQuery<TSource>(
.
Внутрь этого метода передается выражение вызова самого метода Where, и параметры для этого метода (исходное выражение — source.Expression
и само выражение предиката (лямбда выражение) — Expression.Quote(predicate)
)
Теперь попробуем реализовтаь несколько методов которые делают более удобным функционал Where:
public static class SimpleQueryableExtensions
{
// Метод фильтрует все свойства которые содержат текст
public static IQueryable<T> WhereAnyPropertyContainText<T>(
this IQueryable<T> source,
string searchText,
params Expression<Func<T, string>>[] properties)
{
//Это параметр, который будет использоваться во всех лямбдах массива properties, например x=>x.Customer.FirstName
//Имя параметра x
ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
//Достаем метод Contains для вызова поиска текста
MethodInfo? containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
if (containsMethod == null)
throw new InvalidOperationException("Contains method not found.");
Expression body = Expression.Constant(false);
//Проходим по всему массиву лямбд properties, и для каждого выражения
//проверяем содержание текста searchText через вызов метода Contains.
//Если выражение(свойство) содержит текст, то добавляем выражение к результирующему дереву выражений
foreach (Expression<Func<T, string>> property in properties)
{
// Заменяем параметр исходного выражения на наш параметр
// если имя параметр в лямбде не x, то меняем лямбду с o => o.ProductName на x => x.ProductName
// Имя параметра x задано выше в переменной parameter.
// Если не менять имена параметров в лямбдах, и они буду разные, то будет ошибка
Expression propertyAccess = ReplaceAnotherParameter(property.Body, property.Parameters[0], parameter);
MethodCallExpression containsCall = Expression.Call(propertyAccess, containsMethod, Expression.Constant(searchText));
body = Expression.OrElse(body, containsCall);
}
//Возвращаем результирующую лямбду
//На дебаге можно поставить точку останова и посмотреть ее
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, parameter);
return source.Where(lambda);
}
//Метод фильтрует по свойству типа DateTime, и выбирает все сущности во временном диапазоне
public static IQueryable<T> WhereDateTimeBetween<T>(this IQueryable<T> source,
Expression<Func<T, DateTime>> dateSelector,
DateTime startDate, DateTime endDate)
{
//Параметер используемый в лямбде
ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
//Преобразуем делегат в аргумент(результирующую дату)
InvocationExpression dateAccess = Expression.Invoke(dateSelector, parameter);
//Сравниваем что дата больше startDate
BinaryExpression lowerBound = Expression.GreaterThanOrEqual(dateAccess, Expression.Constant(startDate));
//Сравниваем что дата меньше endDate
BinaryExpression upperBound = Expression.LessThanOrEqual(dateAccess, Expression.Constant(endDate));
//Проводим операцию AND, если оба предыдущих выражения верны возвращаем True
BinaryExpression body = Expression.AndAlso(lowerBound, upperBound);
//Возвращаем результирующую лямбду
//На дебаге можно поставить точку останова и посмотреть ее
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, parameter);
return source.Where(lambda);
}
//Метод фильтрует сущности по массиву лямбд predicates.
//Если все лямбды возвращают true, то сущность добавляется в набор
public static IQueryable<T> WhereAndConditions<T>(this IQueryable<T> source,
params Expression<Func<T, bool>>[] predicates)
{
return source.WhereConditions(ConditionType.And, predicates);
}
//Метод фильтрует сущности по массиву лямбд predicates.
//Если одна из лямбд возвращают true, то сущность добавляется в набор
public static IQueryable<T> WhereOrConditions<T>(this IQueryable<T> source,
params Expression<Func<T, bool>>[] predicates)
{
return source.WhereConditions(ConditionType.Or, predicates);
}
//Метод фильтрует сущности по массиву лямбд predicates.
private static IQueryable<T> WhereConditions<T>(this IQueryable<T> source, ConditionType conditionType, params Expression<Func<T, bool>>[] predicates)
{
if (predicates.Length == 0)
return source;
if (predicates.Length == 1)
return source.Where(predicates[0]);
//Параметр используемый в лямбдах
ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
// Заменяем параметр исходного выражения на наш параметр
// если имя параметр в лямбде не x, то меняем лямбду с o => o.ProductName на x => x.ProductName
// Имя параметра x задано выше в переменной parameter.
// Если не менять имена параметров в лямбдах, и они буду разные, то будет ошибка
List<Expression<Func<T, bool>>> replacedPredicates = predicates.Select(p =>
{
var newBody = ReplaceAnotherParameter(p.Body, p.Parameters[0], parameter);
return Expression.Lambda<Func<T, bool>>(newBody, parameter);
}).ToList();
Expression body = replacedPredicates[0].Body;
//Проходим по всему массиву лямбд replacedPredicates, и для каждого выражения
//Строим новое дерево выражений body, в зависимости от условия добавляем сравнение AND или OR
for (int i = 1; i < replacedPredicates.Count; i++)
{
body = conditionType switch
{
ConditionType.And => Expression.AndAlso(body, replacedPredicates[i].Body),
ConditionType.Or => Expression.OrElse(body, replacedPredicates[i].Body),
_ => throw new ArgumentOutOfRangeException(nameof(conditionType), conditionType, null)
};
}
//Возвращаем результирующую лямбду
//На дебаге можно поставить точку останова и посмотреть ее
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, parameter);
return source.Where(lambda);
}
//Перечисление для изменения типа операции
private enum ConditionType
{
And,
Or
}
//Вспомогательный класс для замены старого параметра на новый
private static Expression ReplaceAnotherParameter(
Expression expression,
ParameterExpression oldParameter,
ParameterExpression newParameter)
{
ParameterReplacerVisitor visitor = new ParameterReplacerVisitor(oldParameter, newParameter);
return visitor.Visit(expression);
}
//Класс наследуется от ExpressionVisitor
//ExpressionVisitor - это специальный класс, который позволяет пройти по всем узлам
//дерева выражений (ExpressionTree) и модифицировать их так как нам нужно.
private class ParameterReplacerVisitor(
ParameterExpression oldParameter,
ParameterExpression newParameter) : ExpressionVisitor
{
//Переопределяем базовый метод VisitParameter, и заменяем старый параметр в лямбде на новый
// то есть, если старый параметр x а новый y, то меняем лямбда поменяется с x=>x+1 на y=>y+1
protected override Expression VisitParameter(ParameterExpression node)
{
return node == oldParameter ? newParameter : base.VisitParameter(node);
}
}
}
Я постарался подробно описать в комментариях что происходит в методах. Но будет понятнее, если вы поробуете открыть этот код в своей IDE и запустить/поменять.
Ниже простой пример использования этих методов на примере использования СУБД SQLServer:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
await using var dbContext = new SimpleDbContext();
var isDbCreated = await dbContext.Database.EnsureCreatedAsync();
if (isDbCreated)
{
await dbContext.AddEntities();
}
//Фильтр по нескольким условиям, выбираются сущности для которых одно из условий верно.
var ordersWithOrConditions = dbContext.Orders
.Include(o => o.Customer)
.WhereOrConditions(o => o.Customer.FirstName == "John",
o => o.ProductName == "Onion")
.ToList();
//Фильтр по нескольким условиям, выбираются сущности для которых все условия верны.
var ordersWithAndConditions = dbContext.Orders
.Include(o => o.Customer)
.WhereAndConditions(o => o.Customer.FirstName == "John",
o => o.ProductName == "Onion")
.ToList();
//Фильтр по дате, в указанном диапазоне дат.
var ordersInDateTimeRange = dbContext.Orders
.WhereDateTimeBetween(x => x.DateTime, new DateTime(2025, 4, 25), DateTime.Now)
.ToList();
//Фильтр по наличию подстроки в строковых свойствах
var ordersWithPropertiesContainsText = dbContext.Orders
.Include(o=>o.Customer)
.WhereAnyPropertyContainText("e",
o => o.Customer.FirstName,
o => o.ProductName)
.ToList();
Console.Read();
public class Simpl eDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=localhost;Database=ExpressionDb;TrustServerCertificate=True;");
}
}
public static class EntityCreator
{
public static async Task AddEntities(this SimpleDbContext context)
{
var customer1 = new Customer
{
FirstName = "John",
LastName = "Doe",
Age = 15
};
var customer2 = new Customer
{
FirstName = "Petr",
LastName = "Petrov",
Age = 30
};
var customer3 = new Customer
{
FirstName = "Pettr",
LastName = "Pettrov",
Age = 31
};
await context.Customers.AddRangeAsync(customer1, customer2, customer3);
await context.SaveChangesAsync();
var order1 = new Order
{
OrderNumber = "1",
ProductName = "Tomato",
DateTime = DateTime.Now - TimeSpan.FromDays(3),
Customer = customer1
};
var order2 = new Order
{
OrderNumber = "1",
ProductName = "Onion",
DateTime = DateTime.Now - TimeSpan.FromDays(2),
Customer = customer1
};
var order3 = new Order
{
OrderNumber = "1",
ProductName = "Banana",
DateTime = DateTime.Now - TimeSpan.FromDays(1),
Customer = customer2
};
var order4 = new Order
{
OrderNumber = "1",
ProductName = "Chery",
DateTime = DateTime.Now,
Customer = customer3
};
await context.Orders.AddRangeAsync(order1, order2, order3, order4);
await context.SaveChangesAsync();
}
}
public class Order
{
public int Id { get; set; }
[MaxLength(250)]
public string OrderNumber { get; set; } = null!;
[MaxLength(250)]
public string ProductName { get; set; } = null!;
public DateTime DateTime { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
}
public class Customer
{
public int Id { get; set; }
[MaxLength(250)]
public string FirstName { get; set; } = null!;
[MaxLength(250)]
public string LastName { get; set; } = null!;
public int Age { get; set; }
public ICollection<Order> Orders { get; set; } = null!;
}
Подводим итог. Нюансы. Expression и AOT
Как вы уже поняли, Expression это стоящая штука. И все было хорошо и просто с применеием Expression, пока не появился AOT (здравствуй.Net 7).
AOT (Ahead Of Time) — это возможность компилировать IL код(код программы) в нативный код платформы (операционной системы). Такая компиляция, позволяет нам не использовать CLR, а сразу запускать программу в операционной системе без прослоек (без промежуточной компиляции в процессе исполнения.). И это дает сильный прирост в скорости выполнения. И старые посылы, что не стоит писать нагруженный код на C# уходят в прошлое.
Но у AOT есть ложка дегтя, Expession из первого примера не будет компилироваться в AOT. Проблема в том что Expression генерирует код в процессе работы программы (в рантайме), а AOT не поддерживат такой генерации. Также, в целом с рефлексией, как мы привыкли с ней работать раньше, мы работать в AOT не сможем. Об этом есть хорошая статья здесь.
Так как Entity Framework Core работает с Expression и использует рефлексию (использует генерацию кода в рантайме), то стал несовместим с AOT. У Microsoft началась работа по его адаптации. И уже к 10 версии EF Core ожидается полноценная поддержка. На данный момент поддержка у EF Core — «эксперементальная».
Lewigh
Спасибо за статью.
Смотря что подразумевать под "нагруженный код". С одной стороны, C# всегда был богат инструментами для серьезных оптимизаций и выжать можно было много. С другой там где это по настоящему важно он будет сливать С++, Rust etc хоть с AOT хоть без него.
Kerman
Вообще не факт. JIT-компилятор ориентируется на платформу, на которой запущен и может оптимизировать код под фичи процессора. А классическая картина для скомпилированных крестами бинарей - заточка под все процы, включая говно мамонта. И да, Native AOT работает медленнее IL кода с JIT. Но зато запускается моментально.
Lewigh
Старые истории про волшебный JIT который секретные оптимизации делает в рантайме и виртуалка начинает превосходит нативные языки. Что в Java что в C# одни и те же истории. Только почему то все забывают про серьезные накладные расходы самой виртуальной машины, которые для начала ей нужно как то компенсировать. И почему то никто не задумывается что для нативного языка тоже можно сделать PGO.
Kerman
Вы разберитесь сначала, что за накладные расходы виртуальной машины такие, потом говорите. Для начала узнайте, что такое JIT-компилятор. И почему он называется "компилятор".
Если для вас векторные инструкции вроде AVX - это секретные оптимизации, то я вас слегка огорчу. Они ни для кого не секретные.
Lewigh
Неужели? Ну хорошо, вот мои тезисы человека несомненной непонимающего ничего.
Накладные расходы на загрузку и верификацию метаданных. Виртуальная машина не может быть уверена в корректности загружаемого динамически промежуточного кода так что вынуждена тратить время на проверку корректности метаданных.
Накладные расходы на поиски различных метаданных. К примеру когда метод вызывается в первый раз, его для начала нужно вообще найти, быть может его вообще не существует. После того как он найден, нужно проверить его корректность. Далее нужно провести JIT-компиляцию, скорректировать все метаданные и переходники т.д. Далее каждый вызов будет происходить через переходники а не напрямую. Не поверите, но все эти манипуляции далеко не бесплатные. И это все описание "прямого вызова". Вызов интерфейсного метода будет сопровождаться куда более увлекательной беготней по itables.
Если виртуальная машина хочет что-то оптимизировать, ей нужно собирать статистику использование. Но как понимаете, это не бесплатно. Когда статистика собрана то нужно проводить оптимизацию прямо во время работы программы.
Ах да. Еще у нас кроссплатформенность, поэтому Вы не забиваете себе голову такими вопросами как: в какой кодировке находиться имя полученного файла. Удивительно, но в разных системах разные кодировки а работаем мы с ними одинаково, интересно почему? Может быть потому что для нашего удобства внутри приложения выделяется куча строк в едином формате чтобы нам было удобно.
Умеет виртуальная машина определять конкретное оборудование и проводить оптимизации под него, разумеется во время исполнения программы. Ну так, берете какой-нибудь Rust и делаете PGO и вуаля, мы тоже не лыком шиты.
Разумеется, все вышеперечисленное происходит бесплатно, потому что это волшебная магия.
Вот именно что ни для кого. Удивительно что по Вашей логике языки на VM в это чудо умеют а нативные - нет. А еще удивительней, что VM языки настолько в это умеют что в C# целый API сделан для ручной работы с векторными инструкциями и нигде не пишут мол - выбросьте из головы, чудо JIT сам решит все ваши проблемы. А в Java, учитывая что там есть векторизация на уровне JIT, почему то пилят ручной Vector API, интересно, зачем они это делают если должно произойти чудо и все само должно оптимизироваться?
Kerman
Нет в дотнете виртуальной машины.
Бред. Это компилируемый язык. Если метода нет, код не скомпилируется.
Нет.
Это вы сейчас с джавовым хотспотом путаете. Впрочем там тоже не так всё просто.
Лол
Лол нет. Во время компиляции. Виртуальной машины не существует.
Всё вышеперечисленное просто не существует.
Именно. Потому что для поддержки инструкций нужно компилить с поддержкой инструкций. Дотнет это может делать на целевой машине, а кресты - нет.
FFS_Studios
https://learn.microsoft.com/en-us/dotnet/standard/clr
https://learn.microsoft.com/en-us/dotnet/api/system.missingmethodexception?view=net-9.0
https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/jit/ryujit-overview.md#execution-environment-and-external-interface
Kerman
CLR - это не виртуальная машина. Это рантайм. Там нет никакой прослойки между исполняемым кодом и ОС.
MissingMethodException срабатывает только на typeof(App).InvokeMember(), что является частью рефлекшена.
Про ссылку на RyuJIT не понял ничего. Тоже компилер. Он не виртуальная машина. Интерфейс его - это рантайм компиляция IL кода из рефлекшена.
И картинка здесь зря. Не разбираетесь - не лезьте.
FFS_Studios
Вам в последней ссылке белым по черному написано:
Kerman
Мне всё равно, что там написано. Clr не является виртуальной машиной в том смысле, в котором мы спорим. Там нет накладных расходов на работу кода. Это рантайм-библиотека вроде stdlib, только с GC, JIT и другими вещами. Этот вопрос ещё 2009 году обсуждался на SO
https://stackoverflow.com/questions/1564348/is-the-clr-a-virtual-machine
FFS_Studios
Мы не о чем не спорим, здесь нет места спору вообще.
Прочитайте свою же ссылку еще разок и подумайте, и не вводите людей в заблуждение.
EasyGame
Если CLR виртуальная машина для шарпа, то по этой логике - libc это виртуальная машина для си :D
Lewigh
Официальный сайт компании Microsoft.
Официальный .NET глоссарий.
https://learn.microsoft.com/ru-ru/dotnet/standard/glossary
https://learn.microsoft.com/en-us/dotnet/standard/glossary
На любых языках на Ваш выбор. Официально сами Майки, на русском и английском, недвусмысленно пишут прямым текстом что:
Если у Вас какие то свои термины отличимые от общепринятых, Ваше право. Хотите спорить дальше? Предлагаю написать официальное письмо в Microsoft где Вы можете изложить что они, в отличии от Вас, все глупые и не понимают что технология которую они создали, на самом деле не использует виртуальную машину. От дальнейшего принятия участия в спорах о терминах, которые Вы придумали сами для себя, воздержусь.
Kerman
А давайте вы сначала покажете, где в этой "виртуальной машине" находится резолв методов и всё прочее, о чём мы спорили.
Потому что сейчас вы пытаетесь натянуть сову на глобус, цепляетесь к терминологии.
Да называйте чем хотите. Я с первого сообщения пытаюсь донести мысль, что в дотнете код запускается точно также, как и нативный. Там нет никакой песочницы. Это нативно скомпилированный код, а не какая-то интерпретационная машина. И даже не виртуальная машина, потому что там нет виртуализации.
Давайте, покажите, какие издержки у этой "виртуальной машины". Что там "тормозит". По какой причине код шарпа в принципе не может быть быстрее c++/rust.
И очень забавно, что у @FFS_Studiosбыл только один коммент и только под вашей статьёй. Совпадение? И один плюс в карму при одном комменте. Вот же везунчик, а?