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

С 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 ключ и значение неразделимые понятия. Да, внутри все устроено иначе. Внутри мы имеем сложную структуру, где ключ лежит отдельно от значения. Однако для внешнего пользователя, с точки зрения интерфейса взаимодействия и смысла самой структуры данных пара ключ-значение является неразделимым понятием. Является значением целиком. Если мы по этому ключу расположили другое значение это значит, что изменилась вся пара. Для внешнего наблюдателя нет отдельно ключей, а отдельно — значений, они являются единым целым. Именно поэтому структура в данном случае — идеальный вариант.
+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), которое позволяет передавать неизменяемую ссылку. Соответственно неизменяемая ссылка на неизменяемую структуру не позволит с ней ничего сделать. Так что тут ничего вообще не меняется.