СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H [DotNetBook] Особенности выбора class/struct. Свой boxing, превращение Int в структуру, реализующую интерфейс в черновиках Tutorial

C#, .NET

С этой статьей я продолжаю публиковать целую серию статей, результатом которой будет книга по работе .NET CLR, и .NET в целом. Тема IDisposable была выбрана в качестве разгона, пробы пера. Теперь коснемся разныцы между типами. Вся книга будет доступна на GitHub: DotNetBook. Так что Issues и Pull Requests приветствуются :)


Особенности выбора между class/struct


Давайте подумаем об особенностях обоих типов, об их достоинствах и недостатках и решим, где ими лучше пользоваться. Тут, конечно же стоит вспомнить классиков, дающих утверждение что выбор в сторону значимых типов, стоит дать если у нас тип не планирует быть наследуемым, он не станет меняться в течении своей жизни, а его размер не превышает 16 байт. Но не все так очевидно. Чтобы сделать полноценное сравнение нам необходимо задуматься о выборе типа с разных сторон, мысленно продумав сценарии его будущего использования. Разделить критерии выбора я предлагаю на три группы:


  • с точки зрения архитектуры системы типов, в которой ваш тип будет взаимодействовать;
  • с точки зрения подхода вас как системного программиста: каков выбор будет оптимальным с точки зрения производительности;
  • по-другому просто невозможно.

Каждая сущность, которая проектируется вами должна в полной мере отражать ее назначение. И это касается не только её названия или интерфейса взаимодействия (методы, свойства), но даже выбор между значимым и ссылочным типом может быть сделан из архитектурных соображений. Давайте порассуждаем, почему с точки зрения архитектуры системы типов может быть выбрана структура, а не класс:


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


    • DateTime — это структура, которая инкапсулирует в себе понятие момента времени. Она хранит эти данные в виде uint, однако предоставляет доступ к отдельным характеристикам момента времени. Например: год, месяц, день, час, минуты, секунды, миллисекунды и даже процессорные тики. Однако исходя из того что она инкапсулирует — она не может быть изменяемой по своей природе. Мы не можем изменить конкретный момент времени чтобы он стал другим. Я не могу прожить следующую минуту своей жизни в лучший день рождения своего детства. Время неизменно. Именно поэтому выбор для типа данных может стать либо класс с readonly интерфейсом взаимодействия (который на каждое изменение свойств отдает новый экземпляр) либо структура, которая несмотря на возможность изменения полей своих экземпляров делать этого не должна: описание момента времени является значением. Как число. Вы же не можете залезть в структру числа и поменять его? Если вы хотите получить другой момент времени, который является смещением относительно оригинального на один день, вы просто получаете новый экземпляр структуры;
    • KeyValuePair<TKey, TValue> — это структура, инкапсулирующая в себе понятие связной пары ключ-значение. Замечу что эта структура используется только для выдачи пользователю при перечислении содержимого словаря. Почему выбрана структура с точки зрения архитектуры? Ответ прост: потому что в рамках Dictionary ключ и значение неразделимые понятия. Да, внутри все устроено иначе. Внутри мы имеем сложную структуру, где ключ лежит отдельно от значения. Однако для внешнего пользователя, с точки зрения интерфейса взаимодействия и смысла самой структуры данных пара ключ-значение является неразделимым понятием. Является значением целиком. Если мы по этому ключу расположили другое значение это значит, что изменилась вся пара. Для внешнего наблюдателя нет отдельно ключей, а отдельно — значений, они являются единым целым. Именно поэтому структура в данном случае — идеальный вариант.

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


    • Как, например, если взять структуру заголовка файла, было бы нечестно дать ссылку из одного файла в другой. Мол, заголовок находится в файле header.txt. Это было бы уместно при вставке документа в некий другой, но не вживанием файла, а по относительной ссылке на файловой системе. Хороший пример — файл ярлыка ОС Windows. Однако если мы говорим о заголовке файла (например, о заголовке JPEG файла, в котором указаны размер изображения, методика сжатия, параметры съемки, коодинаты GPS и прочая метаинформация), то при проектировании типов, которые будут использованы для парсинга заголовка будет крайне полезно использовать структуры. Ведь, описав все заголовки в структурах вы получите в памяти абсолютно такое же положение всех полей как в файле. И через простое unsafe преобразование *(Header *)readedBuffer без каких-либо десериализаций — полностью заполненные структуры данных.


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

комментарии (17)

0
sidristij ,  
Прошу прощения за сломанную верстку в самом начале: парсер md шалил
+1
slonopotamus ,  

Магистр Йода стиль письма ваш одобряет.

0
sidristij ,  
Намёк понял, надо вычитать ещё раз :)
+1
shai_hulud ,   * (был изменён)
DateTime — это структура, которая инкапсулирует в себе понятие момента времени. Она хранит эти данные в виде uint

Нет. Хранит оно в UInt64. Статья про .NET а алиасы(uint) из C#.


Мы получили абсолютно полный аналог боксинга.

Нет не получили. Анбоксинг не работает (int)value -> ошибка. Еще скрыли все доступные интерфейсы типа IComparable, IConvertible. Это обычная обертка.


Шутки ради можно даже сделать чтобы в методе финализации объекты воскрешали бы себя, засовывая обратно в пул объектов

Такие советы, даже в шутку, нельзя давать. Это 100% убить производительность и весь профит от пулинга.

0
sidristij ,  
1) обычная опечатка — исправлено
2) Анбоксинг будет работать как boxed.Value
3) Я же не написал что это надо делать :). int в структуру превращать знаете ли тоже сомнительное развлечение.
0
sidristij ,  
Кстати, можно же сделать неявное приведение типа обратно в T. Тогда вообще огонь )
0
sidristij ,  
Нет, кстати, не огонь. Потеряется возможность менять значение. Меняться-то копия будет
+1
unsafePtr ,  
Boxed должен иметь ограничение — where T: struct. Иначе можем в него запихнуть ссылочный тип.
0
sidristij ,  
да, спасибо
+1
fsou11 ,  
Не могли бы вы на пальцах пояснить эту математику?

В нашем примере с 4 байтами — 85К / (от 16 до 32 байт на поле * количество полей = 4) минус размер заголовка массива: примерно от 650 до 1300 элементов на массив в зависимости от платформы (а брать понятное дело надо в меньшую сторону). Всего-то! Не так и много! А ведь могло показаться что магическая константа в 1000 элементов вполне могла подойти!
0
qw1 ,  
И через простое unsafe преобразование *(Header *)readedBuffer без каких-либо десериализаций
Ага, «i just readed a book and it was unbad»
0
MrDaedra ,  
Мы с вами прошли как может показаться и огонь и воду и можем пройти любое собеседование. Возможно даже в команду .NET CLR.

Так думал я после нескольких лет работы в качестве .NET разработчика, пока не увидел код одного известного OpenSource проекта с использованием ILGenerator.
0
fsou11 ,  
Чем плохо использование ILGenerator?
0
shai_hulud ,  
Диеамический код проще собрать на System.Linq.Expressions не имея экспертных знаний в IL и особенностей .NET(чего только стоит взамотношения с value-type).
+1
PsyHaSTe ,  
Мне всегда казалось, что выбор намного проще: нужна семантика копирования, используем struct, нужна семантика ссылок — class. Рассуждения в статье действительно больше про преждевременную оптимизацию. А архитектурно важнее, копируем мы объект или расшариваем ссылку на него. Несколько некорректный, зато простой вывод: делайте неизменяемые структуры и изменяемые классы.
0
fsou11 ,  
В свете неизменяемых классов и передаваемых по ссылке структур всё становится несколько запутаннее.
0
PsyHaSTe ,  
Неизменяемые классы или структуры по сути определяется просто, используем мы их через интерфейс или напрямую. Через интерфейс — класс, нет — структура. Насчет передаваемых по ссылке структур — есть ключевое слово in (C# 7.3), которое позволяет передавать неизменяемую ссылку. Соответственно неизменяемая ссылка на неизменяемую структуру не позволит с ней ничего сделать. Так что тут ничего вообще не меняется.