Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Типы-значения и ссылочные типы
Подобный материал:
1   ...   13   14   15   16   17   18   19   20   ...   47

Типы-значения и ссылочные типы14




В С# существует ключевые слова struct и class. В чем их отличие? На первый взгляд результат их применения одинаков. В результате получаются объекты, которые могут содержать данные и методы. Какой способ выбрать?


Выбор определяется тем, как планируется использовать новый тип. Struct описывает тип-значение. Типы-значения не являются полиморфными. Они лучше подходят для хранения данных. Ссылочные типы могут быть полиморфными и должны применяться для определения поведения объектов. Оцените набор обязанностей нового типа и, исходя из этого, решите, какой тип следует создать. Итак, в структурах хранятся данные, классы определяют поведение.


Идея типов-значений пришла из математики, где при сложении двух чисел меняется не исходное число, а создается новое.


Базовым классом для типов-значений является System.ValueType, который наследует от System.Object и переопределяет некоторые его методы. Для ссылочных типов базовым классом является System.Object.


Типы-значения и ссылочные типы были добавлены в .NET и в С# в связи с проблемами, которые имели место в С++ и Java. В С++ все параметры и возвращаемые значения передаются по значению. Передача по значению эффективна, но страдает от одной проблемы: частичное копирование (которое иногда называют расслоением (slicing) объекта). Если используется производный объект (derived object) как переменная типа базового класса, то копируется только базовая часть объекта. Фактически теряются все сведения о том, что производный объект существовал. Даже вызовы виртуальных функций пересылаются в версию базового класса.


Java среагировал на описанную ситуацию удалением из языка типов-значений. Все определяемые пользователями типы являются ссылочными. В языке Java все параметры и возвращаемые значения передаются по ссылке. У этой стратегии есть преимущество — согласованность, но она обусловливает снижение производительности. Программисты на Java платят за резервирование памяти (для использования ее программой в процессе выполнения) и за возможную сборку мусора по каждой переменной. Кроме того, тратится время на разыменование переменных. Все переменные являются ссылками. В С# для объявления, должен ли новый тип быть типом-значением или ссылочным типом, используются ключевые слова struct и class. Типы-значения должны быть маленькими, легковесными. Ссылочные типы предназначены для создания иерархии классов.


Начнем с типа, который используется как возвращаемое значение:

private MyData _myData;

public MyData Foo()

{

return _myData;

}


// call it:

MyData v = Foo();

TotalSum += v.Value;


Если MyData является типом-значением, возвращаемое значение будет скопировано в память, выделенную для v. Более того, v будет размещена в стеке. Однако если MyData является ссылочным типом, ссылку придется экспортировать во внутреннюю переменную класса. Это нарушит инкапсуляцию данных класса.


private MyData _myData;

public MyData Foo()

{

return _myData.Clone( ) as MyData;

}


// call it:

MyData v = Foo();

TotalSum += v.Value;


Теперь v является копией исходного _myData. Поскольку используется ссылочный тип, в «куче» (heap) создаются два объекта. У вас не возникает проблемы с доступом к внутренним данным. Вместо этого в куче создается дополнительный объект. Если v является локальной переменной, она быстро становится мусором. Кроме того, Clone использует оператор as для приведение типа переменной. Такое решение неэффективно.


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


Рассмотрим альтернативный вариант:

private MyType _myType;

public IMyInterface Foo()

{

return _myType as IMyInterface;

}


// call it:

IMyInterface iMe = Foo();

iMe.DoWork( );


Переменная _myType по-прежнему возвращается методом Foo. Но на этот раз, вместо получения данных через возвращаемое значение, к объекту обращаются для того, чтобы вызвать метод через его интерфейс. Обращение к объекту МуТуре выполняется для знакомства с его поведением, а не с его данными. Поведение выражается через интерфейс IMyInterface, который может быть реализован во многих классах. В этом примере МуТуре должен быть ссылочным типом, а не типом-значением. Обязанности типа МуТуре связаны с определением его поведения, они не связаны с хранением данных.


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

public class C

{

private MyType _a = new MyType( );

private MyType _b = new MyType( );


// Remaining implementation removed.

}


C var = new C();


Сколько объектов будет создано? Насколько они будут велики? Если МуТуре является типом-значением, память будет выделена всего один раз. Размер выделенной памяти будет в два раза больше размера МуТуре. Однако если МуТуре является ссылочным типом, будет сделано три выделения памяти: одно для объекта С, который занимает 8 байтов (если предположить, что у нас действуют 32-битовые указатели), и еще по два для каждого объекта МуТуре, содержащегося в объекте С. Дело в том, что типы-значения хранятся встроенными в объект, а ссылочные типы — нет. Каждая переменная ссылочного типа содержит ссылку, и для ее хранения требуется дополнительное место.


Для пояснения этого момента рассмотрим следующее выделение памяти:


MyType [] var = new MyType[ 100 ];


Если МуТуре является типом-значением, произойдет одно выделение памяти размером, в 100 раз превышающим размер объекта МуТуре. Однако если МуТуре имеет ссылочный тип, сначала будет сделано одно выделение памяти. Каждый элемент массива будет пустым. При условии, что нужно инициализировать каждый элемент массива, будет выполнено 101 выделение памяти, а на это требуется больше времени, чем на одно выделение. Выделение большого количества фрагментов для ссылочных типов фрагментирует кучу и замедляет работу. Если создаются объекты, предназначаемые для хранения данных, то самыми подходящими будут типы-значения.


Выбор решения, создавать тип-значение или ссылочный тип, является важным моментом. Превращение типа-значения в ссылочный тип может разрушить работу программы. Рассмотрим следующий вариант:


public struct Employee

{

private string _name;

private int _ID;

private decimal _salary;


// Properties elided


public void Pay( BankAccount b )

{

b.Balance += _salary;

}

}

Этот довольно простой тип содержит один метод, предназначенный для начисления зарплаты сотрудникам. Идет время, и система работает прекрасно. Потом требования меняются и выясняется, что имеются различные виды служащих (Employees): сотрудники отдела продаж получают комиссионные, а менеджеры получают бонусы. Поэтому принимается решение преобразовать тип Employee в класс:


public class Employee

{

private string _name;

private int _ID;

private decimal _salary;


// Properties elided


public virtual void Pay( BankAccount b )

{

b.Balance += _salary;

}

}


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

Employee e1 = Employees.Find( "CEO" );

e1.Salary += Bonus; // Add one time bonus.

e1.Pay( CEOBankAccount );


To, что было единовременной акцией, становится постоянным начислением. Там, где раньше использовалась копия значения, теперь применяется ссылка. Компилятор с охотой выполнит эти изменения. Генеральный директор (СЕО), скорее всего, не будет иметь ничего против. Но финансовый директор долго будет искать причину несостыковки в расходах . Таким образом, замена типа-значения ссылочным типом меняет поведение клиентского кода, что недопустимо.


Причина происходящего кроется в том, что тип Employee более не следует правилам для типа-значения. Помимо хранения элементов данных для работника, в него добавлена обязанность — платежи. Обязанности распределены неверно. Классы могут определять полиморфное поведение; структуры не умеют этого делать и должны быть ограничены только хранением значений.


В документации по .NET рекомендуется рассматривать размер типа как определяющий фактор при выборе между типами-значениями и ссылочными типами. В действительности, более важный фактор — планирующийся способ применения. Типы, являющиеся просто хранителями данных, прекрасные кандидаты на роль структур (типов-значений). Типы-значения являются более эффективными с точки зрения управления памятью: уменьшается фрагментация кучи, меньше мусора и меньше разыменований (indirection). Важно, что типы-значения копируются, когда они возвращаются из методов или свойств. Отсутствует опасность раскрытия ссылок на внутренние данные класса. С другой стороны типы-значения обладают весьма ограниченной поддержкой принципов ООП. Невозможно создать иерархию объектов, пользуясь типами-значениями. Можно создавать типы-значения, которые реализуют интерфейсы, но их требуется упаковывать (boxing), что приводит к снижению производительности (упаковка будет рассмотрена позже). Следует рассматривать типы-значения как контейнеры для хранения, но не как объекты в объектно-ориентированном смысле.


Ссылочных типов обычно создается больше, чем типов-значений. Если на приведенные ниже вопросы можно ответить утвердительно, создайте тип-значение.


1. Является ли главной обязанностью типа хранение данных?

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

3. Уверены ли вы в том, что у этого типа никогда не будет разновидностей?

4. Уверены ли вы в том, что этот тип никогда не будет трактоваться как полиморфный?


Хранилища данных следует строить как типы-значения. Поведение приложения следует строить, используя ссылочные типы. Этим обеспечивается безопасность возвращения данных из объектов.