Итак, тип UNION — это как бы структура, но не она. Все ее элементы занимают одно адресное пространство, поэтому записывая данные в один элемент, мы перезаписываем целиком или частично данные остальных элементов. В структурах эти элементы называются полями, здесь же сложно использовать термин "поле" потому что адресное поле по сути — одно единственное, а переменных несколько и они могут быть разного типа и разного размера — что, впрочем, тоже является одной из ключевых фишек этого типа данных.
Можно пытаться убедить не применять UNION, что он устарел и больше не нужен, но мы по прежнему одной ногой в эмбедед, где все-таки приходится иногда экономить память (первое применение), использовать трюки с преобразованием типов данных (второе применение) и где только-только появилось ООП (привет CoDeSys 3 и TwinCAT 3).
Расчленение данных
TYPE ST_Word: STRUCT Lo : BYTE; (* младший байт *) Hi : BYTE; (* старший байт *) END_STRUCT END_TYPE
TYPE U_Word: UNION Bytes : ST_Word; Value : WORD; END_UNION END_TYPE
Объявив переменную как UNION, мы получили структуру, внутри которой два элемента (Bytes и Value) хранятся в одном адресном пространстве. Записывая, что-либо в переменную Value мы также записываем эти данные и в Bytes. Верно и обратное — записывая данные в поля структуры Bytes : ST_WORD мы одновременно заполняем данными переменную Value.
Это очень похоже на синонимы (ALIAS), но с разными типами данных, и на самом деле никакой одновременности записи в разные переменные здесь нет — просто данные лежат в одном месте и разные пересекающиеся части этого места называются по разному. К сожалению, построчная запись в объявлении UNION часто сбивает с толку разработчиков, и они забывают, что данные переменных хранятся в ячейках памяти, и контроллер работает с ячейками памяти, а не с какими-то там именами человеческих переменных.
PROGRAM MAIN VAR yourWord : U_Word; hi : BYTE; lo : BYTE; END_VAR yourWord.Value := 16#AABB; hi := yourWord.Bytes.Hi; // == 16#AA lo := yourWord.Bytes.Lo; // == 16#BB
Записываем в поле Value 16-разрядное число, которое накладыватся на 16-разрядную структуру Bytes : ST_WORD, содержащую два байтовых поля. Таким образом записанное значение разбивается на две однобайтных части: старший байт и младший байт. И никаких битовых операций для доступа к байтам WORD.
В TwinCAT 3 появился механизм наследования, который мы можем применить к UNION:
TYPE U_Word EXTENDS ST_Word: UNION Value : WORD; END_UNION END_TYPE
Код становится лаконичнее, так как отпала необходимость в указании промежуточной структуры Bytes:
PROGRAM MAIN yourWord.Value := 16#AABB; hi := yourWord.Hi; // == 16#AA lo := yourWord.Lo; // == 16#BB
Одно поле
Картинка лучше тысячи слов. Возьмем скарпель указателей и пройдемся по плитке памяти:Адреса элементов Value, Lo и адрес в pYourWord (это указатель на UNION) совпадают, так как они расположены в начале области памяти выделенной под UNION. Указатель конечно же не расположен, а ссылается, но нас интересуют адреса, а не механизм работы. Теперь сравните адреса ячеек Lo и Hi (pYourWord_Lo и pYourWord_Hi, соответственно). Они различаются ровно на один байт, так как каждая из них занимает ровно один байт, а расположены они последовательно друг за другом.
Если этого недостаточно, то вот вам код:
PROGRAM MAIN VAR oneWord : WORD; yourWord : U_WORD; pYourWord : ULINT; pYourWord_Value : PVOID; pYourWord_Lo : LWORD; pYourWord_Hi : LWORD; wordLo : BYTE; wordHi : BYTE; END_VAR pYourWord := ADR(yourWord); pYourWord_Value := ADR(yourWord.Value); pYourWord_Lo := ADR(yourWord.Lo); pYourWord_Hi := ADR(yourWord.Hi); oneWord := yourWord.Value; MEMCPY( ADR(wordLo), (* <-- *) pYourWord_Lo, SIZEOF(wordLo)); MEMCPY( ADR(wordHi), (* <-- *) pYourWord_Hi, SIZEOF(wordHi));
Вместо типов LWORD и ULINT здесь лучше использовать POINTER TO U_WORD и POINTER TO BYTE, но мне хотелось убедиться, что указатели теперь 64-х разрядные и что для хранения адресов подходят 64-х разрядные типы: LWORD, ULINT, PVOID. Последний, кстати, как и обещали в TwinCAT 3 стал 64-х разрядным.
PVOID — это синоним для UDINT в TwinCAT 2 (32-х разрядные адреса) или для ULINT в TwinCAT 3 (64-х разрядные адреса). Всегда используйте его как универсальное хранилище адреса, так как независимо от версии TwinCAT, вы всегда получите переменную с правильным типом данных. Вот еще один пример правильного использования ALIAS.Заодно посмотрите на смертельно опасную функцию MEMCPY, позволяющую писать куда угодно в память.
Другие применения UNION
Экономим память — храним данные разных типов в одном месте. Главное, точно знать какие данные сохранены в текущий момент времени и вовремя их перезагружать.
Реинтерпретация типа данных
Мы хотим считать, что вот тот тип данных сейчас не REAL, а DWORD. Например, чтобы без лишних преобразований пересылать его через Modbus:
TYPE U_Real : UNION Re : REAL; Dw : DWORD; END_UNION END_TYPE
Для работы используем поле Re, а для отправки данных Dw. И никакого преобразования данных, все происходит автоматически.
Variant
Или когда мы не знаем какой тип данных будет использован во время работы программы. Этот же прием можно использовать для создания псевдо-полиморфизма в TwinCAT 2.
Сначала готовим болванку для хранения данных разного типа:
TYPE U_VarObject : UNION AsInteger : INT; AsFloat : REAL; AsDouble : LREAL; LikeString : STRING; END_UNION END_TYPE
Затем описываем (перечисляем) доступные типы данных:
TYPE E_VarType : ( Integer, Float, Double, String255 ); END_TYPE
Упаковываем все это в структуру:
TYPE ST_Variant : STRUCT TypeIs : E_VarType; Value : U_VarObject; END_STRUCT END_TYPE
Работает как-то так:
PROGRAM MAIN VAR xVar : ST_Variant; Counter : INT; Text : STRING; END_VAR CASE xVar.TypeIs OF E_VarType.Integer: Counter := xVar.Value.AsInteger; E_VarType.Float: ; E_VarType.Double: ; E_VarType.String255: Text := xVar.Value.LikeString; END_CASE
Синонимы
Очень похоже на вычленение данных, но будем использовать по принципу ALIAS:
TYPE ST_Vector3R: STRUCT X : REAL; Y : REAL; Z : REAL; END_STRUCT END_TYPE
TYPE U_Vector3R EXTENDS ST_Vector3R: UNION E : ARRAY [1..3] OF REAL; END_UNION END_TYPE
Теперь координаты вектора доступны для нас как по именам, так и по индексам:
PROGRAM MAIN VAR Vec : U_Vector3R; i : UINT; END_VAR FOR i := 1 TO 3 DO Vec[i] := i * 100; END_FOR Vec.Y := 0.0;