Значимые и ссылочные переменные — КиберПедия 

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...

Наброски и зарисовки растений, плодов, цветов: Освоить конструктивное построение структуры дерева через зарисовки отдельных деревьев, группы деревьев...

Значимые и ссылочные переменные

2021-04-18 102
Значимые и ссылочные переменные 0.00 из 5.00 0 оценок
Заказать работу

В C# все переменные можно разделить на две категории – переменные значимого и ссылочного типа.

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

Значимыми являются также переменные основных встроенных типов данных – числовые (double, int), символьные (char), логические (bool). А вот переменные строкового типа (String) – ссылочные.

Ссылочная переменная в своей памяти хранит адрес (ссылку) на другое место в памяти, где хранятся данные. Помимо решения проблем с передачей изменяемых параметров, эта двухуровневая схема адресации в большей степени соответствует духу объектно-ориентированного подхода. Вспомним вариант программы, где me была объектом класса Person. Переменные, предназначенные для указания на объекты классов, являются ссылочными переменными, поэтому схема размещения в памяти такова:

Теперь размер памяти, выделяемой любой ссылочной переменной, одинаков – это размер, достаточный для хранения адреса. Данные, сгруппированные в виде информационного объекта, находятся в том месте, на которое указывает адрес. Теперь становится понятнее, зачем объекты необходимо создавать с помощью операции new. Компилятор не занимается выделением памяти для объектов. Эта операция должна быть выполнена динамически, то есть во время выполнения программы. Если Вы забудете осуществить выделение памяти операцией new и начнете использовать такую переменную, то в программе произойдет ошибка времени выполнения «null reference».

Уточним, какие переменные в C# являются значимыми, а какие – ссылочными.

Значимые переменные Ссылочные переменные
Переменные встроенных типов Структуры, не использующие для создания операцию new Массивы Структуры, создаваемые с помощью new Объекты класса String Объекты классов

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

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

Разместим в классе Program рядом с методом Main еще один метод Grow, увеличивающий рост человека, переданного в качестве параметра:

public static void Grow(Person p) //этот метод мог быть проще

{Person local; local=p; local.Weight++;}

Причину появления в заголовке метода ключевого слова static Вы узнаете позже.

Перед вызовом этого метода в Main должен быть создан объект Person.

Person me = new Person();

me.Name = "Это я"; me.Height = 190.0; me.Weight = 85;

Далее осуществляется вызов метода Grow:

Grow(me);

В процессе выполнения метода Grow создается локальная переменная local, которая, благодаря присваиванию local=p; также ссылается на объект Person.

После выполнения метода Grow переменная local исчезает, однако объект Person в памяти остается и к нему имеется возможность доступа через переменную me. Таким образом, благодаря ссылочным переменным легко решается проблема изменяемых параметров.

Теперь зададим себе вопрос – что если переменная me также исчезает, освобождая занимаемую ею память? В этом случае становится невозможным доступ к объекту Person, занимающему свой участок памяти. В этой ситуации возникает опасность “утечки памяти” - в процессе выполнения программы может возникнуть много неиспользуемых объектов. В некоторых языках решение этой проблемы возлагалось на программиста. Он должен был предусмотреть явное уничтожение объекта специальным методом- деструктором.

Однако в современных языках, в частности и в C#, используется другой подход. Во время выполнения программы в фоновом режиме выполняется специальная утилита – сборщик мусора (garbage collector), который автоматически уничтожает объекты, для которых не осталось ссылок в программе.

Отметим еще несколько особенностей.

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

Операции сравнения для ссылочных переменных обычно реализованы как сравнение адресов объектов, на которые они ссылаются. Поэтому имеют смысл обычно только операции == (равно) и!= (не равно). Однако есть возможность самостоятельно переопределить операции сравнения для реализации более содержательного сравнения, основанного на состоянии объекта.

Механизм скрытой передачи адреса на объект решает большую часть проблем, возникающих при передаче параметров. В языке C и его «наследниках» передача параметров (то есть передача значений фактических параметров в формальные переменные) осуществляется только по значению. В этом случае функция, которая «возвращает» результат своей работы через один или несколько параметров, должны были как-то обходить это правило. Понятно, что значимые переменные уже обладают такой способностью. Однако некоторые ситуации таким способом не учитываются.

Сначала рассмотрим две следующие ситуации:

1) Необходимо обеспечит передачу «по ссылке» значимой переменной.

2) Необходимо обеспечить изменение методом самой ссылки (адреса).

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

public void DoSomething(ref Person p, ref int i)

{ Person newMe= new Person();

newMe.Name="Это я"; newMe.Height=190.0; newMe.Weight=85;

me=newMe;

. i++;

}

...

int k=5;

DoSomething(ref me, ref k);

Здесь в методе DoSomething обеспечивается передача по ссылке как ссылочной переменной me типа Person, так и значимой переменной k типа int. Благодаря этому, после вызова метода переменная me ссылается на новый объект, а переменная k изменяет свое значение.

Еще одна ситуация, представляющая интерес - передача в метод неинициализированные переменные.

Инициализация переменных перед их использованием является обязательным требованием C#. Таким образом, компилятор следит за тем, чтобы ссылочные переменные в момент их использования указывали на некоторый объект. В противном случае они считаются неинициализированными и имеют специальное знначнение null. Однако в некоторых случаях это требование становится неудобным. Что, если первоначальное значение для переменной может быть определено только в результате выполнения достаточно сложного метода? В этом случае нужно использовать специальные out-параметры. Ключевое слово out следует указывать, как и слово ref  перед формальными и фактическими параметрами.

class Program

{ static void Main(string[] args)

{ Person me;

MakePerson(out me);

me.PersonAnalyze();

}

public static void MakePerson(out Person p)

{ p = new Person();

p.Name="Это я"; p.Height=190.0; p.Weight=85;

}

}

Здесь мы видим, что переменная me, описанная в методе Main, используется в качестве параметра метода MakePerson. На момент вызова эта переменная не ссылается на некоторый созданный объект. Для ссылочных переменных это и означает, что переменная не инициализирована. Однако создание объекта и связывание его с переменной успешно происходит методе MakePerson с out-параметром.

Для значимых переменных использование out-параметров не столь важно, поскольку значимые переменные инициализируются автоматически нулевыми значениями.

Конструкторы класса

Рассмотрим подробнее, как создается объект класса Person:

newMe = new Person();

Сначала в правой части присваивания выполняется операция new, которая резервирует в памяти участок, способный хранить все переменные класса. Однако назвать это действие полноценным созданием объекта нельзя. Здесь не хватает того, что происходит и в реальной жизни – при рождении объект не только занимает место в пространстве, но и получает полный набор значений своих характеристик (начальное состояние объекта). Это необходимо выполнить и в момент создания объекту. Вот почему после операции new указывается не просто тип Person, а вызывается специальный метод, имя которого совпадает с именем класса. Такой метод называется конструктором. Исходя из такой роли конструктора, он должен быть определен в каждом классе.

Конструктор класса имеет несколько синтаксических особенностей:

1. Обычно (но не всегда!) конструктор описывается как public.

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

3. Имя конструктора всегда совпадает с именем класса.

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

public Person(string n, double h, double w)

{ name=n; Height=h; Weight=w; }

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

public Person(string Name, double Height, double Weight)

{ this.Name= Name; this.Height= Height; this.Weight= Weight; }

Слово this указателем на текущий объект. Это слово обозначает объект данного класса, который вызвал метод.

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

public Person(int age)

{ Height=table[age].height;Weight= table[age].weight; }

Заметим, что этот конструктор не обеспечивает назначение объекту имени

Person p = new Person(10);

p.PersonAnalyze();

Такой эксперимент покажет, что объект, на который ссылается переменная p, имеет имя “”, то есть пустую строку. Это стандартное «нулевое» значение, которое автоматически присваивается строковым переменным, если это инициализация не была выполнена явно. Для переменных числовых типов таким стандартным значением является 0, а для логических переменных - false.

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

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

static Person()

{ Console.WriteLine("Выполняется статический конструктор!"); }

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

public Person(){ name=”noname”; Height=50; Weight=4; }

Если в классе явно не определен ни один конструктор, то конструктор по умолчанию генерируется компилятором. Однако ничего интересного такой конструктор не выполняет – он инициализирует переменные объекта стандартными «нулевыми» значениями. Если в составе класса имеется переменная, являющаяся объектом некоторого класса, то в этом случае она будет иметь значение null – специальное слово, обозначающее отсутствие ссылки на объект в памяти.

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

Вызов перегруженного конструктора

Статические элементы

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

1. Сначала создайте классы – полезные правила для создания объектов.

2. Далее создавайте объекты – экземпляры этих классов и заставляйте их выполнять нужную Вам работу.

Таким образом в процессе выполнения программы непосредственно действуют объекты. Однако в некоторых случаях естественнее считать, что действия выполняются самим классом. Хорошим примером является класс Math – все его методы вызываются от имени класса:

Console.WriteLine(Math.Sin(0.5)+Math.Cos(0.5));

Было бы довольно странно для вычисления математических функций сначала создавать объект-«математику»:

Math m1=new Math();

Console.WriteLine(m1.Sin(0.5)+m2.Cos(0.5));

Это означало бы, что можно создавать несколько «математик». Но ведь они все должны действовать абсолютно одинаково.

Аналогично дело обстоит и с переменными – бывают случаи, когда некоторые данные естественнее представлять не как порцию информации, принадлежащую объекту, а классу в целом. В классе Math эта ситуация встречается при доступе к тригонометрической константе p - Math.PI.

Элементы класса (переменные и методы), которые соотносятся не с объектами, а с классом, называются статическими. В описании статических элементов используется ключевое слово static. В отличие от статических членов обычные элементы класса называются элементами экземпляров класса – методы экземпляров и переменные экземпляров.

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

 

Рассмотрим следующий пример. Класс Person моделирует ситуацию, когда множество людей располагают некоторым общим запасом пищи и делят его поровну. Количество пищи FoodQuantity и количество людей PeopleCount – характеристики всей совокупности людей. Поэтому они описаны как статические переменные. Метод Description описывает состояние всего множества людей и также является статическим методом.

class Person

{ public static double FoodQuantity = 100.0;

public static int PeopleCount = 0;

public double Weight;

public Person()

{ Weight = 10.0; PeopleCount++; }

public void ToEat()

{ double t = FoodQuantity / 2.0 / PeopleCount;

Weight += t; FoodQuantity -= t;

}

public static string Description()

 { return String.Format("Людей - {0} Еды - {1}",

PeopleCount,FoodQuantity);

  }

}

class Program

{ static void Main(string[] args)

{ Person p1 = new Person(); p1.ToEat();

Console.WriteLine("Вес p1 - {0}", p1.Weight);

Console.WriteLine(Person.Description());

Person p2 = new Person(); Person p3 = new Person();

p1.ToEat(); Console.WriteLine("Вес p1 - {0}",p1.Weight);

Console.WriteLine(Person.Description());

}

}

Статические методы могут использовать только статические переменные  и другие статические методы класса. Попытка использовать в методе Description переменную Weight привела бы к ошибке компилятора.

Для инициализации статических переменных можно использовать статический конструктор:

static Person()

{ FoodQuantity = 100.0; PeopleCount = 0; }

Если в начале изучения языка C# не сразу используются его объектно-ориентированные возможности, структура программы выглядит примерно следующим образом:

class Program

{ static void Main(string[] args)

{ Do1();

Console.WriteLine(Do2());

}

static void Do1() { Console.WriteLine("метод Do1"); }

static string Do2() { return "метод Do2"; }

}

Таким образом, можно не обращать внимания на наличие в программе класса Program. Программа сстоит из нескольких статических методов, которые вызываются в «главном» методе Main.

Генерация случайных чисел

Необходимость в создании последовательности случайных чисел возникает в программировании довольно часто. Кроме того, способ, которым это делается в C#, характерен с точки зрения объектно-ориентированного подхода. В других языках программирования для генерации случайных чисел имеется некоторая функция (например, Random в языке Pascal).  Следует учитывать детерминированную природу алгоритма генерации случайных чисел. Обычно такие алгоритмы основаны на некотором перемешивании цифр начального числа («зерна»). Для того чтобы серия случайных чисел каждый раз была другой, в качестве начального зерна алгоритму следует передавать различные значения начального зерна. Для этого в языке Pascal имеется функция Randomize, использующая в качестве начального зерна текущее системное время.

Однако в C# нет самостоятельных функций, а только методы классов. В качестве ближайшей аналогии функциям Random и Randomize можно использовать статические методы некоторого класса. Однако на самом деле в C# имеется класс Random c набором методов для генерации случайных чисел. Таким образом, фрагмент программы, генерирующий два случайных числа, может выглядеть так:

Random rnd=new Random();

//целое случайное число в диапазоне от 1 до 6

int i=rnd.Next(1,7);

//целое вещественное число в диапазоне от 0 до1

double d=rnd.NextDouble();

Таким способом решается и проблема уникального начального зерна – при создании нового объекта в качестве начального зерна неявно используется текущее системное время. Тем не менее, класс Random следует использовать внимательно. Следующий фрагмент продемонстрирует две одинаковые серии случайных чисел – i11 будет равно i21, а i12 – равно i22.

Random rnd1=new Random();

Random rnd2=new Random();

int i11=rnd.Next(1,7);

int i12=rnd.Next(1,7);

int i21=rnd.Next(1,7);

int i22=rnd.Next(1,7);

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

Массивы в языке C#

Хотя основные приемы использования массивов C# унаследовал от C++, следует обратить внимание на ряд важных особенностей.

Каждый массив является объектом класса System.Array. Поэтому в жизненном цикле массива имеется стадия описания массива и стадия создания массива. Внимание – в описании массива не указывается размер (количество элементов):

int [] Arr1;

Person [] Arr2;

Как и раньше, массив – это коллекция однотипных элементов. Массив Arr1 будет содержать целые числа, а массив Arr2 – объекты класса Person. Еще отметим «перемещение» пары квадратных скобок – они в C# указываются перед именем массива. Таким образом, конструкция «int []» является полноценным описателем типа массива.

Далее массив можно создать и на этой стадии нам понадобится операция new:

Arr1 = new int[10];

Использование new почти не изменилось – после new нужно указать тип объекта, а у нас это int[]. Только теперь в квадратных скобках нужно указать количество элементов массива. Кроме того, отсутствуют скобки со списком параметров.

Дальнейшее использование массива может происходить обычным образомю Например:

for (int i=0; i<10; i++) Arr1[i] = i*2;

Как видите, нумерация, по прежнему начинается с 0.

Аналогично поступим с массивом Arr2:

Arr2 = new Person[Arr1[3]];

Здесь проявилась замечательная особенность массивов в C# - их размер может задаваться выражением, значение которого определится только во время выполнения программы. Более того, Вы можете заново создать массив:

Arr2 = new string[Arr2.Length + 2];

Здесь мы воспользовались свойством Length класса System.Array. В результате размер нового массива больше на 2 элемента размера старого. Но учтите, что «новый» массив не содержит элементов старого массива – ведь это совсем новый объект в новом месте памяти.

Отметим, что в примере мы не инициализировали массив. В отношении массива требование инициализации не действует. Дело в том, что при создании массива с помощью new создается множество ссылок, каждая из которых содержит «пустой» указатель null. Этого достаточно, чтобы C# позволил приступить к использованию массива. Однако здесь появляется возможность для ошибки во время выполнения массива – если Вы попытаетесь использовать объект, на который ссылается такая null-ссылка. Поэтому нужно выполнить что-то в таком духе:

for (int i=0; i<Arr2.Length; i++) Arr2[i]=new Person();

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

static int[] ModifyArray(int[] inArr)

{ int [] rArr = new int[inArr.Length * 2];

for (int i = 0; i < inArr.Length; i++)

{ rArr[i] = inArr[i]; rArr[inArr.Length + i] = 0; }

return rArr;

}

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

int[] Arr3 = ModifyArray(Arr1);

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

Следует отметить, что кроме обычных приемов работы с массивами (обращение к элементам с помощью индекса, циклы и т.д.) класс System.Array предоставляет ряд дополнительных и весьма полезных методов. Некоторые из них перечислены в следующей таблице:

Метод Описание
static int IndexOf (Array array, Object value) Возвращает первое вхождение значения value в массив array. Если array не содержит заданного значения, метод возвращает отрицательное целое число.
public static void Sort (Array array) Сортирует элементы во всем одномерном массиве array.
static int BinarySearch (Array array, Object value) Быстрый поиск методом половинного деления позиции значения value в объекте array. Перед вызовом этого метода объект array необходимо отсортировать. Если array не содержит заданного значения, метод возвращает отрицательное целое число.

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

Рассмотрим только 2-х мерные массивы. Использование массивов большей размерности принципиально не отличается. Существуют два вида таких массивов – прямоугольные и «рваные» (ступенчатые).

У прямоугольного массива все строки имеют одинаковое количество элементов (также как и столбцы). Такая структура в математике называется двумерной матрицей.

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

int [, ] matrix;

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

matrix = new int[3,4];

Здесь создается прямоугольный массив из 3-х строк и 4-х столбцов. Дальнейшее использование такого массива вполне традиционно. Например, можно заполнить весь массив содержимым следующим образом:

for(int i = 0; i<matrix.GetLength(0); i++)

for (int j = 0; j< matrix.GetLength(1); j++)

matrix[i, j]=i*j;

Обратим внимание, что для определения количества шагов в циклах перебора вместо свойства Length (общее количество элементов), нужно использовать метод GetLength с параметром – номером измерения.

Другой тип многомерных массивов – рваные массивы – следует представлять как одномерный массив, элементами которого являются в свою очередь, массивы. Описание такого массива несколько отличается:

int [][] jagArray;

Здесь используются две пары квадратных скобок. Создание «рваного» 2-х мерного массива состоит из двух этапов. Сначала создается главный массив:

jagArray = new int[5][];

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

for (int i=0; i<jagArray.Length; i++)

jagArray[i]=new int[i+7];

При переборе ступенчатого массива следует учитывать, что не все элементы главного масива существуют:

int s=0;

for (i=0;i< jagArray.Length; i++)

 if jagArray [i]!=null

for (j=0; j< jagArray [i].Length; j++)

s=s+ jagArray [i][j];

Класс ArrayList

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

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

У класса ArrayList есть еще одно важное отличие от массивов – он способен хранить элементы совершенно произвольного типа.

Для использования ArrayList в программе нужно подключить пространство имен System.Collections.

Описание и создание объекта ArrayList происходит как обычно:

ArrayList persons=new ArrayList();

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

Теперь с помощью метода Add мы можем добавлять в persons элементы:

Person p=new Person();

persons.Add(p);

persons.Add(new Person());

Здесь мы добавили в persons два объекта Person – один объект, на который ссылается переменная p, а второй объект – безымянный (для него не существует переменной). К первому объекту можно получать доступ через «его» переменную p и через объект persons, второй объект доступен только через persons.

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

for (int i=0;i<persons.Count;i++) persons[i].PersonAnalyze();

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

Выполнение такого цикла приведет к ошибке компиляции:

'object' does not contain a definition for 'PersonAnalyze'

Компилятор «говорит», что в классе Object не определен метод PersonAnalyze. Откуда взялся класс Object? Дело в том, что ArrayList является универсальным контейнером, способным хранить объекты любого типа. Платой за это является потеря информации о действительном типе объекта, когда мы обращаемся к нему как к элементу ArrayList. Все, что известно о типе этого объекта – он является любым объектом, то есть объектом класса Object – общем предке всех классов.NET. И именно об отсутствии метода PersonAnalyze в классе Object сообщает компилятор.

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

for (int i=0;i<persons.Count;i++)

((Person)persons[i]).PersonAnalyze();

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

Метод Remove позволяет удалить объект из ArrayList:

persons.Remove(p);

Если параметр-объект не содержится в ArrayList, то метод Remove не имеет никакого эффекта.

Отметим еще несколько полезных методов:

RemoveAt     удаление объекта с указанной позицией

Insert          вставка объекта в указанную позицию

Sort                упорядочивает элементы в ArrayList

Clear             удаление всех элементов из ArrayList

Contains     определяет, содержится ли объект в ArrayList

IndexOf        возвращает позицию объекта в ArrayList

 

Хотя элементы в ArrayList находятся в порядке возрастания их номеров (и обычно в порядке их добавления), во многих случаях этот порядок не имеет значения. Допустим, нужно определить суммарный вес объектов Person в ArrayList. Для этого нужно просмотреть все объекты в ArrayList в любом порядке. Для таких ситуаций очень удобна новая разновидность оператора цикла:

double s = 0;

foreach (Person pers in persons) s = s + pers.Weight;

В заголовке цикла описывается переменная, которая будет использоваться для перебора (Person p) и указывается место, где осуществляется перебор (in persons). Всю остальную работу по организации перебора цикл foreach выполняет автоматически. Заметьте, что явное приведение к типу Person здесь выполнено путем описания переменной цикла p как Person.

Цикл foreach можно использовать и для обычных массивов.

Несмотря на такую простоту, использование цикла foreach ограничено следующим фактом – в цикле foreach доступ к элементам массива или ArrayList может происходить только для чтения.

Класс List<>

Класс List<> является аналогом ArrayList, но позволяет хранить только объекты заданного типа. Тип хранимых объектов указывается при описании в угловых скобках <>:

List<int> integers = new List<int>(); //множество целых чисел

List<Person> persons; //множество людей

persons= new List<Person>();

Теперь у Вас нет возможности нарушить строгую типизацию:

integers.Add(new Person()); //ошибка компиляции

Для использования класса List<> нужно подключить пространство имен System.Collections.Generic.

Инкапсуляция

До сих пор переменные и методы наших классов описывались с модификатором доступа public. Это позволяло использовать их за пределами класса. Например, для увеличения роста человека, на которого ссылается переменная p можно написать оператор:

p.Height++;

Однако в современном объектно-ориентированном программировании действует правило инкапсуляции, согласно которому все переменные класса делаются закрытыми (private), то есть недоступными за пределами класса. Доступ к этим переменным осуществляется через открытый интерфейс – открытые методы класса.

В связи с этим внесем изменения в класс Person:

class Person

{ private string name;

private double height;

private double weight;

public Person(string Name, double Height, double Weight)

{ name=Name; height=Height; weight=Weight; }

public Person(){ name=”noname”; height=50; weight=4; }

public void PersonAnalyze()

{ if (height-weight>100.0)

Console.WriteLine(name+" полный");

else

Console.WriteLine(name + " худой");

}

}

Отметим два вида изменений:

· все переменные класса описаны с использованием модификатора доступа private;

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

Теперь за пределами класса уже нельзя осуществить непосредственное использование переменных. Однако выход есть (и даже несколько). Например, можно создать в классе два открытых метода для доступа к каждой переменной (их еще называют методами-аксессорами или get- и set- методами):

public double GetHeight() {return height; }

public void SetHeight(double newHeight) { height=newHeight; }

Теперь для увеличения роста человека на единицу потребуется два вызова методов:

p.SetHeight(p.GetHeight()+1);

Главный вопрос здесь – зачем нужно такое ограничение? Ведь последняя строка не только «ужасно» выглядит, но и замедляет выполнение программы. Дело в том, что преимущества инкапсуляции намного важнее, чем упомянутые здесь недостатки.

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

Если оператор

p.Height++;

еще может соответствовать реальному процессу (увеличение роста человека), то как можно содержательно трактовать оператор

p.Height--;

или

p.Height=-10;

Таким образом, непосредственное использование переменных не способствует поддержке правил и ограничений предметной области (так называемых бизнес-правил). Эту проблему можно решить несколькими способами.

1. Использование специализированных методов, соответствующих содержательным действиям в предметной области. Например, метод Grow в классе Person может реализовать естественный рост человека на протяжении некоторого периода:

public void Grow(int days) {... }

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

2. Реализацией ограничений в методах доступа:

public void SetHeight (double newHeight)

{ if ((newHeight>0)&&(newHeight<230)&& (newHeight>height))

height=newHeight;

}

3. Реализацией методов-свойств. О свойствах подробнее будет рассказано ниже.

Обратим внимание на важные следствия такого подхода. Класс с его методами и переменными становится в достаточной степени черным ящиком. Пользователю класса не известны ни особенности реализации методов класса, ни даже информационная структура класса. Это позволяет разделить программный проект на разные по роли фрагменты, которые часто взаимодействуют по принципу клиент-сервер. Клиент использует класс, зная его открытый интерфейс. Примером клиента является метод Main, использующий встроенные и пользовательские классы для решения конкретной задачи. Сервер – это класс, предоставляющий свои услуги. Разработчик серверного класса может изменять (совершенствовать) детали его устройства и функционирования, пока это не влияет на открытый интерфейс класса.

Обработка ошибок

Вернемся к рассмотрению метода класса Person, который обеспечивал некоторые ограничения рассматриваемой предметной области.

public void SetHeight (double newHeight)

{ if ((newHeight>0)&&(newHeight<230)&& (newHeight>height))

height=newHeight;

}

Использование этого метода с некорректными данными никак не влияет на состояние объекта Person (у if-оператора нет else-части). Как ни странно, это приводит к еще худшим последствиям. Предположим, что программа выполнила оператор p.SetHeight(-10). После выполнения метода объект остается в прежнем состоянии, и программа продолжает «корректно» работать. Теперь трудно будет обнаружить, почему дальнейшее использование такого объекта приводит к ошибочным последствиям.

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

Попробуем улучшить реализацию метода SetHeight, разместив в части else оператор вывода диагностического сообщения:

 public void SetHeight (newHeight)

{ if (newHeight>0)&&(newHeight<230)&& (newHeight>height)

height=newHeight;

else Console.WriteLine(“Ошибка: недопустимый рост”);

}

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

Все это означает, что в else-части нужно осуществлять завершение программы. Однако в C# нет оператора, позволяющего это сделать в любом месте программного кода. И это не случайный недостаток языка.

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

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

throw new Exception(“строка с описанием ошибки”);

Здесь с помощью конструктора класса Exception создается объект-ошибка


Поделиться с друзьями:

Биохимия спиртового брожения: Основу технологии получения пива составляет спиртовое брожение, - при котором сахар превращается...

Семя – орган полового размножения и расселения растений: наружи у семян имеется плотный покров – кожура...

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

Двойное оплодотворение у цветковых растений: Оплодотворение - это процесс слияния мужской и женской половых клеток с образованием зиготы...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.337 с.