воскресенье, 22 марта 2015 г.

Struct vs Class

На собеседованиях я не раз задавал вопрос: «В каких случаях вы предпочтёте использование структуру вместо класса?». 90% людей, почему-то думают, что их спрашивают о различиях структур и классов. И поэтому начинают рассказывать о том где хранятся значимые и ссылочные типы, о невозможность наследования структур и т. п. Хотя они говорят всё верно, но это не совсем то, что их спрашивают. В этой заметке я попытаюсь ответить на поставленный вопрос.

Базовые отличия

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

 

Классы

Структуры

1

Ссылочные типы, хранятся в куче и очищаются сборщиком мусора

Значимые типы, хранятся в стеке или внутри экземпляра другого типа и очищаются на выходе из области видимости или при очистке экземпляра, содержащего структуру

2

Создание и удаление экземпляров медленное

Создание и удаление экземпляров быстрое

3

Копируются по ссылке (быстро)

Копируются по значению (медленно для больших структур)

4

Массивы хранят ссылки на объекты в куче; создание и удаление массивов медленное

Массивы хранят непосредственно значения структур; создание и удаление массивов быстрое

5

Не боксируются (т.к. всегда находятся в куче)

Боксируются при приведении к ссылочному типу (класс или интерфейс) и разбоксируются при приведении к значимому типу; частое боксирование/разбоксирование снижает производительность

В виду этих отличий можно дать ряд рекомендаций, которые будут служить отправной точкой при выборе:

Старайтесь использовать структуры вместо классов если:

  1. объекты имеют короткий жизненный цикл или часто включены в состав других объектов
  2. происходит создание большого числа объектов

Избегайте использования структур, если тип (экземпляр):

  1. НЕ представляет логически целостное значение, похожее на примитивы (int, double ...)
  2. имеет размер более 16 байт
  3. будет изменять свои свойства (не immutable)
  4. будет часто боксироваться / разбоксироваться

 

Проверим рекомендации на практике

Давайте проведём несколько тестов, в которых мы будем использовать пользовательский тип MyType.

struct MyType
{
    public int Prop1 { get; set; }
    //public int Prop2 { get; set; }
    //public int Prop3 { get; set; }
    //public int Prop4 { get; set; }
    //public int Prop5 { get; set; }
    //public int Prop6 { get; set; }
    //public int Prop7 { get; set; }
    //public int Prop8 { get; set; }
}

1) Fill In 1: в цикле заполним массив значениями (новый массив создаётся один раз)

var array = new MyType[ArraySize];

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < ArraySize; j++)
    {
        array[j] = new MyType();
    }
}

2) Fill In 2: в цикле заполним массив значениями (новый массив создаётся многократно)

for (int i = 0; i < 100; i++)
{
    var array = new MyType[ArraySize];

    for (int j = 0; j < ArraySize; j++)
    {
        array[j] = new MyType();
    }
}

3) Copy: Скопируем значения из одного массива в другой

for (int i = 0; i < 10000; i++)
{
    Array.Copy(src, dest, src.Length);
}

Тесты будут последовательно запущены для случаев когда:



  • тип сначала структура, потом класс

  • у типа есть: 1, 2, 4 или 8 свойств размером 4 байта каждое (Int32).

  • размер массива 100 000.

Результаты:


clip_image001


Выводы:



  1. Заполнение массива структурой происходит на порядок быстрее (не зависимо от размера структуры)
  2. Копирования массива небольших структур происходит очень быстро, но если структуры больше 16 байт, то скорость сильно падает
  3. Копирование массива классов не зависит от размера экземпляра класса.

 


А что если не следовать рекомендациям?


Довольно часто можно встретить игнорирование этих советов во многих классах BCL. Так зачем же тогда советы, если сами авторы им не следуют? Думаю, они будут крайне полезны начинающим программистам, которые ещё не умудрённые премудростями реализации CLR и могут бездумно “наворотить делов”.

Более опытные программисты, должны подходить к вопросу выбора комплексно, взвешенно и в соответствии с ситуацией. Тут лучшим советом будет: “Семь раз замерь, один раз закомить”. Т. е. как бы вы хорошо не разобрались в вопросе, в местах где производительность важна, крайне желательно проверить небольшим тестом правильность сделанного выбора.

Давайте рассмотрим пример из BCL, когда разработчики сознательно решили нарушить рекомендации. Это можно увидеть в классе Dictionary<T,T>. Благодаря открытости исходников, внутри мы можем увидеть приватную структуру Entry, которая используется для хранения значений словаря.

public class Dictionary<TKey,TValue>
{
    //...
    private struct Entry
    {
        public int hashCode;
        public int next;
        public TKey key;
        public TValue value;
    }
    //...
}

В зависимости от размеров TKey и TValue, данная структура может быть гораздо больше, чем 16 байт. Имея доступ к исходному коду класса Dictionary, мы можем его видоизменить и посмотреть, как он будет работать, если структуру Entry сделать классом. Проведём тест на добавление большого количества элементов в словарь и замерим различия по памяти и времени.

В случае перехода на ссылочный тип, потребляемая память не изменилась, а вот скорость работы – упала (а точнее часть кода, отвечающая за копирование значений при изменении размеров). Это произошло потому, что при использовании пустого конструктора, Dictionary не знает сколько элементов будет добавлено и сначала он выделяет внутренний массив небольшого размера, а по мере необходимости - увеличивает размер массива, при этом каждый раз происходит копирование старых значений в новый массив большего размера. Как мы видели ранее, создание значимого типа и копирование значений массива происходит быстрее в случае использования структуры. И поэтому в случае использования небольшого размера TKey и TValue, процедура копирования показывает 5-ти кратное ускорение, при использовании именно структуры, а не класса. И хотя в случае больших TKey и TValue, мы потеряем в производительности, выбор в пользу структур всё-равно остаётся обоснованным – всё же ключ/значение как правило небольшие.

 


Решили начать использовать структуры?


Подождите. Убедитесь, что вы себе не отстрелите ногу, нарушив одно очень важное правило – правило о «неизменяемости» значимого типа (immutability). Нарушив его, вы буквально гарантированно закладываете мину замедленного действия, на которой если не вы, так ваш коллега обязательно подорвётся. Дабы этого не случилось, настоятельно рекомендую ознакомится со следующей статьёй.

 


Bonus Quiz: проверь знания о типах в C#.



  1. Структуры наследуют ссылочный тип System.Object?

  2. Можно ли создать тип, который будет наследовать System.ValueType? Если да, то как?

  3. System.ValueType это класс или структура?

  4. Где в C# коде может пригодиться тип System.ValueType?

  5. Есть ли у структуры 2 дополнительны поля (указатель на тип и SyncBlockIndex)?

Ответы:


  1. Да.

  2. Объявив структуру, мы получим прямого наследника System.ValueType.

  3. Это абстрактный класс, который компилятор C# запрещает использовать в качестве базового класса.

  4. Что бы создать полиморфный метод, принимающий только значимые типы (см. метод Compare)

  5. Только если структура упакована (boxing), т.е. когда находится в куче.

2 комментария:

  1. ---Где в C# коде может пригодиться тип System.Value?

    Возможно вы имели в виду System.ValueType?

    ОтветитьУдалить