Обрезка и ограничения отражения
Обрабатывайте предупреждения об обрезке, используйте [DynamicallyAccessedMembers] и заменяйте отражение во время выполнения альтернативами, сгенерированными из исходного кода.
«Обрезка и ограничения отражения» — бесплатный урок C# Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Что такое удаление IL-кода
Удаление IL-кода — это оптимизация во время публикации, которая удаляет неиспользуемые типы, методы и сборки из выходных данных. В результате исполняемый файл становится меньше. Эта возможность автоматически включается для Native AOT и автономных публикаций одного файла.
Включение удаления кода
Включите удаление кода в файле проекта. Вы можете управлять режимом удаления — от partial (удаляются только неиспользуемые сборки) до full (удаляются неиспользуемые члены внутри сборок).
// .csproj
<PropertyGroup>
<!-- Enable trimming on publish -->
<PublishTrimmed>true</PublishTrimmed>
<!-- full: aggressive — trims unused members within assemblies
partial: conservative — only removes unused assemblies -->
<TrimMode>full</TrimMode>
<!-- Treat all trimmer warnings as errors (recommended for CI) -->
<TrimmerRootAssemblies>MyApp</TrimmerRootAssemblies>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
// Publish:
dotnet publish -c Release -r linux-x64 --self-containedКак работает инструмент удаления кода
Инструмент удаления кода выполняет анализ достижимости, начиная с точек входа (Main, зарегистрированных служб и атрибутов). Любой код, недостижимый из этих корней, удаляется.
// Example: only MyApp.Program.Main is a root
// Code flow:
// Main → WebApplication.Run → MapGet → MyHandler
// Everything else is trimmed
// What gets KEPT:
class MyHandler { public string Handle() => "ok"; }
// What gets TRIMMED (if not reached):
class UnusedService { public void DoWork() { } }
// Problem: reflection can access UnusedService at runtime
// But the trimmer can't know that at compile time
// → UnusedService is trimmed → MissingMethodException at runtimeОтражение и удаление кода
Отражение — основной источник несовместимости с удалением кода. Инструмент удаления кода не может статически проанализировать, к каким типам вы обратитесь через Type.GetType() или Activator.CreateInstance().
// PROBLEMATIC: type name comes from config at runtime
var typeName = config["Plugin:Type"]!;
var type = Type.GetType(typeName); // type may have been trimmed!
var instance = Activator.CreateInstance(type!); // MissingMethodException
// PROBLEMATIC: LINQ expressions with reflection
var query = dbContext.Set<T>() // T discovered via reflection
.Where(BuildExpression<T>("Name", "Alice"));
// PROBLEMATIC: attribute scanning
var handlers = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.HasCustomAttribute<HandlerAttribute>());
// All handler types may be trimmed awayАтрибут DynamicDependency
[DynamicDependency] сообщает инструменту удаления кода, что необходимо сохранить определенные члены, к которым, как вам известно, будет осуществляться доступ через отражение.
// Keep all public constructors of MyPlugin:
[DynamicDependency(
DynamicallyAccessedMemberTypes.PublicConstructors,
typeof(MyPlugin))]
public static IPlugin CreatePlugin()
=> (IPlugin)Activator.CreateInstance(typeof(MyPlugin))!;
// Keep a specific method by name:
[DynamicDependency("ProcessOrder", typeof(OrderHandler))]
public static void Bootstrap() { }
// Keep everything on a type (use sparingly):
[DynamicDependency(
DynamicallyAccessedMemberTypes.All,
typeof(LegacyReflectionHelper))]
public static void EnsurePreserved() { }Аннотация DynamicallyAccessedMembers
[DynamicallyAccessedMembers] снабжает параметры, поля и свойства аннотациями, сообщающими инструменту удаления кода, к каким членам Type будет осуществляться доступ через отражение во время выполнения.
// Annotate a method parameter:
public void Register(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
Type serviceType)
{
// trimmer knows to preserve public constructors of whatever Type is passed
var instance = Activator.CreateInstance(serviceType)!;
}
// Annotate a generic parameter:
public static T Create<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
T>() where T : new()
=> new T();
// Annotate a field:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
private Type _modelType = typeof(OrderModel);RequiresUnreferencedCode
Помечайте методы, принципиально зависящие от отражения, атрибутом [RequiresUnreferencedCode]. Это передает вызывающему коду предупреждения, чтобы было понятно: после удаления кода он может перестать работать.
// Mark a method that uses reflection internally:
[RequiresUnreferencedCode("Uses Type.GetType() — not safe for trimming")]
public static object LoadPlugin(string typeName)
{
var type = Type.GetType(typeName)!
?? throw new InvalidOperationException($"Type {typeName} not found");
return Activator.CreateInstance(type)!;
}
// Callers will see a trimmer warning:
// IL2026: Members decorated with [RequiresUnreferencedCode]
// need their callers to be decorated with the same attribute
// or be suppressed with [UnconditionalSuppressMessage]
// The warning reminds callers to use a trim-safe alternative
// or add a DynamicDependency for the types they expectБезопасные при удалении кода альтернативы
Для многих подходов с активным использованием отражения в современном .NET существуют безопасные при удалении кода альтернативы. Предпочитайте их, чтобы избежать проблем с удалением кода.
// INSTEAD OF: Type.GetType(name) + Activator.CreateInstance
// USE: registered factories or switch expressions:
public IHandler CreateHandler(string name) => name switch
{
"order" => new OrderHandler(),
"email" => new EmailHandler(),
_ => throw new ArgumentException($"Unknown: {name}")
};
// INSTEAD OF: reflection-based JSON serialization
// USE: source-generated JsonSerializerContext
// INSTEAD OF: attribute scanning via Assembly.GetTypes()
// USE: compile-time source generators
// INSTEAD OF: dynamic proxy libraries (Castle, DispatchProxy)
// USE: source-generated interceptors (.NET 8+)XML-дескриптор для удаления кода
Используйте корневой XML-дескриптор, чтобы сохранить целые пространства имен или сборки, которые инструмент удаления кода не может проанализировать, — например, сторонние библиотеки с активным использованием отражения.
<!-- TrimmerRoots.xml -->
<linker>
<!-- Keep everything in LegacyLib -->
<assembly fullname="LegacyLib">
<type fullname="LegacyLib.*" preserve="all" />
</assembly>
<!-- Keep a specific type -->
<assembly fullname="MyApp">
<type fullname="MyApp.Plugins.PluginLoader" preserve="all" />
</assembly>
</linker>
// Reference in .csproj:
<ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>Практический пример: устранение предупреждений об удалении кода
Практический процесс устранения предупреждений инструмента удаления кода в реальной кодовой базе.
// Step 1: enable analyzer during development
// .csproj:
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
// Step 2: build and read warnings:
// warning IL2026: 'MyMapper.MapDynamic(Type)' uses
// [RequiresUnreferencedCode] via 'Type.GetMethod'
// Step 3: fix the root cause:
// BEFORE:
public void MapDynamic(Type t)
{
var method = t.GetMethod("MapFrom")!; // IL2026
method.Invoke(null, null);
}
// AFTER: use an interface instead of reflection
public void Map(IMapper mapper) => mapper.MapFrom();
// Step 4: if fix is not feasible, add annotation and document:
[RequiresUnreferencedCode("Uses reflection: ensure target types are preserved")]
public void MapDynamic(Type t) { /* ... */ }Быстрая проверка
Что сообщает атрибут [DynamicallyAccessedMembers] инструменту удаления IL-кода?
Итоги: ограничения удаления кода и отражения
Основные выводы:
- Удаление кода убирает недостижимый код во время публикации — отражение нарушает этот анализ
- Включите с помощью
<PublishTrimmed>true</PublishTrimmed>; используйтеTrimMode=fullдля максимального уменьшения размера [DynamicDependency]: явно сохраняет определенные члены для отражения[DynamicallyAccessedMembers]: помечает параметрыType, чтобы инструмент удаления кода знал, что нужно сохранить[RequiresUnreferencedCode]: предупреждает вызывающий код о том, что метод небезопасен при удалении кода- Предпочитайте генераторы исходного кода и интерфейсы подходам с активным использованием отражения
Часто задаваемые вопросы
Урок «Обрезка и ограничения отражения» бесплатный?
Да — полный текст урока «Обрезка и ограничения отражения» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Обрезка и ограничения отражения»?
Обрабатывайте предупреждения об обрезке, используйте [DynamicallyAccessedMembers] и заменяйте отражение во время выполнения альтернативами, сгенерированными из исходного кода. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Обрезка и ограничения отражения»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Компиляция Native AOT
- Обрезка и ограничения отражения
- ReadyToRun и многоуровневая компиляция
- Бенчмаркинг с BenchmarkDotNet