Структуры в си шарп

Структуры в си шарп

Структуры в С# (си шарп) — это облегченная версия классов:

Создавая структуры, нет необходимости объявлять конструктор по умолчанию (без аргументов).
Структуры, в отличие от классов, не поддерживают наследование.

Классы в С# (си шарп) стоит заменять структурами тогда, когда создается простой объект небольших объемов. Такой способ создания объектов, наряду с перечислениями, делает код удобным для понимания человеком. Структуры объявляются с помощью ключевого слова struct.
С точки зрения логики использования структура представляет собой группу связанных между собой переменных. Под связанностью имеется в виду логическая связанность в рамках конкретной задачи. Например, структура «Сотрудник», объединяющая переменные «ФИО», «Отдел», «Должность», «Зарплата». Эти переменные называют полями данных.
struct Sotrudnik
<
public string fio;
public string otdel;
public string dolgns;
public double zp;
>

При использования полей структуры имя структуры (Sotrudnik) воспринимается как тип данных. Необходимо создать новый объект этого типа данных (sotr1), и поля структуры использовать в качестве свойств этого объекта, задавая им конкретные значения:
Sotrudnik sotr1;
sotr1.fio = «Бережнов Федор Ммихайлович»;
sotr1.otdel = «Логистика»;
sotr1.dolgns = «Начальник склада»;
sotr1.zp = 15000;

Теперь выведем значения на экран. Пользователь увидит фразу: «Бережнов Федор Михайлович из отдела Логистика в должности Начальник склада получает 15000 руб.»

string str = sotr1.fio + » из отдела » + sotr1.otdel + » в должности » + sotr1.dolgns + » получает » + sotr1.zp + «руб. «;
MessageBox.Show(str);

Немного усложним структуру и включим в нее конструктор. Структура становится более похожей на класс:
struct Sotrudnik
<
private string fio;
private double zp;

public Sotrudnik (string f, double z)
<
this.fio = f;
this.zp = z;
>

public override string ToString()
<
return System.String.Format(«Информация о сотруднике: <0>ФИО , <1>ЗП», fio, zp);
>
>

Как и при создании классов в си шарп, имя конструктора совпадает с именем структуры. Еще раз обращаем внимание на отсутствие конструктора по умолчанию: мы сразу же придали ему аргументы. Кроме того, добавился переопределенный («перегруженный») метод ToString. Перегрузка метода заключается в том, что мы назначили новое действие существующему методу, то есть переопределили его. Теперь, по вызову метода ToString() будет происходит не конвертация в строковый формат, а вывод на экран.

Расположение

В большинстве случаев вы можете описать и использовать структуру без знания о том, как она реализована — особенно как расположены в памяти ее поля. Если вы должны создать структуру для использования ее другими приложениями, или сами должны использовать чужую структуру, то в этом случае вопросы памяти становятся важными. Как вы думаете, каков размер следующей структуры?

Разумный ответ — 8 байт, просто сумма размеров всех полей. Однако если вы попытаетесь узнать размер структуры:

… то (в большинстве случаев) обнаружите, что структура занимает 12 байт. Причина кроется в том, что большинство процессоров лучше работает с данными, занимающими больше, чем байт, и выровненными по определенным адресным границам. Pentium предпочитает данные в блоках по 16 байт с выравниванием по адресным границам с размером, идентичным размеру самих данных. Например, 4-байтовый integer должен быть выровнен по границе 4 байта. Детальные подробности в данном случае неважны. Важно то, что компилятор добавит недостающие байты, чтобы выровнять данные внутри структуры. Вы можете контролировать это вручную, однако обратите внимание, что некоторые процессоры могут возвращать ошибку в случае использования невыровненных данных. Это создает дополнительные проблемы для пользователей .NET Compact Framework (интересно, много таких? — прим. пер.).

Для работы вам понадобится ссылка на InteropServices:

Для ручного расположения полей в памяти используется атрибут StructLayout. Например:

Это заставляет компилятор располагать поля последовательно, в порядке объявления, что и делает по умолчанию. Другими значениями атрибута являются значение Auto, которое позволяет компилятору самому определять порядок размещения полей, и значение Explicit, которое позволяет программисту указать размер каждого поля. Тип Explicit часто используется для последовательного расположения без упаковки, но в большинстве случаев проще использовать параметр Pack. Он сообщает компилятору о том, сколько должно выделяться памяти и как должны быть выровнены данные. Например, если вы укажете Pack=1, тогда структура будет организована таким образом, что каждое поле будет находиться в границах одного байта и может быть считано побайтно — т.е. никакого упаковывания не требуется. Если вы измените объявление структуры:

… то обнаружите, что теперь структура занимает ровно 8 байт, что отвечает последовательному расположению полей в памяти без дополнительных «упаковывающих» байт. Именно таким образом нужно работать с большинством структур, объявленных в Windows API и C/C++. В большинстве случаев вам не придется использовать другие значения параметра Pack. Если вы установите Pack=2, тогда обнаружите, что структура станет занимать 10 байт, потому что будет добавлено по одному байту к каждому однобайтовому полю, чтобы данные могли читаться кусками по 2 байта. Если установить Pack=4, размер структуры увеличится до 12 байт, чтобы структура могла быть прочитана блоками по 4 байта. Дальше значение параметра перестанет учитываться, потому что размер Pack игнорируется, если он равен или превышает выравнивание, использующееся в данном процессоре, и составляющее 8 байт для архитектуры Intel. Расположение структуры в памяти при разных значения Pack показано на рисунке:

Стоит также упомянуть, что может изменить способ упаковки структуры, изменяя порядок полей в ней. Например, при изменении порядка полей на:

… структуре не понадобится упаковка, она и так займет ровно 8 байт.

Если быть точным

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

Читайте также:  Ноутбук греется во время игры что делать

Так вы получите 8-байтовую структуры без дополнительны выравнивающих байтов. В данном случае это эквивалентно использованию Pack=1. Однако использование Explicit позволяет вам полностью контролировать память. Например:

Эта структура займет 16 байт, вместе с дополнительными байтами после поля b. До версии C# 2.0, тип Explicit использовался в основном для указания буферов с фиксированными размерами при вызове сторонних функций. Вы не можете объявить массив фиксированной длины в структуре, потому что инициализация полей запрещена.

Этот код выдаст ошибку. Если вам нужен массив длиной 10 байт, вот один из способов:

Таким образом, вы оставляете 10 байт для массива. Тут существует ряд интересных нюансов. Первое, почему нужно использовать смещение в 8 байт? Причина в том, что вы не можете начать массив с нечетного адреса. Если вы воспользуетесь смещением в 7 байт, то увидите ошибку времени выполнения, сообщающую о том, что структура не может быть загружена из-за проблем с выравниванием. Это важно, потому что при использовании Explicit вы можете столкнуться с проблемами, если не будете понимать, что вы делаете. Второй момент связан с тем, что в конец структуры добавляются дополнительные байты, чтобы размер структуры был кратен 8 байтам. Компилятор все еще участвует в том, как структура будет размещена в памяти. Конечно, на практике, любая внешняя структура, которую вы попытаетесь конвертировать в структуру C#, должна быть корректна выровнена.
Наконец, стоит упомянуть, что вы не можете обратиться к 10-байтовому массиву, используя имя массива (например, buffer[1]), потому что C# думает, что массиву не назначено значение. Поэтому если вы не можете использовать массив и это вызывает проблему с выравниванием, гораздо лучше объявить структуру так:

Для доступа к массиву придется воспользоваться арифметикой на указателях, что является unsafe кодом. Чтобы под структуру было выделено фиксированное количество байт, используйте параметр Size в атрибуте StructLayout:

Сейчас в C# 2.0 массивы фиксированного размера разрешены, поэтому все вышеприведенные конструкции в общем-то необязательны. Стоит заметить, что массивы фиксированной длины используют тот же механизм: выделение фиксированного числа байт и указатели (что тоже является небезопасным). Если вам нужно использовать массивы для вызова функций из библиотек, возможно, лучшим способом будет явный маршалинг массивов, который считается «безопасным». Давайте рассмотрим все три упомянутых способа.

Вызовы API

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

Это довольно просто конвертируется в C#:

Структура DISPLAY_DEVICE определена так:

Понятно, что она содержит четыре символьных массива с фиксированной длиной. Используя тип выравнивания Explicit, перепишем структуру в C#:

Обратите внимание на использования параметра Size для указания места, необходимого для хранения поля DeviceKey. Теперь если использовать эту структуру при вызове функции:

… то все, к чему вы можете обратиться напрямую — это первые символы массивов. Например, DeviceString содержит первый символ строки информации об устройстве. Если вы хотите получить остальные символы из массива, нужно получить указатель на DeviceString и использовать арифметику на указателях, чтобы пройти по массиву.
При использовании C# 2.0 самым простым решением является использовать в структуре массивы:

Обратите внимание, что структура должна быть помечена модификатором unsafe. Теперь после API вызова мы можем получить данные из массивов без использования указателей. Впрочем, неявно они все-таки используются, и любой код, обращающийся к массивам, должен быть помечен как небезопасный.
Третий и последний метод заключается в кастомном маршалинге. Многие C# программисты не понимают, что суть маршалинга заключается не только в том, как данные о типах передаются в библиотечные вызовы, — это еще и активный процесс, который копирует и изменяет управляемые данные. Например, если вы захотите передать ссылку на типизированный массив, вы можете передать его по значению, и система сконвертирует его в массив фиксированной длины и обратно в управляемый массив без дополнительных действий с вашей стороны.
В этом случае все, что нам остается сделать, это добавить атрибут MarshalAs, указывающий типа и размер массивов:

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

Сериализация структур

Теперь, после того как мы рассмотрели довольно сложные вопросы, связанные с размещением структур в памяти, самое время узнать, как получить все байты, составляющие структуру. Иными словами, как сериализовать структуру? Существует много способов сделать это, чаще всего используется метод Marshal.AllocHGlobal для выделения памяти в куче под неуправляемый массив. После этого все делается функциями, работающими с памятью, такими как StructToPtr или Copy. Пример:

Фактически, надобность в стольких действиях отсутствует, проще переместить байты структуры напрямую в байтовый массив без использования промежуточного буфера. Ключевым объектом в этом способе является GCHandle. Он возвратит хэндл Garbage Collector’а, и вы можете использовать метод AddrOfPinnedObject для получения стартового адреса структуры. Метод RawSerialize может быть переписан следующим образом:

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

Читайте также:  Фитнес браслет принцип работы

Чтение структур из потоков

Иногда возникает потребность зачитать структуру, возможно написанную на другом языке, в C# структуру. Например, вам нужно прочитать bitmap-файл, который начинается с заголовка файла, затем следует заголовок битмапа и затем собственно битовые данные. Структура заголовка файла выглядит так:

Функция, которая будет читать любой поток, и возвращать структуру, может быть написана без использования обобщений:

Для передачи данных здесь используется GCHandle. Новое в этом коде — использование параметра, указывающего на тип структуры. К сожалению, нельзя использовать этот тип для возвращаемого значения, поэтому после вызова функции необходимо преобразовать ее результат:

Если мы хотим избежать преобразования, тогда нужно использовать обобщенный метод:

Обратите внимание, что теперь мы должны преобразовать объект, возвращаемый методом PtrToStructure, в самом методе, а не в месте вызова, который теперь выглядит следующим образом:

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

Ручной маршалинг

Маршалинг так хорошо работает в подавляющем количестве случаев, что можно вообще забыть о его существовании. Однако если вы сталкиваетесь с чем-то необычным, вы можете удивиться, что происходит, когда маршалинг перестает работать. Например, некоторым API вызовам нужно передавать указатель на указатель на структуру. Вы уже знаете, как передать указатель на структуру — это просто передача по ссылке — и поэтому вам может показаться, что передать указатель на указатель тоже просто. Однако все сложнее, чем вы ожидаете. Давайте посмотрим.
В функции AVIFileCreateStream два последних параметра передаются как указатели на IntPtr и структуру соответственно:

Для вызова это функции вы бы написали:

Основываясь на предыдущих примерах, кажется более легким изменить передачу указателя на структуру самим указателем. Казалось бы, что может быть неверного в следующем объявлении:

Однако если вы попытаетесь передать адрес закрепленной (pinned) структуры:

… то увидите ошибку.

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

Тут просто возвращается IntPtr на область в куче, которая содержит копию данных. Единственный неприятный момент заключается в том, что нужно помнить об освобождении выделенной памяти:

Приведенный код работает в точности как стадартный маршалинг. Однако не забудьте, что lpstruct передается по значению как integer. Для того чтобы скопировать результат обратно в структуру, понадобится еще один метод:

Теперь, после того как мы реализовали ручной маршалинг указателя в структуру, нам нужно получить указатель на указатель на структуру. К счастью, нам не потребуется писать нового кода, потому что наша функция преобразования структуры в указатель может преобразовывать любой тип данных в неуправляемый указатель — включая и сам указатель.

В качестве примера возьмем функцию AVISaveOption, т.к. она принимает два указателя на указатель в качестве параметров:

Фактически параметр ppavi — это указатель на хэндл (который в свою очередь является указателем), а ppOptions — указатель на указатель на структуру. Для вызова этого метода нам понадобится структура:

Определение этой структуры можно посмотреть в документации по стандарту AVI. На следующем шаге нам нужно получить маршализованный указатель на структуру:

… а затем указатель на указатель:

… а за ним указатель на хэндл:

Теперь вызов функции:

… где остальные параметры не представляют интереса, сведения о них могут быть найдены в документации.

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

Обратите внимание, нужно использовать указатель на саму структуру, а не указатель на указатель! Ну и в конце освобождаем память:

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

Вы уже знаете, что классы относятся к ссылочным типам данных. Это означает, что объекты конкретного класса доступны по ссылке. Ссылка -это 32/64-х битный адрес, указывающий на расположение членов-данных в управляемой куче, в отличие от значений простых типов, доступных непосредственно.

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

Для разрешения подобных затруднений в C# предусмотрена структура, которая подобна классу, но относится к типу значений, а не к ссылочному типу данных.

Структуры отличаются от классов тем, как они сохраняются в памяти и как к ним осуществляется доступ (для начала – краткая формула «классы — это ссылочные типы, размещаемые в куче, структуры — типы значений, размещаемые в стеке»). Эта формула не совсем точная, пояснения см. позже. Структуры отличаются от классов некоторыми свойствами (например, структуры не поддерживают наследование).

Читайте также:  Раскрывающиеся списки в word

Из соображений производительности следует использовать структуры для небольших типов данных. Однако в отношении синтаксиса структуры очень похожи на классы.

Первое и очевидное отличие состоит в том, что при их объявлении используется ключевое слово struct вместо class. Ниже приведена общая форма объявления структуры:

struct имя
<
объявления членов
>
где имя обозначает конкретное имя структуры.

Как у класса, так и у каждой структуры имеются свои члены: методы, поля, индексаторы, свойства, операторные методы и события. Так в структуре Double (библиотека System) определено 6 констант, 6 операторов отношения, 19 методов, 5 интерфейсов (проверьте, используя интеллектуальную подсказку). Не многовато ли для вещественного числа, как вы думаете?

В структурах допускается также определять конструкторы. В то же время для структуры нельзя определить конструктор, используемый по умолчанию (без параметров), который определяется для всех структур автоматически и не подлежит изменению. Такой конструктор инициализирует поля структуры значениями, задаваемыми по умолчанию (false для типа bool, 0 – для чисел). А поскольку структуры в отличие от классов не поддерживают наследование, то их члены нельзя указывать как abstract, virtual или protected.

Экземпляр (можно говорить и «объект») структуры может быть создан с помощью оператора new таким же образом, как и объект класса, но в этом нет особой необходимости. Когда используется оператор new, то вызывается конструктор, используемый по умолчанию. А когда этот оператор не используется, объект по-прежнему создается, хотя и не инициализируется. В этом случае инициализацию любых членов структуры придется выполнить вручную. Рассмотрим пример.

Результат выполнения программы:


Исследуем программу. Закомментируем вторую строку в методе Main():
// stud1.name = «Петр»;
При запуске программы получим сообщение об ошибке:
Использование локальной переменной «stud1», которой не присвоено значение.
Хотя структура stud1 объявлена, но полю name никакого значения не присвоено, что приводит к ошибке. Такая же ошибка возникнет, если мы станем использовать переменную целого типа, которая объявлена, но не инициализирована.
Такой вот жесткий контроль компилятора за нашими действиями!

Проверим, как работает конструктор без параметров. Первую строку метода Main() заменим на
student stud1 = new student();
Вторую и третью строку – закомментируем. Тогда при выводе результата в 1 строке получим:
студент 1: Имя: , возраст: 0
Следовательно, полю name была присвоена пустая строка, а полю возраст – число 0.

Проверим, как работает защита данных. В второй строке описания структуры student удалим модификатор поля public, т.е. получим byte age;
При запуске программы получим два сообщения об ошибке:
«ConsoleApplication1.student.age» недоступен из-за его уровня защиты
в операторах stud1.age = 18; и stud2.age = 19;

Это означает, что так как по умолчанию поле age является полем private, то оно не может быть изменено в другом классе с помощью оператора присваивания, однако оно доступно методам класса student, объявленным как public. С учетом этих результатов, программа может быть записана так:

Здесь поля объектов stud1 и stud2 структуры student являются private-полями, а метод и конструктор объявлены как public.

А теперь ВНИМАНИЕ! Ключевая идея — Создание сложных конструкций данных, используя один тип данных, как элемент другой конструкции. В статье про типы данных мы выяснили, что все встроенные типы, относящиеся к типам значений (их еще называют простыми типами данных), являются структурами, хотя фактически каждая из них имеет одно поле, содержащее значение числа, символа или булевой переменной.

Структура позволяет объединять несколько различных по типу элементов под одним именем. Более того можно использовать неоднократное вложение одних типов данных в другие, используя структуры.

Для понимания этой идеи рассмотрим пример. Прототипом для него являются структуры из библиотеки System. Drawing: Point, Size и Rectangle.

Известно, что точка на экране задается парой целых чисел (X,Y) в пикселях. Объединим эти два числа в структуру точка. Установим максимальную защиту (private – по умолчанию) для этих полей. Добавим конструктор с параметрами точка(x,y) для инициализации объектов структуры точка, а также метод Get() для извлечения полей в строковом формате.

Размер прямоугольника на экране также задается парой чисел: шириной и высотой. Соответственно объявим структуру размер с полями (W,H). Добавим аналогичный конструктор с параметрами размер(w,h) для инициализации объектов структуры размер, а также метод Get() для извлечения полей в строковом формате.

Прямоугольник на экране определяется координатами верхнего левого угла (точка) и размером изображения (размер). Поэтому третью структуру построим на основе первых двух, назовем ее Rect (сокращенно от «прямоугольник»), полями которой будут объекты первых двух структур: точка P и размер S. Эти поля также будут защищенными (private – по умолчанию). Добавим в эту структуру конструктор с параметрами public Rect(int x, int y, int w, int h) и три метода: Get() для извлечения информации о прямоугольнике, Get_P() и Get_S для извлечения координат и размеров по отдельности.

В методе Main( ) последовательно инициализируем объект Rect r и покажем работу методов всех этих трех структур.


Конечно, если бы поля всех структур были объявлены с модификатором public, то в принципе можно обходиться структурами, имеющими только поля без методов. Но помня о том, что структуры похожи на классы, следует и к ним применять те же общие принципы, что и для классов в части защиты данных.

Ссылка на основную публикацию
Стиральная машина самсунг горит красный замок
Любая стиральная машина в независимости от марки производителя иногда выходит из строя. Довольно частым признаком неисправности, является мигание индикатора замка....
Справка по форматированию steam
С помощью этих тегов разметки можно форматировать текст ваших сообщений, примерно как в HTML. Маркированный список Маркированный список Маркированный список...
Справочные материалы база данных
АРМ предназначено для комплексной автоматизации операций, связанных с первичным размещением и вторичным обращением ценных бумаг. Оно рассчитано на работу с...
Стиральная машинка lg не выжимает
Покупка стиральной машинки – знаменательное событие для любой хозяйки. Незаменимая помощница позволяет женщинам экономить личное время, не тратя его на...
Adblock detector