Динамическая загрузка сборок и позднее связывание

Последнее обновление: 17.02.2022

При создании приложения для него определяется набор сборок, которые будут использоваться. В проекте указываются ссылки на эти сборки, и когда приложение выполняется, при обращении к функционалу этих сборок они автоматически подгружаются.

Но также мы можем сами динамически подгружать другие сборки, на которые в проекте нет ссылок.

Для управления сборками в пространстве имен System.Reflection имеется класс Assembly. С его помощью можно загружать сборку, исследовать ее.

Чтобы динамически загрузить сборку в приложение, надо использовать статические методы Assembly.LoadFrom() или Assembly.Load().

Метод LoadFrom() принимает в качестве параметра путь к сборке.

Допустим, у нас есть два проекта:

Загрузка сборок в C# и .NET

Пусть в проекте MyApp, который компилируется в сборку MyApp.dll, имеется файл Program.cs со следующим кодом:

Person tom = new Person("Tom");
Console.WriteLine($"Hello, {tom.Name}");

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name;
}

В другом проект исследуем сборку MyApp.dll на наличие в ней различных типов:

using System.Reflection;

Assembly asm = Assembly.LoadFrom("MyApp.dll");

Console.WriteLine(asm.FullName);
// получаем все типы из сборки MyApp.dll
Type[] types = asm.GetTypes();
foreach (Type t in types)
{
    Console.WriteLine(t.Name);
}

В данном случае для исследования указывается сборка MyApp.dll. Здесь использован относительный путь, так как сборка находится в одной папке с приложением - в проекте в каталоге bin/Debug/net6.x. Можно в принципе в качестве имени указать и имя текущего приложение. В этом случае программа будет исследовать саму себя. В любом случае стоит учитывать, что загрузке подлежат (по крайней мере в .NET 6.0) сборки с расширением dll, но не exe.

И в моем случае я получу следующий консольный вывод:

MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
EmbeddedAttribute
NullableAttribute
NullableContextAttribute
Program
Person

Как видно из вывода, полное название сборки: MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null. А сама сборка MyApp.dll содержит пять типов - кроме класса Person и неявно определяемого класса Program добавляется еще три автоматически генерируемых класса.

Метод Load() действует аналогично, только в качестве его параметра передается дружественное имя сборки, которое нередко совпадает с именем приложения: Assembly asm = Assembly.Load("MyApp");

Получив все типы сборки с помощью метода GetTypes(), мы опять же можем применить к каждому типу все те методы, которые были рассмотрены в прошлой теме.

Позднее связывание

С помощью динамической загрузки мы можем реализовать технологию позднего связывания. Позднее связывание позволяет создавать экземпляры некоторого типа, а также использовать его во время выполнения приложения.

Использование позднего связывания менее безопасно в том плане, что при жестком кодировании всех типов (ранее связывание) на этапе компиляции мы можем отследить многие ошибки. В то же время позднее связывание позволяет создавать расширяемые приложения, когда дополнительный функционал программы неизвестен, и его могут разработать и подключить сторонние разработчики.

Ключевую роль в позднем связывании играет класс System.Activator. С помощью его статического метода Activator.CreateInstance() можно создавать экземпляры заданного типа.

Например, динамически загрузим сборку и вызовем у ней некоторый метод. Допустим, загружаемая сборка MyApp.exe представляет следующую программу:

class Program
{
    static void Main(string[] args)
    {
        var number = 5;
        var result = Square(number);
        Console.WriteLine($"Квадрат {number} равен {result}");
    }
    static int Square(int n) => n * n;
}

В данном случае мы явным образом определили класс Program с методом Main. И кроме того, в классе Program определен статический метод Square, который в качестве параметра принимает число и возвращает его квадрат.

Теперь динамически подключим сборку с этой программой в другой программе и вызовем ее методы.

Пусть наша основная программа будет выглядеть так:

using System.Reflection;

Assembly asm = Assembly.LoadFrom("MyApp.dll");

Type? t = asm.GetType("Program");
if (t is not null)
{
    // получаем метод Square
    MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);

    // вызываем метод, передаем ему значения для параметров и получаем результат
    object? result = square?.Invoke(null, new object[] { 7 });
    Console.WriteLine(result); // 49
}

Сначала получаем ссылку на исследуемую сборку в переменную asm:

Assembly asm = Assembly.LoadFrom("MyApp.dll")

Затем с помощью метода GetType получаем тип - класс Program, который находится в сборке MyApp.dll:

Type? t = asm.GetType("Program");

И в конце остается вызвать метод. Во-первых, получаем сам метод:

MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);

Поскольку метод Square приватный и статический, то в качестве второго параметра в метод передаются флаги BindingFlags.NonPublic | BindingFlags.Static

И потом с помощью метода Invoke вызываем его:

object? result = square?.Invoke(null, new object[] { 7 });

Здесь первый параметр представляет объект, для которого вызывается метод, а второй - набор параметров в виде массива object[]. Однако поскольку вызываемый метод - статический и не относится к какому-то определенному объекту, то первым аргументом в метод передается null.

Так как метод Square возвращает некоторое значение, то мы можем его получить из метода в виде объекта типа object.

Если бы метод не принимал параметров, то вместо массива объектов использовалось бы значение null: method.Invoke(null, null)

В сборке MyApp.exe в классе Program также есть и другой метод - метод Main, который также выполняет некоторую работу. Вызовем теперь его:

using System.Reflection;

Assembly asm = Assembly.LoadFrom("MyApp.dll");

Type? program = asm.GetType("Program");
if (program is not null)
{
    // получаем метод Main
    MethodInfo? main = program.GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static);

    // вызываем метод Main
    main?.Invoke(null, new object[] { new string[] { } });   // Квадрат 5 равен 25
}

Так как метод Main является статическим и не публичным, то к нему также применяется битовая маска BindingFlags.NonPublic | BindingFlags.Static. И поскольку он в качестве параметра принимает массив строк, то при вызове метода передается соответствующее значение: main.Invoke(null, new object[]{new string[]{}})

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850