Что такое обертка в программировании
7 приемов, упрощающих программирование
Программирование — это сложное дело. Поэтому каждая техника, которая помогает его упростить, весьма полезна. Рассмотрим простые и эффективные приемы, которые помогают упростить работу программиста.
1. Функция-обертка
Это самый простой и распространенный способ упрощения. Функция-обертка — это функция, которая вызывает некоторую функцию с заданными параметрами.
Например, мы хотим вывести число в окне сообщений. Для этого служит функция MessageBox. Согласно MSDN эта функция имеет целых четыре аргумента:
Если каждый раз при вызове этой функции мы будем вспоминать, что значит каждый аргумент, то это замедлит работу. Поэтому пишем функцию-обертку, которая берет всего один аргумент, а остальные подставляет по умолчанию и вызывает MessageBox.
Как видите, эту функцию проще запомнить и вызвать.
Важно! Функция-обертка не меняет функционал. Иначе вам придется каждый раз вспоминать, что именно вы поменяли и весь выигрыш в простоте вызова пропадет.
Этот прием настолько прост и эффективен, что имеет смысл в начале создания программы сразу написать несколько оберток для часто вызываемых функций.
2. Функция с защитой
Часто бывает, что стандартные функции не могут быть выполнены по разным причинам. Например, нет места на диске или пользователь ввел не те данные. Если за этим не следить, то программа вылетит с ошибкой, что не очень хорошо. Даже если виноват пользователь.
Прием состоит в том, чтобы не вызывать потенциально опасные функции, а писать защищенные функции. Например, часто требуется, чтобы в имени файла были только латинские буквы.
Мы запрашиваем у пользователя имя файла, но перед сохранением файла можно вставить функцию, которая заменит русские буквы на латинскую «a».
Для простоты применения приема можно вначале кодирования писать защищенные функции без защиты, например:
А потом уже на этапе окончательной доводки программы вставлять защиту в каждую функцию.
3. Функция с отображением ситуации
Одна из самых сложных задач в работе программиста — это отладка. Конечно же, самый простой способ смотреть состояние данных во время отладки — это окно отладчика. Но отладчик показывает только то, значения стандартных объектов: переменных, строк, массивов.
Но, например, вы пишете игру, а вам нужно найти ошибку в поведении элемента игры. В этом случае информации отладчика будет явно недостаточно.Другой вариант, вы пишете бухгалтерскую программу и у вас неверно считается некоторое число в счет-фактуре. Опять же отладчик тут не помощник.
В подобных случаях пригодится функция с отображением ситуации.
Вы пишете специальную функцию, которая отображает состояние вашего объекта данных максимально приближено к реальной ситуации. После чего вы можете вызывать эту функцию специально для отладки.
4. Функция инициализация объектов данных
Во многих программах какой-то части объектов данных нужно присвоить начальные значения. Это называется инициализацией. Часто программисты делают инициализацию перед первым использованием объекта. Но это порождает трудно находимые ошибки, если объект был использован ранее. Дело в том, что при создании объекта данных там уже содержатся некоторые случайные данные. И в отладчике трудно отличить — это правильные данные или случайные.
Для избежания подобных ошибок рекомендуется добавить функцию инициализации, которую вызывать в самом начале программы. Например.
.NET-обёртки нативных библиотек на C++/CLI
Содержание
Создание обёрток для нативных библиотек
Прежде, чем начать
Отдельная DLL или интеграция в проект нативной библиотеки?
Проекты Visual C++ могут содержать файлы, компилируемые в управляемый код [подробнее можно прочитать в главе 7 книги]. Интеграция обёрток в нативную библиотеку может показаться хорошей затеей, потому что тогда у вас будет на одну библиотеку меньше. Кроме того, если вы интегрируется обёртки в DLL, то клиентском приложению не придётся загружать лишнюю динамическую библиотеку. Чем меньше загружается DLL, тем меньше время загрузки, требуется меньше виртуальной памяти, меньше вероятность, что библиотека будет перемещена в памяти из-за невозможности её загрузить по изначальному базовому адресу.
Тем не менее, включение обёрток в оборачиваемую библиотеку как правило не несёт пользы. Чтобы лучше понять почему, следует отдельно рассмотреть сборку статической библиотеки и DLL.
Как бы странно это не звучало, но управляемые типы можно включить в статическую библиотеку. Однако это легко может повлечь за собой проблемы идентичности типов. Сборка, в которой определён тип, является частью его идентификатора. Таким образом, CLR может различить два типа из разных сборок, даже если имена типов совпадают. Если два разных проекта использую один и тот же управляемый тип из статической библиотеки, то этот тип будет скомпонован в обе сборки. Так как сборка является частью идентификатора типа, то получится два типа с разными идентификаторами, хотя они и были определены в одной статической библиотеке.
Интеграция управляемых типов в нативную DLL также не рекомендуется, так как загрузка библиотеки потребует CLR 2.0 во времени загрузки. Даже если приложение, использующее библиотеку, обращается только к нативному коду, для загрузки библиотеки потребуется, чтобы на компьютере была установлена CLR 2.0, и чтобы приложение не загружало более раннюю версию CLR.
Какая часть нативной библиотеки должна быть доступна через обёртку?
Как это часто бывает, очень полезно чётко определить задачи разработчика, прежде чем начинать писать код. Знаю, это звучит как цитата из второсортной книженции о разработке программ из начала 90х, но для обёрток нативных библиотек постановка задачи особенно важна.
Когда вы приступаете к созданию обёртки для нативной библиотеки, задача кажется очевидной — у вас уже есть существующая библиотека и управляемое API должно принести её функциональность в мир управляемого кода.
Для большинства проектов такого общего описания совершенно недостаточно. Без более чёткого понимания проблемы вы, вероятно, напишете по обёртке для каждого нативного класса C++ библиотеки. Если библиотека содержит больше одной единственной центральной абстракции, то зачастую не стоит создавать обёртки один-к-одному с нативным кодом. Это заставит вас решать проблемы, не связанные с вашей конкретной задачей, а также породит много неиспользованного кода.
Чтобы лучше описать задачу, подумайте над проблемой в целом. Чтобы сформулировать задачу более чётко, вам надо ответить на два вопроса:
Это API даёт программисту следующие возможности:
Однако весьма вероятно, что обёртка нужна только для одного или двух алгоритмов. Если обёртка для этого API не будет поддерживать наследование, то её создание упрощается. С таким упрощением вам не придётся создавать обёртку для абстрактного класса CryptoAlgorithm. С виртуальными методами Encrypt и Decrypt можно будет работать так же, как и с любыми другими. Чтобы дать понять, что вы не хотите поддерживать наследование, достаточно объявить обёртки для SampleCipher и AnotherCipherAlgorithm как sealed классы.
Взаимодействие языков
Можно использовать атрибут CLSCompliantAttribute, чтобы обозначить тип или его член как соответствующий CLS. По умолчанию, не помеченные этим атрибутом типы считаются не соответствующими CLS. Если вы примените этот атрибут на уровне сборки, то по умолчанию все типы будут считаться соответствующими CLS. Следующий пример показывает, как применять этот атрибут к сборкам и типам:
Согласно правилу 11 CLS все типы, присутствующие в сигнатурах членов класса (методов, свойств, полей и событий), видимых снаружи сборки, должны соответствовать CLS. Чтобы правильно применять атрибут [CLSCompliant], вы должны знать, соответствуют ли типы параметров метода CLS. Чтобы определить соответствие CLS, надо проверить атрибуты сборки, в которой объявлен тип, а также атрибуты самого типа.
В Framework Class Library (FCL) также используется атрибут CLSCompliant. mscorlib и большинство других библиотек FCL применяют атрибут [CLSCompliant(true)] на уровне сборки и помечают типы, не соответствующие CLS, атрибутом [CLSCompliant(false)].
Учтите, что следующие примитивные типы в mscorlib помечены как несоответствующие CLS: System::SByte, System::UInt16, System::UInt32 и System::UInt64. Эти типы (или эквивалентные им имена типов char, unsigned short, unsigned int, unsigned long и unsigned long long в C++) нельзя использовать в сигнатурах членов типов, которые считаются соответствующими CLS.
Если тип считается соответствующим CLS, то все его члены также считаются таковыми, если явно не указано обратного. Пример:
К сожалению, компилятор C++/CLI не показывает предупреждений, когда тип, помеченный как соответствующий CLS, нарушает правила CLS. Чтобы понять, помечать тип как соответствующий CLS или нет, надо знать следующие важные правила CLS:
В отличие от C#, в C++/CLI допускается явное указание типа упакованного значения. Например:
Создание обёрток для классов C++
Объявить управляемый класс с полем типа NativeLib::SampleCipher нельзя [подробнее можно прочитать в главе 8 книги]. Так как поля управляемых классов могут быть только указателями на нативные типы, следует использовать поле типа NativeLib::SampleCipher*. Экземпляр нативного класса должен быть создан в конструкторе обёртки и уничтожен в деструкторе.
Кроме деструктора, также стоит реализовать финализатор [подробнее можно прочитать в главе 11 книги].
Отображение нативных типов на типы, соответствующие CLS
Если в нативную функцию передаётся ссылка C++ или указатель с семантикой передачи по ссылке, то в обёртке функции рекомендуется использовать отслеживаемую ссылку [см. примечание ниже]. Допустим, нативная функция имеет следующий вид:
Для вызова нативной функции требуется передать нативную ссылку на int. Для этого аргумента необходимо осуществить маршаллинг вручную, так как преобразования типов из отслеживающей ссылки в нативную ссылку не существует. Поскольку существует стандартное преобразование типов из int в int&, используется локальная переменная типа int, которая служит в качестве буфера для аргумента, передаваемого по ссылке. Перед вызовом нативной функции буфер инициализируется значением, переданным в качестве параметра i. После возврата из нативной функции в обёртку значение параметра i обновляется в соответствии с изменениями буфера j.
Как видно из этого примера, помимо затрат на переход между управляемым и нативным кодом, обёртки зачастую вынуждены тратить процессорное время на маршаллинг типов. Как будет показано позже, для более сложных типов эти затраты могут быть значительно выше.
По умолчанию считается, что отслеживающая ссылка означает передачу по ссылке. Если вы хотите, чтобы аргумент использовался только для возврата значения, следует применить атрибут OutAttribute из пространства имён System::Runtime::InteropServices, как показано в следующем примере:
Типы аргументов нативных функций часто содержат модификатор const, как в примере ниже:
Модификатор const транслируется в необязательный модификатор сигнатуры метода [подробнее можно прочитать в главе 8 книги]. Метод fWrapper всё равно можно вызвать из управляемого кода, даже если вызывающая сторона не воспринимает модификатор const:
Для передачи указателя на массив в качестве параметра нативной функции недостаточно просто использовать отслеживающую ссылку. Чтобы разобрать этот случай, предположим, что у нативного класса SampleCipher есть конструктор, который принимает ключ шифрования:
В данном случае недостаточно просто отобразить const unsigned char* в const unsigned char%, потому что ключ шифрования, передаваемый в конструктор нативного типа, содержит более одного байта. Лучшим использовать следующий подход:
В этом конструкторе оба аргумента нативного конструктора (pKey и nKeySizeInBytes) отображаются на единственный аргумент типа управляемый массив. Так можно сделать, потому что размер управляемого массива можно определить во время выполнения.
Как реализовывать этот конструктор, зависит от реализации нативного класса SampleCipher. Если конструктор создаёт внутреннюю копию ключа, переданного в качестве аргумента pKey, то можно передать закрепляющий указатель на ключ:
Этот конструктор требует, чтобы клиент не освобождал память, содержащую ключ, и указатель на ключ оставался корректным пока экземпляр класса SampleCipher не будет уничтожен. Конструктор обёртки не выполняет ни одно из этих требований. Так как обёртка не содержит дескриптора управляемого массива, сборщик мусора может собрать массив раньше, чем экземпляр нативного класса будет уничтожен. Даже если сохранить дескриптор объекта, чтобы память не освобождалась, массив может быть перемещён во время сборки мусора. В этом случае нативный указатель больше не будет указывать на управляемый массив. Чтобы память, содержащая ключ, не освобождалась и не перемещалась при сборке мусора, ключ надо скопировать в нативную кучу.
Для этого надо внести изменения как в конструктор, так и в деструктор управляемой обёртки. Следующий код показывает возможную реализацию конструктора и деструктора:
Если не брать в расчёт некоторые эзотерические проблемы [обсуждаются в главе 11 книги], приведённый здесь код обеспечивает корректное выделение и освобождение ресурсов даже при возникновении исключений. Если при создании экземпляра ManagedWrapper::SampleCipher возникнет ошибка, все выделенные ресурсы будут освобождены. Деструктор реализован таким образом, чтобы освободить нативный массив, содержащий ключ, даже если деструктор оборачиваемого объекта выбросит исключение.
Этот код также показывает характерные накладные расходы управляемых обёрток. Помимо накладных расходов на вызов оборачиваемых нативных функций из управляемого кода, зачастую добавляются накладные расходы на отображение между нативными и управляемыми типами.
Отображение исключений C++ на управляемые исключения
Помимо управления ресурсами, устойчивого к возникновению исключений, управляемая обёртка также должна позаботиться об отображении исключений C++, выбрасываемых нативной библиотекой, в управляемые исключения. Для примера предположим, что алгоритм SampleCipher поддерживает только 128 и 256-битные ключи. Конструктор NativeLib::SampleCipher мог бы выбрасывать исключение NativeLib::CipherException, если в него передан ключ неправильного размера. Исключения C++ отображаются в исключения типа System::Runtime::InteropServices::SEHException, что не очень удобно для потребителя библиотеки [обсуждается в главе 9 книги]. Следовательно, необходимо перехватывать нативные исключения и перебрасывать управляемые исключения, содержащие эквивалентные данные.
Для того, чтобы отобразить исключения конструктора, можно использовать блок try на уровне функции, как показано в следующем примере. Это позволит перехватить исключения, выброшенные как при инициализации членов класса, так и в теле конструктора.
Здесь используется блок try на уровне функции, хотя инициализация членов класса в этом примере и не должна приводить к выбросу исключений. Таким образом исключения будут перехвачены, даже если вы добавите новые члены класса или добавите наследование SampleCipher от другого класса.
Отображение управляемых массивов на нативные типы
Теперь, разобравшись с реализацией конструктора, разберём методы Encrypt и Decrypt. Ранее указание сигнатур этих методов было отложено, теперь приведём их полностью:
Данные, которые должны быть зашифрованы или расшифрованы, передаются при помощи параметров pData и nDataLength. Перед вызовом Encrypt или Decrypt следует выделить буфер памяти. Значение параметра pBuffer должно быть указателем на этот буфер, а его размер должен быть передан в качестве значения параметра nBufferLength. Размер данных на выходе возвращается при помощи параметра nNumEncryptedBytes.
Для отображения Encrypt и Decrypt можно добавить следующий метод в ManagedWrapper::SampleCipher:
Отображение других непримитивных типов
Ранее все типы параметров отображаемых функций были либо примитивными типами, либо указателями или ссылками на примитивные типы. Если вам надо отображать функции, принимающие в качестве параметров классы C++ либо указатели или ссылки на классы C++, то зачастую требуются дополнительные действия. В зависимости от конкретной ситуации, решения могут быть разными. Чтобы показать различные варианты решений на конкретном примере, рассмотрим другой нативный класс, для которого требуется обёртка:
Как можно догадаться о имени этого класса, его назначение — отправлять зашифрованные данные. В рамках дискуссии неважно куда отправляются данные и какой протокол при этом используется. Для шифрования можно использовать классы, наследованные от CryptoAlgorithm (например, SampleCipher). Алгоритм шифрования можно указать при помощи параметра конструктора типа CryptoAlgorithm&. Экземпляр класса CryptoAlgorithm, переданный в конструктор, используется в методе SendData при вызове виртуального метода Encrypt. Следующий пример показывает, как можно использовать EncryptingSender в нативном коде:
Чтобы создать обёртку над NativeLib::EncryptingSender, вы можете определить управляемый класс ManagedWrapper::EncryptingSender. Как и обёртка класса SampleCipher, он должен хранить указатель на обёрнутый объект в поле. Чтобы создать экземпляра оборачиваемого класса EncryptingSender требуется экземпляр класса NativeLib::CryptoAlgorithm. Допустим, единственный алгоритм шифрования, который вы хотите поддерживать, это SampleCipher. Тогда можно определить конструктор, принимающий значение типа array ^ в качестве ключа шифрования. Также, как и конструктор класса ManagedWrapper::SampleCipher, конструктор EncryptingSender может использовать этот массив для создания экземпляра нативного класса NativeLib::SampleCipher. Затем, ссылку на этот объект можно передать в конструктор NativeLib::EncryptingSender:
При таком подходе вам не придётся отображать параметр типа CryptoAlgorithm& на управляемый тип. Однако иногда этот подход слишком ограничен. Например, вы хотите дать возможность передать существующий экземпляр SampleCipher, а не создавать новый. Для этого у конструктора ManagedWrapper::EncryptingSender должен быть параметр типа SampleCipher^. Чтобы создать экземпляр класса NativeLib::EncryptingSender внутри конструктора, надо получить объект класса NativeLib::SampleCipher, который обёрнут в ManagedWrapper::SampleCipher. Для получения обёрнутого объекта требуется добавить новый метод:
Следующий код показывать возможную реализацию такого конструктора:
Пока что эта реализация позволяет передавать только экземпляры класса ManagedWrapper::SampleCipher. Чтобы использовать EncryptingSender с любой реализацией обёртки над CryptoAlgorithm, придётся изменить дизайн таким образом, чтобы реализация GetWrappedObject различными обёртками была полиморфной. Этого можно добиться при помощи управляемого интерфейса:
Для реализации этого интерфейса надо изменить обёртку SampleCipher следующим образом:
Этот метод реализован как internal, потому что код, использующий библиотеку-обёртку, не должен напрямую вызывать методы обёрнутого объекта. Если вы хотите предоставить клиенту доступ непосредственно к обёрнутому объекту, вам следует передавать указатель на него при помощи System::IntPtr, потому что тип System::IntPtr соответствует CLS.
Теперь конструктор класса ManagedWrapper::EncryptingSender принимает параметр типа INativeCryptoAlgorithm^. Чтобы получить объект класса NativeLib::CryptoAlgorithm, необходимый для создания оборачиваемого экземпляра EncryptingSender, можно вызвать метод GetWrappedObject у параметра типа INativeCryptoAlgorithm^:
Поддержка наследования и виртуальных методов
Если вы сделаете обёртки для других алгоритмов шифрования и добавите в них поддержку INativeCryptoAlgorithm, то их тоже можно будет передать в конструктор ManagedWrapper::EncryptingSender. Однако реализовать свой алгоритм шифрования в управляемом коде и передать его в EncryptingSender пока что нельзя. Для этого потребуется сделать доработки, потому что в управляемом классе нельзя просто переопределить виртуальный метод нативного класса. Для этого придётся снова изменить реализацию управляемых классов-обёрток.
На этот раз потребуется абстрактный управляемый класс, который обернёт NativeLib::CryptoAlgorithm. Помимо метода GetWrappedObject, этот класс-обёртка должен предоставлять два абстрактных метода:
Чтобы реализовать свой криптографический алгоритм, надо создать управляемый класс-наследник ManagedWrapper::CryptoAlgorithm и переопределить виртуальные методы Encrypt и Decrypt. Однако этих абстрактных методов недостаточно, чтобы переопределить виртуальные методы NativeLib::CryptoAlgorithm Encrypt и Decrypt. Виртуальные методы нативного класса, в нашем случае, NativeLib::CryptoAlgorithm, можно переопределить только в нативном классе-наследнике. Следовательно, надо создать нативный класс, который наследуется от NativeLib::CryptoAlgorithm и переопределяет требуемые виртуальные методы:
Этот класс назван CryptoAlgorithmProxy, потому что он служит посредником для управляемого класса, реализующего Encrypt и Decrypt. Его реализация виртуальных методов должна вызывать эквивалентные виртуальные методы класса ManagedWrapper::CryptoAlgorithm. Для этого CryptoAlgorithmProxy нужен дескриптор экземпляра класса ManagedWrapper::CryptoAlgorithm. Он может быть передан в качестве параметра конструктора. Чтобы сохранить дескриптор, нужен шаблон gcroot. (Так как CryptoAlgorithmProxy — это нативный класс, он не может содержать полей типа дескриптор.)
Вместо того, чтобы служить обёрткой нативного абстрактного класса CryptoAlgorithm, управляемый класс служит обёрткой для конкретного наследника CryptoAlgorithmProxy. Следующий код показывает, как это сделать:
Как говорилось ранее, класс CryptoAlgorithmProxy должен реализовать виртуальные методы таким образом, чтобы управление передавалось эквивалентным методам ManagedWrapper::CryptoAlgorithm. Следующий код показывает, как CryptoAlgorithmProxy::Encrypt вызывает ManagedWrapper::CryptoAlgorithm::Encrypt:
Общие рекомендации
Упростите обёртки с самого начала
Как видно из предыдущих разделов, создание обёрток для иерархий классов может быть очень трудозатратным. Иногда нужно создавать обёртки классов C++ таким образом, чтобы управляемые классы могли переопределять их виртуальные методы, но зачастую такая реализация не несёт практического смысла. Определение необходимой функциональности — вот ключ к упрощению задачи.
Не нужно заново изобретать колесо. Прежде чем создавать обёртку для некоторой библиотеки, убедитесь, что FCL не содержит готового класса с требуемыми методами. FCL может предложить больше, чем кажется на первый взгляд. Например, BCL уже содержит довольно много алгоритмов шифрования. Они находятся в пространстве имён System::Security::Cryptography. Если нужным вам алгоритм шифрования уже есть в FCL, вам не нужно заново создавать для него обёртку. Если FCL не содержит реализации алгоритма, для которого вы хотите создать обёртку, но приложение не завязано на алгоритм, реализованный в нативном API, то обычно предпочтительней использовать один из стандартных алгоритмов из FCL.
Адаптация к дизайну FCL как правило упрощает библиотеку обёрток с точки зрения потребителя библиотеки. Трудоёмкость этого подхода зависит от дизайна нативного API и дизайна типов FCL, поддержку которых вы хотите реализовать. Приемлемы ли эти дополнительные трудозатраты определяется отдельно в каждом конкретном случае. В этом примере можно предположить, что алгоритм безопасности используется только в одном конкретном случае и, следовательно, не стоит того, чтобы интегрировать его с FCL.
Если библиотека, для которой вы создаёте отображение, управляет табличными данными, следует рассмотреть классы System::Data::DataTable и System::Data::DataSet из части FCL, именуемой ADO.NET. Хотя рассмотрение этих типов и выходит за рамки данного текста, они заслуживают упоминания благодаря своей применимости в создании обёрток.
Заключение
Создание обёртки над нативной библиотекой также предполагает обёртывание нативных ресурсов (например, неуправляемой памяти, необходимой для создания оборачиваемых объектов). Надёжное освобождение ресурсов — это тема для отдельной статьи [рассматривается в главе 11 книги].
Типы-обертки в Java
1. Список типов-оберток
Все вы знаете, что в Java есть 8 примитивных типов, которые не являются классами. С одной стороны, это хорошо: они простые и занимают мало места, а с другой — иногда нужны именно классы. Зачем именно они, вы узнаете в следующей лекции.
Вот список таких типов, ничего не узнаете?
Примитивный тип | Класс-обертка |
---|
Все объекты классов-оберток являются неизменяемыми ( immutable ).
Упрощенный код класса Integer выглядит примерно так:
Метод возвращает значение
Статический метод создает новый объект Integer для переменной типа int
2. Преобразование типа int к Integer
Типы-обертки считаются аналогами их более примитивных собратьев: можно легко создать соответствующий объект-обертку для примитивного типа.
Разберем взаимодействие примитивных типов и их типов-оберток на примере типа int. Вот как бы выглядел код преобразования типа int к типу Integer и наоборот:
3. Autoboxing и unboxing
Однако даже простые операции с типом Integer писать непросто.
Код | Описание |
---|---|
Оборачиваем 5 в класс Integer Получаем значение из объекта Integer Создаем новое значение Integer == 10 |
Код довольно громоздкий, не находите?
Ваш код | Что видит компилятор |
---|
Код | Что сгенерирует компилятор |
---|
4. Сравнение переменных классов-оберток
Код | Вывод на экран |
---|
Переменные a и b хранят не значения (как типы int ), а ссылки на объекты. Поэтому важно помнить, как правильно их сравнивать: