Разработка приложений для мобильных устройств на платформе Windows Mobile

Особенности управления памятью при создании мобильных приложений

Показывать лекцию целиком

Цель лабораторной работы - знакомство с основными методами и способами управления памятью при создании мобильного приложения.

Для выполнения данной лабораторной работы необходимо наличие ранее созданного проекта. Изучив теоретические основы управления памятью при создании приложений для мобильных устройств, необходимо применить описанные приемы и методы в созданном приложении (разумеется, при наличии таковой возможности). Результатом лабораторной работы должна являться таблица сравнения результатов выполнения алгоритмов до оптимизации и после, при аналогичной программно - аппаратной базе.

Задание на лабораторную работу

  • Ознакомиться с уровнями управления памятью.
  • Применить метод Dispose() в целесообразных ситуациях, если в использовании данного метода нет необходимости - обосновать причины такого решения.
  • Рассмотреть модели загрузки данных по требованию.
  • Разбить программный код приложения на "структуры".
  • Оптимизировать работу со строками.
  • Составить сравнительную таблицу времени выполнения алгоритмов до и после оптимизации.
  • 8.1. Уровни управления памятью:

  • Управление памятью на макроскопическом "уровне приложения". Этот уровень относится к данным и ресурсам уровня приложения, которые поддерживаются вашим приложением в процессе выполнения. Эти данные обычно существуют в течение длительного времени, и их область видимости не ограничивается пределами отдельных функций. Для создания эффективно функционирующего мобильного приложения очень важно иметь надежную модель, управляющую объемом данных, подлежащих хранению в памяти в каждый момент времени, и удалением из памяти данных и ресурсов, непосредственное использование которых в ближайшее время не ожидается. Чрезмерный объем долгоживущих данных состояния загромождает память, которую можно было бы использовать для кэширования JIT-компилированного кода или как рабочую память для функций, и заставляет многократно и не самым эффективным образом очищать память от "мусора".
  • Распределение памяти па микроскопическом "уровне алгоритма". Временную память для выполнения команд, определяемых вашими алгоритмами, распределяют функции. Эффективность этого процесса зависит от вашей стратегии реализации алгоритмов. Например, при написании кода, который должен выполняться в циклах, необходимо как можно тщательнее продумывать его эффективность в отношении использования ресурсов, чтобы свести к минимуму непроизводительные накладные расходы. Уделяя пристальное внимание эффективности распределения памяти в создаваемых вами алгоритмах, вы сможете значительно повысить общую производительность приложения.
  • ОЗУ мобильных устройств имеют гораздо меньший объем и, как правило, не позволяют надлежащим образом реализовать механизмы, использующие дополнительные внешние накопители для организации быстрого обмена страницами с памятью. Кроме того, в условиях ограниченных ресурсных возможностей на мобильных устройствах могут быть реализованы лишь сравнительно простые механизмы очистки памяти от неиспользуемых объектов. Это означает, что небрежно организованное управление памятью в приложениях для мобильных устройств будет иметь весьма заметные отрицательные последствия как на макроскопическом, так и на микроскопическом уровнях. По сравнению с настольными компьютерами мобильные устройства гораздо менее терпимы к любым просчетам в управлении памятью.

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

    Макромодель управления памятью

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

  • объекты и ресурсы, которые необходимы приложению для эффективного выполнения
  • фактическая пользовательская информация, с которой работает приложение.
  • Необходимые служебные данные приложения. В эту разновидность входят ресурсы, необходимые приложению для поддержания пользовательского интерфейса и других аспектов выполнения приложения. В качестве примера можно привести открытые соединения с базами данных, открытые файлы, потоки выполнения, графические объекты, а также такие объекты пользовательского интерфейса, как кисти, перья, элементы управления форм и формы как таковые. Все эти объекты не имеют непосредственного отношения к данным, с которыми фактически работает пользователь, но жизненно необходимы для фактического взаимодействия приложения с пользователем или с внешними источниками информации.
  • Пользовательские данные. К этой разновидности относятся фактические данные, в работе с которыми заинтересован пользователь. Под этими данными подразумевается та часть данных, которая удерживается в памяти, а не хранится в базе данных или файле на устройстве или вне его. Например, если мобильное приложение предназначено для того, чтобы облегчить пользователю проезд по улицам Лондона, то пользовательскими данными является информация о расположении улиц, которая в данный момент загружена в память. Если приложение предназначено для ведения инвентарного учета, то пользовательскими данными являются загруженные инвентаризационные данные. Если приложение представляет собой игру в шахматы, то пользовательскими данными является представление состояния шахматной доски в памяти машины.
  • 8.2. Служебные данные приложения

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

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

  • Загрузка данных из базы данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и загрузить данные конкретного пациента.
  • Сохранение данных в базе данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и сохранить данные конкретного пациента.
  • Основной экран приложения. Этот экран отображает данные истории болезни пациента, которые были загружены из базы данных, и предоставляет пользователю устройства возможность просматривать данные и переходить от одних данных к другим.
  • Экран, отображающий подробную информацию, необходимую для работы с конкретными данными и их редактирования. Этот экран отображается тогда, когда пользователю необходимо редактировать отдельные элементы данных о пациенте или вводить новые данные.
  • Экран диаграмм для отображения наборов точек, соответствующих данным. Этот экран предназначается для отображения графической информации, связанной с некоторыми аспектами истории болезни пациента. Например, могут быть построены диаграммы, представляющие изменение кровяного давления или количество белых кровяных телец с течением времени, по которым можно судить о наличии инфекции. К необходимым служебным данным приложения относятся следующие данные:
  • Графические перья и кисти, используемые для рисования диаграмм.
  • Объекты шрифтов, используемых для отображения надписей на диаграммах.
  • Кэшированные фоновые изображения.
  • Внеэкранная поверхность растрового изображения, используемая для подготовки изображения диаграммы перед его копированием на экран.
  • Некоторые из этих состояний разделяют общие ресурсы. Например, графическое перо черного цвета, или шрифт определенного размера, или растровое изображение могут использоваться в нескольких из перечисленных выше состояний. В соединении с базой данных нуждаются два состояния. Кроме того, некоторые объекты могут не требоваться для всех без исключения состояний, но их создание занимает длительное время; в этом случае целесообразно прибегнуть к кэшированию объектов, которое предварительно следует протестировать с точки зрения влияния на производительность.

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

    Управление объемом пользовательских данных, хранящихся в памяти

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

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

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

    Для размещения объектов в памяти существуют две стратегии:

  • При вхождении приложения в новое состояние создаются все объекты, которые требуются для этого состояния. Достоинством этой стратегии является ее простота. Когда приложение переходит в новое состояние, вы просто вызываете функцию, которая и обеспечивает доступность и возможность использования всех необходимых объектов. Эта стратегия очень хорошо работает в тех случаях, когда имеется уверенность в том, что в ближайшее время приложению потребуются все созданные объекты. Возможные проблемы связаны с тем, что если ваше приложение находится в стадии становления и в его проект могут вноситься изменения то применение указанной стратегии может привести к хранению в памяти большого количества ненужных объектов. Поскольку старые объекты, необходимости в которых больше нет, все равно создаются и загружаются в память, то драгоценные ресурсы тратятся понапрасну. Будьте внимательны при групповом создании наборов объектов, ибо в процессе выполнения вашего приложения может наступить такой момент, когда создаваемые объекты не используются, но связанные с ними накладные расходы ухудшают производительность.
  • Создание любого объекта откладывается до тех пор, пока необходимость в его создании не станет очевидной. Эта модель немного сложнее в проектировании, но зато во многих случаях оказывается более эффективной, поскольку объекты создаются лишь тогда, когда в них возникает действительная необходимость. При обсуждении этой модели часто употребляются такие выражения, как "фабрика классов" ("class factory"), "диспетчер ресурсов" ("resource dispenser ) и отложенная загрузка" ("lazy loading").
  • Приведенный ниже пример кода иллюстрирует два способа отложенного создания и кэширования глобально используемых графических ресурсов. Существует два способа создания объектов:

  • Пакетное создание групповых ресурсов. Приведенный ниже код создает списочный массив, содержащий четыре растровых изображения. Эти изображения являются кадрами анимации, поэтому они загружаются все вместе и помещаются в индексированный массив, откуда их можно легко извлекать. Программный код, которому требуется доступ к этой коллекции изображений, должен использовать вызов GraphicsGlobals. PlayerBitmapsCollection()/. Если массив изображений уже загружен в память, функция незамедлительно возвращает кэшированный объект. В противном случае отдельные ресурсы изображений сначала загружаются в массив и лишь затем возвращаются. Если приложение переходит в состояние, в котором пребывание изображений в памяти не требуются, код приложения может выполнить вызов GraphicsGlobals.g_PlayerBitmapsCollection_CleanUp();, в результате чего произойдет освобождение растровых ресурсов и массива. Системные ресурсы, задействованные для обслуживания растровых изображений, будут немедленно освобождены, а управляемая память, которую занимали эти объекты, будет соответствующим образом восстановлена в процессе сборки мусора.
  • Индивидуальное создание графических ресурсов. В случае ресурсов, которые не должны обязательно использоваться вместе, как в приведенном выше примере, часто оказывается удобным создать функцию кэшированного доступа, посредством которой и реализуется управление доступом к ресурсу. Когда происходит первое обращение к этой функции с запросом ресурса (например, GraphicsGlobals .g_GetBlackPen ();), она создает его экземпляр. В случае часто используемых ресурсов такой подход оказывается намного более эффективным, чем постоянное создание и уничтожение экземпляров ресурса всякий раз, когда он требуется для выполнения того или иного фрагмента кода. Создавая приведенный ниже код, я допустил, что все ресурсы должны освобождаться одновременно, и написал функцию ( GraphicsGlobals.g_CleanUpDrawingResources (); ), которая освобождает все каптированные ресурсы, которые были созданы. Эта функция должна вызываться тогда, когда приложение переходит в состояние, в котором эти ресурсы не требуются.
  • public static void g_PlayerBitmapsCollection_CleanUp(){
    //Если не загружено ни одно изображение, то и память освобождать не от чего if(s_colPlayerBitmaps == null)  
    { return; }
    //Дать указание каждому из этих объектов освободить 
    //любые удерживаемые ими неуправляемые ресурсы s_Player_Bitmapl.Dispose();   
    s_Player_Bitmap2.Dispose(); 
    s_Player_Bitmap3.Dispose(); 
    s_Player_Bitmap4.Dispose();
    //Обнулить каждую из этих переменных, чтобы им не соответствовали 
    //никакие объекты в памяти
    s_Player_Bitmapl = null; 
    s_Player_Bitmap2 = null; 
    s_Player_Bitmap3 = null;        
    s_Player_Bitmap4 = null;
    //Избавиться от массива s_colPlayerBitmaps = null;
    //Функция: возвращает коллекцию изображений
    public static System.Collections.ArrayList g_PlayerBitmapsCollection()
    {
    //	
    //Если изображения уже загружены, их достаточно только возвратить
    //	
    if(s_colPlayerBitmaps != null)  {return s_colPlayerBitmaps;)
     //Загрузить изображения как ресурсы из исполняемого двоичного файла 
    System.Reflection.Assembly thisAssembly =System.Reflection.Assembly.GetExecutingAssembly(); 
    System.Reflection.AssemblyName thisAssemblyName =thisAssembly.GetName();
    string assemblyName = thisAssemblyName.Name; /
    /Загрузить изображения
    s_Player_Bitmapl = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName + 
    ".HankJRightRunl.bmp"));
    s_Player_Bitmap2 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
    ".Hank_RightRun2.bmp")); 
    s_Player_Bitmap3 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
    ".Hank_LeftRunl.bmp")); 
    s_Player_Bitmap4 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
    ".Hank_LeftRun2.bmp"));
    //Добавить изображения в коллекцию
    s_colPlayerBitmaps = new System.Collections.ArrayList(); 
    s_colPlayerBitmaps.Add(s_Player_Bitmapl);
     s_colPlayerBitmaps.Add(s_Player_Bitmap2); 
    s_colPlayerBitmaps.Add(s_Player_Bitmap3); 
    s_colPlayerBitmaps.Add(s_PlayerJBitmap4);
    //Возвратить коллекцию return s_colPlayerBitmaps;
    }
    private static System.Drawing.Pen s_blackPen; 
    private static System.Drawing.Pen s_whitePen; 
    private static System.Drawing.Imaging.ImageAttributes s_ImageAttribute;
    private static System.Drawing.Font s_boldFont;
    //	
    //Вызывается для освобождения от любых графических
    //ресурсов, которые могли быть кэшированы
    //	
    private static void g_CleanUpDrawingResources() {
    //Освободить память от черного пера, если таковое имеется if(s_blackPen != null)
    {s_blackPen.Dispose(); s_blackPen = null;}
    // Освободить память от белого пера, если таковое имеется if(s_whitePen != null)
    { s_whitePen.Dispose(); s_whitePen = null;}
    //Освободить память от атрибута ImageAttribute, если таковой имеется. 
    //Примечание. Метод Dispose() для этого типа не предусмотрен, 
    //поскольку все его данные являются управляемыми 
    if(s_ImageAttribute != null) {s_ImageAttribute = null;}
    //Освободить память от полужирного шрифта, если таковой имеется 
    if(s_boldFont != null)
    {s_boldFont.Dispose(); s_boldFont = null;}
    //	
    //Эта функция позволяет получить доступ
    //к черному перу, находящемуся в кэш-памяти
    //	
    private static System.Drawing.Pen g_GetBlackPen() {
    //Если перо еще не существует, создать его
    if(s_blackPen == null)
    {
    s_blackPen = new System.Drawing.Pen( System.Drawing.Color.Black) ;
    }
    //Возвратить черное перо return s blackPen;
    //	
    //Эта функция позволяет получить доступ
    //к белому перу, находящемуся в кэш-памяти
    //	
    private static System.Drawing.Pen g_GetWhitePen() {
    //Если перо еще не существует, создать его
    if(s_whitePen == null)
    {s_whitePen   = new System.Drawing.Pen(
    System.Drawing.Color.White);} //Возвратить белое перо return s_whitePen;
    }
    //	
    //Эта функция позволяет получить доступ
    //к полужирному шрифту, находящемуся в кэш-памяти
    //	
    private static System.Drawing.Font g_GetBoldFont() {
    //Если перо еще не существует, создать его
    if(s_boldFont == null)
    {
    s_boldFont   = new System.Drawing.Font( System.Drawing.FontFamily.GenericSerif, 10, System.Drawing.FontStyle.Bold) ;
    }
    //Возвратить полужирный шрифт return s_boldFont;
    }
    //	
    //Эта функция позволяет осуществлять доступ
    //к находящемуся в кэш-памяти объекту imageAttributes,
    // который мы используем для изображений с прозрачностью
    //	private static System.Drawing.Imaging.ImageAttributes g_GetTransparencyImageAttribute()
    {
    //Если объект не существует, создать его
    if(s_ImageAttribute == null)
    {
    //Создать атрибут изображения s_ImageAttribute =new System.Drawing.Imaging.ImageAttributes(); 
    s_ImageAttribute.SetColorKey(System.Drawing.Color.White,
    System.Drawing.Color.White);
    }
    //Возвратить его return s_ImageAttribute;
    }
    } //Конец класса

    8.3. Управление памятью на микроскопическом "уровне алгоритма"

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

  • Вычислительная неэффективность алгоритма. Этот вид неэффективности наблюдается в тех случаях, когда спроектированный вами алгоритм предусматривает интенсивные вычисления или выполнение большего количества циклов, чем это объективно необходимо, от чего можно было бы избавиться, используя более эффективные алгоритмы. В качестве классического примера можно привести сортировку массива данных. Иногда у вас может появляться возможность выбирать между несколькими возможными вариантами алгоритмов сортировки, отдельными частными случаями которых могут, например, быть алгоритмы "порядка N" (линейная зависимость времени вычислений от количества сортируемых элементов), "порядка N*Log(N)" (зависимость времени вычислений от количества сортируемых элементов отличается от линейной, но остается все же лучшей, чем экспоненциальная) или "порядка $$N^A2$$ " (экспоненциальная зависимость времени вычислений от количества сортируемых элементов). Кроме вышеперечисленных "порядков" возможно множество других (например, $$N^A3$$ ). Выбор наиболее подходящего алгоритма зависит от объема данных, с которыми вы работаете, объема доступной памяти и ряда других факторов, например, от состояния рабочих данных. Отдельные стратегии, например, предварительная обработка данных перед отправкой их на устройство или хранение данных в формате, специфическом для использования памяти в качестве хранилища, способны обеспечить значительное повышение производительности алгоритма. Существует огромное количество компьютерной литературы, посвященной проектированию эффективных алгоритмов и оценке их быстродействия, поэтому никаких попыток более подробного анализа этих вопросов в данной книге не делается. Необходимо только отметить, что чем больше объем обрабатываемых данных, тем ответственнее необходимо отнестись к принятию решения относительно выбора вычислительного алгоритма. Во всех затруднительных случаях тщательно анализируйте алгоритм и обращайтесь к существующей литературе по этому вопросу. Очень часто оказывается так, что кто-то другой уже прошел этот путь, и вам остается лишь перенять их опыт.
  • Неэффективное распределение памяти. После того как вы определитесь со стратегией алгоритма, следующим фактором, от которого в значительной степени зависит производительность приложения, является способ реализации этого алгоритма. При этом едва ли не наибольшие усилия вы должны приложить к тому, чтобы избежать распределения лишних объемов памяти, особенно если память распределяется в циклах. В данном разделе этого курса основное внимание уделяется именно этому вопросу.
  • Вашей целью должно быть распределение "нулевых объемов памяти" внутри циклов в написанном вами коде. Существуют случаи, когда это является неизбежным, как, например, при построении дерева объектов, которое требует размещения в памяти новых узлов для помещения их в иерархическую структуру. Во многих других случаях эффективность приложения можно существенно повысить, тщательно анализируя распределение памяти для каждого объекта и рассматривая альтернативные решения. Чего, как правило, следует избегать - так это выполнения операций размещения объектов в памяти и удаления их из памяти внутри алгоритмических циклов.

    8.4. "Структуры" и .NET Compact Framework

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

    Структуры обладают более простыми свойствами по сравнению с объектами, но могут "упаковываться" в объекты и передаваться внутри программы так же, как они, если в этом возникает необходимость. Использование структур предоставляет определенные удобства и может привести к некоторому увеличению производительности (по сравнению с вариантом, когда используются объекты), но поскольку они выглядят, а во многих случаях и действуют подобно объектам и могут заключаться в объекты-оболочки, необходимо тщательно взвешивать, когда их следует использовать, чтобы избежать дополнительных накладных расходов и не создать лишнего мусора. В сомнительных случаях тестируйте алгоритмы, используя как отдельные переменные (например, базовые типы, подобные int, string, double ), так и структуры, чтобы сравнить производительность приложения в обоих случаях и убедиться в том, что она остается примерно одинаковой.

    8.5. Использование строк в алгоритмах

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

    string strl = "internet"; 
    string str2 = "explorer"; 
    string str3 = strl + str2; 
    string str3 = str3 + str3;

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

  • Строки неизменчивы (постоянны). Этот странный термин неизменчивый (immutable) просто означает, что текстовые данные строки не могут быть изменены в памяти. Те операции в коде, которые, как вам кажется, изменяют данные строки, на самом деле создают новую строку. Постоянство обладает некоторыми весьма привлекательными свойствами. Например, поскольку строковые данные сами по себе являются статическими, несколько переменных могут указывать на одни и те же данные; благодаря этому присвоение одной строковой переменной значения другой сводится к простому копированию "указателя" вместо глубокого копирования всех данных, которые ему соответствуют. Отрицательной стороной неизменчивости является невозможность изменения данных. Если вы хотите изменить, добавить или отсечь данные, то эти изменения будут отражаться в новой копии строки.
  • Когда па строковые данные не ссылается пи одна "активная" ("live") переменная, они становятся "мусором". Рассмотрим пример:
    string   strl = "internet";
     //"internet" - статические данные, скомпилированные
    //в двоичные данные вашего приложения
    string str2 = strl + strl; 
    //только что была создана новая строка, являющаяся результатом конкатенации двух строк
    str2 = "explorer"; 
    //Поскольку отсутствуют другие переменные, указывающие на те данные, на которые указывала переменная str2, 
    //эти данные становятся мусором, и память должна быть 
    //очищена от них.
  • Если вы хотите сослаться на некоторую часть строки, то во многих случаях это проще всего сделать, используя целочисленные индексы в строке. Поскольку строки - это данные, представленные массивами символов, то использование индексов для получения этих данных не составляет труда. Существует множество функций, позволяющих осуществлять поиск и просмотр данных внутри строк (но только не изменять эти данные!).
  • Если вы создаете новые строки внутри циклов, настоятельно рекомендуется рассмотреть возможность использования объекта StringBuilder. Все виды строк создаются на основе других переменных, обрабатываемых в циклах. Типичным примером динамического создания строк может служить цикл, генерирующий текстовый отчет, каждая строка которого содержит следующие данные:
    //Неэффективный код, выполняющийся внутри цикла
    {
    myString = myString +"CustomerID: "
    + System.Convert.ToString(customer[idx].id) + ", Name: " + System.Convert.ToString(customer[idx].name);
    }
    Вместо того чтобы конкатенировать строки и создавать новую строку, для создания отчета можно было бы использовать класс StringBuilder. Класс StringBuilder очень удобно использовать для работы с массивами переменной размерности с целью создания строк. Он позволяет эффективно изменять длину или содержимое массива и, что самое важное, создавать новые строки на ос нове символьных массивов. Обязательно изучите класс StringBuilder, поскольку умение использовать его имеет решающее значение для написания эффективных алгоритмов, генерирующих строковые данные.
  • Измеряйте объективные количественные показатели своих алгоритмов. Занимаясь написанием алгоритма обработки строк, тестируйте его быстродействие! Испробуйте несколько различных подходов. Вы очень быстро научитесь распознавать, какой алгоритм будет эффективным, а какой - нет.
  • В листинге представлены два аналогичных алгоритма, которые приводят к одному и тому же результату. В обоих алгоритмах осуществляется инкрементирование счетчика, и каждый раз, когда значение счетчика увеличивается, его строковое представление добавляется в расширяемый фрагмент текста. Оба алгоритма выполняют одинаковое количество итераций и характеризуются одинаковой степенью сложности написания, тем не менее, один из них работает гораздо быстрее другого.

    //Для имитации создания типичного набора строк используются 
    //обычные строки
    private void buttonl_Click(object sender, System.EventArgs e) 
    {
    //Вызвать сборщик мусора, чтобы тест 
    //начинался с чистого состояния.
    System.GC.Collect ();
    int numberToStore = 0;
    PerformanceSampling.StartSample (0, "StringAllocaitons"); 
    string total_result = "";
    for (int outer_loop = 0; outer_loop < LOOP_ITERATIONS; outer_loop++)
    {
    //Сбросить старый результат 
    total_result = "";
    //Выполнять цикл до максимального значения x_counter, каждый 
    //раз присоединяя очередную тестовую строку к рабочей строке 
    for(int x_counter = 0; x_counter < COUNT_UNTIL; x_counter++) 
    {
    total_result = total_result + numberToStore.ToString();
    //Увеличить значение счетчика 
    numberToStore ++;
    }
    }
    PerformanceSampling.StopSample(0); 
    //Отобразить длину строки
    System.Windows.Forms.MessageBox.Show("Длина строки: " +total_result.Length.ToString());
    //Отобразить строку
    System.Windows.Forms.MessageBox.Show("Строка : " +total_result);
    //Отобразить длительность интервала времени, ушедшего на вычисления 
    System.Windows.Forms.MessageBox.Show(PerformanceSampling.GetSampleDurationText(0));
    }
    //Для имитации создания типичного набора строк используется 
    //объект StringBuilder
    private void button2_Click(object sender, System.EventArgs e) {
    //Вызвать сборщик мусора, чтобы тест 
    //начинался с чистого состояния.
    System.GC.Collect();
    System.Text.StringBuilder sb = new System.Text.StringBuilder(); string total_result = ""; int numberToStore = 0;
    PerformanceSampling.StartSample(1, "StringBuilder");
    for (int outerJLoop = 0; outer_loop < LOOP_ITERATIONS; outer_loop++)
    {
    //Очистить объект StringBuilder (не создавая нового объекта) 
    sb.Length = 0;
    //Очистить строку со старым результатом 
    total_result = "";
    //Выполнять цикл до максимального значения x_counter, каждый раз 
    //присоединяя очередную тестовую
     //строку к рабочей строке 
    for(int x_counter = 0; x_counter < COUNTJJNTIL; x_counter++) 
    {
    sb.Append(numberToStore ); 
    sb.Append(", "); 
    //Увеличить значение счетчика numberToStore ++;
    }
    //Имитируем выполнение некоторых операций над строкой... 
    total_result = sb.ToStringO ;
    }
    PerformanceSampling.StopSample(1); 
    //Отобразить длину строки
    System.Windows.Forms.MessageBox.Show("Длина строки: "+ total_result.Length.ToString()) ; 
    //Отобразить строку
    System.Windows.Forms.MessageBox.Show("String : " + total_result);
    //Отобразить длительность интервала времени, ушедшего на вычисления System.Windows.Forms.MessageBox.Show(
    PerformanceSampling.GetSampleDurationText(1));
    }
    Вернуться к учебному плану