Объявление строк. Конструкторы класса string
⇐ ПредыдущаяСтр 4 из 4 Объекты класса String объявляются как все прочие объекты простых типов - с явной или отложенной инициализацией, с явным или неявным вызовом конструктора класса. Чаще всего, при объявлении строковой переменной конструктор явно не вызывается, а инициализация задается строковой константой. Но у класса String достаточно много конструкторов. Они позволяют сконструировать строку из: символа, повторенного заданное число раз; массива символов char[]; части массива символов. Некоторым конструкторам в качестве параметра инициализации можно передать строку, заданную типом char*. Но все это небезопасно, и подобные примеры приводиться и обсуждаться не будут. Приведу примеры объявления строк с вызовом разных конструкторов: public void TestDeclStrings(){ //конструкторы string world = "Мир"; //string s1 = new string("s1"); //string s2 = new string(); string sssss = new string('s',5); char[] yes = "Yes".ToCharArray(); string stryes = new string(yes); string strye = new string(yes,0,2); Console.WriteLine("world = {0}; sssss={1}; stryes={2};"+ " strye= {3}", world, sssss, stryes, strye);}Объект world создан без явного вызова конструктора, а объекты sssss, stryes, strye созданы разными конструкторами класса String. Заметьте, не допускается явный вызов конструктора по умолчанию - конструктора без параметров. Нет также конструктора, которому в качестве аргумента можно передать обычную строковую константу. Соответствующие операторы в тексте закомментированы. Операции над строками Над строками определены следующие операции: присваивание (=); две операции проверки эквивалентности (==) и (!=); конкатенация или сцепление строк (+); взятие индекса ([]). Начну с присваивания, имеющего важную особенность. Поскольку string - это ссылочный тип, то в результате присваивания создается ссылка на константную строку, хранимую в "куче". С одной и той же строковой константой в "куче" может быть связано несколько переменных строкового типа. Но эти переменные не являются псевдонимами - разными именами одного и того же объекта. Дело в том, что строковые константы в "куче" не изменяются (о неизменяемости строкового типа будем далее говорить подробно), поэтому когда одна из переменных получает новое значение, она связывается с новым константным объектом в "куче". Остальные переменные сохраняют свои связи. Для программиста это означает, что семантика присваивания строк аналогична семантике значимого присваивания.
В отличие от других ссылочных типов операции, проверяющие эквивалентность, сравнивают значения строк, а не ссылки. Эти операции выполняются как над значимыми типами. Бинарная операция "+" сцепляет две строки, приписывая вторую строку к хвосту первой. Возможность взятия индекса при работе со строками отражает тот приятный факт, что строку можно рассматривать как массив и получать без труда каждый ее символ. Каждый символ строки имеет тип char, доступный только для чтения, но не для записи. Вот пример, в котором над строками выполняются данные операции: public void TestOpers(){ //операции над строками string s1 ="ABC", s2 ="CDE"; string s3 = s1+s2;bool b1 = (s1==s2); char ch1 = s1[0], ch2=s2[0]; Console.WriteLine("s1={0}, s2={1}, b1={2}," + "ch1={3}, ch2={4}", s1,s2,b1,ch1,ch2); s2 = s1; b1 = (s1!=s2); ch2 = s2[0]; Console.WriteLine("s1={0}, s2={1}, b1={2}," + "ch1={3}, ch2={4}", s1,s2,b1,ch1,ch2); //Неизменяемые значенияs1= "Zenon"; //s1[0]='L';} Цикл foreach Новым видом цикла, не унаследованным от С++, является цикл foreach, удобный при работе с массивами, коллекциями и другими подобными контейнерами данных. Его синтаксис: foreach(тип идентификатор in контейнер) операторЦикл работает в полном соответствии со своим названием - тело цикла выполняется для каждого элемента в контейнере. Тип идентификатора должен быть согласован с типом элементов, хранящихся в контейнере данных. Предполагается также, что элементы контейнера (массива, коллекции) упорядочены. На каждом шаге цикла идентификатор, задающий текущий элемент контейнера, получает значение очередного
элемента в соответствии с порядком, установленным на элементах контейнера. С этим текущим элементом и выполняется тело цикла - выполняется столько раз, сколько элементов находится в контейнере. Цикл заканчивается, когда полностью перебраны все элементы контейнера. Серьезным недостатком циклов foreach в языке C# является то, что цикл работает только на чтение, но не на запись элементов. Так что наполнять контейнер элементами приходится с помощью других операторов цикла. В приведенном ниже примере показана работа с трехмерным массивом. Массив создается с использованием циклов типа for, а при нахождении суммы его элементов, минимального и максимального значения используется цикл foreach: /// Демонстрация цикла foreach. Вычисление суммы,/// максимального и минимального элементов/// трехмерного массива, заполненного случайными числами.public void SumMinMax(){ int [,,] arr3d = new int[10,10,10]; Random rnd = new Random(); for (int i =0; i<10; i++) for (int j =0; j<10; j++) for (int k =0; k<10; k++) arr3d[i,j,k]= rnd.Next(100); long sum =0; int min=arr3d[0,0,0], max=arr3d[0,0,0]; foreach(int item in arr3d) { sum +=item; if (item > max) max = item; else if (item < min) min = item; } Console.WriteLine("sum = {0}, min = {1}, max = {2}", sum, min, max);}//SumMinMax25. Классы, создание объектов. Формальное определение класса в С# Класс в С#, как и в других языках программирования, — это пользовательский тип данных (user defined type, UDT), который состоит из данных (часто называе- мых атрибутами или свойствами) и функциями для выполнения с этими данными различных действий (эти функции обычно называются методами). Классы позво- ляют группировать в единое целое данные и функциональность, моделируя объек- ты реального мира. Именно это свойство классов и обеспечивает одно из наиболее важных преимуществ объектно-ориентированных языков программирования. К примеру, предположим, что нам потребовалось создать модель сотрудника нашей организации. Конечно же, для этой цели удобнее всего создать специаль- ный класс. Этот класс, как минимум, должен хранить данные об имени работника,
его идентификационном номере и текущей заработной плате. Помимо этого, пусть в нашем классе будут определены два метода — GiveBonusQ для увеличения зара-ботной платы сотрудника и 01 spl ayStats {) для вывода всех имеющихся данных об этом сотруднике (рис. 3.1).Как уже говорилось в главе 2, для классов С# можно определить любое количе- ство конструкторов. Эти специальные методы классов позволяют создавать объек- ты данного класса, настраивая при этом их исходное состояние нужным вам обра- зом. Любой класс С# автоматически снабжается конструктором по умолчанию (не принимающим никаких параметров). Этот конструктор в С# (в отличие от C++) при создании объекта автоматически присвоит всем переменным-членам класса безопасные значения по умолчанию. Кроме того, вы можете как переопределить конструктор по умолчанию, так и создать такое количество пользовательских кон- структоров (принимающих параметры), сколько вам необходимо. Давайте опре- делим наш класс для сотрудника на С#: // Исходное определение класса class Employee { // Внутренние закрытые данные класса private string fullName; private int erapID; private float currPay; // Конструкторы public EmployeeO {} public EmployeeCstring fullName, { this. fullName = fullName; this.empID = empID; this. currPay = currPay: int empID. float currPay) // Метод для увеличения зарплаты сотрудника public void CiveBonusffloat amount) { currPay += amount; } // Метод для вывода сведений о текущем состоянии объекта public virtual void DisplayStatsO Формальное определение класса в С# 141 Console. WriteUnet"Name: {0}", full Name): Console.WriteLineC'Pay: {0}". currPay): Console.UrtteLineCID: {0}". empID); Console.WriteLineC'SSN: {0}". ssn); } Обратите внимание на то, что определение для конструктора по умолчанию не содержит каких-либо действий, которые должен выполнять конструктор: class Employee { // Всей переменнын-членан значения по умолчанию будут присвоены автоматически public EmployeeО {} Необходимо обязательно помнить о следующем обстоятельстве: в С# (кик и в C++), если вы определили хотя бы один пользовательский конструктор (при- нимающий параметры), конструктор по умолчанию автоматически создаваться уже не будет и его придется определять явно. В противном случае при попытке выпол-
нения такого, к примеру, кода: // Вызываем конструктор по умолчанию Employee e = new EmployeeO: вы получите сообщение об ошибке компилятора. Использование пользовательского конструктора очевидно: // Выэываен пользовательский конструктор (двуня способами) public static void MainO i Employee e = new EmployeeC'Joe", 80, 30000); e.GiveBonusC200): Employee e2: e2 - new Employee("Beth". 81. 50000); e2.GiveBonus(1000); e2.DisplayStats(): } Код приложения Employees (с которым мы будем работать в продолжение всей этой главы) можно найти в подкаталоге Chapter 3.26. Методы класса и их параметры. 27. Свойства класса. Второй способ инкапсуляции: применение свойств класса Помимо традиционных методов доступа и изменения для обращения к закрытым чле- нам класса можно также использовать свойства (properties). В Visual Basic и СОМ свойства — это привычный инструмент. Свойства позволяют имитировать доступ к внутренним данным класса: при получении информации или внесении изменений через свойство синтаксис выглядит так же, как при обращении к обычной открытой переменной. Но на самом деле любое свойство состоит из двух скрытых внутренних методов. Преимущество свойств заключается в том, что вместо того, чтобы использо- вать два отдельных метода, пользователь класса может использовать единствен» >е свойство, работая с ним так же, как и с открытой переменной-членом данного класса: // Обращение к имени сотрудника через свойство public static int Main(sthng[] args) Employee p = new EmployeeC); // Устанавливаем значение p.EmpIO = 81; // Получаем значение Console.WriteLineC'Person ID Is: {0}". p.EmpID): return 0; |Если заглянуть внутрь определения класса, то свойства всегда отображаются в «реальные» методы доступа и изменения. А уже в определении этих методов вы можете реализовать любую логику (например, для устранения лишних символов, проверки допустимости вводимых числовых значений и прочего). Ниже представ- лен синтаксис класса Employee с определением свойства EmpID: // Пользовательское свойство EmpID для доступа к переменной етрШ public class Employee{ private Int етрШ: // Свойство для empID public Int EmpID {get {return empIO:} set {// Здесь вы можете реализовать логику для проверки вводимых // значений и выполнения других действий empID - value;}.} Свойство С# состоит из двух блоков — блока доступа (get block) и блока изме- нения (set block). Ключевое слово val ue представляет правую часть выражения при присвоении значения посредством свойства. Как и все в С#, то, что представлено словом val ue — это также объект. Совместимость того, что передается свойству как value, с самим свойством, зависит от определения свойства. Например, свойство EmpID предназначено (согласно своему определению в классе) для работы с закры-
тым целочисленным empID, поэтому число 81 вполне его устроит: // В данной случае типом данных, используемым для value, будет Int еЗ.EmpID - 81: Показать дополнительные возможности применения ключевого слова value можно на таком примере: 11 Свойство для ешрЮ public Int EmpID { get {return empID:} set Средства инкапсуляции в С# 153 // Как еще можно использовать value Console.WnteLlneC'value is the Instance of: {0}". value.GetTypeO): Console.WriteLineC'value as string: {0}". value.ToStrlngO); erapID = value; Результат работы данной программы представлен на рис. 3.7. •- ^ D:\CSharp8ook\Labs\Chapter 3\Cmploye_. НИ* иа.1»е is an instance of:Int32 jalue as string:81 Press any Itey to continue Рис. 3.7. Значение «value» при EmpID = 81 Необходимо отметить, что обращаться к объекту v a l u e можно только в преде- лах программного блока set внутри определения свойства. Попытка обратиться к этому объекту из любого другого места приведет к ошибке компилятора. Последнее, что мы отметим — использование свойств (по сравнению с тради- ционными методами доступа и изменения) делает применение ваших типов более простым. Например, предположим, что в классе Employee предусмотрена внутрен- няя закрытая переменная для хранения информации о возрасте сотрудника. Вы хотите, чтобы при наступлении дня рождения этого сотрудника значение этой пе- ременной увеличивалось на единицу. При использовании традиционных методов доступа и изменения эта операция будет выглядеть так: Employee joe * new EmployeeO; joe.SetAge(joe.GetAgeO + 1): Используя свойство, вы можете сделать РТО проще: Employee joe = new EmployeeO; joe.Ag&t+: Внутреннее представление свойств С# Многие программисты стараются использовать для имен методов доступа и изме - нения соответственно приставки get_ и set_ (например, get_Name() и set_Name()). Само по себе это не представляет проблемы. Проблему представляет другое: С# для внутреннего представления свойства использует методы с теми же самыми префиксами. К примеру, если вы откроете сборку EmpLoyees.exe при помощи ути- литы ILDastn.exe, вы увидите, что каждому свойству соответствуют два отдельных; (и скрытых) метода (рис. 3.8). Поэтому подобное определение класса вызовет ошибку компилятора: Рис. З.8. Свойства отображаются в скрытые методы get_ и set_ Свойства только для чтения, только для записи и статические Наш рассказ о свойствах классов С# будет неполон, если мы не упомянем еще о некоторых связанных с ними моментах. Как мы помним, наше свойство EmpID было создано как свойство, доступное и для чтения, и для записи. Однако при со- Средства инкапсуляции в С# 155 здании пользовательских свойств класса часто возникает необходимость создать свойство, которое будет доступно только для чтения. Делается это очень просто: необходимо в определении свойства пропустить блок set. В результате свойство станет доступным только для чтения: public class Employee // Будем считать, что исходное значение этой переменной присваивается с помощью // конструктора класса private string ssn; // А вот так выглядит свойство только для чтения public string SSN (get { return ssn: } } C# также поддерживает статические свойства. Как мы помним, статические переменные предназначены для хранения информации на уровне всего класса, а не его отдельных объектов. Если у нас объявлены статические данные (то есть те же переменные), то обращаться к ним и устанавливать значения должны статические свойства. Предположим, что в нашем классе Employee мы собираемся, помимо все- го прочего, хранить еще и информацию об имени организации, в которой работают все сотрудники — объекты класса Employee. Для этого будет использована специ- альная статическая переменная. Соответствующее статическое свойство для ра- боты с этой переменной может выглядеть так: // Со статическими данными должны работать статические свойства27. Свойства класса. Второй способ инкапсуляции: применение свойств класса. Помимо традиционных методов доступа и изменения для обращения к закрытым членам класса можно также использовать свойства (properties). В Visual Basic и СОМ свойства — это привычный инструмент. Свойства позволяют имитировать доступ к внутренним данным класса: при получении информации или внесении изменений через свойство синтаксис выглядит так же, как при обращении к обычной открытой переменной. Но на самом деле любое свойство состоит из двух скрытых внутренних методов. Преимущество свойств заключается в том, что вместо того, чтобы использовать два отдельных метода, пользователь класса может использовать единствен» >е свойство, работая с ним так же, как и с открытой переменной-членом данного класса: // Обращение к имени сотрудника через свойство public static int Main(sthng[] args) Employee p = new EmployeeC); // Устанавливаем значение p.EmpIO = 81; // Получаем значение Console.WriteLineC'Person ID Is: {0}". p.EmpID): return 0; |Если заглянуть внутрь определения класса, то свойства всегда отображаются в «реальные» методы доступа и изменения. А уже в определении этих методов вы можете реализовать любую логику (например, для устранения лишних символов, проверки допустимости вводимых числовых значений и прочего). Ниже представлен синтаксис класса Employee с определением свойства EmpID: // Пользовательское свойство EmpID для доступа к переменной етрШ public class Employee { private Int етрШ: // Свойство для empID public Int EmpID {get {return empIO:} set {// Здесь вы можете реализовать логику для проверки вводимых // значений и выполнения других действий empID - value; }.} Свойство С# состоит из двух блоков — блока доступа (get block) и блока изменения (set block). Ключевое слово val ue представляет правую часть выражения при присвоении значения посредством свойства. Как и все в С#, то, что представлено словом val ue — это также объект. Совместимость того, что передается свойству как value, с самим свойством, зависит от определения свойства. Например, свойство EmpID предназначено (согласно своему определению в классе) для работы с закрытым целочисленным empID, поэтому число 81 вполне его устроит: // В данной случае типом данных, используемым для value, будет Int еЗ.EmpID - 81: Показать дополнительные возможности применения ключевого слова value можно на таком примере: 11 Свойство для ешрЮ public Int EmpID { get {return empID:} set Средства инкапсуляции в С# 153 // Как еще можно использовать value Console.WnteLlneC'value is the Instance of: {0}". value.GetTypeO): Console.WriteLineC'value as string: {0}". value.ToStrlngO); erapID = value; Результат работы данной программы представлен на рис. 3.7. •- ^ D:\CSharp8ook\Labs\Chapter 3\Cmploye_. НИ* иа.1»е is an instance of:Int32 jalue as string:81 Press any Itey to continue Рис. 3.7. Значение «value» при EmpID = 81 Необходимо отметить, что обращаться к объекту v a l u e можно только в пределах программного блока set внутри определения свойства. Попытка обратиться к этому объекту из любого другого места приведет к ошибке компилятора. Последнее, что мы отметим — использование свойств (по сравнению с традиционными методами доступа и изменения) делает применение ваших типов более простым. Например, предположим, что в классе Employee предусмотрена внутренняя закрытая переменная для хранения информации о возрасте сотрудника. Вы хотите, чтобы при наступлении дня рождения этого сотрудника значение этой переменной увеличивалось на единицу. При использовании традиционных методов доступа и изменения эта операция будет выглядеть так: Employee joe * new EmployeeO; joe.SetAge(joe.GetAgeO + 1): Используя свойство, вы можете сделать РТО проще: Employee joe = new EmployeeO; joe.Ag&t+: Внутреннее представление свойств С# Многие программисты стараются использовать для имен методов доступа и изменения соответственно приставки get_ и set_ (например, get_Name() и set_Name()). Само по себе это не представляет проблемы. Проблему представляет другое: С# для внутреннего представления свойства использует методы с теми же самыми префиксами. К примеру, если вы откроете сборку EmpLoyees.exe при помощи утилиты ILDastn.exe, вы увидите, что каждому свойству соответствуют два отдельных; (и скрытых) метода (рис. 3.8). Поэтому подобное определение класса вызовет ошибку компилятора: Рис. З.8. Свойства отображаются в скрытые методы get_ и set_ Свойства только для чтения, только для записи и статические. Наш рассказ о свойствах классов С# будет неполон, если мы не упомянем еще о некоторых связанных с ними моментах. Как мы помним, наше свойство EmpID было создано как свойство, доступное и для чтения, и для записи. Средства инкапсуляции в С# 155 здании пользовательских свойств класса часто возникает необходимость создать свойство, которое будет доступно только для чтения. Делается это очень просто: необходимо в определении свойства пропустить блок set. В результате свойство станет доступным только для чтения: public class Employee // Будем считать, что исходное значение этой переменной присваивается с помощью // конструктора класса private string ssn; // А вот так выглядит свойство только для чтения public string SSN (get { return ssn: } } C# также поддерживает статические свойства. Как мы помним, статические переменные предназначены для хранения информации на уровне всего класса, а не его отдельных объектов. Если у нас объявлены статические данные (то есть те же переменные), то обращаться к ним и устанавливать значения должны статические свойства. Предположим, что в нашем классе Employee мы собираемся, помимо всего прочего, хранить еще и информацию об имени организации, в которой работают все сотрудники — объекты класса Employee. Для этого будет использована специальная статическая переменная. Соответствующее статическое свойство для а боты с этой переменной может выглядеть так: // Со статическими данными должны работать статические свойства 28. Наследование и виртуальные функции. Наследование Мощь ООП основана на наследовании. Когда построен полезный класс, то он может многократно использоваться. Повторное использование - это одна из главных целей ООП. Но и для хороших классов неизбежно наступает момент, когда необходимо расширить возможности класса, придать ему новую функциональность, изменить интерфейс. Всякая попытка изменять сам работающий класс чревата большими неприятностями - могут перестать работать прекрасно работавшие программы, многим клиентам класса вовсе не нужен новый интерфейс и новые возможности. Здесь-то и приходит на выручку наследование. Существующий класс не меняется, но создается его потомок, продолжающий дело отца, только уже на новом уровне. Класс-потомок наследует все возможности родительского класса - все поля и все методы, открытую и закрытую часть класса. Правда, не ко всем полям и методам класса возможен прямой доступ потомка. Поля и методы родительского класса, снабженные атрибутом private, хотя и наследуются, но по-прежнему остаются закрытыми, и методы, создаваемые потомком, не могут к ним обращаться напрямую, а только через методы, наследованные от родителя. Единственное, что не наследует потомок - это конструкторы родительского класса. Конструкторы потомок должен создавать сам. В этом есть некоторая разумная идея, и я позже поясню ее суть. Рассмотрим класс, названный Found, играющий роль родительского класса. У него есть обычные поля, конструкторы и методы, один из которых снабжен новым модификатором virtual, ранее не появлявшимся в классах, другой - модификатором override: public class Found{ //поля protected string name; protected int credit; public Found() { } public Found(string name, int sum) { this.name = name; credit = sum; } public virtual void VirtMethod() {Console.WriteLine ("Отец: " + this.ToString());} public override string ToString() { return(String.Format("поля: name = {0}, credit = {1}", name, credit)); } public void NonVirtMethod() { Console.WriteLine ("Мать: " + this.ToString());} public void Analysis() {Console.WriteLine ("Простой анализ");} public void Work() { VirtMethod(); NonVirtMethod(); Analysis(); }}Заметьте, класс Found, как и все классы, по умолчанию является наследником класса Object, его потомки наследуют методы этого класса уже не напрямую, а через методы родителя, который мог переопределить методы класса Object. В частности, класс Found переопределил метод ToString, задав собственную реализацию возвращаемой методом строки, которая связывается с объектами класса. Как часто делается, в этой строке отображаются значения полей объекта данного класса. На переопределение родительского метода ToString указывает модификатор метода override. Класс Found закрыл свои поля для клиентов, но открыл для потомков, снабдив их модификатором доступа protected. Создадим теперь класс Derived - потомка класса Found. В простейшем случае объявление класса может выглядеть так: public class Derived:Found{}Тело класса Derived пусто, но это вовсе не значит, что объекты этого класса не имеют полей и методов: они "являются" объектами класса Found, наследуя все его поля и методы (кроме конструктора) и поэтому могут делать все, что могут делать объекты родительского класса. Вот пример работы с объектами родительского и производного класса: public void TestFoundDerived(){ Found bs = new Found ("father", 777); Console.WriteLine("Объект bs вызывает методы базового класса"); bs.VirtMethod(); bs.NonVirtMethod(); bs.Analysis(); bs.Work(); Derived der = new Derived(); Console.WriteLine("Объект der вызывает методы класса потомка"); der.VirtMethod(); der.NonVirtMethod(); der.Analysis(); der.Work();}В чем отличие работы объектов bs и der? Поскольку класс-потомок Derived ничего самостоятельно не определял, то он наследовал все поля и методы у своего родителя, за исключением конструкторов. У этого класса имеется собственный конструктор без аргументов, задаваемый по умолчанию. При создании объекта der вызывался его собственный конструктор по умолчанию, инициализирующий поля класса значениями по умолчанию. Об особенностях работы конструкторов потомков скажу чуть позже, сейчас же упомяну лишь, что конструктор по умолчанию потомка вызывает конструктор без аргументов своего родителя, поэтому для успеха работы родитель должен иметь такой конструктор. Заметьте, поскольку родитель не знает, какие у него могут быть потомки, то желательно конструктор без аргументов включать в число конструкторов класса, как это сделано для класса Found. Добавление полей потомком Ничего не делающий самостоятельно потомок не эффективен, от него мало проку. Что же может делать потомок? Прежде всего, он может добавить новые свойства - поля класса. Заметьте, потомок не может ни отменить, ни изменить модификаторы или типы полей, наследованных от родителя - он может только добавить собственные поля. Модифицируем наш класс Derived. Пусть он добавляет новое поле класса, закрытое для клиентов этого класса, но открытое для его потомков: protected int debet;Напомню, хорошей стратегией является стратегия "ничего не скрывать от потомков". Какой родитель знает, что именно из сделанного им может понадобиться потомкам?
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|