При создании приложения для него определяется набор сборок, которые будут использоваться. В проекте указываются ссылки на эти сборки, и когда приложение выполняется, при обращении к функционалу этих сборок они автоматически подгружаются.
Но также мы можем сами динамически подгружать другие сборки, на которые в проекте нет ссылок.
Для управления сборками в пространстве имен System.Reflection
имеется класс Assembly. С его помощью можно загружать сборку,
исследовать ее.
Чтобы динамически загрузить сборку в приложение, надо использовать статические методы Assembly.LoadFrom() или Assembly.Load().
Метод LoadFrom()
принимает в качестве параметра путь к сборке.
Допустим, у нас есть два проекта:
Пусть в проекте 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[]{}})