t7_dll.md 22 KB

Предыдущая лекция   Следующая лекция
Шаблоны проектирования. Содержание Обзор типов оконных приложений в C#. Знакомство со структорой проекта. Компоновка.

Библиотеки классов

DLL (англ. Dynamic Link Library — «библиотека динамической компоновки», «динамически подключаемая библиотека») в операционных системах Microsoft Windows и IBM OS/2 — динамическая библиотека, позволяющая многократное использование различными приложениями.

Disclaimer

Во-первых, надо отметить, что "классические" DLL содержат функции. А .NET, как мы уже знаем, оперирует класами и пространствами имён. Поэтому DLL, создаваемые в .NET не совместимы с обычными. (Но если очень надо, то как-то это реализуется. Технология Windows Forms использует Win32 API)

Во-вторых, в рамках нашего курса вообще нет особой необходимости в DLL (просто не будет достаточно сложных проектов).

Но, на демо-экзамене и чемпионате в модуле тестирование может встретится задание написать библиотеку классов и unit-тесты для неё. Поэтому надо знать что это такое и уметь реализовать.

Содержание

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

Создание

  1. Возьмем имеющийся проект приложения .NET, например, созданный в прошлых темах.

    Запомним какую платформу (.NET Core или .NET Framefork) и версию .NET используем.

    Например, в моем случае .NET Core версии net8.0

  2. В структуре проекта нажмем правой кнопкой на название решения и далее в появившемся контекстном меню выберем Add -> New Project...

  3. Далее в списке шаблонов найдем пункт Class Library. Будьте внимательны, в списке шаблонов несколько вариантов "Библиотек классов" под разные платформы. Создаваемая библиотека классов должна соответсвовать платформе и версии проекта, к которому её планируется подключать:

  4. После создания проекта он появится в решении, в моем случае с названием ClassLibrary1:

  5. По умолчанию новый проект имеет один пустой класс Class1 в файле Class1.cs. Мы можем этот файл удалить или переименовать, как нам больше нравится.

    namespace ClassLibrary;
    
    public class Class1
    {
    }
    

    Например, переименуем файл Class1.cs в Person.cs, а класс Class1 в Person. Определим в классе Person простейший код:

    public class Person
    {
        public string name;
        public int age;
    }
    

    ОБРАТИТЕ ВНИМАНИЕ!!!

    IDE может создать класс без модификатора public - установите его, иначе не сможете использовать этот класс в других проектах.

    Теперь скомпилируем библиотеку классов. Для этого нажмём правой кнопкой на проект библиотеки классов и в контекстном меню выберем пункт Build Selected Projects.

    После компиляции библиотеки классов в папке проекта в каталоге bin/Debug мы сможем найти скомпилированный файл dll (ClassLibrary.dll).

Подключение

Подключим созданную библиотеку классов в основной проект. Для этого в основном проекте нажмем правой кнопкой на Dependencies (Зависимости) и в контекстном меню выберем пункт Reference...:

Далее нам откроется окно для добавления зависимостей. Если Вы всё сделали правильно (библиотека находится в том же решении) то в этом будет пункт Projects со списком проектов в решениии, среди которых будет ваша библиотека (в моём случае <ClassLibrary1>).Поставим отметку рядом с нашей библиотекой.

Если наша библиотека вдруг представляет файл dll, который не связан ни с каким проектом в нашем решении, то с помощью кнопки Add From мы можем найти местоположение файла dll и также его подключить.

После успешного подключения библиотеки в главном проекте изменим код, чтобы он использовал класс Person из библиотеки классов:

using ClassLibrary1;

var tom = new Person { 
    name = "Tom", 
    age = 35 };

Console.WriteLine(tom.name);

Такое подключение библиотек назвается статическим.

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

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

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

Чтобы не делать новых библиотек, мы можем удалить ссылку на уже добавленную:

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

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

Метод LoadFrom() принимает в качестве параметра путь к сборке. Например, исследуем сборку на наличие в ней различных типов:

Чтобы не писать длинных относительных или, не дай бог, абсолютных путей нужно скопировать ClassLibrary1.dll в каталог bin/Debug/<версия .net>/ (можно прямо в IDE)

using System.Reflection;

var asm = Assembly.LoadFrom(
    "ClassLibrary1.dll");

Console.WriteLine(asm.FullName);

var types = asm.GetTypes();

foreach(Type t in types)
{
    Console.WriteLine(t.Name);
}

На выходе получим примерно такое:

ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Person

Process finished with exit code 0.

В данном случае для исследования указывается библиотека ClassLibrary1.dll. Здесь использован относительный путь, так как сборка находится в одной папке с приложением.

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

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

Исследование типов

Класс System.Type представляет изучаемый тип, инкапсулируя всю информацию о нем. С помощью его свойств и методов можно получить эту информацию. Некоторые из его свойств и методов:

  • Метод FindMembers() возвращает массив объектов MemberInfo данного типа
  • Метод GetConstructors() возвращает все конструкторы данного типа в виде набора объектов ConstructorInfo
  • Метод GetEvents() возвращает все события данного типа в виде массива объектов EventInfo
  • Метод GetFields() возвращает все поля данного типа в виде массива объектов FieldInfo
  • Метод GetInterfaces() получает все реализуемые данным типом интерфейсы в виде массива объектов Type
  • Метод GetMembers() возвращает все члены типа в виде массива объектов MemberInfo
  • Метод GetMethods() получает все методы типа в виде массива объектов MethodInfo
  • Метод GetProperties() получает все свойства в виде массива объектов PropertyInfo
  • Свойство Name возвращает имя типа
  • Свойство Assembly возвращает название сборки, где определен тип
  • Свойство Namespace возвращает название пространства имен, где определен тип
  • Свойство IsArray возвращает true, если тип является массивом
  • Свойство IsClass возвращает true, если тип представляет класс
  • Свойство IsEnum возвращает true, если тип является перечислением
  • Свойство IsInterface возвращает true, если тип представляет интерфейс

Получение типа

Чтобы управлять типом и получать всю информацию о нем, нам надо сперва получить данный тип. Это можно сделать тремя способами: с помощью ключевого слова typeof, с помощью метода GetType() класса Object и применяя статический метод Type.GetType().

  • Получение типа через typeof:

    Type myType = typeof(User);
    
    Console.WriteLine(myType.ToString());
    Console.ReadLine();
        
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public User(string n, int a)
        {
            Name = n;
            Age = a;
        }
        public void Display()
        {
            Console.WriteLine($"Имя: {Name}  Возраст: {Age}");
        }
        public int Payment(int hours, int perhour)
        {
            return hours * perhour;
        }
    }
    

    Здесь определен класс User с некоторым набором свойств и полей. И чтобы получить его тип, используется выражение Type myType = typeof(User);

  • Получение типа с помощью метода GetType класса Object:

    User user = new User("Tom", 30);
    Type myType = user.GetType();
    

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

  • И третий способ получения типа - статический метод Type.GetType():

    Type myType = Type.GetType("TestConsole.User", false, true);
    

    Первый параметр указывает на полное имя класса с пространством имен. В данном случае класс User находится в пространстве имен TestConsole. Второй параметр указывает, будет ли генерироваться исключение, если класс не удастся найти. В данном случае значение false означает, что исключение не будет генерироваться. И третий параметр указывает, надо ли учитывать регистр символов в первом параметре. Значение true означает, что регистр игнорируется.

    В данном случае класс основной программы и класс User находятся в одном проекте и компилируются в одну сборку exe. Однако может быть, что нужный нам класс находится в другой сборке dll. Для этого после полного имени класса через запятую указывается имя сборки:

    Type myType = Type.GetType("TestConsole.User, MyLibrary", false, true);
    

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

Примеры из этого раздела я на Rider не переписывал - попробуйте сами

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

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

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

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

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GetResult(6, 100, 2));
 
        Console.ReadLine();
    }
 
    public static double GetResult(int percent, double capital, int year)
    {
        for (int i = 0; i < year; i++ )
        {
            capital += capital /100 * percent;
        }
        return capital;
    }
}

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

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

static void Main(string[] args)
{
    try
    {
        var asm = Assembly.LoadFrom(
            "MyApp.dll");
         
        var t = asm.GetType(
            "MyApp.Program", true, true);
         
        // создаем экземпляр класса Program
        object obj = Activator.CreateInstance(t);
         
        // получаем метод GetResult
        MethodInfo method = t.GetMethod("GetResult");
         
        // вызываем метод, передаем ему значения для параметров и получаем результат
        object result = method.Invoke(obj, new object[] { 6, 100, 3 });
        Console.WriteLine((result));
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    Console.ReadLine();
}

Исследуемую сборку можно получить с помощью выражения var asm = Assembly.LoadFrom("MyApp.dll").

Затем с помощью метода GetType получаем тип - класс Program, который находится в сборке MyApp.dll: var t = asm.GetType("MyApp.Program", true, true);

Получив тип, создаем его экземпляр: object obj = Activator.CreateInstance(t). Результат создания - объект класса Program представляет собой переменную obj.

И в конце остается вызвать метод. Во-первых, получаем сам метод: MethodInfo method = t.GetMethod("GetResult");. И потом с помощью метода Invoke вызываем его: object result = method.Invoke(obj, new object[] { 6, 100, 3 }). Здесь первый параметр представляет объект, для которого вызывается метод, а второй - набор параметров в виде массива object[].

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

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

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

Console.WriteLine("Вызов метода Main");
method = t.GetMethod("Main", BindingFlags.DeclaredOnly 
    | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
     
method.Invoke(obj, new object[]{new string[]{}});

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


Задание:

Реализуйте примеры из лекции используя классы из своей предметной области.

Предыдущая лекция   Следующая лекция
Шаблоны проектирования. Содержание Обзор типов оконных приложений в C#. Знакомство со структорой проекта. Компоновка.