Чем отличается хранение в памяти массивов из величин значимого и ссылочного типов
Перейти к содержимому

Чем отличается хранение в памяти массивов из величин значимого и ссылочного типов

  • автор:

Чем отличается хранение в памяти массивов из величин значимого и ссылочного типов

Ранее мы рассматривали следующие элементарные типы данных: int, byte, double, string, object и др. Также есть сложные типы: структуры, перечисления, классы. Все эти типы данных можно разделить на типы значений, еще называемые значимыми типами, (value types) и ссылочные типы (reference types). Важно понимать между ними различия.

  • Целочисленные типы ( byte, sbyte, short, ushort, int, uint, long, ulong )
  • Типы с плавающей запятой ( float, double )
  • Тип decimal
  • Тип bool
  • Тип char
  • Перечисления enum
  • Структуры ( struct )
  • Тип object
  • Тип string
  • Классы ( class )
  • Интерфейсы ( interface )
  • Делегаты ( delegate )

В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные метода, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек — это некоторая область памяти в адресном пространстве.

Когда программа только запускается на выполнение, в конце блока памяти, зарезервированного для стека устанавливается указатель стека. При помещении данных в стек указатель переустанавливается таким образом, что снова указывает на новое свободное место. При вызове каждого отдельного метода в стеке будет выделяться область памяти или фрейм стека, где будут храниться значения его параметров и переменных.

class Program < static void Main(string[] args) < Calculate(5); >static void Calculate(int t) < int x = 6; int y = 7; int z = y + t; >>

При запуске такой программы в стеке будут определяться два фрейма — для метода Main (так как он вызывается при запуске программы) и для метода Calculate:

Структура стека в языке программирования C#

При вызове этого метода Calculate в его фрейм в стеке будут помещаться значения t, x, y и z. Они определяются в контексте данного метода. Когда метод отработает, область памяти, которая выделялась под стек, впоследствии может быть использована другими методами.

Причем если параметр или переменная метода представляет тип значений, то в стеке будет храниться непосредсвенное значение этого параметра или переменной. Например, в данном случае переменные и параметр метода Calculate представляют значимый тип — тип int, поэтому в стеке будут храниться их числовые значения.

Ссылочные типы хранятся в куче или хипе, которую можно представить как неупорядоченный набор разнородных объектов. Физически это остальная часть памяти, которая доступна процессу.

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

Так, в частности, если мы изменим метод Calculate следующим образом:

static void Calculate(int t)

То теперь значение переменной x будет храниться в куче, так как она представляет ссылочный тип object, а в стеке будет храниться ссылка на объект в куче.

Ссылочные типы в куче в языке программирования C#

Составные типы

Теперь рассмотим ситуацию, когда тип значений и ссылочный тип представляют составные типы — структуру и класс:

State state1 = new State(); // State — структура, ее данные размещены в стеке Country country1 = new Country(); // Country — класс, в стек помещается ссылка на адрес в хипе // а в хипе располагаются все данные объекта country1 struct State < public int x; public int y; >class Country

Здесь в методе Main в стеке выделяется память для объекта state1. Далее в стеке создается ссылка для объекта country1 ( Country country1 ), а с помощью вызова конструктора с ключевым словом new выделяется место в хипе ( new Country() ). Ссылка в стеке для объекта country1 будет представлять адрес на место в хипе, по которому размещен данный объект..

Ссылычные типы и типы значений в C#

Таким образом, в стеке окажутся все поля структуры state1 и ссылка на объект country1 в хипе.

Но, допустим, в структуре State также определена переменная ссылочного типа Country. Где она будет хранить свое значение, если она определена в типе значений?

State state1 = new State(); Country country1 = new Country(); struct State < public int x; public int y; public Country country; public State() < x = 0; y = 0; country = new Country(); >> class Country

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

Стек и куча в языке программирования C#

Копирование значений

Тип данных надо учитывать при копировании значений. При присвоении данных объекту значимого типа он получает копию данных. При присвоении данных объекту ссылочного типа он получает не копию объекта, а ссылку на этот объект в хипе. Например:

State state1 = new State(); // Структура State State state2 = new State(); state2.x = 1; state2.y = 2; state1 = state2; state2.x = 5; // state1.x=1 по-прежнему Console.WriteLine(state1.x); // 1 Console.WriteLine(state2.x); // 5 Country country1 = new Country(); // Класс Country Country country2 = new Country(); country2.x = 1; country2.y = 4; country1 = country2; country2.x = 7; // теперь и country1.x = 7, так как обе ссылки и country1 и country2 // указывают на один объект в хипе Console.WriteLine(country1.x); // 7 Console.WriteLine(country2.x); // 7

Так как state1 — структура, то при присвоении state1 = state2 она получает копию структуры state2. А объект класса country1 при присвоении country1 = country2; получает ссылку на тот же объект, на который указывает country2. Поэтому с изменением country2, так же будет меняться и country1.

Ссылочные типы внутри типов значений

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

State state1 = new State(); State state2 = new State(); state2.country.x = 5; state1 = state2; state2.country.x = 8; // теперь и state1.country.x=8, так как state1.country и state2.country // указывают на один объект в хипе Console.WriteLine(state1.country.x); // 8 Console.WriteLine(state2.country.x); // 8 struct State < public int x; public int y; public Country country; public State() < x = 0; y = 0; country = new Country(); // выделение памяти для объекта Country >> class Country

Переменные ссылочных типов в структурах также сохраняют в стеке ссылку на объект в хипе. И при присвоении двух структур state1 = state2; структура state1 также получит ссылку на объект country в хипе. Поэтому изменение state2.country повлечет за собой также изменение state1.country.

Объекты классов как параметры методов

Организацию объектов в памяти следует учитывать при передаче параметров по значению и по ссылке. Если параметры методов представляют объекты классов, то использование параметров имеет некоторые особенности. Например, создадим метод, который в качестве параметра принимает объект Person:

Person p = new Person < name = "Tom", age = 23 >; ChangePerson(p); Console.WriteLine(p.name); // Alice Console.WriteLine(p.age); // 23 void ChangePerson(Person person) < // сработает person.name = "Alice"; // сработает только в рамках данного метода person = new Person < name = "Bill", age = 45 >; Console.WriteLine(person.name); // Bill > class Person

При передаче объекта класса по значению в метод передается копия ссылки на объект. Эта копия указывает на тот же объект, что и исходная ссылка, потому мы можем изменить отдельные поля и свойства объекта, но не можем изменить сам объект. Поэтому в примере выше сработает только строка person.name = «Alice» .

А другая строка person = new Person < name = "Bill", age = 45 >создаст новый объект в памяти, и person теперь будет указывать на новый объект в памяти. Даже если после этого мы его изменим, то это никак не повлияет на ссылку p в методе Main, поскольку ссылка p все еще указывает на старый объект в памяти.

Но при передаче параметра по ссылке (с помощью ключевого слова ref ) в метод в качестве аргумента передается сама ссылка на объект в памяти. Поэтому можно изменить как поля и свойства объекта, так и сам объект:

Person p = new Person < name = "Tom", age = 23 >; ChangePerson(ref p); Console.WriteLine(p.name); // Bill Console.WriteLine(p.age); // 45 void ChangePerson(ref Person person) < // сработает person.name = "Alice"; // сработает person = new Person < name = "Bill", age = 45 >; > class Person

Операция new создаст новый объект в памяти, и теперь ссылка person (она же ссылка p из метода Main) будет указывать уже на новый объект в памяти.

Некоторые аспекты работы с Dictionary при разработке нагруженных приложений

На написание этой небольшой заметки меня подтолкнуло несколько проведенных в последнее время собеседований на должность ведущего разработчика в нашу компанию. Некоторые соискатели, как оказалось, недостаточно разбираются в том, что же это за механизм такой, Dictionary, и как его нужно использовать. Столкнулся даже с весьма радикальным мнением: мол, словарь работает очень медленно, причем из-за того, что при создании сразу же помещается в куче больших объектов (LOH), использовать его в коде нельзя и лучше применять запросы к обычным коллекциям с помощью фильтров LINQ!

Конечно же, это не совсем верные утверждения. Словарь как механизм очень часто бывает востребован как при построении высоконагруженного кода, так и при реализации бизнес-логики. Разработчик должен представлять, как устроен словарь, как он работает с памятью, насколько затратен, сколько «следов» оставит в поколениях и LOH, вызывая излишнюю нагрузку на сборщик мусора; как его лучше инициализировать, в каких случаях вместо него лучше использовать коллекцию пар ключ-значение, в каких – сортированную коллекцию и т.д.

В этой заметке мы постараемся разобраться с некоторыми из этих вопросов.

Реализация словаря от Майкрософт базируется, как всем известно, на механизме хэш-таблиц. Это дает в некоторых сценариях недостижимую для других алгоритмов идеальную константную сложность O(1) на операции вставки, поиска, удаления. В некоторых же сценариях отказ от использования этого механизма чреват существенными проблемами с производительностью и потреблением памяти.

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

 int[] _buckets = new int[size]; Entry[] _entries = new Entry[size];

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

Вспомогательная сущность Entry, «оборачивающая» каждую пару ключ-значение, представляет собой значимый тип:

 private struct Entry

То есть для хранения такой сущности не выделяется отдельное место в куче, она помещается по месту объявления, то есть в данном случае в области памяти, занимаемой массивом _entries. Это заметно лучше, чем в реализации ConcurrentDictionary, где аналогичная сущность представлена ссылочным типом. Подход позволяет снижать нагрузку на память (ведь каждый экземпляр ссылочного типа в 64 разрядной системе требует дополнительно 16 байт на служебные данные и 8 байт непосредственно на ссылку) и на сборщик мусора, которому не нужно тратить время на анализ множества мелких и по сути служебных объектов.

С другой стороны такой массив _entries при активной работе с Dictionary достаточно быстро достигнет 85000 байт и переместится в кучу больших объектов LOH (например, если ключ и значение будут ссылочного типа, то для 64 разрядного приложения это случится при 3372 добавленных значений, а в некоторых случаях и при 1932). Как известно, куча больших объектов при активной работе подвержена фрагментации, что ведет к росту потребляемой приложением неиспользуемой памяти.

Почему разработчики Microsoft не разделили _entries на четыре массива, соответствующих полям Entry? Это бы отдалило перспективу попадания в LOH и в некоторых сценариях снизило потребление памяти (увеличив, скорее всего, частоту сборок мусора). Видимо, посчитали, что выигрыш не настолько велик.

При работе со словарем разработчик должен учитывать, что это не бесплатная структура данных. Для хранения одного значения дополнительно потребляется как минимум 12 байт памяти (4 байта в массиве _buckets и по 4 байта на поля hashCode и next сущности Entry). Создавая в памяти приложения словарь и заполняя его, например, миллионом значений, мы получим как минимум 12МБ перерасхода памяти. Однако только ли этим все ограничится? Нет.

Механизм Dictionary всегда резервирует определенное количество памяти для еще не добавленных элементов. Иначе ни о какой быстрой вставке не могло бы быть и речи. На графике представлена динамика выделения памяти для словаря типа Dictionary при добавлении значений (красный цвет). Для сравнения показано, сколько байт занимает полезная нагрузка – хранимые в словаре данные (синий цвет). Количество добавленных элементов от 500 тыс. до 13 млн. Словарь инициализируется на стартовую емкость 500 тыс.

Как видно, стратегия резервирования памяти по умолчанию при работе со словарем достаточно затратна. Если свободное место в словаре закончилось, то алгоритм обычно решает удвоить текущую емкость, что во многих случаях может оказаться ненужным, так как в приложении не наберется столько полезных данных.

Еще одна особенность работы механизмов алгоритма заключается в том, что при каждом расширении емкости _buckets и _entries создаются заново и все существующие значения просто копируются из старых массивов в новые, после чего ссылки на старые «отпускаются», становятся доступными для сборщика мусора. Для словарей с большим количеством значений каждое такое выделение памяти осуществляется сразу в LOH, что приближает вызов полной сборки мусора.
Например, при работе тестов для создания представленного выше графика приложение аллоцировало суммарно 746МБ и выполнило 3 сборки мусора во втором поколении.

Следует так же учитывать, что в текущей реализации Dictionary самостоятельно не умеет отдавать в систему чрезмерно занятую память после массового удаления элементов. До недавних пор такую операцию нельзя было запустить и в коде, оставалось только пересоздавать словарь полностью, впрочем, в последние версии .NET Core был включен соответствующий метод TrimExcess, позволяющий принудительно привести объем выделенной памяти в соответствие количеству хранимых элементов.

В зависимости от логики работы с данными обычно можно подобрать определенную экономную стратегию:

  • Если необходимо быстро заполнить словарь, то нужно предварительно оценить объем данных и задать конечную емкость в конструкторе или через EnsureCapacity. Это позволит избежать лишних аллокаций больших областей памяти.
  • По графику видно, что стратегия заполнения словаря по умолчанию привела к резервированию 400МБ памяти для хранения 10млн. значений (80МБ полезных данных). Но если задать емкость при вызове конструктора new Dictionary(10001000), то для хранения этих же данных будет зарезервировано всего 190МБ. Если объем хранимых данных со временем меняется незначительно, то с этим способом можно будет избежать не только лишних аллокаций при заполнении, но и длительного резервирования ненужной памяти.
  • Если объем хранимых данных умеренно растет, то можно запоминать текущую емкость Dictionary и перед ее достижением увеличивать емкость методом EnsureCapacity(capacity*коэффициент), не дав словарю расширяться самостоятельно. Следует учитывать, что вызов этого метода приводит к пересозданию массивов _buckets и _entries и копированию данных из старых сущностей в новые. Поэтому величину новой емкости следует подбирать аккуратно. Текущую емкость можно оценить, вызвав EnsureCapacity(0).
  • Если объем хранимых данных в целом находится на одном уровне, но периодически (не часто) ненадолго, но значительно увеличивается, то можно разделить хранение между двумя экземплярами Dictionary: постоянным и периодически создаваемым. Это приведет к тому, что часть выделенной памяти будет существовать долговременно и не внесет вклад в фрагментацию и нагрузку на сборщик мусора. Если пики очень частые, то имеет смысл зарезервировать под них постоянную область памяти (создать словарь под максимально возможную оценку объема данных).
  • В некоторых случаях можно вызывать метод TrimExcess. Однако его периодическое использование с большим количеством хранимых значений приведет к фрагментации LOH и поэтому во многих случаях не может считаться самым удачным решением. Не стоит использовать этот метод при частых увеличениях и последующих уменьшениях объема хранимых данных.

Значительное влияние на объем потребляемой памяти оказывает выбор между использованием значимых и ссылочных типов для хранения в Dictionary. Модифицируем предыдущий опыт так, что будет заполняться словарь вида Dictionary, где:

 public class Key < public int Item < get; set; >public Key(int item) < Item = item; >public override bool Equals(object obj) < return Item.Equals(obj); >public override int GetHashCode() < return Item.GetHashCode(); >> public class Value < public int Item < get; set; >public Value(int item) < Item = item; >>

График примет вид.

В чем же причина такого взрывного роста?

Их даже несколько:

  • Каждый созданный объект в 64 разрядной системе содержит указатель на блок синхронизации (8 байт) и указатель на таблицу методов (8 байт).
  • Ссылка, которая хранится в словаре, добавляет 8 байт к «весу» объекта.
  • Поле Item в объектах Key и Value занимает 8 байт, несмотря даже на то, что размер поля Item в обоих случаях составляет 4 байта.

В итоге затраты на хранение одного значения увеличиваются с 20 до 76 байт.

Из результатов опыта можно сделать следующие выводы:

  • Если суммарный размер хранимых полей не сильно отличается от размера указателя (то есть не превышает нескольких десятков байт), то следует сделать выбор в сторону использования структурного типа для хранения значений.
  • Не стоит злоупотреблять ключами ссылочного типа, в том числе текстового. Содержание такого ключа стоит достаточно дорого.

Мы выяснили, что нужно учитывать при работе с большими объемами данных. А что же с небольшими словарями? Несколько раз сталкивался с проектами, где хранение данных было организовано с помощью огромной коллекции небольших (на 10-20-30 значений) словарей. Оптимальное ли это решение?

Выполним еще один опыт. Проверим, сколько будет занимать поиск по Dictionary(int count) в сравнении с поиском по структуре:

 struct Entry  < public int[] Keys; public TValue[] Values; public Entry(int count) < Keys = new int[count]; Values = new TValue[count]; >public bool TryGetValue(int key, out TValue value) < for (int j = 0; j < Keys.Length; ++j) < if (Keys[j] == key) < value = Values[j]; return true; >> value = default(TValue); return false; > >

Здесь count – количество пар ключ-значение, среди которых осуществляется поиск.
Результаты представлены на графике.

Как видно, время поиска с перебором элементов массива линейно растет от 18 наносекунд при count=10 до 134 нс при count=190 (при тестировании создается 50 тыс. таких поисковых массивов). Затраты времени на поиск по словарю сначала существенно превышают поиск перебором (в пике – на 29 нс), но при определенном количестве элементов (в моей тестовой конфигурации при 150) алгоритмы меняются местами, после чего рост времени на поиск по словарю практически прекращается (помним, поиск в словаре при идеальных условиях константный).

Причиной такого поведения является упорядоченность перебора при поиске по массиву, что делает этот поиск предсказуемым для процессора (точнее, для его кэша, попробуйте например поменять при поиске в Entry проход по циклу на обратный, поиск сразу замедлится).
Другой причиной можно назвать заложенную в словарь гибкость, с использованием вызова виртуальных функций (callvirt для GetHashCode и Equals). Такой вызов – достаточно долгая операция. Кстати, в некоторых случаях, если требования к скорости работы алгоритма высоки, можно рассмотреть вопрос о самостоятельной переработке словаря с заменой обобщенного (generic) ключа на ключ конкретного типа. На графике выше приведены результаты скорости поиска в таком модифицированном словаре для нашего тестового случая.

Там же для сравнения приведены результаты аналогичного теста для SortedList.
Теперь рассмотрим затраты памяти на работу поисковых алгоритмов.

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

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

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

9. Лекция: Массивы

Лекция посвящена описанию массивов в Java. Массивы издавна присутствуют в языках программирования, поскольку при выполнении многих задач приходится оперировать целым рядом однотипных значений. Массивы в Java – один из ссылочных типов, который, однако, имеет особенности при инициализации, создании и оперировании со своими значениями. Наибольшие различия проявляются при преобразовании таких типов. Также объясняется, почему многомерные массивы в Java можно (и зачастую более правильно) рассматривать как одномерные. Завершается классификация типов переменных и типов значений, которые они могут хранить. В заключение рассматривается механизм клонирования Java, позволяющий в любом классе описать возможность создания точных копий объектов, порожденных от него.

Массивы как тип данных в Java

В отличие от обычных переменных, которые хранят только одно значение, массивы (arrays) используются для хранения целого набора значений. Количество значений в массиве называется его длиной, сами значения – элементами массива. Значений может не быть вовсе, в этом случае массив считается пустым, а его длина равной нулю.

Элементы не имеют имен, доступ к ним осуществляется по номеру индекса. Если массив имеет длину n, отличную от нуля, то корректными значениями индекса являются числа от 0 до n-1. Все значения имеют одинаковый тип и говорится, что массив основан на этом базовом типе. Массивы могут быть основаны как на примитивных типах (например, для хранения числовых значений 100 измерений), так и на ссылочных (например, если нужно хранить описание 100 автомобилей в гараже в виде экземпляров класса Car ).

Сразу оговоримся, что в Java массив символов char[] и класс String являются различными типами. Их значения могут легко конвертироваться друг в друга с помощью специальных методов, но все же они не относятся к идентичным типам.

Как уже говорилось, массивы в Java являются объектами (примитивных типов в Java всего восемь и их количество не меняется), их тип напрямую наследуется от класса Object, поэтому все элементы данного класса доступны у объектов-массивов.

Базовый тип также может быть массивом. Таким образом конструируется массив массивов, или многомерный массив.

Работа с любым массивом включает обычные операции, уже описанные для других типов, — объявление, инициализация и т.д. Начнем последовательно изучать их в приложении к массивам.

Объявление массивов

В качестве примера рассмотрим объявление переменной типа «массив, основанный на примитивном типе int «:

Как мы видим, сначала указывается базовый тип. Затем идет имя переменной, а пара квадратных скобок указывает на то, что используемый тип является именно массивом. Также допустима запись:

Количество пар квадратных скобок указывает на размерность массива. Для многомерных массивов допускается смешанная запись:

Переменная a имеет тип «двумерный массив, основанный на int «. Аналогично объявляются массивы с базовым объектным типом:

Создание переменной типа массив еще не создает экземпляры этого массива. Такие переменные имеют объектный тип и хранят ссылки на объекты, однако изначально имеют значение null (если они являются полями класса; напомним, что локальные переменные необходимо явно инициализировать). Чтобы создать экземпляр массива, нужно воспользоваться ключевым словом new, после чего указывается тип массива и в квадратных скобках – длина массива.

Point[] p = new Point[10];

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

int array[]=new int[5];

System.out.println(j+» «+j+» sup»>31 -1, то есть немногим больше 2 млрд.

Продолжая рассматривать тип массива, подчеркнем, что в качестве базового типа может использоваться любой тип Java, в том числе:

* интерфейсы. В таком случае элементы массива могут иметь значение null или ссылаться на объекты любого класса, реализующего этот интерфейс;

* абстрактные классы. В этом случае элементы массива могут иметь значение null или ссылаться на объекты любого неабстрактного класса-наследника.

Поскольку массив является объектным типом данных, его значения могут быть приведены к типу Object или, что то же самое, присвоены переменной типа Object. Например,

Object o = new int[4];

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

Object arr[] = new Object[3];

Инициализация массивов

Теперь, когда мы выяснили, как создавать экземпляры массива, рассмотрим, какие значения принимают его элементы.

Если создать массив на основе примитивного числового типа, то изначально после создания все элементы массива имеют значение по умолчанию, то есть 0. Если массив объявлен на основе примитивного типа boolean, то и в этом случае все элементы будут иметь значение по умолчанию false. Выше рассматривался пример инициализации элементов с помощью цикла for.

Рассмотрим создание массива на основе ссылочного типа. Предположим, это будет класс Point. При создании экземпляра массива с применением ключевого слова new не создается ни один объект класса Point, создается лишь один объект массива. Каждый элемент массива будет иметь пустое значение null. В этом можно убедиться на простом примере:

Point p[]=new Point[5];

Результатом будут лишь слова null.

Далее нужно инициализировать элементы массива по отдельности, например, в цикле. Вообще, создание массива длиной n можно рассматривать как заведение n переменных и работать с элементами массива (в последнем примере p[i] ) по правилам обычных переменных.

Кроме того, существует и другой способ создания массивов – инициализаторы. В этом случае ключевое слово new не используется, а ставятся фигурные скобки, и в них через запятую перечисляются значения всех элементов массива. Например, для числового массива явная инициализация записывается следующим образом:

// эквивалентно new int[0]

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

Аналогично можно порождать массивы на основе объектных типов, например:

Point p=new Point(1,3);

p, new Point(2,2), null, p

// или String sarr[]=

Однако инициализатор нельзя использовать для анонимного создания экземпляров массива, то есть не для инициализации переменной, а, например, для передачи параметров метода или конструктора.

public class Parent

private String[] values;

protected Parent(String[] s)

public class Child extends Parent

public Child(String firstName,

// требуется анонимное создание массива

В конструкторе класса Child необходимо осуществить обращение к конструктору родителя и передать в качестве параметра ссылку на массив. Теоретически можно передать null, но это приведет в большинстве случаев к некорректной работе классов. Можно вставить выражение new String[2], но тогда вместо значений firstName и lastName будут переданы пустые строки. Попытка записать

приведет к ошибке компиляции, так можно только инициализировать переменные.

Корректное выражение выглядит так:

Что является некоторой смесью выражения, создающего массивы с помощью new, и инициализатора. Длина массива определяется количеством указанных значений.

Многомерные массивы

Теперь перейдем к рассмотрению многомерных массивов. Так, в следующем примере

переменная i ссылается на двумерный массив, который можно представить себе в виде таблицы 3х5. Суммарно в таком массиве содержится 15 элементов, к которым можно обращаться через комбинацию индексов от (0, 0) до (2, 4). Пример заполнения двумерного массива через цикл:

int pithagor_table[][]=new int[5][5];

Результатом выполнения программы будет:

Однако такой взгляд на двумерные и многомерные массивы является неполным. Более точный подход заключается в том, что в Java нет двумерных, и вообще многомерных массивов, а есть массивы, базовыми типами которых являются также массивы. Например, тип int[] означает «массив чисел», а int[][] означает «массив массивов чисел». Поясним такую точку зрения.

Если создать двумерный массив и определить переменную x, которая на него ссылается, то, используя x и два числа в паре квадратных скобок каждое (например, x[0][0] ), можно обратиться к любому элементу двумерного массива. Но в то же время, используя x и одно число в паре квадратных скобок, можно обратиться к одномерному массиву, который является элементом двумерного массива. Его можно проинициализировать новым массивом с некоторой другой длиной и таблица перестанет быть прямоугольной – она примет произвольную форму. В частности, можно одному из одномерных массивов присвоить даже значение null.

После таких операций массив, на который ссылается переменная x, назвать прямоугольным никак нельзя. Зато хорошо видно, что это просто набор одномерных массивов или значений null.

Полезно подсчитать, сколько объектов порождается выражением new int[3][5]. Правильный подсчет таков: создается один массив массивов (один объект) и три массива чисел, каждый длиной 5 (три объекта). Итого, четыре объекта.

В рассмотренном примере три из них (массивы чисел) были тут же переопределены новыми значениями. Для таких случаев полезно использовать упрощенную форму выражения создания массивов:

Такая запись порождает один объект – массив массивов – и заполняет его значениями null. Теперь понятно, что и в этом, и в предыдущем варианте выражение x.length возвращает значение 3 – длину массива массивов. Далее можно с помощью выражений x[i].length узнать длину каждого вложенного массива чисел, при условии, что i неотрицательно и меньше x.length, а также x[i] не равно null. Иначе будут возникать ошибки во время выполнения программы.

Вообще, при создании многомерных массивов с помощью new необходимо указывать все пары квадратных скобок, соответственно количеству измерений. Но заполненной обязательно должна быть лишь крайняя левая пара, это значение задаст длину верхнего массива массивов. Если заполнить следующую пару, то этот массив заполнится не значениями по умолчанию null, а новыми созданными массивами с меньшей на единицу размерностью. Если заполнена вторая пара скобок, то можно заполнить третью, и так далее.

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

В этом примере порождается четыре объекта. Это, во-первых, массив массивов длиной 4, а во-вторых, три массива чисел с длинами 2, 1, 0, соответственно.

Все рассмотренные примеры и утверждения одинаково верны для многомерных массивов, основанных как на примитивных, так и на ссылочных типах.

Класс массива

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

Рассмотрим гипотетическое объявление класса для массива, основанного на неком объектном типе Element.

Объявление класса начинается с перечисления модификаторов, среди которых особую роль играют модификаторы доступа. Класс массива будет иметь такой же уровень доступа, как и базовый тип. То есть если Element объявлен как public -класс, то и массив будет иметь уровень доступа public. Для любого примитивного типа класс массива будет public. Можно также указать модификатор final, поскольку никакой класс не может наследоваться от класса массива.

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

Затем нужно указать родительский класс. Все массивы наследуются напрямую от класса Object. Далее перечисляются интерфейсы, которые реализует класс. Для массива это будут интерфейсы Cloneable и Serializable. Первый из них подробно рассматривается в конце этой лекции, а второй будет описан в следующих лекциях.

Тело класса содержит объявление одного public final поля length типа int. Кроме того, переопределен метод clone() для поддержки интерфейса Cloneable.

Сведем все вышесказанное в формальную запись класса:

[public] class A implements Cloneable,

public final int length;

// инициализируется при создании

public Object clone()

catch (CloneNotSupportedException e)

throw new InternalError(e.getMessage());

Таким образом, экземпляр типа массив является полноценным объектом, который, в частности, наследует все методы, определенные в классе Object, например, toString(), hashCode() и остальные.

// результат работы метода toString()

// результат работы метода hashCode()

Результатом выполнения программы будет:

Преобразование типов для массивов

Теперь, когда массив введен как полноценный тип данных в Java, рассмотрим, какое влияние он окажет на преобразование типов.

Ранее подробно рассматривались переходы между примитивными и обычными (не являющимися массивами) ссылочными типами. Хотя массивы являются объектными типами, их также будет полезно разделить по базовому типу на две группы – основанные на примитивном или ссылочном типе.

Имейте в виду, что переходы между массивами и примитивными типами являются запрещенными. Преобразования между массивами и другими объектными типами возможны только для класса Object и интерфейсов Cloneable и Serializable. Массив всегда можно привести к этим трем типам, обратный же переход является сужением и должен производиться явным образом по усмотрению разработчика. Таким образом, интерес представляют только переходы между разными типами массивов. Очевидно, что массив, основанный на примитивном типе, принципиально нельзя преобразовать к типу массива, основанному на ссылочном типе, и наоборот.

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

Для ссылочных же типов такого строгого правила нет. Например, если создать экземпляр массива, основанного на типе Child, то ссылку на него можно привести к типу массива, основанного на типе Parent.

Child c[] = new Child[3];

Вообще, существует универсальное правило: массив, основанный на типе A, можно привести к массиву, основанному на типе B, если сам тип A приводится к типу B.

// если допустимо такое приведение:

// то допустимо и приведение массивов:

Применяя это правило рекурсивно, можно преобразовывать многомерные массивы. Например, массив Child[][] можно привести к Parent[][], так как их базовые типы приводимы ( Child[] к Parent[] ) также на основе этого правила (поскольку базовые типы Child и Parent приводимы в силу правил наследования).

Как обычно, расширения можно проводить неявно (как в предыдущем примере), а сужения – только явным приведением.

Вернемся к массивам, основанным на примитивном типе. Невозможность их участия в преобразованиях типов связана, конечно, с различиями между простыми и ссылочными типами данных. Поскольку элементами объектных массивов являются ссылки, они легко могут участвовать в приведении. Напротив, элементы простых типов действительно хранят числовые или булевские значения. Предположим, такое преобразование осуществимо:

// пример вызовет ошибку компиляции

В таком случае, элементы b[0] и i[0] хранили бы значения разных типов. Стало быть, преобразование потребовало бы копирования с одновременным преобразованием типа всех элементов исходного массива. В результате был бы создан новый массив, элементы которого равнялись бы по значению элементам исходного массива.

Но преобразование типа не может порождать новые объекты. Такие операции должны выполняться только явным образом с применением ключевого слова new. По этой причине преобразования типов массивов, основанных на примитивных типах, запрещены.

Если же копирование элементов действительно требуется, то нужно сначала создать новый массив, а затем воспользоваться стандартной функцией System.arraycopy(), которая эффективно выполняет копирование элементов одного массива в другой.

Ошибка ArrayStoreException

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

Child c[] = new Child[5];

С точки зрения компилятора код совершенно корректен. Преобразование во второй строке допустимо. В третьей строке элементу массива типа Parent присваивается значение того же типа.

Однако при выполнении такой программы возникнет ошибка. Нельзя забывать, что преобразование не меняет объект, изменяется лишь способ доступа к нему. В свою очередь, объект всегда «помнит», от какого типа он был порожден. С учетом этих замечаний становится ясно, что в третьей строке делается попытка добавить в массив Child значение типа Parent, что некорректно.

Действительно, ведь переменная с продолжает ссылаться на этот массив, а значит, следующей строкой может быть такое обращение:

где метод onlyChildMethod() определен только в классе Child. Данное обращение совершенно корректно, а значит, недопустима ситуация, когда элемент c[0] ссылается на объект, несовместимый с Child.

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

Может сложиться впечатление, что разобранная ситуация является надуманной,– зачем преобразовывать массив и тут же задавать для него неверное значение? Однако преобразование при присвоении значений является лишь примером. Рассмотрим объявление метода:

public void process(Parent[] p)

if (p!=null && p.length>0)

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

И это будет как раз ошибка ArrayStoreException.

Переменные типа массив и их значения

Завершим описание взаимосвязи типа переменной и типа значений, которые она может хранить.

Как обычно, массивы, основанные на простых и ссылочных типах, мы описываем раздельно.

Переменная типа массив примитивных величин может хранить значения только точно такого же типа, либо null.

Переменная типа «массив ссылочных величин» может хранить следующие значения:

значения точно такого же типа, что и тип переменной;

все значения типа массив, основанный на типе, приводимом к базовому типу исходного массива.

Все эти утверждения непосредственно следуют из рассмотренных выше особенностей приведения типов массивов.

Еще раз напомним про исключительный класс Object. Переменные такого типа могут ссылаться на любые объекты, порожденные как от классов, так и от массивов.

Сведем все эти утверждения в таблицу.

Таблица Табл. 9.1.. Тип переменной и тип ее значения.

Допустимые типы ее значения

Массив простых чисел

* в точности совпадающий с типом переменной

Массив ссылочных значений

* совпадающий с типом переменной

* массивы ссылочных значений, удовлетворяющих следующему условию: если тип переменной – массив на основе типа A, то значение типа массив на основе типа B допустимо тогда и только тогда, когда B приводимо к A

* любой ссылочный, включая массивы

Клонирование

Механизм клонирования, как следует из названия, позволяет порождать новые объекты на основе существующего, которые обладали бы точно таким же состоянием, что и исходный. То есть ожидается, что для исходного объекта, представленного ссылкой x, и результата клонирования, возвращаемого методом x.clone(), выражение

должно быть истинным, как и выражение

также верно. Реализация такого метода clone() осложняется целым рядом потенциальных проблем, например:

* класс, от которого порожден объект, может иметь разнообразные конструкторы, которые к тому же могут быть недоступны (например, модификатор доступа private );

* цепочка наследования, которой принадлежит исходный класс, может быть довольно длинной, и каждый родительский класс может иметь свои поля – недоступные, но важные для воссоздания состояния исходного объекта;

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

* возможна ситуация, когда объект нельзя клонировать, дабы не нарушить целостность системы.

Поэтому было реализовано следующее решение.

Класс Object содержит метод clone(). Рассмотрим его объявление:

protected native Object clone()

Именно он используется для клонирования. Далее возможны два варианта.

Первый вариант: разработчик может в своем классе переопределить этот метод и реализовать его по своему усмотрению, решая перечисленные проблемы так, как того требует логика разрабатываемой системы. Упомянутые условия, которые должны быть истинными для клонированного объекта, не являются обязательными и программист может им не следовать, если это требуется для его класса.

Второй вариант предполагает использование реализации метода clone() в самом классе Object. То, что он объявлен как native, говорит о том, что его реализация предоставляется виртуальной машиной. Естественно, перечисленные трудности легко могут быть преодолены самой JVM, ведь она хранит в памяти все свойства объектов.

При выполнении метода clone() сначала проверяется, можно ли клонировать исходный объект. Если разработчик хочет сделать объекты своего класса доступными для клонирования через Object.clone(), то он должен реализовать в своем классе интерфейс Cloneable. В этом интерфейсе нет ни одного элемента, он служит лишь признаком для виртуальной машины, что объекты могут быть клонированы. Если проверка не выполняется успешно, метод порождает ошибку CloneNotSupportedException.

Если интерфейс Cloneable реализован, то порождается новый объект от того же класса, от которого был создан исходный объект. При этом копирование выполняется на уровне виртуальной машины, никакие конструкторы не вызываются. Затем значения всех полей, объявленных, унаследованных либо объявленных в родительских классах, копируются. Полученный объект возвращается в качестве клона.

Обратите внимание, что сам класс Object не реализует интерфейс Cloneable, а потому попытка вызова new Object().clone() будет приводить к ошибке. Метод clone() предназначен скорее для использования в наследниках, которые могут обращаться к нему с помощью выражения super.clone(). При этом могут быть сделаны следующие изменения:

* модификатор доступа расширен до public ;

* удалено предупреждение об ошибке CloneNotSupportedException ;

* результирующий объект может быть модифицирован любым способом, на усмотрение разработчика.

Напомним, что все массивы реализуют интерфейс Cloneable и, таким образом, доступны для клонирования.

Важно помнить, что все поля клонированного объекта приравниваются, их значения никогда не клонируются. Рассмотрим пример:

public class Test implements Cloneable

public Test(int x, int y, int z)

public static void main(String s[])

Test t1=new Test(1, 2, 3), t2=null;

catch (CloneNotSupportedException e)

System.out.println(«t2.p.x=» + t2.p.x + «, t2.height t2.p.x , t2.height неглубокого» клонирования в методе Object.clone() необходима, так как в противном случае клонирование второстепенного объекта могло бы привести к огромным затратам ресурсов, ведь этот объект может содержать ссылки на более значимые объекты, а те при клонировании также начали бы копировать свои поля, и так далее. Кроме того, типом поля клонируемого объекта может быть класс, не реализующий Cloneable, что приводило бы к дополнительным проблемам. Как показано в примере, при необходимости дополнительное копирование можно добавить самостоятельно.

Клонирование массивов

Итак, любой массив может быть клонирован. В этом разделе хотелось бы рассмотреть особенности, возникающие из-за того, что Object.clone() копирует только один объект.

Результатом будет единица, что вполне очевидно, так как весь массив представлен одним объектом, который не будет зависеть от своей копии. Усложняем пример:

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

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

Во втором случае модифицируется существующий массив, что скажется на обоих двухмерных массивах. На консоли появится 0.

Обратите внимание, что если из примера убрать условие if-else, так, чтобы отрабатывал первый вариант, а затем второй, то результатом будет опять 1, поскольку в части второго варианта модифицироваться будет уже новый массив, порожденный в части первого варианта.

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

Заключение

В этой лекции было рассмотрено устройство массивов в Java. Подобно массивам в других языках, они представляют собой набор значений одного типа. Основным свойством массива является длина, которая в Java может равняться нулю. В противном случае, массив обладает элементами в количестве, равном длине, к которым можно обратиться, используя индекс, изменяющийся от 0 до величины длины без единицы. Длина задается при создании массива и у созданного массива не может быть изменена. Однако она не входит в определение типа, а потому одна переменная может ссылаться на массивы одного типа с различной длиной.

Создать массив можно с помощью ключевого слова new, поскольку все массивы, включая определенные на основе примитивных значений, имеют объектный тип. Другой способ – воспользоваться инициализатором и перечислить значения всех элементов. В первом случае элементы принимают значения по умолчанию (0, false, null).

Особым образом в Java устроены многомерные массивы. Они, по сути, являются одномерными, основанными на массивах меньшей размерности. Такой подход позволяет единообразно работать с многомерными массивами. Также он дает возможность создавать не только «прямоугольные» массивы, но и массивы любой конфигурации.

Хотя массив и является ссылочным типом, работа с ним зачастую имеет некоторые особенности. Рассматриваются правила приведения типа массива. Как для любого объектного типа, приведение к Object является расширяющим. Приведение массивов, основанных на ссылочных типах, во многом подчиняется обычным правилам. А вот примитивные массивы преобразовывать нельзя. С преобразованиями связано и возникновение ошибки ArrayStoreException, причина которой – невозможность точного отслеживания типов в преобразованном массиве для компилятора.

В заключение рассматриваются последние случаи взаимосвязи типа переменной и ее значения.

Наконец, изучается механизм клонирования, существующий с самых первых версий Java и позволяющий создавать точные копии объектов, если их классы позволяют это делать, реализуя интерфейс Cloneable. Поскольку стандартное клонирование порождает только один новый объект, это приводит к особым эффектам при работе с объектными полями классов и массивами.

Читайте также

Лекция 5. Списки и массивы

Массивы

Массивы Массив — это пронумерованный набор переменных (элементов), фактически хранящийся в одной переменной. Доступ к отдельному элементу массива выполняется по его порядковому номеру, называемому индексом. А общее число элементов массива называется его

Массивы

Массивы СУБД InterBase была одной из первых, в которой появились массивы. Поддержка массивов в базе данных является расширением традиционной реляционной модели. Наличие массивов позволяет упростить работу со множествами данных одного типа.Массив — это совокупность значений

Массивы

Массивы По умолчанию указатели, передаваемые через параметры, полагаются указателями на единичные экземпляры, а не на массивы. Для передачи массива в качестве параметра можно использовать синтаксис С для массивов и/или специальные атрибуты IDL для представления

Массивы

Массивы Массивы в С++ объявляются с указанием количества элементов массива в квадратных скобках после имени переменной массива. Допускаются двумерные массивы, т.е. массив массивов. Ниже приводится определение одномерного массива, содержащего 10 элементов типа int:int

3. МАССИВЫ

3. МАССИВЫ Массив — это группа переменных одного типа, доступ к которым осуществляется с помощью общего имени. Для объявления типа массива используются квадратные скобки. В приведенной ниже строке объявляется переменная month_days, тип которой — «массив целых чисел типа int».int

R.8.2.4 Массивы

R.8.2.4 Массивы В описании T D, в котором D имеет видD1 [ выражение-константа opt ]описывается идентификатор типа «… массив T». Если выражение-константа присутствует (§R.5.19), то оно должно иметь целочисленный тип и значение, большее 0. Это выражение задает число элементов массива.

Массивы

Массивы Для создания множества одинаковых объектов в 3ds Max есть специальная команда Array (Массив). Преимущество массивов заключается в том, что можно быстро создать большое количество объектов, сразу же указав, на сколько они будут сдвинуты, на какой угол повернуты и как

8.1. Массивы

8.1. Массивы В Ruby массивы индексируются целыми числами; индексация начинается с нуля, как в языке С. На этом, впрочем, сходство и заканчивается.Массивы в Ruby динамические. Можно (хотя это и не обязательно) задать размер массива при создании. Но после создания он может расти без

Массивы

Массивы Массив — это упорядоченная именованная совокупность однотипных значений, к которым можно обращаться по их порядковому номеру (индексу). Для описания массивов в языке Object Pascal используют следующие формы:• array [1..N1] of type — одномерный массив фиксированного размера

Массивы

Массивы Во многих отношениях массивы являются простейшей структурой данных. Проще могут быть только такие базовые типы данных, как integer или Boolean. Массив (array) представляет собой последовательный список определенного количества элементов. Все элементы в массиве

Массивы

Массивы Массивы представляют собой простейшую реализацию набора элементов, для которой можно использовать алгоритм последовательного поиска. Возможны два случая: первый — элементы массива расположены в произвольном порядке и второй — элементы отсортированы. Сначала

Массивы

Массивы Предположим, что у нас имеется отсортированный массив. Как было показано ранее, алгоритм последовательного поиска даже при использовании выхода из цикла в случае отсутствия в списке искомого элемента принадлежит к классу O(n). Каким образом можно улучшить

Массивы

Массивы Массив представляет собой набор элементов одного типа, каждый из которых имеет свой номер, называемый индексом (индексов может быть несколько, тогда массив называется многомерным).Массивы в PascalABC.NET делятся на статические и динамические.При выходе за границы

Массивы

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

Массивы, символы и строки

Тип string , предназначенный для работы со строками символов в кодировке Unicode, является встроенным типом C#. Ему соответствует базовый класс System.String библиотеки .NET.

Создать строку можно несколькими способами:

string s; // инициализация отложена string t = "qqq"; // инициализация строковым литералом string u = new string(' ', 20); // конструктор создает строку из 20 пробелов char[] a = < '0', '0', '0' >; // массив для инициализации строки string v = new string( a ); // создание из массива символов

Для строк определены следующие операции:

  • присваивание ( = );
  • проверка на равенство ( == );
  • проверка на неравенство ( != );
  • обращение по индексу ( [] );
  • сцепление (конкатенация) строк ( + ).

Несмотря на то, что строки являются ссылочным типом данных , на равенство и неравенство проверяются не ссылки, а значения строк. Строки равны, если имеют одинаковое количество символов и совпадают посимвольно.

Обращаться к отдельному элементу строки по индексу можно только для получения значения, но не для его изменения. Это связано с тем, что строки типа string относятся к так называемым неизменяемым типам данных. Методы, изменяющие содержимое строки, на самом деле создают новую копию строки. Неиспользуемые «старые» копии автоматически удаляются сборщиком мусора.

В классе System.String предусмотрено множество методов, полей и свойств, позволяющих выполнять со строками практически любые действия. Некоторые элементы класса приведены в таблилце 6.3, с остальными можно ознакомиться по учебнику.

Таблица 6.3. Некоторые элементы класса System.String

Название Вид Описание
Compare Статический метод Сравнение двух строк в лексикографическом (алфавитном) порядке. Разные реализации метода позволяют сравнивать строки и подстроки с учетом и без учета регистра и особенностей национального представления дат и т. д.
Concat Статический метод Конкатенация строк. Метод допускает сцепление произвольного числа строк
Copy Статический метод Создание копии строки
Format Статический метод Форматирование в соответствии с заданными спецификаторами формата (см. далее)
IndexOf , IndexOfAny , LastIndexOf , LastIndexOfAny Методы Определение индексов первого и последнего вхождения заданной подстроки или любого символа из заданного набора
Insert Метод Вставка подстроки в заданную позицию
Join Статический метод Слияние массива строк в единую строку. Между элементами массива вставляются разделители (см. далее)
Length Свойство Длина строки (количество символов)
Split Метод Разделение строки на элементы, используя заданные разделители. Результаты помещаются в массив строк
Substring Метод Выделение подстроки , начиная с заданной позиции
ToCharArray Метод Преобразование строки в массив символов
ToLower , ToUpper Методы Преобразование символов строки к нижнему или верхнему регистру

Пример применения методов приведен в листинге 6.8.

using System; namespace ConsoleApplication1 < class Class1 < static void Main() < string s = "прекрасная королева Изольда"; Console.WriteLine( s ); string sub = s.Substring( 3 ).Remove( 12, 2 ); // 1 Console.WriteLine( sub ); string[] mas = s.Split(' '); // 2 string joined = string.Join( "! ", mas ); Console.WriteLine( joined ); Console.WriteLine( "Введите строку" ); string x = Console.ReadLine(); // 3 Console.WriteLine( "Вы ввели строку " + x ); double a = 12.234; int b = 29; Console.WriteLine( " a = b = ", a, b ); // 4 Console.WriteLine( " a = a = ", a, b ); // 5 > > >

Листинг 6.8. Работа со строками типа string

Результат работы программы:

прекрасная королева Изольда красная корова Изольда прекрасная! королева! Изольда Введите строку не хочу! Вы ввели строку не хочу! a = 12,23p. b = 1D a = 12,23 a=29 pуб.
Форматирование строк

В операторе 4 из листинга 6.7 неявно применяется метод Format , который заменяет все вхождения параметров в фигурных скобках значениями соответствующих переменных из списка вывода. После номера параметра можно задать минимальную ширину поля вывода, а также указать спецификатор формата, который определяет форму представления выводимого значения.

В общем виде параметр задается следующим образом:

Здесь n — номер параметра. Параметры нумеруются с нуля, нулевой параметр заменяется значением первой переменной из списка вывода, первый параметр — второй переменной, и т. д. Параметр m определяет минимальную ширину поля, которое отводится под выводимое значение. Если выводимому числу достаточно меньшего количества позиций, неиспользуемые позиции заполняются пробелами. Если числу требуется больше позиций, параметр игнорируется.

Спецификатор формата, как явствует из его названия, определяет формат вывода значения. Например, спецификатор C (Currency) означает, что параметр должен форматироваться как валюта с учетом национальных особенностей представления, а спецификатор Х ( Hexadecimal ) задает шестнадцатеричную форму представления выводимого значения.

В операторе 5 используются так называемые пользовательские шаблоны форматирования. Если приглядеться, в них нет ничего сложного: после двоеточия задается вид выводимого значения посимвольно, причем на месте каждого символа может стоять либо # , либо 0. Если указан знак # , на этом месте будет выведена цифра числа, если она не равна нулю. Если указан 0, будет выведена любая цифра, в том числе и 0.

Строки типа StringBuilder

Возможности, предоставляемые классом string , широки, однако требование неизменности его объектов может оказаться неудобным. В этом случае для работы со строками применяется класс StringBuilder , определенный в пространстве имен System.Text и позволяющий изменять значение своих экземпляров. О нем можно прочитать в учебнике [4].

Вопросы и задания для самостоятельной работы студента

  1. Перечислите способы описания массивов.
  2. Чем отличается хранение в памяти массивов из величин значимого и ссылочного типов?
  3. Является ли размерность частью описания массива?
  4. Может ли размерность массива описана переменной (а не константой)?
  5. Можно ли изменить размерность массива после выделения памяти под него?
  6. Какие виды массивов используются в С#?
  7. Что происходит, если количество инициализаторов массива не соответствует заявленной размерности?
  8. Что происходит при присваивании массивов?
  9. Опишите два-три метода сортировки массивов.
  10. Опишите основные методы и свойства класса System.Array
  11. Какие ограничения имеет оператор foreach по сравнению с оператором for?
  12. Что такое кодировка Unicode?
  13. Какие средства работы с отдельными символами предоставляет C#?
  14. Опишите основные методы и свойства класса string.
  15. Можно ли изменить длину строки после того, как память под нее была выделена?
  16. Какое основное ограничение имеет класс string?
  17. Какие существуют возможности форматирования строк?
  18. Перечислите спецификации формата.
  19. Опишите пользовательский формат вещественного числа с двумя ведущими нулями и тремя знаками после запятой.
  20. Изучите по справочной системе свойства и методы стандартного класса System.Array.
  21. Изучите по справочной системе свойства и методы стандартного класса System.String.
  22. Изучите по справочной системе свойства и методы стандартного класса System.Char.
  23. Изучите по справочной системе свойства и методы стандартного класса System.Text.StringBuilder.
  24. Изучите разделы стандарта C#, касающиеся массивов.
  25. Изучите разделы стандарта C#, касающиеся символов.
  26. Изучите разделы стандарта C#, касающиеся строк.

Лабораторные работы

Лабораторная работа 5. Одномерные массивы

В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  • сумму отрицательных элементов массива;
  • произведение элементов массива, расположенных между максимальным и минимальным элементами.

Упорядочить элементы массива по возрастанию.

Лабораторная работа 6. Двумерные массивы

Дана целочисленная прямоугольная матрица. Определить:

  • количество строк, не содержащих ни одного нулевого элемента;
  • максимальное из чисел, встречающихся в заданной матрице более одного раза.
Лабораторная работа 7. Строки

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

В качестве результата выполнения работы вывести на консоль предложения, преобразованные в соответствии с вариантом задания. Каждое предложение начинать с новой строки.

Задание выполнить двумя способами: без использования элементов стандартных классов System.Array, System.Char и System.String и с их использованием.

Задание: упорядочить предложения по возрастанию количества содержащихся в них слов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *