Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Передача параметров в методы класса
Методы с возвратом типа void нередко применяются в программировании, тем не менее, большинство методов возвращает конкретное значение. В действительности способность возвращать значение является одним из самых полезных свойств метода. Для возврата значения из метода в вызывающую часть программы служит следующая форма оператора return: return значение; где значение — это конкретное возвращаемое значение. При вызове метода ему можно передать одно или несколько значений. Значение, передаваемое методу, называется аргументом. А переменная, получающая аргумент, называется формальным параметром, или просто параметром. Параметры объявляются в скобках после имени метода. Синтаксис объявления параметров такой же, как и у переменных. А областью действия параметров является тело метода. За исключением особых случаев передачи аргументов методу, параметры действуют так же, как и любые другие переменные. Если в методе используется несколько параметров, то для каждого из них указывается свой тип, отличающийся от других. Например, приведенный ниже код является вполне допустимым. int MyMeth(int a, double b, float с) { //... Хотя определение метода в С# выглядит довольно понятно, существует несколько ключевых слов, с помощью которых можно управлять способом передачи аргументов интересующему методу. Все эти ключевые слова описаны в табл. 1.
Таблица 1 Модификаторы параметров в С#
Конструкторы и деструкторы Конструктор (constructor) — это специальный метод класса, который вызывается неявно при создании объекта с использованием ключевого слова new. Однако в отличие от " нормального" метода, конструктор никогда не имеет возвращаемого значения (даже void) и всегда именуется идентично имени класса, который он конструирует. Ниже приведена общая форма конструктора. доступ имя_класса(список_параметров) { // тело конструктора } Как правило, конструктор используется для задания первоначальных значений переменных экземпляра, определенных в классе, или же для выполнения любых других установочных процедур, которые требуются для создания полностью сформированного объекта. Кроме того, доступ обычно представляет собой модификатор доступа типа public, поскольку конструкторы зачастую вызываются в классе. А список_параметров может быть как пустым, так и состоящим из одного или более указываемых параметров. У всех классов имеются конструкторы, независимо от того, определите вы их или нет, поскольку в С# автоматически предоставляется конструктор, используемый по умолчанию и инициализирующий все переменные экземпляра их значениями по умолчанию. Для большинства типов данных значением по умолчанию является нулевое, для типа bool — значение false, а для ссылочных типов — пустое значение. Но как только вы определите свой собственный конструктор, то конструктор по умолчанию больше не используется. Ниже приведен простой пример применения конструктора. // Простой конструктор. using System; class MyClass { public int x; public MyClass() { x = 10; } } class ConsDemo { static void Main() { MyClass tl = new MyClass(); MyClass t2 = new MyClass(); Console.WriteLine(tl, x + " " + t2.x); } } Зачастую конструктор, должен принимать один или несколько параметров. В конструктор параметры вводятся таким же образом, как и в метод. Для этого достаточно объявить их в скобках после имени конструктора. При использовании оператора new свободная память для создаваемых объектов динамически распределяется из доступной буферной области оперативной памяти. Разумеется, оперативная память не бесконечна, и поэтому свободно доступная память рано или поздно исчерпывается. Это может привести к неудачному выполнению оператора new из-за нехватки свободной памяти для создания требуемого объекта. Именно по этой причине одной из главных функций любой схемы динамического распределения памяти является освобождение свободной памяти от неиспользуемых объектов, чтобы сделать ее доступной для последующего перераспределения. Во многих языках программирования освобождение распределенной ранее памяти осуществляется вручную. Например, в C++ для этой цели служит оператор delete. Но в С# применяется другой, более надежный подход: " сборка мусора". Система " сборки мусора" в С# освобождает память от лишних объектов автоматически, действуя незаметно и без всякого вмешательства со стороны программиста. " Сборка мусора" происходит следующим образом. Если ссылки на объект отсутствуют, то такой объект считается ненужным, и занимаемая им память в итоге освобождается и накапливается. Эта утилизированная память может быть затем распределена для других объектов. " Сборка мусора" происходит лишь время от времени по ходу выполнения программы. Она не состоится только потому, что существует один или более объектов, которые больше не используются. Следовательно, нельзя заранее знать или предположить, когда именно произойдет " сборка мусора". В языке С# имеется возможность определить метод, который будет вызываться непосредственно перед окончательным уничтожением объекта системой " сборки мусора". Такой метод называется деструктором и может использоваться в ряде особых случаев, чтобы гарантировать четкое окончание срока действия объекта. Например, деструктор может быть использован для гарантированного освобождения системного ресурса, задействованного освобождаемым объектом. Следует, однако, сразу же подчеркнуть, что деструкторы — весьма специфические средства, применяемые только в редких, особых случаях. И, как правило, они не нужны. Ниже приведена общая форма деструктора: ~имя_класса() { // код деструктора } где имя_класса означает имя конкретного класса. Следовательно, деструктор объявляется аналогично конструктору, за исключением того, что перед его именем указывается знак " тильда" (~). Обратите внимание на то, что у деструктора отсутствуют возвращаемый тип и передаваемые ему аргументы. Для того чтобы добавить деструктор в класс, достаточно включить его в класс в качестве члена. Он вызывается всякий раз, когда предполагается утилизировать объект его класса. В деструкторе можно указать те действия, которые следует выполнить перед тем, как уничтожать объект. Следует, однако, иметь в виду, что деструктор вызывается непосредственно перед " сборкой мусора". Он не вызывается, например, в тот момент, когда переменная, содержащая ссылку на объект, оказывается за пределами области действия этого объекта. (В этом отношении деструкторы в С# отличаются от деструкторов в C++, где они вызываются в тот момент, когда объект оказывается за пределами области своего действия.) Это означает, что заранее нельзя знать, когда именно следует вызывать деструктор. Кроме того, программа может завершиться до того, как произойдет " сборка мусора", а следовательно, деструктор может быть вообще не вызван.
2.4. Инкапсуляция в C# Первый основной принцип ООП называется инкапсуляцией. Этот принцип касается способности языка скрывать излишние детали реализации от пользователя объекта. С идеей инкапсуляции программной логики тесно связана идея защиты данных. В идеале данные состояния объекта должны быть специфицированы с использованием ключевого слова private (или, возможно, protected). Таким образом, внешний мир должен вежливо попросить, если захочет изменить или получить лежащее в основе значение. Это хороший принцип, поскольку общедоступные элементы данных можно легко повредить (даже нечаянно, а не преднамеренно). При работе с инкапсуляцией всегда следует принимать во внимание то, какие аспекты типа видимы различным частям приложения. В частности, типы (классы, интерфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы, конструкторы и поля) определяются с использованием определенного ключевого слова, управляющего " видимостью" элемента другим частям приложения. Хотя в С# определены многочисленные ключевые слова для управления доступом, их значение может отличаться в зависимости от места применения (к типу или члену). В табл. 2 описаны роли и применение модификаторов доступа. Таблица 2 Модификаторы доступа С#
Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние данные объекта не должны быть напрямую доступны через экземпляр объекта. Вместо этого, если вызывающий код желает изменить состояние объекта, то должен делать это через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В С# инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых слов public, private, internal и protected. Чтобы проиллюстрировать необходимость в службах инкапсуляции, предположим, что создано следующее определение класса: // Класс с единственным общедоступным полем. class Book { public int numberOfPages; } Проблема с общедоступными данными состоит в том, что сами по себе эти данные не имеют возможности распознать, является ли присваиваемое значение допустимым в рамках существующих бизнес-правил системы. Как известно, верхний предел значений для типа int в С# довольно велик B 147 483 647), поэтому компилятор разрешит следующее присваивание: //Хм… Ничего себе — мини-новелла! static void Main(string[] args) { Book miniNovel = new Book(); miniNovel.numberOfPages = 30000000; } Хотя границы типа данных int не превышены, ясно, что мини-новелла на 30 миллионов страниц выглядит несколько неправдоподобно. Как видите, общедоступные поля не дают возможности перехватывать ошибки, связанные с преодолением верхних (или нижних) логических границ. Если в текущей системе установлено правило, гласящее, что книга должна иметь от 1 до 1000 страниц, его придется обеспечить программно. По этой причине общедоступным полям обычно нет места в определении класса производственного уровня. Инкапсуляция предоставляет способ предохранения целостности данных о состоянии объекта. Вместо определения общедоступных полей (которые легко приводят к повреждению данных), необходимо выработать привычку определения приватных данных, управление которыми осуществляется опосредованно, с применением одной из двух техник: • определение пары методов доступа и изменения; • определение свойства.NET. Свойство — способ доступа к внутреннему состоянию объекта, имитирующий переменную некоторого типа. Обращение к свойству объекта реализовано через вызов функции. При попытке задать значение данного свойства вызывается один метод, а при попытке получить значение данного свойства — другой. Свойства в C# — поля с логическим блоком, в котором есть ключевые слова get и set. Пример класса со свойством: public class Date { private int month = 7;
public int Month { get { return month; } set { if ((value > 0) & & (value < 13)) { month = value; } } } } Какая бы техника не была выбрана, идея состоит в том, что хорошо инкапсулированный класс должен защищать свои данные и скрывать подробности своего устройства от любопытных глаз из внешнего мира. Это часто называют программированием черного ящика. Преимущество такого подхода состоит в том, что объект может свободно изменять внутреннюю реализацию любого метода. Чтобы упростить процесс простой инкапсуляции данных полей, можно применять синтаксис автоматических свойств, который появился в C# в поздних версиях платформы.NET 3.5. Как следует из названия, это средство перекладывает работу по определению лежащего в основе приватного поля и связанного свойства С# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим класс Car, в котором этот синтаксис применяется для быстрого создания трех свойств: class Car { // Автоматические свойства public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; } } При определении автоматических свойств указывается модификатор доступа, лежащий в основе тип данных, имя свойства и пустые контексты get/set. Во время компиляции тип будет оснащен автоматически сгенерированным полем и соответствующей реализацией логики set/get. Определяемое автоматическое свойство должно поддерживать функциональность и чтения, и записи. // Свойство только для чтения? Ошибка! public int MyReadOnlyProp { get; } // Свойство только для записи? Ошибка! public int MyWriteOnlyProp { set; } Поскольку компилятор будет определять лежащие в основе приватные поля во время компиляции, класс с автоматическими свойствами всегда должен использовать синтаксис свойств для установки и чтения лежащих в основе значений. Индексаторы и свойства Индексаторы Индексаторы предоставляют механизм для индексирования объектов подобно массивам. Индексирование массива осуществляется с помощью оператора [ ]. Для создаваемых классов можно определить оператор [ ], но с этой целью вместо операторного метода создается индексатор, который позволяет индексировать объект, подобно массиву. Индексаторы применяются, главным образом, в качестве средства, поддерживающего создание специализированных массивов, на которые накладывается одно или несколько ограничений. Тем не менее индексаторы могут служить практически любым целям, для которых выгодным оказывается такой же синтаксис, как и у массивов. Индексаторы могут быть одно- или многомерными. Ниже приведена общая форма одномерного индексатора: тип_элемента this[int индекс] { // Аксессор для получения данных, get { // Возврат значения, которое определяет индекс. } // Аксессор для установки данных, set { // Установка значения, которое определяет индекс. } } где тип_элемента обозначает конкретный тип элемента индексатора. Следовательно, у каждого элемента, доступного с помощью индексатора, должен быть определенный тип_элемента. Этот тип соответствует типу элемента массива. Параметр индекс получает конкретный индекс элемента, к которому осуществляется доступ. Формально этот параметр совсем не обязательно должен иметь тип int, но поскольку индексаторы, как правило, применяются для индексирования массивов, то чаще всего используется целочисленный тип данного параметра. В теле индексатора определены два аксессора (т.е. средства доступа к данным): get и set. Аксессор подобен методу, за исключением того, что в нем не объявляется тип возвращаемого значения или параметры. Аксессоры вызываются автоматически при использовании индексатора, и оба получают индекс в качестве параметра. Так, если индексатор указывается в левой части оператора присваивания, то вызывается аксессор set и устанавливается элемент, на который указывает параметр индекс. В противном случае вызывается аксессор get и возвращается значение, соответствующее параметру индекс. Кроме того, аксессор set получает неявный параметр value, содержащий значение, присваиваемое по указанному индексу. Преимущество индексатора заключается, в частности, в том, что он позволяет полностью управлять доступом к массиву, избегая нежелательного доступа. Наличие обоих аксессоров, get и set, в индексаторе не является обязательным. Так, можно создать индексатор только для чтения, реализовав в нем один лишь аксессор get, или же индексатор только для записи с единственным аксессором set. Индексатор может быть перегружен. В этом случае для выполнения выбирается тот вариант индексатора, в котором точнее соблюдается соответствие его параметра и аргумента, указываемого в качестве индекса. Свойства Еще одной разновидностью члена класса является свойство. Как правило, свойство сочетает в себе поле с методами доступа к нему. Поле зачастую создается, чтобы стать доступным для пользователей объекта, но при этом желательно сохранить управление над операциями, разрешенными для этого поля, например, ограничить диапазон значений, присваиваемых данному полю. Этой цели можно, конечно, добиться и с помощью закрытой переменной, а также методов доступа к ее значению, но свойство предоставляет более совершенный и рациональный путь для достижения той же самой цели. Свойство так же, как и индексаторы состоит из имени и аксессоров get и set. Аксессоры служат для получения и установки значения переменной. Главное преимущество свойства заключается в том, что его имя может быть использовано в выражениях и операторах присваивания аналогично имени обычной переменной, но в действительности при обращении к свойству по имени автоматически вызываются его аксессоры get и set. Аналогичным образом используются аксессоры get и set индексатора. Ниже приведена общая форма свойства: тип имя{ get{ // код аксессора для чтения из поля } set{ // код аксессора для записи в поле } где тип обозначает конкретный тип свойства, например int, а имя — присваиваемое свойству имя. Как только свойство будет определено, любое обращение к свойству по имени приведет к автоматическому вызову соответствующего аксессора. Кроме того, аксессор set принимает неявный параметр value, который содержит значение, присваиваемое свойству. Следует, однако, иметь в виду, что свойства не определяют место в памяти для хранения полей, а лишь управляют доступом к полям. Это означает, что само свойство не предоставляет поле, и поэтому поле должно быть определено независимо от свойства. Исключение из этого правила составляет автоматически реализуемое свойство. В поздних версиях С# появилась возможность для реализации очень простых свойств, не прибегая к явному определению переменной, которой управляет свойство. Вместо этого базовую переменную для свойства автоматически предоставляет компилятор. Такое свойство называется автоматически реализуемым и принимает следующую общую форму: тип имя { get; set; } где тип обозначает конкретный тип свойства, а имя — присваиваемое свойству имя. После обозначений аксессоров get и set сразу же следует точка с запятой, а тело у них отсутствует. Такой синтаксис предписывает компилятору создать автоматически переменную, иногда еще называемую поддерживающим полем, для хранения значения. Такая переменная недоступна непосредственно и не имеет имени. Но в то же время она может быть доступна через свойство. Ниже приведен пример объявления свойства, автоматически реализуемого под именем Prop. public int Prop { get; set; } В этой строке кода переменная явно не объявляется. И компилятор автоматически создает анонимное поле, в котором хранится значение. А в остальном автоматически реализуемое свойство Prop подобно всем остальным свойствам. Но в отличие от обычных свойств автоматически реализуемое свойство не может быть доступным только для чтения или только для записи. При объявлении этого свойства в любом случае необходимо указывать оба аксессора — get и set. Хотя добиться желаемого (т.е. сделать автоматически реализуемое свойство доступным только для чтения или только для записи) все же можно, объявив ненужный аксессор как private.
4. Наследование в C# Наследование является одним из трех основополагающих принципов объектно-ориентированного программирования, поскольку оно допускает создание иерархических классификаций. Благодаря наследованию можно создать общий класс, в котором определяются характерные особенности, присущие множеству связанных элементов. От этого класса могут затем наследовать другие, более конкретные классы, добавляя в него свои индивидуальные особенности. В языке С# класс, который наследуется, называется базовым, а класс, который наследует, — производным. Следовательно, производный класс представляет собой специализированный вариант базового класса. Он наследует все переменные, методы, свойства и индексаторы, определяемые в базовом классе, добавляя к ним свои собственные элементы. Ниже приведена общая форма объявления класса, наследующего от базового класса. class имя_производного_класса: имя_базового_класса { // тело класса } Для любого производного класса можно указать только один базовый класс. В С# не предусмотрено наследование нескольких базовых классов в одном производном классе. Тем не менее можно создать иерархию наследования, в которой производный класс становится базовым для другого производного класса. (Разумеется, ни один из классов не может быть базовым для самого себя как непосредственно, так и косвенно.) Но в любом случае производный класс наследует все члены своего базового класса, в том числе переменные экземпляра, методы, свойства и индексаторы. Главное преимущество наследования заключается в следующем: как только будет создан базовый класс, в котором определены общие для множества объектов атрибуты, он может быть использован для создания любого числа более конкретных производных классов. А в каждом производном классе может быть точно выстроена своя собственная классификация. Члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. И наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в производный класс и входят все члены его базового класса, в нем все равно оказываются недоступными те члены базового класса, которые являются закрытыми. Для преодоления ограничения на доступ к частным членам базового класса из производного класса в С# предусмотрены разные способы: 1) Первый из них состоит в применении открытых свойств для доступа к закрытым данным. Свойство позволяет управлять доступом к переменной экземпляра. Например, с помощью свойства можно ввести ограничения на доступ к значению переменной или же сделать ее доступной только для чтения. Так, если сделать свойство открытым, но объявить его базовую переменную закрытой, то этим свойством можно будет воспользоваться в производном классе, но нельзя будет получить непосредственный доступ к его базовой закрытой переменной. 2) Второй же состоит в использовании защищенных (protected) членов класса. Защищенный член является открытым в пределах иерархии классов, но закрытым за пределами этой иерархии. Защищенный член создается с помощью модификатора доступа protected. Если член класса объявляется как protected, он становится закрытым, но за исключением одного случая, когда защищенный член наследуется. В этом случае защищенный член базового класса становится защищенным членом производного класса, а значит, доступным для производного класса. Таким образом, используя модификатор доступа protected, можно создать члены класса, являющиеся закрытыми для своего класса, но все же наследуемыми и доступными для производного класса. Модификатор доступа protected следует применять в том случае, если требуется создать член класса, доступный для всей иерархии классов, но для остального кода он должен быть закрытым. А для управления доступом к значению члена класса лучше воспользоваться свойством. В иерархии классов допускается, чтобы у базовых и производных классов были свои собственные конструкторы. Когда конструктор определен только в производном классе, то конструируется объект производного класса, а базовая часть объекта автоматически конструируется его конструктором, используемым по умолчанию. Когда конструкторы определяются как в базовом, так и в производном классе, процесс построения объекта несколько усложняется, поскольку должны выполняться конструкторы обоих классов. В данном случае приходится обращаться к еще одному ключевому слову языка С#: base, которое находит двоякое применение: во-первых, для вызова конструктора базового класса; и во-вторых, для доступа к члену базового класса, скрывающегося за членом производного класса. С помощью формы расширенного объявления конструктора производного класса и ключевого слова base в производном классе может быть вызван конструктор, определенный в его базовом классе. Ниже приведена общая форма этого расширенного объявления: конструктор_производного_класса(список_параметров): base (список_аргументов) { // тело конструктора } где список_аргументов обозначает любые аргументы, необходимые конструктору в базовом классе. С помощью ключевого слова base можно вызвать конструктор любой формы, определяемой в базовом классе, причем выполняться будет лишь тот конструктор, параметры которого соответствуют переданным аргументам. В С# строго соблюдается принцип совместимости типов. Это означает, что переменная ссылки на объект класса одного типа, как правило, не может ссылаться на объект класса другого типа. Но из этого принципа имеется одно важное исключение: переменной ссылки на объект базового класса может быть присвоена ссылка на объект любого производного от него класса. Такое присваивание считается вполне допустимым, поскольку экземпляр объекта производного типа инкапсулирует экземпляр объекта базового типа. Следовательно, по ссылке на объект базового класса можно обращаться к объекту производного класса. В С# имеется возможность предотвратить наследование класса с помощью ключевого слова sealed. Популярное:
|
Последнее изменение этой страницы: 2016-07-14; Просмотров: 1179; Нарушение авторского права страницы